@heysalad/cheri-cli 0.1.0 → 0.3.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
@@ -1,60 +1,88 @@
1
- # Cheri CLI
1
+ # cheri-cli
2
2
 
3
- AI-powered cloud IDE by [HeySalad](https://heysalad.app). Like Claude Code, but for cloud workspaces.
3
+ CLI for [Cheri](https://cheri.heysalad.app) the AI-powered cloud IDE that never forgets.
4
+
5
+ Manage workspaces, track API usage, and access your AI memory from the terminal.
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- npm install -g @heysalad/cheri-cli
10
+ npm install -g cheri-cli
9
11
  ```
10
12
 
11
- ## Usage
13
+ Requires Node.js 18+.
14
+
15
+ ## Quick Start
12
16
 
13
17
  ```bash
14
- # Login to your Cheri account
18
+ # Authenticate with your Cheri account
15
19
  cheri login
16
20
 
21
+ # Launch a cloud workspace
22
+ cheri workspace launch owner/my-repo
23
+
17
24
  # Check account status
18
25
  cheri status
19
26
 
20
- # Launch a cloud workspace
21
- cheri workspace launch owner/repo
22
-
23
- # List your workspaces
24
- cheri workspace list
27
+ # View API usage and rate limits
28
+ cheri usage
29
+ ```
25
30
 
26
- # Stop a workspace
27
- cheri workspace stop
31
+ ## Commands
32
+
33
+ | Command | Description |
34
+ |---|---|
35
+ | `cheri login` | Authenticate with GitHub |
36
+ | `cheri status` | Show account and workspace status |
37
+ | `cheri usage` | Show API usage and rate limit status |
38
+ | `cheri workspace launch <repo>` | Launch a new cloud workspace |
39
+ | `cheri workspace list` | List all workspaces |
40
+ | `cheri workspace stop <id>` | Stop a running workspace |
41
+ | `cheri workspace status <id>` | Get workspace status |
42
+ | `cheri memory show` | Show current memory entries |
43
+ | `cheri memory add <text>` | Add a memory entry |
44
+ | `cheri memory clear` | Clear all memory |
45
+ | `cheri memory export` | Export memory to JSON |
46
+ | `cheri config list` | Show all configuration |
47
+ | `cheri config get <key>` | Get a config value |
48
+ | `cheri config set <key> <value>` | Set a config value |
49
+ | `cheri init` | Initialize a project |
50
+
51
+ ## Interactive REPL
52
+
53
+ Run `cheri` with no arguments to enter the interactive REPL:
28
54
 
29
- # Initialize AI project config
30
- cheri init
55
+ ```
56
+ $ cheri
57
+ 🍒 cheri > help
58
+ 🍒 cheri > workspace list
59
+ 🍒 cheri > usage
60
+ 🍒 cheri > exit
61
+ ```
31
62
 
32
- # Manage persistent memory
33
- cheri memory show
34
- cheri memory add "Always use TypeScript strict mode"
35
- cheri memory clear
63
+ ## Rate Limits
36
64
 
37
- # View/update configuration
38
- cheri config list
39
- cheri config set apiUrl https://cheri.heysalad.app
40
- ```
65
+ | Plan | Limit |
66
+ |---|---|
67
+ | Free | 100 requests/hour |
68
+ | Pro | 1,000 requests/hour |
41
69
 
42
- ## How it works
70
+ Use `cheri usage` to check your current rate limit status.
43
71
 
44
- 1. **`cheri login`** opens your browser for GitHub OAuth, then you paste your API token
45
- 2. **`cheri workspace launch`** spins up a cloud workspace with code-server (VS Code in browser)
46
- 3. **`cheri memory`** stores persistent context that follows you across sessions
47
- 4. **`cheri init`** creates a local `.ai/` directory with project constitution files
72
+ ## Configuration
48
73
 
49
- ## Requirements
74
+ Config is stored in `~/.cheri/`. Set the API URL if self-hosting:
50
75
 
51
- - Node.js >= 18
76
+ ```bash
77
+ cheri config set apiUrl https://your-instance.example.com
78
+ ```
52
79
 
53
80
  ## Links
54
81
 
55
82
  - [Cheri Cloud IDE](https://cheri.heysalad.app)
56
- - [GitHub](https://github.com/Hey-Salad/cheri-cli)
83
+ - [Dashboard](https://cheri.heysalad.app/dashboard)
84
+ - [GitHub](https://github.com/chilu18/cloud-ide)
57
85
 
58
86
  ## License
59
87
 
60
- MIT - HeySalad
88
+ MIT
package/bin/cheri.js CHANGED
@@ -7,6 +7,7 @@ import { registerStatusCommand } from "../src/commands/status.js";
7
7
  import { registerMemoryCommand } from "../src/commands/memory.js";
8
8
  import { registerConfigCommand } from "../src/commands/config.js";
9
9
  import { registerWorkspaceCommand } from "../src/commands/workspace.js";
10
+ import { registerUsageCommand } from "../src/commands/usage.js";
10
11
 
11
12
  program
12
13
  .name("cheri")
@@ -19,10 +20,12 @@ registerStatusCommand(program);
19
20
  registerMemoryCommand(program);
20
21
  registerConfigCommand(program);
21
22
  registerWorkspaceCommand(program);
23
+ registerUsageCommand(program);
22
24
 
23
- program.parse(process.argv);
24
-
25
- // Show help if no command provided
25
+ // If no args, launch interactive command REPL
26
26
  if (!process.argv.slice(2).length) {
27
- program.outputHelp();
27
+ const { startCommandRepl } = await import("../src/repl.js");
28
+ await startCommandRepl();
29
+ } else {
30
+ program.parse(process.argv);
28
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heysalad/cheri-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Cheri CLI - AI-powered cloud IDE by HeySalad. Like Claude Code, but for cloud workspaces.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,15 +8,14 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "src/",
12
- "README.md"
11
+ "src/"
13
12
  ],
14
13
  "scripts": {
15
14
  "start": "node bin/cheri.js",
16
15
  "dev": "node bin/cheri.js",
17
- "release:patch": "npm version patch && npm publish --access public && git push && git push --tags",
18
- "release:minor": "npm version minor && npm publish --access public && git push && git push --tags",
19
- "release:major": "npm version major && npm publish --access public && git push && git push --tags"
16
+ "release:patch": "npm version patch && npm publish && git push && git push --tags",
17
+ "release:minor": "npm version minor && npm publish && git push && git push --tags",
18
+ "release:major": "npm version major && npm publish && git push && git push --tags"
20
19
  },
21
20
  "keywords": [
22
21
  "cloud-ide",
@@ -29,9 +28,9 @@
29
28
  ],
30
29
  "repository": {
31
30
  "type": "git",
32
- "url": "https://github.com/Hey-Salad/cheri-cli.git"
31
+ "url": "https://github.com/chilu18/cloud-ide.git",
32
+ "directory": "cli"
33
33
  },
34
- "homepage": "https://cheri.heysalad.app",
35
34
  "author": "HeySalad",
36
35
  "license": "MIT",
37
36
  "engines": {
@@ -2,6 +2,42 @@ import chalk from "chalk";
2
2
  import { getConfig, getConfigValue, setConfigValue } from "../lib/config-store.js";
3
3
  import { log } from "../lib/logger.js";
4
4
 
5
+ export function listConfig() {
6
+ const cfg = getConfig();
7
+
8
+ log.blank();
9
+ log.brand("Configuration");
10
+ log.blank();
11
+
12
+ printObject(cfg, "");
13
+ log.blank();
14
+ }
15
+
16
+ export function getConfigKey(key) {
17
+ const value = getConfigValue(key);
18
+ if (value === undefined) {
19
+ throw new Error(`Key '${key}' not found.`);
20
+ }
21
+ if (typeof value === "object") {
22
+ console.log(JSON.stringify(value, null, 2));
23
+ } else {
24
+ console.log(value);
25
+ }
26
+ }
27
+
28
+ export function setConfigKey(key, value) {
29
+ // Try to parse as JSON (for arrays, numbers, booleans)
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(value);
33
+ } catch {
34
+ parsed = value;
35
+ }
36
+
37
+ setConfigValue(key, parsed);
38
+ log.success(`Set ${chalk.cyan(key)} = ${chalk.white(value)}`);
39
+ }
40
+
5
41
  export function registerConfigCommand(program) {
6
42
  const config = program
7
43
  .command("config")
@@ -11,46 +47,26 @@ export function registerConfigCommand(program) {
11
47
  .command("list")
12
48
  .description("Show all configuration values")
13
49
  .action(() => {
14
- const cfg = getConfig();
15
-
16
- log.blank();
17
- log.brand("Configuration");
18
- log.blank();
19
-
20
- printObject(cfg, "");
21
- log.blank();
50
+ listConfig();
22
51
  });
23
52
 
24
53
  config
25
54
  .command("get <key>")
26
55
  .description("Get a configuration value")
27
56
  .action((key) => {
28
- const value = getConfigValue(key);
29
- if (value === undefined) {
30
- log.error(`Key '${key}' not found.`);
57
+ try {
58
+ getConfigKey(key);
59
+ } catch (err) {
60
+ log.error(err.message);
31
61
  process.exit(1);
32
62
  }
33
- if (typeof value === "object") {
34
- console.log(JSON.stringify(value, null, 2));
35
- } else {
36
- console.log(value);
37
- }
38
63
  });
39
64
 
40
65
  config
41
66
  .command("set <key> <value>")
42
67
  .description("Set a configuration value")
43
68
  .action((key, value) => {
44
- // Try to parse as JSON (for arrays, numbers, booleans)
45
- let parsed;
46
- try {
47
- parsed = JSON.parse(value);
48
- } catch {
49
- parsed = value;
50
- }
51
-
52
- setConfigValue(key, parsed);
53
- log.success(`Set ${chalk.cyan(key)} = ${chalk.white(value)}`);
69
+ setConfigKey(key, value);
54
70
  });
55
71
  }
56
72
 
@@ -1,7 +1,7 @@
1
1
  import ora from "ora";
2
2
  import inquirer from "inquirer";
3
3
  import chalk from "chalk";
4
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
4
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { setConfigValue } from "../lib/config-store.js";
7
7
  import { log } from "../lib/logger.js";
@@ -79,6 +79,86 @@ const TEMPLATES = {
79
79
  `,
80
80
  };
81
81
 
82
+ export async function initProject(options = {}) {
83
+ log.blank();
84
+ log.brand("Initializing project...");
85
+ log.blank();
86
+
87
+ const aiDir = join(process.cwd(), AI_DIR);
88
+
89
+ if (existsSync(aiDir) && !options.yes) {
90
+ const { overwrite } = await inquirer.prompt([
91
+ {
92
+ type: "confirm",
93
+ name: "overwrite",
94
+ message: "A .ai/ directory already exists. Add missing files?",
95
+ default: true,
96
+ },
97
+ ]);
98
+ if (!overwrite) {
99
+ log.info("Initialization cancelled.");
100
+ return;
101
+ }
102
+ }
103
+
104
+ let projectName = options.name;
105
+ if (!projectName && !options.yes) {
106
+ const answers = await inquirer.prompt([
107
+ {
108
+ type: "input",
109
+ name: "name",
110
+ message: "Project name:",
111
+ default: process.cwd().split("/").pop(),
112
+ },
113
+ ]);
114
+ projectName = answers.name;
115
+ }
116
+
117
+ projectName = projectName || process.cwd().split("/").pop();
118
+
119
+ const spinner = ora("Creating project constitution...").start();
120
+
121
+ try {
122
+ if (!existsSync(aiDir)) {
123
+ mkdirSync(aiDir, { recursive: true });
124
+ }
125
+
126
+ const created = [];
127
+ for (const [filename, content] of Object.entries(TEMPLATES)) {
128
+ const filePath = join(aiDir, filename);
129
+ if (!existsSync(filePath)) {
130
+ writeFileSync(filePath, content);
131
+ created.push(filename);
132
+ }
133
+ }
134
+
135
+ spinner.succeed("Project constitution created");
136
+ log.blank();
137
+
138
+ if (created.length > 0) {
139
+ created.forEach((file) => {
140
+ console.log(` ${chalk.green("+")} .ai/${file}`);
141
+ });
142
+ } else {
143
+ log.info("All constitution files already exist.");
144
+ }
145
+
146
+ // Save project config
147
+ setConfigValue("project.name", projectName);
148
+ setConfigValue("project.initializedAt", new Date().toISOString());
149
+
150
+ log.blank();
151
+ log.success(`Project '${chalk.bold(projectName)}' initialized.`);
152
+ log.dim(
153
+ `Run ${chalk.cyan("cheri workspace launch owner/repo")} to start coding in the cloud.`
154
+ );
155
+ log.blank();
156
+ } catch (err) {
157
+ spinner.fail("Failed to initialize project");
158
+ throw err;
159
+ }
160
+ }
161
+
82
162
  export function registerInitCommand(program) {
83
163
  program
84
164
  .command("init")
@@ -86,81 +166,9 @@ export function registerInitCommand(program) {
86
166
  .option("-n, --name <name>", "Project name")
87
167
  .option("-y, --yes", "Skip prompts and use defaults")
88
168
  .action(async (options) => {
89
- log.blank();
90
- log.brand("Initializing project...");
91
- log.blank();
92
-
93
- const aiDir = join(process.cwd(), AI_DIR);
94
-
95
- if (existsSync(aiDir) && !options.yes) {
96
- const { overwrite } = await inquirer.prompt([
97
- {
98
- type: "confirm",
99
- name: "overwrite",
100
- message: "A .ai/ directory already exists. Add missing files?",
101
- default: true,
102
- },
103
- ]);
104
- if (!overwrite) {
105
- log.info("Initialization cancelled.");
106
- return;
107
- }
108
- }
109
-
110
- let projectName = options.name;
111
- if (!projectName && !options.yes) {
112
- const answers = await inquirer.prompt([
113
- {
114
- type: "input",
115
- name: "name",
116
- message: "Project name:",
117
- default: process.cwd().split("/").pop(),
118
- },
119
- ]);
120
- projectName = answers.name;
121
- }
122
-
123
- projectName = projectName || process.cwd().split("/").pop();
124
-
125
- const spinner = ora("Creating project constitution...").start();
126
-
127
169
  try {
128
- if (!existsSync(aiDir)) {
129
- mkdirSync(aiDir, { recursive: true });
130
- }
131
-
132
- const created = [];
133
- for (const [filename, content] of Object.entries(TEMPLATES)) {
134
- const filePath = join(aiDir, filename);
135
- if (!existsSync(filePath)) {
136
- writeFileSync(filePath, content);
137
- created.push(filename);
138
- }
139
- }
140
-
141
- spinner.succeed("Project constitution created");
142
- log.blank();
143
-
144
- if (created.length > 0) {
145
- created.forEach((file) => {
146
- console.log(` ${chalk.green("+")} .ai/${file}`);
147
- });
148
- } else {
149
- log.info("All constitution files already exist.");
150
- }
151
-
152
- // Save project config
153
- setConfigValue("project.name", projectName);
154
- setConfigValue("project.initializedAt", new Date().toISOString());
155
-
156
- log.blank();
157
- log.success(`Project '${chalk.bold(projectName)}' initialized.`);
158
- log.dim(
159
- `Run ${chalk.cyan("cheri workspace launch owner/repo")} to start coding in the cloud.`
160
- );
161
- log.blank();
170
+ await initProject(options);
162
171
  } catch (err) {
163
- spinner.fail("Failed to initialize project");
164
172
  log.error(err.message);
165
173
  process.exit(1);
166
174
  }
@@ -4,53 +4,60 @@ import { setConfigValue, getConfigValue } from "../lib/config-store.js";
4
4
  import { apiClient } from "../lib/api-client.js";
5
5
  import { log } from "../lib/logger.js";
6
6
 
7
+ export async function loginFlow() {
8
+ const apiUrl = getConfigValue("apiUrl") || "https://cheri.heysalad.app";
9
+
10
+ log.blank();
11
+ log.brand("Login to Cheri");
12
+ log.blank();
13
+
14
+ log.info("Step 1: Open this URL in your browser to authenticate:");
15
+ log.blank();
16
+ console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/github`)}`);
17
+ log.blank();
18
+ log.info("Step 2: After login, visit the token page:");
19
+ console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/token?user=YOUR_USERNAME`)}`);
20
+ log.blank();
21
+ log.info("Step 3: Copy your API token and paste it below.");
22
+ log.blank();
23
+
24
+ const { token } = await inquirer.prompt([
25
+ {
26
+ type: "password",
27
+ name: "token",
28
+ message: "Paste your API token:",
29
+ mask: "*",
30
+ },
31
+ ]);
32
+
33
+ if (!token || !token.trim()) {
34
+ throw new Error("No token provided.");
35
+ }
36
+
37
+ setConfigValue("token", token.trim());
38
+
39
+ // Verify token works
40
+ try {
41
+ const me = await apiClient.getMe();
42
+ log.blank();
43
+ log.success(`Logged in as ${chalk.cyan(me.ghLogin || me.userId)}`);
44
+ log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
45
+ log.blank();
46
+ } catch (err) {
47
+ setConfigValue("token", "");
48
+ throw new Error(`Token verification failed: ${err.message}`);
49
+ }
50
+ }
51
+
7
52
  export function registerLoginCommand(program) {
8
53
  program
9
54
  .command("login")
10
55
  .description("Authenticate with Cheri cloud IDE")
11
56
  .action(async () => {
12
- const apiUrl = getConfigValue("apiUrl") || "https://cheri.heysalad.app";
13
-
14
- log.blank();
15
- log.brand("Login to Cheri");
16
- log.blank();
17
-
18
- log.info("Step 1: Open this URL in your browser to authenticate:");
19
- log.blank();
20
- console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/github`)}`);
21
- log.blank();
22
- log.info("Step 2: After login, visit the token page:");
23
- console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/token?user=YOUR_USERNAME`)}`);
24
- log.blank();
25
- log.info("Step 3: Copy your API token and paste it below.");
26
- log.blank();
27
-
28
- const { token } = await inquirer.prompt([
29
- {
30
- type: "password",
31
- name: "token",
32
- message: "Paste your API token:",
33
- mask: "*",
34
- },
35
- ]);
36
-
37
- if (!token || !token.trim()) {
38
- log.error("No token provided.");
39
- process.exit(1);
40
- }
41
-
42
- setConfigValue("token", token.trim());
43
-
44
- // Verify token works
45
57
  try {
46
- const me = await apiClient.getMe();
47
- log.blank();
48
- log.success(`Logged in as ${chalk.cyan(me.ghLogin || me.userId)}`);
49
- log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
50
- log.blank();
58
+ await loginFlow();
51
59
  } catch (err) {
52
- log.error(`Token verification failed: ${err.message}`);
53
- setConfigValue("token", "");
60
+ log.error(err.message);
54
61
  process.exit(1);
55
62
  }
56
63
  });
@@ -4,6 +4,86 @@ import { writeFileSync } from "fs";
4
4
  import { apiClient } from "../lib/api-client.js";
5
5
  import { log } from "../lib/logger.js";
6
6
 
7
+ export async function showMemory(options = {}) {
8
+ const spinner = ora("Fetching memories...").start();
9
+
10
+ try {
11
+ const { memories } = await apiClient.getMemory();
12
+ spinner.stop();
13
+
14
+ log.blank();
15
+ log.brand("AI Memory");
16
+ log.blank();
17
+ log.keyValue("Entries", `${memories.length}`);
18
+
19
+ if (memories.length === 0) {
20
+ log.blank();
21
+ log.dim(" No memories yet. Use 'memory add <content>' to store context.");
22
+ log.blank();
23
+ return;
24
+ }
25
+
26
+ const limit = parseInt(options.limit || "10");
27
+ const recent = memories.slice(-limit);
28
+
29
+ log.keyValue(
30
+ "Last updated",
31
+ memories.length > 0
32
+ ? timeSince(new Date(memories[memories.length - 1].timestamp))
33
+ : "never"
34
+ );
35
+
36
+ log.blank();
37
+ log.dim(" Recent memories:");
38
+
39
+ recent.forEach((entry) => {
40
+ const category = chalk.dim(`(${entry.category})`);
41
+ const content =
42
+ entry.content.length > 60
43
+ ? entry.content.slice(0, 60) + "..."
44
+ : entry.content;
45
+ console.log(` ${chalk.dim("-")} "${content}" ${category}`);
46
+ });
47
+
48
+ log.blank();
49
+ } catch (err) {
50
+ spinner.fail("Failed to fetch memories");
51
+ throw err;
52
+ }
53
+ }
54
+
55
+ export async function addMemory(content, category = "general") {
56
+ const { entry, count } = await apiClient.addMemory(content, category);
57
+ log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
58
+ }
59
+
60
+ export async function clearMemory() {
61
+ await apiClient.clearMemory();
62
+ log.success("All memories cleared.");
63
+ }
64
+
65
+ export async function exportMemory(options = {}) {
66
+ const spinner = ora("Exporting memories...").start();
67
+
68
+ try {
69
+ const { memories } = await apiClient.getMemory();
70
+
71
+ const exportData = {
72
+ version: "0.1.0",
73
+ exportedAt: new Date().toISOString(),
74
+ entryCount: memories.length,
75
+ entries: memories,
76
+ };
77
+
78
+ const outputFile = options.output || `cheri-memory-export.json`;
79
+ writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
80
+ spinner.succeed(`Exported ${memories.length} entries to ${chalk.cyan(outputFile)}`);
81
+ } catch (err) {
82
+ spinner.fail("Failed to export memories");
83
+ throw err;
84
+ }
85
+ }
86
+
7
87
  export function registerMemoryCommand(program) {
8
88
  const memory = program
9
89
  .command("memory")
@@ -14,49 +94,9 @@ export function registerMemoryCommand(program) {
14
94
  .description("Show current memory entries")
15
95
  .option("-l, --limit <n>", "Number of entries to show", "10")
16
96
  .action(async (options) => {
17
- const spinner = ora("Fetching memories...").start();
18
-
19
97
  try {
20
- const { memories } = await apiClient.getMemory();
21
- spinner.stop();
22
-
23
- log.blank();
24
- log.brand("AI Memory");
25
- log.blank();
26
- log.keyValue("Entries", `${memories.length}`);
27
-
28
- if (memories.length === 0) {
29
- log.blank();
30
- log.dim(" No memories yet. Use 'cheri memory add' to store context.");
31
- log.blank();
32
- return;
33
- }
34
-
35
- const limit = parseInt(options.limit);
36
- const recent = memories.slice(-limit);
37
-
38
- log.keyValue(
39
- "Last updated",
40
- memories.length > 0
41
- ? timeSince(new Date(memories[memories.length - 1].timestamp))
42
- : "never"
43
- );
44
-
45
- log.blank();
46
- log.dim(" Recent memories:");
47
-
48
- recent.forEach((entry) => {
49
- const category = chalk.dim(`(${entry.category})`);
50
- const content =
51
- entry.content.length > 60
52
- ? entry.content.slice(0, 60) + "..."
53
- : entry.content;
54
- console.log(` ${chalk.dim("-")} "${content}" ${category}`);
55
- });
56
-
57
- log.blank();
98
+ await showMemory(options);
58
99
  } catch (err) {
59
- spinner.fail("Failed to fetch memories");
60
100
  log.error(err.message);
61
101
  process.exit(1);
62
102
  }
@@ -69,8 +109,7 @@ export function registerMemoryCommand(program) {
69
109
  .option("-c, --category <category>", "Category", "general")
70
110
  .action(async (content, options) => {
71
111
  try {
72
- const { entry, count } = await apiClient.addMemory(content, options.category);
73
- log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
112
+ await addMemory(content, options.category);
74
113
  } catch (err) {
75
114
  log.error(err.message);
76
115
  process.exit(1);
@@ -82,8 +121,7 @@ export function registerMemoryCommand(program) {
82
121
  .description("Clear all memory")
83
122
  .action(async () => {
84
123
  try {
85
- await apiClient.clearMemory();
86
- log.success("All memories cleared.");
124
+ await clearMemory();
87
125
  } catch (err) {
88
126
  log.error(err.message);
89
127
  process.exit(1);
@@ -95,23 +133,9 @@ export function registerMemoryCommand(program) {
95
133
  .description("Export memory to JSON file")
96
134
  .option("-o, --output <file>", "Output file path")
97
135
  .action(async (options) => {
98
- const spinner = ora("Exporting memories...").start();
99
-
100
136
  try {
101
- const { memories } = await apiClient.getMemory();
102
-
103
- const exportData = {
104
- version: "0.1.0",
105
- exportedAt: new Date().toISOString(),
106
- entryCount: memories.length,
107
- entries: memories,
108
- };
109
-
110
- const outputFile = options.output || `cheri-memory-export.json`;
111
- writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
112
- spinner.succeed(`Exported ${memories.length} entries to ${chalk.cyan(outputFile)}`);
137
+ await exportMemory(options);
113
138
  } catch (err) {
114
- spinner.fail("Failed to export memories");
115
139
  log.error(err.message);
116
140
  process.exit(1);
117
141
  }
@@ -4,53 +4,63 @@ import { getConfigValue } from "../lib/config-store.js";
4
4
  import { apiClient } from "../lib/api-client.js";
5
5
  import { log } from "../lib/logger.js";
6
6
 
7
+ export async function showStatus() {
8
+ log.blank();
9
+ log.brand("Status");
10
+
11
+ // Account info
12
+ log.header("Account");
13
+ const spinner = ora("Fetching account info...").start();
14
+
15
+ try {
16
+ const me = await apiClient.getMe();
17
+ spinner.stop();
18
+
19
+ log.keyValue("User", chalk.cyan(me.ghLogin || me.userId));
20
+ log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
21
+ } catch (err) {
22
+ spinner.stop();
23
+ log.keyValue("User", chalk.yellow("Not logged in"));
24
+ log.dim(` Run ${chalk.cyan("cheri login")} to authenticate.`);
25
+ }
26
+
27
+ // Workspaces
28
+ log.header("Workspaces");
29
+ try {
30
+ const { workspaces } = await apiClient.listWorkspaces();
31
+ if (workspaces.length === 0) {
32
+ log.keyValue("Count", "0");
33
+ log.dim(` Run ${chalk.cyan("cheri workspace launch owner/repo")} to create one.`);
34
+ } else {
35
+ log.keyValue("Count", `${workspaces.length}`);
36
+ workspaces.forEach((ws) => {
37
+ const statusIcon = ws.status === "running" ? chalk.green("●") : chalk.dim("○");
38
+ const statusColor = ws.status === "running" ? chalk.green : chalk.dim;
39
+ console.log(` ${statusIcon} ${ws.id} (${ws.repo || ws.id}) — ${statusColor(ws.status)}`);
40
+ });
41
+ }
42
+ } catch {
43
+ log.dim(" Could not fetch workspaces.");
44
+ }
45
+
46
+ // Config info
47
+ log.header("Configuration");
48
+ log.keyValue("API URL", getConfigValue("apiUrl") || "https://cheri.heysalad.app");
49
+ log.keyValue("Config dir", chalk.dim("~/.cheri/"));
50
+
51
+ log.blank();
52
+ }
53
+
7
54
  export function registerStatusCommand(program) {
8
55
  program
9
56
  .command("status")
10
57
  .description("Show account and workspace status")
11
58
  .action(async () => {
12
- log.blank();
13
- log.brand("Status");
14
-
15
- // Account info
16
- log.header("Account");
17
- const spinner = ora("Fetching account info...").start();
18
-
19
59
  try {
20
- const me = await apiClient.getMe();
21
- spinner.stop();
22
-
23
- log.keyValue("User", chalk.cyan(me.ghLogin || me.userId));
24
- log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
60
+ await showStatus();
25
61
  } catch (err) {
26
- spinner.stop();
27
- log.keyValue("User", chalk.yellow("Not logged in"));
28
- log.dim(` Run ${chalk.cyan("cheri login")} to authenticate.`);
29
- }
30
-
31
- // Workspaces
32
- log.header("Workspaces");
33
- try {
34
- const { workspaces } = await apiClient.listWorkspaces();
35
- if (workspaces.length === 0) {
36
- log.keyValue("Count", "0");
37
- log.dim(` Run ${chalk.cyan("cheri workspace launch owner/repo")} to create one.`);
38
- } else {
39
- log.keyValue("Count", `${workspaces.length}`);
40
- workspaces.forEach((ws) => {
41
- const statusColor = ws.status === "running" ? chalk.green : chalk.dim;
42
- console.log(` ${statusColor("●")} ${ws.id} (${ws.repo || ws.id}) — ${statusColor(ws.status)}`);
43
- });
44
- }
45
- } catch {
46
- log.dim(" Could not fetch workspaces.");
62
+ log.error(err.message);
63
+ process.exit(1);
47
64
  }
48
-
49
- // Config info
50
- log.header("Configuration");
51
- log.keyValue("API URL", getConfigValue("apiUrl") || "https://cheri.heysalad.app");
52
- log.keyValue("Config dir", chalk.dim("~/.cheri/"));
53
-
54
- log.blank();
55
65
  });
56
66
  }
@@ -0,0 +1,64 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { apiClient } from "../lib/api-client.js";
4
+ import { log } from "../lib/logger.js";
5
+
6
+ export async function showUsage() {
7
+ log.blank();
8
+ log.brand("Usage");
9
+
10
+ const spinner = ora("Fetching usage data...").start();
11
+
12
+ try {
13
+ const data = await apiClient.getUsage();
14
+ spinner.stop();
15
+
16
+ // Rate limit
17
+ log.header("Rate Limit");
18
+ log.keyValue("Plan", data.plan === "pro" ? chalk.green("Pro") : "Free");
19
+ log.keyValue("Limit", `${data.rateLimit.limit} requests/hour`);
20
+ const remaining = data.rateLimit.remaining;
21
+ const limit = data.rateLimit.limit;
22
+ const remainColor = remaining > limit * 0.5 ? chalk.green : remaining > limit * 0.1 ? chalk.yellow : chalk.red;
23
+ log.keyValue("Remaining", remainColor(`${remaining}`));
24
+ log.keyValue("Resets at", data.rateLimit.resetsAt);
25
+
26
+ // Today's usage
27
+ log.header("Today");
28
+ log.keyValue("Requests", `${data.usage.today.requests}`);
29
+ const endpoints = data.usage.today.endpoints || {};
30
+ if (Object.keys(endpoints).length > 0) {
31
+ for (const [ep, count] of Object.entries(endpoints)) {
32
+ console.log(` ${chalk.dim(ep)} ${chalk.cyan(count)}`);
33
+ }
34
+ }
35
+
36
+ // Summary
37
+ log.header("Summary");
38
+ log.keyValue("Last 7 days", `${data.usage.last7d.requests} requests`);
39
+ log.keyValue("Last 30 days", `${data.usage.last30d.requests} requests`);
40
+ log.keyValue("All time", `${data.summary.totalRequests} requests`);
41
+ if (data.summary.memberSince) {
42
+ log.keyValue("Member since", new Date(data.summary.memberSince).toLocaleDateString());
43
+ }
44
+ } catch (err) {
45
+ spinner.stop();
46
+ log.error(err.message);
47
+ }
48
+
49
+ log.blank();
50
+ }
51
+
52
+ export function registerUsageCommand(program) {
53
+ program
54
+ .command("usage")
55
+ .description("Show API usage and rate limit status")
56
+ .action(async () => {
57
+ try {
58
+ await showUsage();
59
+ } catch (err) {
60
+ log.error(err.message);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ }
@@ -3,6 +3,105 @@ import ora from "ora";
3
3
  import { apiClient } from "../lib/api-client.js";
4
4
  import { log } from "../lib/logger.js";
5
5
 
6
+ export async function launchWorkspace(repo) {
7
+ log.blank();
8
+ log.brand("Launching workspace");
9
+ log.blank();
10
+
11
+ const spinner = ora("Creating workspace...").start();
12
+
13
+ try {
14
+ const { workspace: ws } = await apiClient.createWorkspace(repo);
15
+ spinner.succeed("Workspace is live!");
16
+
17
+ log.blank();
18
+ log.keyValue("ID", chalk.cyan(ws.id));
19
+ log.keyValue("Repo", chalk.cyan(ws.repo));
20
+ log.keyValue("Status", chalk.green(ws.status));
21
+ log.keyValue("URL", chalk.cyan.underline(ws.url));
22
+ log.blank();
23
+ log.dim("Open the URL above in your browser to start coding.");
24
+ log.blank();
25
+ } catch (err) {
26
+ spinner.fail("Failed to launch workspace");
27
+ throw err;
28
+ }
29
+ }
30
+
31
+ export async function stopWorkspace(id) {
32
+ const spinner = ora(`Stopping workspace: ${id}...`).start();
33
+
34
+ try {
35
+ await apiClient.deleteWorkspace(id);
36
+ spinner.succeed(`Workspace '${chalk.cyan(id)}' stopped.`);
37
+ log.dim(" Data preserved. Launch again to resume.");
38
+ } catch (err) {
39
+ spinner.fail("Failed to stop workspace");
40
+ throw err;
41
+ }
42
+ }
43
+
44
+ export async function listWorkspaces() {
45
+ const spinner = ora("Fetching workspaces...").start();
46
+
47
+ try {
48
+ const { workspaces } = await apiClient.listWorkspaces();
49
+ spinner.stop();
50
+
51
+ log.blank();
52
+ log.brand("Workspaces");
53
+ log.blank();
54
+
55
+ if (workspaces.length === 0) {
56
+ log.dim(" No workspaces found. Run 'cheri workspace launch owner/repo' to create one.");
57
+ log.blank();
58
+ return;
59
+ }
60
+
61
+ // Table header
62
+ console.log(
63
+ ` ${chalk.dim("ID".padEnd(24))} ${chalk.dim("REPO".padEnd(24))} ${chalk.dim("STATUS".padEnd(10))} ${chalk.dim("URL")}`
64
+ );
65
+ console.log(chalk.dim(" " + "─".repeat(90)));
66
+
67
+ workspaces.forEach((ws) => {
68
+ const statusIcon =
69
+ ws.status === "running" ? chalk.green("●") : chalk.dim("○");
70
+ const statusColor =
71
+ ws.status === "running" ? chalk.green : chalk.dim;
72
+ console.log(
73
+ ` ${ws.id.padEnd(24)} ${(ws.repo || "").padEnd(24)} ${statusIcon} ${statusColor(ws.status.padEnd(9))} ${chalk.cyan(ws.url || "")}`
74
+ );
75
+ });
76
+
77
+ log.blank();
78
+ } catch (err) {
79
+ spinner.fail("Failed to fetch workspaces");
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ export async function getWorkspaceStatus(id) {
85
+ const spinner = ora("Fetching workspace status...").start();
86
+
87
+ try {
88
+ const { workspace: ws } = await apiClient.getWorkspaceStatus(id);
89
+ spinner.stop();
90
+
91
+ log.blank();
92
+ log.brand(`Workspace: ${ws.id}`);
93
+ log.blank();
94
+ log.keyValue("Status", ws.status === "running" ? chalk.green("● " + ws.status) : chalk.dim("○ " + ws.status));
95
+ log.keyValue("Repo", chalk.cyan(ws.repo || ""));
96
+ log.keyValue("URL", chalk.cyan.underline(ws.url || ""));
97
+ log.keyValue("Created", new Date(ws.createdAt).toLocaleString());
98
+ log.blank();
99
+ } catch (err) {
100
+ spinner.fail("Failed to fetch workspace status");
101
+ throw err;
102
+ }
103
+ }
104
+
6
105
  export function registerWorkspaceCommand(program) {
7
106
  const workspace = program
8
107
  .command("workspace")
@@ -13,26 +112,9 @@ export function registerWorkspaceCommand(program) {
13
112
  .description("Launch a new cloud workspace")
14
113
  .argument("<repo>", "GitHub repository (owner/repo)")
15
114
  .action(async (repo) => {
16
- log.blank();
17
- log.brand("Launching workspace");
18
- log.blank();
19
-
20
- const spinner = ora("Creating workspace...").start();
21
-
22
115
  try {
23
- const { workspace: ws } = await apiClient.createWorkspace(repo);
24
- spinner.succeed("Workspace is live!");
25
-
26
- log.blank();
27
- log.keyValue("ID", chalk.cyan(ws.id));
28
- log.keyValue("Repo", chalk.cyan(ws.repo));
29
- log.keyValue("Status", chalk.green(ws.status));
30
- log.keyValue("URL", chalk.cyan.underline(ws.url));
31
- log.blank();
32
- log.dim("Open the URL above in your browser to start coding.");
33
- log.blank();
116
+ await launchWorkspace(repo);
34
117
  } catch (err) {
35
- spinner.fail("Failed to launch workspace");
36
118
  log.error(err.message);
37
119
  process.exit(1);
38
120
  }
@@ -43,14 +125,9 @@ export function registerWorkspaceCommand(program) {
43
125
  .description("Stop a running workspace")
44
126
  .argument("<id>", "Workspace ID")
45
127
  .action(async (id) => {
46
- const spinner = ora(`Stopping workspace: ${id}...`).start();
47
-
48
128
  try {
49
- await apiClient.deleteWorkspace(id);
50
- spinner.succeed(`Workspace '${chalk.cyan(id)}' stopped.`);
51
- log.dim(" Data preserved. Launch again to resume.");
129
+ await stopWorkspace(id);
52
130
  } catch (err) {
53
- spinner.fail("Failed to stop workspace");
54
131
  log.error(err.message);
55
132
  process.exit(1);
56
133
  }
@@ -60,39 +137,9 @@ export function registerWorkspaceCommand(program) {
60
137
  .command("list")
61
138
  .description("List all workspaces")
62
139
  .action(async () => {
63
- const spinner = ora("Fetching workspaces...").start();
64
-
65
140
  try {
66
- const { workspaces } = await apiClient.listWorkspaces();
67
- spinner.stop();
68
-
69
- log.blank();
70
- log.brand("Workspaces");
71
- log.blank();
72
-
73
- if (workspaces.length === 0) {
74
- log.dim(" No workspaces found. Run 'cheri workspace launch owner/repo' to create one.");
75
- log.blank();
76
- return;
77
- }
78
-
79
- // Table header
80
- console.log(
81
- ` ${chalk.dim("ID".padEnd(24))} ${chalk.dim("REPO".padEnd(24))} ${chalk.dim("STATUS".padEnd(10))} ${chalk.dim("URL")}`
82
- );
83
- console.log(chalk.dim(" " + "-".repeat(90)));
84
-
85
- workspaces.forEach((ws) => {
86
- const statusColor =
87
- ws.status === "running" ? chalk.green : chalk.dim;
88
- console.log(
89
- ` ${ws.id.padEnd(24)} ${(ws.repo || "").padEnd(24)} ${statusColor(ws.status.padEnd(10))} ${chalk.cyan(ws.url || "")}`
90
- );
91
- });
92
-
93
- log.blank();
141
+ await listWorkspaces();
94
142
  } catch (err) {
95
- spinner.fail("Failed to fetch workspaces");
96
143
  log.error(err.message);
97
144
  process.exit(1);
98
145
  }
@@ -103,22 +150,9 @@ export function registerWorkspaceCommand(program) {
103
150
  .description("Get workspace status")
104
151
  .argument("<id>", "Workspace ID")
105
152
  .action(async (id) => {
106
- const spinner = ora("Fetching workspace status...").start();
107
-
108
153
  try {
109
- const { workspace: ws } = await apiClient.getWorkspaceStatus(id);
110
- spinner.stop();
111
-
112
- log.blank();
113
- log.brand(`Workspace: ${ws.id}`);
114
- log.blank();
115
- log.keyValue("Status", ws.status === "running" ? chalk.green(ws.status) : chalk.dim(ws.status));
116
- log.keyValue("Repo", chalk.cyan(ws.repo || ""));
117
- log.keyValue("URL", chalk.cyan.underline(ws.url || ""));
118
- log.keyValue("Created", new Date(ws.createdAt).toLocaleString());
119
- log.blank();
154
+ await getWorkspaceStatus(id);
120
155
  } catch (err) {
121
- spinner.fail("Failed to fetch workspace status");
122
156
  log.error(err.message);
123
157
  process.exit(1);
124
158
  }
@@ -83,4 +83,9 @@ export const apiClient = {
83
83
  async clearMemory() {
84
84
  return request("/api/memory", { method: "DELETE" });
85
85
  },
86
+
87
+ // Usage
88
+ async getUsage() {
89
+ return request("/api/usage");
90
+ },
86
91
  };
package/src/lib/logger.js CHANGED
@@ -36,4 +36,14 @@ export const log = {
36
36
  console.log(chalk.dim(prefix) + " " + item);
37
37
  });
38
38
  },
39
+ banner(version = "0.1.0") {
40
+ console.log();
41
+ console.log(` ${chalk.red("🍒")} ${chalk.red.bold("Cheri")}`);
42
+ console.log(` ${chalk.dim("AI-powered cloud IDE by HeySalad")}`);
43
+ console.log(` ${chalk.dim("v" + version)}`);
44
+ console.log();
45
+ },
46
+ tip(msg) {
47
+ console.log(` ${chalk.blue("tip")} ${chalk.dim(msg)}`);
48
+ },
39
49
  };
package/src/repl.js ADDED
@@ -0,0 +1,217 @@
1
+ import readline from "readline";
2
+ import chalk from "chalk";
3
+ import { log } from "./lib/logger.js";
4
+
5
+ // Import standalone action handlers
6
+ import { launchWorkspace, stopWorkspace, listWorkspaces, getWorkspaceStatus } from "./commands/workspace.js";
7
+ import { showMemory, addMemory, clearMemory, exportMemory } from "./commands/memory.js";
8
+ import { showStatus } from "./commands/status.js";
9
+ import { listConfig, getConfigKey, setConfigKey } from "./commands/config.js";
10
+ import { loginFlow } from "./commands/login.js";
11
+ import { initProject } from "./commands/init.js";
12
+ import { showUsage } from "./commands/usage.js";
13
+
14
+ const COMMANDS = {
15
+ "workspace launch": { args: "<repo>", desc: "Launch a new cloud workspace" },
16
+ "workspace list": { args: "", desc: "List all workspaces" },
17
+ "workspace stop": { args: "<id>", desc: "Stop a running workspace" },
18
+ "workspace status": { args: "<id>", desc: "Get workspace status" },
19
+ "memory show": { args: "", desc: "Show current memory entries" },
20
+ "memory add": { args: "<text>", desc: "Add a memory entry" },
21
+ "memory clear": { args: "", desc: "Clear all memory" },
22
+ "memory export": { args: "", desc: "Export memory to JSON" },
23
+ "status": { args: "", desc: "Show account & workspace status" },
24
+ "usage": { args: "", desc: "Show API usage & rate limits" },
25
+ "config list": { args: "", desc: "Show all configuration" },
26
+ "config get": { args: "<key>", desc: "Get a config value" },
27
+ "config set": { args: "<k> <v>",desc: "Set a config value" },
28
+ "login": { args: "", desc: "Authenticate with Cheri" },
29
+ "init": { args: "", desc: "Initialize a project" },
30
+ "help": { args: "", desc: "Show this help" },
31
+ "clear": { args: "", desc: "Clear the terminal" },
32
+ "exit": { args: "", desc: "Exit Cheri" },
33
+ };
34
+
35
+ function showHelp() {
36
+ log.blank();
37
+ log.brand("Available Commands");
38
+ log.blank();
39
+
40
+ const maxCmd = 20;
41
+ const maxArgs = 10;
42
+
43
+ for (const [cmd, { args, desc }] of Object.entries(COMMANDS)) {
44
+ const cmdStr = chalk.cyan(cmd.padEnd(maxCmd));
45
+ const argsStr = chalk.dim(args.padEnd(maxArgs));
46
+ console.log(` ${cmdStr} ${argsStr} ${desc}`);
47
+ }
48
+
49
+ log.blank();
50
+ log.tip("Type a command above, or 'exit' to quit. Use arrow keys for history.");
51
+ log.blank();
52
+ }
53
+
54
+ async function dispatch(input) {
55
+ const parts = input.split(/\s+/);
56
+ const cmd = parts[0];
57
+ const sub = parts[1];
58
+ const rest = parts.slice(2).join(" ");
59
+
60
+ switch (cmd) {
61
+ case "help":
62
+ showHelp();
63
+ return;
64
+
65
+ case "clear":
66
+ console.clear();
67
+ return;
68
+
69
+ case "exit":
70
+ case "quit":
71
+ console.log(chalk.dim("\nGoodbye! 🍒\n"));
72
+ process.exit(0);
73
+
74
+ case "status":
75
+ await showStatus();
76
+ return;
77
+
78
+ case "usage":
79
+ await showUsage();
80
+ return;
81
+
82
+ case "login":
83
+ await loginFlow();
84
+ return;
85
+
86
+ case "init":
87
+ await initProject();
88
+ return;
89
+
90
+ case "workspace":
91
+ switch (sub) {
92
+ case "launch":
93
+ if (!rest) {
94
+ log.error("Usage: workspace launch <owner/repo>");
95
+ return;
96
+ }
97
+ await launchWorkspace(rest);
98
+ return;
99
+ case "list":
100
+ await listWorkspaces();
101
+ return;
102
+ case "stop":
103
+ if (!rest) {
104
+ log.error("Usage: workspace stop <id>");
105
+ return;
106
+ }
107
+ await stopWorkspace(rest);
108
+ return;
109
+ case "status":
110
+ if (!rest) {
111
+ log.error("Usage: workspace status <id>");
112
+ return;
113
+ }
114
+ await getWorkspaceStatus(rest);
115
+ return;
116
+ default:
117
+ log.error("Usage: workspace [launch|list|stop|status]");
118
+ return;
119
+ }
120
+
121
+ case "memory":
122
+ switch (sub) {
123
+ case "show":
124
+ await showMemory();
125
+ return;
126
+ case "add":
127
+ if (!rest) {
128
+ log.error("Usage: memory add <content>");
129
+ return;
130
+ }
131
+ await addMemory(rest);
132
+ return;
133
+ case "clear":
134
+ await clearMemory();
135
+ return;
136
+ case "export":
137
+ await exportMemory();
138
+ return;
139
+ default:
140
+ log.error("Usage: memory [show|add|clear|export]");
141
+ return;
142
+ }
143
+
144
+ case "config":
145
+ switch (sub) {
146
+ case "list":
147
+ listConfig();
148
+ return;
149
+ case "get":
150
+ if (!rest) {
151
+ log.error("Usage: config get <key>");
152
+ return;
153
+ }
154
+ getConfigKey(rest);
155
+ return;
156
+ case "set": {
157
+ const setParts = rest.split(/\s+/);
158
+ if (setParts.length < 2) {
159
+ log.error("Usage: config set <key> <value>");
160
+ return;
161
+ }
162
+ setConfigKey(setParts[0], setParts.slice(1).join(" "));
163
+ return;
164
+ }
165
+ default:
166
+ log.error("Usage: config [list|get|set]");
167
+ return;
168
+ }
169
+
170
+ default:
171
+ log.warn(`Unknown command: '${cmd}'. Type ${chalk.cyan("help")} for available commands.`);
172
+ }
173
+ }
174
+
175
+ export async function startCommandRepl() {
176
+ log.banner();
177
+ console.log(chalk.dim(" " + "─".repeat(40)));
178
+ log.tip(`Type ${chalk.cyan("help")} for commands, ${chalk.cyan("exit")} to quit.`);
179
+ log.blank();
180
+
181
+ const rl = readline.createInterface({
182
+ input: process.stdin,
183
+ output: process.stdout,
184
+ prompt: chalk.red("🍒 cheri") + chalk.dim(" > "),
185
+ terminal: true,
186
+ });
187
+
188
+ rl.prompt();
189
+
190
+ rl.on("line", async (line) => {
191
+ const input = line.trim();
192
+
193
+ if (!input) {
194
+ rl.prompt();
195
+ return;
196
+ }
197
+
198
+ try {
199
+ await dispatch(input);
200
+ } catch (err) {
201
+ log.error(err.message);
202
+ }
203
+
204
+ rl.prompt();
205
+ });
206
+
207
+ rl.on("close", () => {
208
+ console.log(chalk.dim("\nGoodbye! 🍒\n"));
209
+ process.exit(0);
210
+ });
211
+
212
+ // Handle Ctrl+C gracefully
213
+ rl.on("SIGINT", () => {
214
+ console.log(chalk.dim("\nGoodbye! 🍒\n"));
215
+ process.exit(0);
216
+ });
217
+ }