@heysalad/cheri-cli 0.3.0 → 0.4.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.
@@ -0,0 +1,73 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { resolve } from "path";
4
+
5
+ export const readFile = {
6
+ name: "read_file",
7
+ description: "Read the contents of a file at the given path. Returns the file contents as a string.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ path: { type: "string", description: "Absolute or relative file path to read" },
12
+ },
13
+ required: ["path"],
14
+ },
15
+ handler: async ({ path }) => {
16
+ const resolved = resolve(path);
17
+ if (!existsSync(resolved)) {
18
+ return { error: `File not found: ${resolved}` };
19
+ }
20
+ const content = readFileSync(resolved, "utf-8");
21
+ return { path: resolved, content, lines: content.split("\n").length };
22
+ },
23
+ };
24
+
25
+ export const writeFile = {
26
+ name: "write_file",
27
+ description: "Create or overwrite a file with the given content. Creates parent directories if needed.",
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ path: { type: "string", description: "Absolute or relative file path to write" },
32
+ content: { type: "string", description: "The content to write to the file" },
33
+ },
34
+ required: ["path", "content"],
35
+ },
36
+ handler: async ({ path, content }) => {
37
+ const resolved = resolve(path);
38
+ const dir = dirname(resolved);
39
+ if (!existsSync(dir)) {
40
+ mkdirSync(dir, { recursive: true });
41
+ }
42
+ writeFileSync(resolved, content, "utf-8");
43
+ return { path: resolved, bytesWritten: Buffer.byteLength(content, "utf-8") };
44
+ },
45
+ };
46
+
47
+ export const editFile = {
48
+ name: "edit_file",
49
+ description: "Edit a file by replacing an exact string match with new content. The old_string must match exactly (including whitespace and indentation).",
50
+ parameters: {
51
+ type: "object",
52
+ properties: {
53
+ path: { type: "string", description: "Absolute or relative file path to edit" },
54
+ old_string: { type: "string", description: "The exact string to find and replace" },
55
+ new_string: { type: "string", description: "The replacement string" },
56
+ },
57
+ required: ["path", "old_string", "new_string"],
58
+ },
59
+ handler: async ({ path, old_string, new_string }) => {
60
+ const resolved = resolve(path);
61
+ if (!existsSync(resolved)) {
62
+ return { error: `File not found: ${resolved}` };
63
+ }
64
+ const content = readFileSync(resolved, "utf-8");
65
+ if (!content.includes(old_string)) {
66
+ return { error: "old_string not found in file. Make sure it matches exactly, including whitespace." };
67
+ }
68
+ const count = content.split(old_string).length - 1;
69
+ const newContent = content.replace(old_string, new_string);
70
+ writeFileSync(resolved, newContent, "utf-8");
71
+ return { path: resolved, replacements: 1, totalOccurrences: count };
72
+ },
73
+ };
@@ -0,0 +1,32 @@
1
+ import { readFile, writeFile, editFile } from "./file-tools.js";
2
+ import { runCommand } from "./command-tools.js";
3
+ import { searchFiles, searchContent, listDirectory } from "./search-tools.js";
4
+
5
+ const tools = [readFile, writeFile, editFile, runCommand, searchFiles, searchContent, listDirectory];
6
+
7
+ const toolMap = new Map(tools.map((t) => [t.name, t]));
8
+
9
+ export function getToolDefinitions() {
10
+ return tools.map(({ name, description, parameters }) => ({ name, description, parameters }));
11
+ }
12
+
13
+ export function getTool(name) {
14
+ return toolMap.get(name);
15
+ }
16
+
17
+ export async function executeTool(name, input) {
18
+ const tool = toolMap.get(name);
19
+ if (!tool) {
20
+ return { error: `Unknown tool: ${name}` };
21
+ }
22
+ try {
23
+ return await tool.handler(input);
24
+ } catch (err) {
25
+ return { error: err.message };
26
+ }
27
+ }
28
+
29
+ export function requiresConfirmation(name) {
30
+ const tool = toolMap.get(name);
31
+ return tool?.requiresConfirmation === true;
32
+ }
@@ -0,0 +1,95 @@
1
+ import { readdirSync, statSync, existsSync } from "fs";
2
+ import { resolve, join, relative } from "path";
3
+ import { execSync } from "child_process";
4
+
5
+ export const searchFiles = {
6
+ name: "search_files",
7
+ description: "Search for files matching a glob/name pattern. Returns matching file paths.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ pattern: { type: "string", description: "File name pattern to search for (e.g., '*.js', 'config*', 'test')" },
12
+ path: { type: "string", description: "Directory to search in (defaults to current directory)" },
13
+ },
14
+ required: ["pattern"],
15
+ },
16
+ handler: async ({ pattern, path }) => {
17
+ const dir = resolve(path || ".");
18
+ try {
19
+ const result = execSync(`find ${JSON.stringify(dir)} -name ${JSON.stringify(pattern)} -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -50`, {
20
+ encoding: "utf-8",
21
+ timeout: 10_000,
22
+ });
23
+ const files = result.trim().split("\n").filter(Boolean);
24
+ return { pattern, searchPath: dir, matches: files, count: files.length };
25
+ } catch {
26
+ return { pattern, searchPath: dir, matches: [], count: 0 };
27
+ }
28
+ },
29
+ };
30
+
31
+ export const searchContent = {
32
+ name: "search_content",
33
+ description: "Search for a text pattern inside files (like grep). Returns matching lines with file paths and line numbers.",
34
+ parameters: {
35
+ type: "object",
36
+ properties: {
37
+ pattern: { type: "string", description: "Text or regex pattern to search for" },
38
+ path: { type: "string", description: "Directory to search in (defaults to current directory)" },
39
+ include: { type: "string", description: "File glob to filter (e.g., '*.js')" },
40
+ },
41
+ required: ["pattern"],
42
+ },
43
+ handler: async ({ pattern, path, include }) => {
44
+ const dir = resolve(path || ".");
45
+ try {
46
+ let cmd = `grep -rn --include='${include || "*"}' ${JSON.stringify(pattern)} ${JSON.stringify(dir)} 2>/dev/null | head -50`;
47
+ const result = execSync(cmd, { encoding: "utf-8", timeout: 10_000 });
48
+ const lines = result.trim().split("\n").filter(Boolean);
49
+ const matches = lines.map((line) => {
50
+ const match = line.match(/^(.+?):(\d+):(.*)$/);
51
+ if (match) return { file: match[1], line: parseInt(match[2]), content: match[3].trim() };
52
+ return { raw: line };
53
+ });
54
+ return { pattern, searchPath: dir, matches, count: matches.length };
55
+ } catch {
56
+ return { pattern, searchPath: dir, matches: [], count: 0 };
57
+ }
58
+ },
59
+ };
60
+
61
+ export const listDirectory = {
62
+ name: "list_directory",
63
+ description: "List files and directories at the given path. Shows names, types, and sizes.",
64
+ parameters: {
65
+ type: "object",
66
+ properties: {
67
+ path: { type: "string", description: "Directory path to list (defaults to current directory)" },
68
+ },
69
+ required: [],
70
+ },
71
+ handler: async ({ path }) => {
72
+ const dir = resolve(path || ".");
73
+ if (!existsSync(dir)) {
74
+ return { error: `Directory not found: ${dir}` };
75
+ }
76
+ try {
77
+ const entries = readdirSync(dir).map((name) => {
78
+ try {
79
+ const fullPath = join(dir, name);
80
+ const stat = statSync(fullPath);
81
+ return {
82
+ name,
83
+ type: stat.isDirectory() ? "directory" : "file",
84
+ size: stat.isDirectory() ? undefined : stat.size,
85
+ };
86
+ } catch {
87
+ return { name, type: "unknown" };
88
+ }
89
+ });
90
+ return { path: dir, entries, count: entries.length };
91
+ } catch (err) {
92
+ return { error: err.message };
93
+ }
94
+ },
95
+ };
package/src/repl.js CHANGED
@@ -9,7 +9,7 @@ import { showStatus } from "./commands/status.js";
9
9
  import { listConfig, getConfigKey, setConfigKey } from "./commands/config.js";
