@heysalad/cheri-cli 0.2.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,12 +7,12 @@ 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 { registerChatCommand } from "../src/commands/chat.js";
10
+ import { registerUsageCommand } from "../src/commands/usage.js";
11
11
 
12
12
  program
13
13
  .name("cheri")
14
14
  .description("Cheri CLI - AI-powered cloud IDE by HeySalad")
15
- .version("0.2.0");
15
+ .version("0.1.0");
16
16
 
17
17
  registerLoginCommand(program);
18
18
  registerInitCommand(program);
@@ -20,12 +20,12 @@ registerStatusCommand(program);
20
20
  registerMemoryCommand(program);
21
21
  registerConfigCommand(program);
22
22
  registerWorkspaceCommand(program);
23
- registerChatCommand(program);
23
+ registerUsageCommand(program);
24
24
 
25
- // If no args, launch interactive REPL
25
+ // If no args, launch interactive command REPL
26
26
  if (!process.argv.slice(2).length) {
27
- const { startRepl } = await import("../src/lib/repl.js");
28
- await startRepl();
27
+ const { startCommandRepl } = await import("../src/repl.js");
28
+ await startCommandRepl();
29
29
  } else {
30
30
  program.parse(process.argv);
31
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heysalad/cheri-cli",
3
- "version": "0.2.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,23 +28,18 @@
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": {
38
37
  "node": ">=18"
39
38
  },
40
39
  "dependencies": {
41
- "@anthropic-ai/sdk": "^0.74.0",
42
- "@google/generative-ai": "^0.24.1",
43
40
  "chalk": "^5.3.0",
44
41
  "commander": "^12.1.0",
45
42
  "inquirer": "^9.2.23",
46
- "marked": "^15.0.12",
47
- "marked-terminal": "^7.3.0",
48
- "openai": "^6.22.0",
49
43
  "ora": "^8.0.1"
50
44
  }
51
45
  }
@@ -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
  });