@heysalad/cheri-cli 0.1.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 ADDED
@@ -0,0 +1,60 @@
1
+ # Cheri CLI
2
+
3
+ AI-powered cloud IDE by [HeySalad](https://heysalad.app). Like Claude Code, but for cloud workspaces.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @heysalad/cheri-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Login to your Cheri account
15
+ cheri login
16
+
17
+ # Check account status
18
+ cheri status
19
+
20
+ # Launch a cloud workspace
21
+ cheri workspace launch owner/repo
22
+
23
+ # List your workspaces
24
+ cheri workspace list
25
+
26
+ # Stop a workspace
27
+ cheri workspace stop
28
+
29
+ # Initialize AI project config
30
+ cheri init
31
+
32
+ # Manage persistent memory
33
+ cheri memory show
34
+ cheri memory add "Always use TypeScript strict mode"
35
+ cheri memory clear
36
+
37
+ # View/update configuration
38
+ cheri config list
39
+ cheri config set apiUrl https://cheri.heysalad.app
40
+ ```
41
+
42
+ ## How it works
43
+
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
48
+
49
+ ## Requirements
50
+
51
+ - Node.js >= 18
52
+
53
+ ## Links
54
+
55
+ - [Cheri Cloud IDE](https://cheri.heysalad.app)
56
+ - [GitHub](https://github.com/Hey-Salad/cheri-cli)
57
+
58
+ ## License
59
+
60
+ MIT - HeySalad
package/bin/cheri.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import { registerInitCommand } from "../src/commands/init.js";
5
+ import { registerLoginCommand } from "../src/commands/login.js";
6
+ import { registerStatusCommand } from "../src/commands/status.js";
7
+ import { registerMemoryCommand } from "../src/commands/memory.js";
8
+ import { registerConfigCommand } from "../src/commands/config.js";
9
+ import { registerWorkspaceCommand } from "../src/commands/workspace.js";
10
+
11
+ program
12
+ .name("cheri")
13
+ .description("Cheri CLI - AI-powered cloud IDE by HeySalad")
14
+ .version("0.1.0");
15
+
16
+ registerLoginCommand(program);
17
+ registerInitCommand(program);
18
+ registerStatusCommand(program);
19
+ registerMemoryCommand(program);
20
+ registerConfigCommand(program);
21
+ registerWorkspaceCommand(program);
22
+
23
+ program.parse(process.argv);
24
+
25
+ // Show help if no command provided
26
+ if (!process.argv.slice(2).length) {
27
+ program.outputHelp();
28
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@heysalad/cheri-cli",
3
+ "version": "0.1.0",
4
+ "description": "Cheri CLI - AI-powered cloud IDE by HeySalad. Like Claude Code, but for cloud workspaces.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cheri": "./bin/cheri.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "start": "node bin/cheri.js",
16
+ "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"
20
+ },
21
+ "keywords": [
22
+ "cloud-ide",
23
+ "ai",
24
+ "code-server",
25
+ "workspace",
26
+ "cli",
27
+ "cheri",
28
+ "heysalad"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/Hey-Salad/cheri-cli.git"
33
+ },
34
+ "homepage": "https://cheri.heysalad.app",
35
+ "author": "HeySalad",
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "dependencies": {
41
+ "chalk": "^5.3.0",
42
+ "commander": "^12.1.0",
43
+ "inquirer": "^9.2.23",
44
+ "ora": "^8.0.1"
45
+ }
46
+ }
@@ -0,0 +1,71 @@
1
+ import chalk from "chalk";
2
+ import { getConfig, getConfigValue, setConfigValue } from "../lib/config-store.js";
3
+ import { log } from "../lib/logger.js";
4
+
5
+ export function registerConfigCommand(program) {
6
+ const config = program
7
+ .command("config")
8
+ .description("Manage CLI configuration");
9
+
10
+ config
11
+ .command("list")
12
+ .description("Show all configuration values")
13
+ .action(() => {
14
+ const cfg = getConfig();
15
+
16
+ log.blank();
17
+ log.brand("Configuration");
18
+ log.blank();
19
+
20
+ printObject(cfg, "");
21
+ log.blank();
22
+ });
23
+
24
+ config
25
+ .command("get <key>")
26
+ .description("Get a configuration value")
27
+ .action((key) => {
28
+ const value = getConfigValue(key);
29
+ if (value === undefined) {
30
+ log.error(`Key '${key}' not found.`);
31
+ process.exit(1);
32
+ }
33
+ if (typeof value === "object") {
34
+ console.log(JSON.stringify(value, null, 2));
35
+ } else {
36
+ console.log(value);
37
+ }
38
+ });
39
+
40
+ config
41
+ .command("set <key> <value>")
42
+ .description("Set a configuration value")
43
+ .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)}`);
54
+ });
55
+ }
56
+
57
+ function printObject(obj, prefix) {
58
+ for (const [key, value] of Object.entries(obj)) {
59
+ const fullKey = prefix ? `${prefix}.${key}` : key;
60
+ if (key === "token" && value) {
61
+ console.log(` ${chalk.dim(fullKey.padEnd(28))} ${chalk.dim("****" + String(value).slice(-8))}`);
62
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
63
+ console.log(` ${chalk.dim(fullKey)}`);
64
+ printObject(value, fullKey);
65
+ } else if (Array.isArray(value)) {
66
+ console.log(` ${chalk.dim(fullKey.padEnd(28))} ${chalk.white(value.join(", "))}`);
67
+ } else {
68
+ console.log(` ${chalk.dim(fullKey.padEnd(28))} ${chalk.white(value)}`);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,168 @@
1
+ import ora from "ora";
2
+ import inquirer from "inquirer";
3
+ import chalk from "chalk";
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
5
+ import { join } from "path";
6
+ import { setConfigValue } from "../lib/config-store.js";
7
+ import { log } from "../lib/logger.js";
8
+
9
+ const AI_DIR = ".ai";
10
+
11
+ const TEMPLATES = {
12
+ "style.md": `# Code Style Guide
13
+
14
+ ## Naming Conventions
15
+ - camelCase for variables and functions
16
+ - PascalCase for components and classes
17
+ - UPPER_SNAKE_CASE for constants
18
+
19
+ ## TypeScript
20
+ - Use strict mode
21
+ - Prefer named exports over default exports
22
+ - Use Zod for all validation schemas
23
+ - Prefer interfaces over type aliases for object shapes
24
+
25
+ ## Formatting
26
+ - 2 space indentation
27
+ - Single quotes for strings
28
+ - Trailing commas in multi-line expressions
29
+ - Max line length: 100 characters
30
+ `,
31
+
32
+ "architecture.md": `# Architecture
33
+
34
+ ## API Layer
35
+ - Use Hono for all API routes
36
+ - File-based routing organized by feature
37
+ - Middleware-first pattern for auth, validation, logging
38
+
39
+ ## Data Layer
40
+ - Repository pattern for all data access
41
+ - Raw SQL queries (no ORMs)
42
+ - Migrations tracked in /migrations
43
+
44
+ ## Frontend
45
+ - React with functional components
46
+ - Server components by default
47
+ - Client components only when interactivity needed
48
+ - Colocate styles with components
49
+
50
+ ## Deployment
51
+ - Edge-first deployment strategy
52
+ - Environment-based configuration
53
+ - Feature flags for staged rollouts
54
+ `,
55
+
56
+ "constraints.md": `# Constraints
57
+
58
+ ## Security
59
+ - No secrets in environment variables at build time
60
+ - All endpoints must have rate limiting
61
+ - Input validation on every API endpoint
62
+ - Parameterized queries only (no string interpolation in SQL)
63
+
64
+ ## Performance
65
+ - Bundle size < 100KB (client-side)
66
+ - API response time < 200ms (p95)
67
+ - No N+1 queries
68
+
69
+ ## Dependencies
70
+ - Minimize third-party dependencies
71
+ - No client-side state management libraries
72
+ - No CSS-in-JS runtime libraries
73
+ - Audit all new dependencies before adding
74
+
75
+ ## Compatibility
76
+ - Support latest 2 major browser versions
77
+ - Node.js >= 20
78
+ - Mobile-first responsive design
79
+ `,
80
+ };
81
+
82
+ export function registerInitCommand(program) {
83
+ program
84
+ .command("init")
85
+ .description("Initialize a new Cheri project with AI constitution")
86
+ .option("-n, --name <name>", "Project name")
87
+ .option("-y, --yes", "Skip prompts and use defaults")
88
+ .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
+ 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();
162
+ } catch (err) {
163
+ spinner.fail("Failed to initialize project");
164
+ log.error(err.message);
165
+ process.exit(1);
166
+ }
167
+ });
168
+ }
@@ -0,0 +1,57 @@
1
+ import chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import { setConfigValue, getConfigValue } from "../lib/config-store.js";
4
+ import { apiClient } from "../lib/api-client.js";
5
+ import { log } from "../lib/logger.js";
6
+
7
+ export function registerLoginCommand(program) {
8
+ program
9
+ .command("login")
10
+ .description("Authenticate with Cheri cloud IDE")
11
+ .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
+ 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();
51
+ } catch (err) {
52
+ log.error(`Token verification failed: ${err.message}`);
53
+ setConfigValue("token", "");
54
+ process.exit(1);
55
+ }
56
+ });
57
+ }
@@ -0,0 +1,127 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { writeFileSync } from "fs";
4
+ import { apiClient } from "../lib/api-client.js";
5
+ import { log } from "../lib/logger.js";
6
+
7
+ export function registerMemoryCommand(program) {
8
+ const memory = program
9
+ .command("memory")
10
+ .description("Manage AI context memory");
11
+
12
+ memory
13
+ .command("show")
14
+ .description("Show current memory entries")
15
+ .option("-l, --limit <n>", "Number of entries to show", "10")
16
+ .action(async (options) => {
17
+ const spinner = ora("Fetching memories...").start();
18
+
19
+ 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();
58
+ } catch (err) {
59
+ spinner.fail("Failed to fetch memories");
60
+ log.error(err.message);
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ memory
66
+ .command("add")
67
+ .description("Add a memory entry")
68
+ .argument("<content>", "Memory content")
69
+ .option("-c, --category <category>", "Category", "general")
70
+ .action(async (content, options) => {
71
+ try {
72
+ const { entry, count } = await apiClient.addMemory(content, options.category);
73
+ log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
74
+ } catch (err) {
75
+ log.error(err.message);
76
+ process.exit(1);
77
+ }
78
+ });
79
+
80
+ memory
81
+ .command("clear")
82
+ .description("Clear all memory")
83
+ .action(async () => {
84
+ try {
85
+ await apiClient.clearMemory();
86
+ log.success("All memories cleared.");
87
+ } catch (err) {
88
+ log.error(err.message);
89
+ process.exit(1);
90
+ }
91
+ });
92
+
93
+ memory
94
+ .command("export")
95
+ .description("Export memory to JSON file")
96
+ .option("-o, --output <file>", "Output file path")
97
+ .action(async (options) => {
98
+ const spinner = ora("Exporting memories...").start();
99
+
100
+ 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)}`);
113
+ } catch (err) {
114
+ spinner.fail("Failed to export memories");
115
+ log.error(err.message);
116
+ process.exit(1);
117
+ }
118
+ });
119
+ }
120
+
121
+ function timeSince(date) {
122
+ const seconds = Math.floor((new Date() - date) / 1000);
123
+ if (seconds < 60) return "just now";
124
+ if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`;
125
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
126
+ return `${Math.floor(seconds / 86400)} days ago`;
127
+ }
@@ -0,0 +1,56 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { getConfigValue } from "../lib/config-store.js";
4
+ import { apiClient } from "../lib/api-client.js";
5
+ import { log } from "../lib/logger.js";
6
+
7
+ export function registerStatusCommand(program) {
8
+ program
9
+ .command("status")
10
+ .description("Show account and workspace status")
11
+ .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
+ 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");
25
+ } 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.");
47
+ }
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
+ });
56
+ }
@@ -0,0 +1,126 @@
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 function registerWorkspaceCommand(program) {
7
+ const workspace = program
8
+ .command("workspace")
9
+ .description("Manage cloud workspaces");
10
+
11
+ workspace
12
+ .command("launch")
13
+ .description("Launch a new cloud workspace")
14
+ .argument("<repo>", "GitHub repository (owner/repo)")
15
+ .action(async (repo) => {
16
+ log.blank();
17
+ log.brand("Launching workspace");
18
+ log.blank();
19
+
20
+ const spinner = ora("Creating workspace...").start();
21
+
22
+ 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();
34
+ } catch (err) {
35
+ spinner.fail("Failed to launch workspace");
36
+ log.error(err.message);
37
+ process.exit(1);
38
+ }
39
+ });
40
+
41
+ workspace
42
+ .command("stop")
43
+ .description("Stop a running workspace")
44
+ .argument("<id>", "Workspace ID")
45
+ .action(async (id) => {
46
+ const spinner = ora(`Stopping workspace: ${id}...`).start();
47
+
48
+ try {
49
+ await apiClient.deleteWorkspace(id);
50
+ spinner.succeed(`Workspace '${chalk.cyan(id)}' stopped.`);
51
+ log.dim(" Data preserved. Launch again to resume.");
52
+ } catch (err) {
53
+ spinner.fail("Failed to stop workspace");
54
+ log.error(err.message);
55
+ process.exit(1);
56
+ }
57
+ });
58
+
59
+ workspace
60
+ .command("list")
61
+ .description("List all workspaces")
62
+ .action(async () => {
63
+ const spinner = ora("Fetching workspaces...").start();
64
+
65
+ 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();
94
+ } catch (err) {
95
+ spinner.fail("Failed to fetch workspaces");
96
+ log.error(err.message);
97
+ process.exit(1);
98
+ }
99
+ });
100
+
101
+ workspace
102
+ .command("status")
103
+ .description("Get workspace status")
104
+ .argument("<id>", "Workspace ID")
105
+ .action(async (id) => {
106
+ const spinner = ora("Fetching workspace status...").start();
107
+
108
+ 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();
120
+ } catch (err) {
121
+ spinner.fail("Failed to fetch workspace status");
122
+ log.error(err.message);
123
+ process.exit(1);
124
+ }
125
+ });
126
+ }
@@ -0,0 +1,86 @@
1
+ import { getConfigValue } from "./config-store.js";
2
+
3
+ function getBaseUrl() {
4
+ return getConfigValue("apiUrl") || "https://cheri.heysalad.app";
5
+ }
6
+
7
+ function getToken() {
8
+ return getConfigValue("token") || "";
9
+ }
10
+
11
+ async function request(path, options = {}) {
12
+ const baseUrl = getBaseUrl();
13
+ const token = getToken();
14
+
15
+ if (!token) {
16
+ throw new Error("Not logged in. Run 'cheri login' first.");
17
+ }
18
+
19
+ const url = `${baseUrl}${path}`;
20
+ const res = await fetch(url, {
21
+ ...options,
22
+ headers: {
23
+ Authorization: `Bearer ${token}`,
24
+ "Content-Type": "application/json",
25
+ ...options.headers,
26
+ },
27
+ });
28
+
29
+ if (!res.ok) {
30
+ const body = await res.text();
31
+ let msg;
32
+ try {
33
+ msg = JSON.parse(body).error || body;
34
+ } catch {
35
+ msg = body;
36
+ }
37
+ throw new Error(`API error (${res.status}): ${msg}`);
38
+ }
39
+
40
+ return res.json();
41
+ }
42
+
43
+ export const apiClient = {
44
+ // Auth
45
+ async getMe() {
46
+ return request("/api/me");
47
+ },
48
+
49
+ // Workspaces
50
+ async listWorkspaces() {
51
+ return request("/api/workspaces");
52
+ },
53
+
54
+ async createWorkspace(repo) {
55
+ return request("/api/workspaces", {
56
+ method: "POST",
57
+ body: JSON.stringify({ repo }),
58
+ });
59
+ },
60
+
61
+ async deleteWorkspace(id) {
62
+ return request(`/api/workspaces/${encodeURIComponent(id)}`, {
63
+ method: "DELETE",
64
+ });
65
+ },
66
+
67
+ async getWorkspaceStatus(id) {
68
+ return request(`/api/workspaces/${encodeURIComponent(id)}/status`);
69
+ },
70
+
71
+ // Memory
72
+ async getMemory() {
73
+ return request("/api/memory");
74
+ },
75
+
76
+ async addMemory(content, category = "general") {
77
+ return request("/api/memory", {
78
+ method: "POST",
79
+ body: JSON.stringify({ content, category }),
80
+ });
81
+ },
82
+
83
+ async clearMemory() {
84
+ return request("/api/memory", { method: "DELETE" });
85
+ },
86
+ };
@@ -0,0 +1,78 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ const CONFIG_DIR = join(homedir(), ".cheri");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+
8
+ function ensureConfigDir() {
9
+ if (!existsSync(CONFIG_DIR)) {
10
+ mkdirSync(CONFIG_DIR, { recursive: true });
11
+ }
12
+ }
13
+
14
+ export function getConfig() {
15
+ ensureConfigDir();
16
+ if (!existsSync(CONFIG_FILE)) {
17
+ return getDefaultConfig();
18
+ }
19
+ try {
20
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
21
+ } catch {
22
+ return getDefaultConfig();
23
+ }
24
+ }
25
+
26
+ export function setConfig(config) {
27
+ ensureConfigDir();
28
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
29
+ }
30
+
31
+ export function getConfigValue(key) {
32
+ const config = getConfig();
33
+ return key.split(".").reduce((obj, k) => obj?.[k], config);
34
+ }
35
+
36
+ export function setConfigValue(key, value) {
37
+ const config = getConfig();
38
+ const keys = key.split(".");
39
+ let current = config;
40
+ for (let i = 0; i < keys.length - 1; i++) {
41
+ if (!current[keys[i]] || typeof current[keys[i]] !== "object") {
42
+ current[keys[i]] = {};
43
+ }
44
+ current = current[keys[i]];
45
+ }
46
+ current[keys[keys.length - 1]] = value;
47
+ setConfig(config);
48
+ }
49
+
50
+ function getDefaultConfig() {
51
+ return {
52
+ apiUrl: "https://cheri.heysalad.app",
53
+ token: "",
54
+ workspace: {
55
+ defaultResources: {
56
+ cpu: 2,
57
+ ram: "4GB",
58
+ disk: "10GB",
59
+ },
60
+ idleTimeout: 600,
61
+ },
62
+ sync: {
63
+ ignore: [
64
+ "node_modules",
65
+ ".git",
66
+ ".next",
67
+ "dist",
68
+ ".env",
69
+ ".env.local",
70
+ ],
71
+ debounce: 300,
72
+ },
73
+ editor: {
74
+ theme: "dark",
75
+ fontSize: 14,
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,39 @@
1
+ import chalk from "chalk";
2
+
3
+ export const log = {
4
+ info(msg) {
5
+ console.log(chalk.blue("info") + " " + msg);
6
+ },
7
+ success(msg) {
8
+ console.log(chalk.green("done") + " " + msg);
9
+ },
10
+ warn(msg) {
11
+ console.log(chalk.yellow("warn") + " " + msg);
12
+ },
13
+ error(msg) {
14
+ console.log(chalk.red("error") + " " + msg);
15
+ },
16
+ dim(msg) {
17
+ console.log(chalk.dim(msg));
18
+ },
19
+ brand(msg) {
20
+ console.log(chalk.red.bold("cheri") + " " + msg);
21
+ },
22
+ blank() {
23
+ console.log();
24
+ },
25
+ header(title) {
26
+ console.log();
27
+ console.log(chalk.bold(title));
28
+ console.log(chalk.dim("─".repeat(title.length)));
29
+ },
30
+ keyValue(key, value) {
31
+ console.log(` ${chalk.dim(key.padEnd(16))} ${value}`);
32
+ },
33
+ tree(items) {
34
+ items.forEach((item, i) => {
35
+ const prefix = i === items.length - 1 ? " └─" : " ├─";
36
+ console.log(chalk.dim(prefix) + " " + item);
37
+ });
38
+ },
39
+ };