10
10
  import { loginFlow } from "./commands/login.js";
11
11
  import { initProject } from "./commands/init.js";
12
- import { showUsage } from "./commands/usage.js";
12
+ import { runAgent } from "./commands/agent.js";
13
13
 
14
14
  const COMMANDS = {
15
15
  "workspace launch": { args: "<repo>", desc: "Launch a new cloud workspace" },
@@ -21,12 +21,13 @@ const COMMANDS = {
21
21
  "memory clear": { args: "", desc: "Clear all memory" },
22
22
  "memory export": { args: "", desc: "Export memory to JSON" },
23
23
  "status": { args: "", desc: "Show account & workspace status" },
24
- "usage": { args: "", desc: "Show API usage & rate limits" },
25
24
  "config list": { args: "", desc: "Show all configuration" },
26
25
  "config get": { args: "<key>", desc: "Get a config value" },
27
26
  "config set": { args: "<k> <v>",desc: "Set a config value" },
28
27
  "login": { args: "", desc: "Authenticate with Cheri" },
29
28
  "init": { args: "", desc: "Initialize a project" },
29
+ "chat": { args: "", desc: "Start AI coding session" },
30
+ "agent": { args: "<request>", desc: "AI agent - natural language command" },
30
31
  "help": { args: "", desc: "Show this help" },
31
32
  "clear": { args: "", desc: "Clear the terminal" },
32
33
  "exit": { args: "", desc: "Exit Cheri" },
@@ -75,10 +76,6 @@ async function dispatch(input) {
75
76
  await showStatus();
76
77
  return;
77
78
 
78
- case "usage":
79
- await showUsage();
80
- return;
81
-
82
79
  case "login":
83
80
  await loginFlow();
84
81
  return;
@@ -87,6 +84,25 @@ async function dispatch(input) {
87
84
  await initProject();
88
85
  return;
89
86
 
87
+ case "chat": {
88
+ const { startRepl: startChatRepl } = await import("./lib/repl.js");
89
+ await startChatRepl({
90
+ provider: sub || undefined,
91
+ model: rest || undefined,
92
+ });
93
+ return;
94
+ }
95
+
96
+ case "agent": {
97
+ const agentInput = parts.slice(1).join(" ");
98
+ if (!agentInput) {
99
+ log.error("Usage: agent <request>");
100
+ return;
101
+ }
102
+ await runAgent(agentInput);
103
+ return;
104
+ }
105
+
90
106
  case "workspace":
91
107
  switch (sub) {
92
108
  case "launch":
@@ -209,7 +225,7 @@ export async function startCommandRepl() {
209
225
  process.exit(0);
210
226
  });
211
227
 
212
- // Handle Ctrl+C gracefully
228
+ // Handle Ctrl+C gracefully — don't crash, just exit
213
229
  rl.on("SIGINT", () => {
214
230
  console.log(chalk.dim("\nGoodbye! 🍒\n"));
215
231
  process.exit(0);
@@ -1,64 +0,0 @@
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
- }