@zhongqian97-code/ecode 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/dist/index.js +286 -0
  4. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zhongqian97-code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # ecode
2
+
3
+ A minimal [Claude Code](https://claude.ai/code) clone — REPL interface with streaming LLM responses and bash tool calling.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @zhongqian-code/ecode
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Set your API key (OpenAI-compatible endpoint)
15
+ export ECODE_API_KEY=sk-...
16
+
17
+ # Start the REPL
18
+ ecode
19
+ ```
20
+
21
+ Type your message, press Enter. Type `exit` to quit.
22
+
23
+ ## Configuration
24
+
25
+ Priority: **env vars > config file > defaults**
26
+
27
+ ### Environment variables
28
+
29
+ | Variable | Default | Description |
30
+ |---|---|---|
31
+ | `ECODE_API_KEY` | *(required)* | API key |
32
+ | `ECODE_BASE_URL` | `https://api.openai.com/v1` | Base URL (any OpenAI-compatible endpoint) |
33
+ | `ECODE_MODEL` | `gpt-4o` | Model name |
34
+
35
+ ### Config file
36
+
37
+ `~/.ecode/config.json`
38
+
39
+ ```json
40
+ {
41
+ "apiKey": "sk-...",
42
+ "baseUrl": "https://api.openai.com/v1",
43
+ "model": "gpt-4o"
44
+ }
45
+ ```
46
+
47
+ ### Using with other providers
48
+
49
+ ```bash
50
+ # DeepSeek
51
+ export ECODE_BASE_URL=https://api.deepseek.com/v1
52
+ export ECODE_API_KEY=sk-...
53
+ export ECODE_MODEL=deepseek-chat
54
+ ecode
55
+
56
+ # Local Ollama
57
+ export ECODE_BASE_URL=http://localhost:11434/v1
58
+ export ECODE_API_KEY=ollama
59
+ export ECODE_MODEL=llama3
60
+ ecode
61
+ ```
62
+
63
+ ## Bash tool calling
64
+
65
+ ecode gives the LLM access to a bash tool. Commands are classified into three tiers:
66
+
67
+ | Tier | Examples | Behavior |
68
+ |---|---|---|
69
+ | **Allow** | `ls`, `cat`, `pwd`, `echo` | Auto-execute, no prompt |
70
+ | **Normal** | `git status`, `npm install` | Single confirmation |
71
+ | **Danger** | `rm -rf`, `sudo`, `chmod` | Double confirmation |
72
+
73
+ ## Requirements
74
+
75
+ - Node.js >= 18
76
+
77
+ ## License
78
+
79
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config.ts
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ var DEFAULTS = {
8
+ baseUrl: "https://api.openai.com/v1",
9
+ apiKey: "",
10
+ model: "gpt-4o"
11
+ };
12
+ function loadConfig() {
13
+ const configPath = join(homedir(), ".ecode", "config.json");
14
+ let fileConfig = {};
15
+ if (existsSync(configPath)) {
16
+ try {
17
+ const raw = readFileSync(configPath, "utf-8");
18
+ fileConfig = JSON.parse(raw);
19
+ } catch {
20
+ }
21
+ }
22
+ return {
23
+ baseUrl: process.env.ECODE_BASE_URL ?? fileConfig.baseUrl ?? DEFAULTS.baseUrl,
24
+ apiKey: process.env.ECODE_API_KEY ?? fileConfig.apiKey ?? DEFAULTS.apiKey,
25
+ model: process.env.ECODE_MODEL ?? fileConfig.model ?? DEFAULTS.model
26
+ };
27
+ }
28
+
29
+ // src/repl.ts
30
+ import * as readline from "readline/promises";
31
+
32
+ // src/safety.ts
33
+ var ALLOWLIST = [
34
+ "ls",
35
+ "cat",
36
+ "pwd",
37
+ "echo",
38
+ "head",
39
+ "tail",
40
+ "wc",
41
+ "date",
42
+ "whoami",
43
+ "which",
44
+ "env",
45
+ "printenv"
46
+ ];
47
+ var DANGER_LIST = [
48
+ "rm -rf",
49
+ "sudo",
50
+ "chmod",
51
+ "chown",
52
+ "mkfs",
53
+ "dd",
54
+ "fdisk",
55
+ "kill",
56
+ "pkill",
57
+ "killall",
58
+ "reboot",
59
+ "shutdown",
60
+ "halt",
61
+ "curl -X DELETE",
62
+ "wget --delete-after"
63
+ ];
64
+ function matchesEntry(cmd, entry) {
65
+ return cmd === entry || cmd.startsWith(entry + " ");
66
+ }
67
+ function classifyCommand(cmd) {
68
+ const trimmed = cmd.trim();
69
+ for (const entry of DANGER_LIST) {
70
+ if (matchesEntry(trimmed, entry)) return "danger";
71
+ }
72
+ for (const entry of ALLOWLIST) {
73
+ if (matchesEntry(trimmed, entry)) return "allow";
74
+ }
75
+ return "normal";
76
+ }
77
+
78
+ // src/llm.ts
79
+ import OpenAI from "openai";
80
+ function createLLMClient(config2) {
81
+ const openai = new OpenAI({
82
+ baseURL: config2.baseUrl,
83
+ apiKey: config2.apiKey
84
+ });
85
+ return {
86
+ stream(messages, tools) {
87
+ return {
88
+ [Symbol.asyncIterator]: async function* () {
89
+ const requestParams = {
90
+ model: config2.model,
91
+ messages,
92
+ stream: true
93
+ };
94
+ if (tools && tools.length > 0) {
95
+ requestParams.tools = tools;
96
+ }
97
+ const response = await openai.chat.completions.create(requestParams);
98
+ const tcAccumulator = /* @__PURE__ */ new Map();
99
+ for await (const chunk of response) {
100
+ const choice = chunk.choices[0];
101
+ if (!choice) continue;
102
+ const deltaToolCalls = choice.delta.tool_calls;
103
+ if (deltaToolCalls) {
104
+ for (const tc of deltaToolCalls) {
105
+ if (!tcAccumulator.has(tc.index)) {
106
+ tcAccumulator.set(tc.index, {
107
+ id: tc.id ?? "",
108
+ name: tc.function?.name ?? "",
109
+ arguments: ""
110
+ });
111
+ }
112
+ const existing = tcAccumulator.get(tc.index);
113
+ if (tc.id) existing.id = tc.id;
114
+ if (tc.function?.name) existing.name = tc.function.name;
115
+ existing.arguments += tc.function?.arguments ?? "";
116
+ }
117
+ }
118
+ const isLast = choice.finish_reason !== null;
119
+ if (isLast) {
120
+ yield {
121
+ text: choice.delta.content ?? "",
122
+ done: true,
123
+ finishReason: choice.finish_reason,
124
+ toolCalls: tcAccumulator.size > 0 ? Array.from(tcAccumulator.values()) : void 0
125
+ };
126
+ } else {
127
+ yield {
128
+ text: choice.delta.content ?? "",
129
+ done: false
130
+ };
131
+ }
132
+ }
133
+ }
134
+ };
135
+ }
136
+ };
137
+ }
138
+
139
+ // src/tools/bash.ts
140
+ import { exec } from "child_process";
141
+ var DEFAULT_TIMEOUT_MS = 3e4;
142
+ function executeBash(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
143
+ return new Promise((resolve) => {
144
+ exec(cmd, { timeout: timeoutMs }, (err, result) => {
145
+ const stdout = result?.stdout ?? "";
146
+ const stderr = result?.stderr ?? "";
147
+ if (err) {
148
+ const exitCode = err.code ?? 1;
149
+ resolve({ stdout, stderr, exitCode });
150
+ } else {
151
+ resolve({ stdout, stderr, exitCode: 0 });
152
+ }
153
+ });
154
+ });
155
+ }
156
+
157
+ // src/repl.ts
158
+ var SKIP_MESSAGE = "Command skipped by user.";
159
+ var BASH_TOOL = {
160
+ type: "function",
161
+ function: {
162
+ name: "bash",
163
+ description: "Execute a shell command and return its output.",
164
+ parameters: {
165
+ type: "object",
166
+ properties: {
167
+ command: { type: "string", description: "The shell command to run." }
168
+ },
169
+ required: ["command"]
170
+ }
171
+ }
172
+ };
173
+ async function handleBashTool(command, deps) {
174
+ const { confirm, print } = deps;
175
+ const cls = classifyCommand(command);
176
+ if (cls === "normal") {
177
+ const ok = await confirm(`Execute command: ${command}
178
+ Proceed? (y/n) `);
179
+ if (!ok) return SKIP_MESSAGE;
180
+ } else if (cls === "danger") {
181
+ print(`\u26A0\uFE0F DANGEROUS COMMAND: ${command}`);
182
+ const first = await confirm("Are you sure? (y/n) ");
183
+ if (!first) return SKIP_MESSAGE;
184
+ const second = await confirm(
185
+ "Confirm again \u2014 this is destructive. Continue? (y/n) "
186
+ );
187
+ if (!second) return SKIP_MESSAGE;
188
+ }
189
+ const result = await deps.executeBash(command);
190
+ let output = "";
191
+ if (result.stdout) output += result.stdout;
192
+ if (result.stderr) output += result.stderr;
193
+ if (result.exitCode !== 0) output += `
194
+ [exit code: ${result.exitCode}]`;
195
+ return output || "(no output)";
196
+ }
197
+ async function startRepl(config2) {
198
+ const rl = readline.createInterface({
199
+ input: process.stdin,
200
+ output: process.stdout
201
+ });
202
+ const llm = createLLMClient(config2);
203
+ const messages = [];
204
+ const print = (text) => process.stdout.write(text);
205
+ const confirm = async (prompt) => {
206
+ const answer = await rl.question(prompt);
207
+ return answer.trim().toLowerCase() === "y";
208
+ };
209
+ const deps = { executeBash, confirm, print };
210
+ console.log('ecode \u2014 type "exit" to quit\n');
211
+ while (true) {
212
+ const input = await rl.question("you: ").catch(() => "exit");
213
+ const trimmed = input.trim();
214
+ if (trimmed === "exit" || trimmed === "quit") break;
215
+ if (!trimmed) continue;
216
+ messages.push({ role: "user", content: trimmed });
217
+ let continueLoop = true;
218
+ while (continueLoop) {
219
+ continueLoop = false;
220
+ process.stdout.write("assistant: ");
221
+ let assistantText = "";
222
+ const toolCalls = [];
223
+ for await (const chunk of llm.stream(messages, [BASH_TOOL])) {
224
+ if (chunk.text) {
225
+ process.stdout.write(chunk.text);
226
+ assistantText += chunk.text;
227
+ }
228
+ if (chunk.done && chunk.toolCalls) {
229
+ toolCalls.push(...chunk.toolCalls);
230
+ }
231
+ }
232
+ if (toolCalls.length > 0) {
233
+ messages.push({
234
+ role: "assistant",
235
+ content: assistantText || null,
236
+ tool_calls: toolCalls.map((tc) => ({
237
+ id: tc.id,
238
+ type: "function",
239
+ function: { name: tc.name, arguments: tc.arguments }
240
+ }))
241
+ });
242
+ for (const tc of toolCalls) {
243
+ if (tc.name === "bash") {
244
+ let args;
245
+ try {
246
+ args = JSON.parse(tc.arguments);
247
+ } catch {
248
+ args = { command: "" };
249
+ }
250
+ print(`
251
+ [bash] ${args.command}
252
+ `);
253
+ const toolResult = await handleBashTool(args.command, deps);
254
+ print(toolResult + "\n");
255
+ messages.push({
256
+ role: "tool",
257
+ tool_call_id: tc.id,
258
+ content: toolResult
259
+ });
260
+ }
261
+ }
262
+ continueLoop = true;
263
+ } else {
264
+ process.stdout.write("\n");
265
+ if (assistantText) {
266
+ messages.push({ role: "assistant", content: assistantText });
267
+ }
268
+ }
269
+ }
270
+ }
271
+ rl.close();
272
+ console.log("\nBye!");
273
+ }
274
+
275
+ // src/index.ts
276
+ var config = loadConfig();
277
+ if (!config.apiKey) {
278
+ console.error(
279
+ "Error: no API key configured.\nSet ECODE_API_KEY or add apiKey to ~/.ecode/config.json"
280
+ );
281
+ process.exit(1);
282
+ }
283
+ startRepl(config).catch((err) => {
284
+ console.error("Fatal error:", err);
285
+ process.exit(1);
286
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@zhongqian97-code/ecode",
3
+ "version": "0.0.1",
4
+ "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
+ "type": "module",
6
+ "author": "zhongqian97-code",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "cli",
10
+ "repl",
11
+ "llm",
12
+ "openai",
13
+ "claude",
14
+ "ai",
15
+ "tool-calling",
16
+ "bash"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/zhongqian97-code/ecode.git"
21
+ },
22
+ "homepage": "https://github.com/zhongqian97-code/ecode#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/zhongqian97-code/ecode/issues"
25
+ },
26
+ "bin": {
27
+ "ecode": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsx src/index.ts",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest"
39
+ },
40
+ "dependencies": {
41
+ "openai": "^4.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^18.0.0",
45
+ "tsup": "^8.0.0",
46
+ "tsx": "^4.0.0",
47
+ "typescript": "^5.0.0",
48
+ "vitest": "^2.0.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }