@zhongqian97-code/ecode 0.2.5 → 0.2.6
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 +106 -13
- package/dist/index.js +43 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
# ecode
|
|
2
2
|
|
|
3
|
-
A minimal [Claude Code](https://claude.ai/code) clone —
|
|
3
|
+
A minimal [Claude Code](https://claude.ai/code) clone — fullscreen TUI with streaming LLM responses, bash tool calling, Emacs keybindings, and a skill system.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install -g @
|
|
8
|
+
npm install -g @zhongqian97-code/ecode
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
# Set your API key (OpenAI-compatible endpoint)
|
|
15
14
|
export ECODE_API_KEY=sk-...
|
|
16
|
-
|
|
17
|
-
# Start the REPL
|
|
18
15
|
ecode
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
Type your message, press Enter. Type `exit` to quit.
|
|
22
|
-
|
|
23
18
|
## Configuration
|
|
24
19
|
|
|
25
20
|
Priority: **env vars > config file > defaults**
|
|
@@ -31,19 +26,33 @@ Priority: **env vars > config file > defaults**
|
|
|
31
26
|
| `ECODE_API_KEY` | *(required)* | API key |
|
|
32
27
|
| `ECODE_BASE_URL` | `https://api.openai.com/v1` | Base URL (any OpenAI-compatible endpoint) |
|
|
33
28
|
| `ECODE_MODEL` | `gpt-4o` | Model name |
|
|
29
|
+
| `ECODE_LOG_DIR` | *(disabled)* | Directory for session logs (JSONL) |
|
|
34
30
|
|
|
35
31
|
### Config file
|
|
36
32
|
|
|
37
|
-
`~/.ecode/config.json`
|
|
33
|
+
`~/.ecode/config.json` — all fields are optional, only set what you need.
|
|
38
34
|
|
|
39
35
|
```json
|
|
40
36
|
{
|
|
41
37
|
"apiKey": "sk-...",
|
|
42
38
|
"baseUrl": "https://api.openai.com/v1",
|
|
43
|
-
"model": "gpt-4o"
|
|
39
|
+
"model": "gpt-4o",
|
|
40
|
+
"logDir": "~/.ecode/logs",
|
|
41
|
+
"contextLimit": 128000,
|
|
42
|
+
"dangerousPatterns": [
|
|
43
|
+
"rm -rf", "sudo", "chmod", "chown",
|
|
44
|
+
"mkfs", "dd", "fdisk",
|
|
45
|
+
"kill", "pkill", "killall",
|
|
46
|
+
"reboot", "shutdown", "halt",
|
|
47
|
+
"curl -X DELETE", "wget --delete-after"
|
|
48
|
+
]
|
|
44
49
|
}
|
|
45
50
|
```
|
|
46
51
|
|
|
52
|
+
**`contextLimit`** — override automatic context window detection (tokens). Useful for unlisted or self-hosted models.
|
|
53
|
+
|
|
54
|
+
**`dangerousPatterns`** — list of command prefixes that require double confirmation. Setting this field replaces the entire default list.
|
|
55
|
+
|
|
47
56
|
### Using with other providers
|
|
48
57
|
|
|
49
58
|
```bash
|
|
@@ -53,6 +62,12 @@ export ECODE_API_KEY=sk-...
|
|
|
53
62
|
export ECODE_MODEL=deepseek-chat
|
|
54
63
|
ecode
|
|
55
64
|
|
|
65
|
+
# Anthropic Claude (via proxy or compatible gateway)
|
|
66
|
+
export ECODE_BASE_URL=https://your-openai-proxy/v1
|
|
67
|
+
export ECODE_API_KEY=sk-ant-...
|
|
68
|
+
export ECODE_MODEL=claude-sonnet-4-6
|
|
69
|
+
ecode
|
|
70
|
+
|
|
56
71
|
# Local Ollama
|
|
57
72
|
export ECODE_BASE_URL=http://localhost:11434/v1
|
|
58
73
|
export ECODE_API_KEY=ollama
|
|
@@ -60,15 +75,93 @@ export ECODE_MODEL=llama3
|
|
|
60
75
|
ecode
|
|
61
76
|
```
|
|
62
77
|
|
|
78
|
+
Context window sizes are pre-configured for common models (GPT-4o, Claude, DeepSeek, o1/o3). Unknown models default to 128K.
|
|
79
|
+
|
|
80
|
+
## Keyboard shortcuts
|
|
81
|
+
|
|
82
|
+
### Submitting and navigation
|
|
83
|
+
|
|
84
|
+
| Key | Action |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `Enter` | Submit message |
|
|
87
|
+
| `Shift+Enter` | Insert newline (multi-line input) |
|
|
88
|
+
| `Tab` | Toggle tool call / thinking expansion |
|
|
89
|
+
| `PageUp` / `PageDown` | Scroll conversation history |
|
|
90
|
+
| `Ctrl+V` | Scroll down (same as PageDown) |
|
|
91
|
+
| `Alt+V` | Scroll up (same as PageUp) |
|
|
92
|
+
| `Ctrl+P` | Previous command (input history) |
|
|
93
|
+
| `Ctrl+N` | Next command (input history) |
|
|
94
|
+
|
|
95
|
+
### Emacs cursor movement
|
|
96
|
+
|
|
97
|
+
| Key | Action |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `Ctrl+A` | Jump to start of line |
|
|
100
|
+
| `Ctrl+E` | Jump to end of line |
|
|
101
|
+
| `Ctrl+B` / `←` | Move left one character |
|
|
102
|
+
| `Ctrl+F` / `→` | Move right one character |
|
|
103
|
+
| `Alt+B` | Move left one word |
|
|
104
|
+
| `Alt+F` | Move right one word |
|
|
105
|
+
|
|
106
|
+
### Emacs editing
|
|
107
|
+
|
|
108
|
+
| Key | Action |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `Backspace` | Delete character before cursor |
|
|
111
|
+
| `Ctrl+D` | Delete character at cursor |
|
|
112
|
+
| `Ctrl+K` | Kill from cursor to end of line |
|
|
113
|
+
| `Ctrl+U` | Kill from start of line to cursor |
|
|
114
|
+
| `Ctrl+W` | Kill word backward |
|
|
115
|
+
|
|
63
116
|
## Bash tool calling
|
|
64
117
|
|
|
65
118
|
ecode gives the LLM access to a bash tool. Commands are classified into three tiers:
|
|
66
119
|
|
|
67
120
|
| Tier | Examples | Behavior |
|
|
68
121
|
|---|---|---|
|
|
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 |
|
|
122
|
+
| **Allow** | `ls`, `cat`, `pwd`, `echo`, `head`, `tail`, `wc`, `date`, `whoami`, `which`, `env` | Auto-execute, no prompt |
|
|
123
|
+
| **Normal** | `git status`, `npm install`, `grep` | Single confirmation |
|
|
124
|
+
| **Danger** | `rm -rf`, `sudo`, `chmod`, `kill`, `reboot` | Double confirmation |
|
|
125
|
+
|
|
126
|
+
The danger list is fully customizable via `dangerousPatterns` in the config file.
|
|
127
|
+
|
|
128
|
+
## Skills (slash commands)
|
|
129
|
+
|
|
130
|
+
Type `/` to see available skills with Tab autocomplete. Skills inject structured instructions into the LLM context.
|
|
131
|
+
|
|
132
|
+
| Skill | Description |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `/plan` | Restate requirements and create a step-by-step implementation plan |
|
|
135
|
+
| `/tdd` | Test-driven development with red-green-refactor loop |
|
|
136
|
+
| `/diagnose` | Disciplined debug loop: reproduce → minimise → hypothesise → instrument → fix |
|
|
137
|
+
| `/grill-me` | Relentless interview to stress-test a plan or design |
|
|
138
|
+
| `/grill-with-docs` | Grilling session that challenges plans against the project's domain model |
|
|
139
|
+
| `/improve-codebase-architecture` | Find deepening opportunities and architectural friction |
|
|
140
|
+
| `/security-review` | Security vulnerability scan of pending changes |
|
|
141
|
+
| `/search-first` | Research-before-coding workflow |
|
|
142
|
+
| `/zoom-out` | Get a higher-level map of the relevant modules and callers |
|
|
143
|
+
| `/to-prd` | Turn current context into a PRD |
|
|
144
|
+
| `/to-issues` | Break a plan into independently-grabbable issues |
|
|
145
|
+
| `/triage` | Move issues through a triage state machine |
|
|
146
|
+
| `/write-a-skill` | Create new agent skills |
|
|
147
|
+
| `/caveman` | Ultra-compressed mode (~75% token reduction) |
|
|
148
|
+
|
|
149
|
+
Run `/setup-matt-pocock-skills` once to configure the issue tracker and triage vocabulary for your repo.
|
|
150
|
+
|
|
151
|
+
## Session logging
|
|
152
|
+
|
|
153
|
+
Enable JSONL session logs to replay or analyze conversations:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# via env var
|
|
157
|
+
export ECODE_LOG_DIR=~/.ecode/logs
|
|
158
|
+
ecode
|
|
159
|
+
|
|
160
|
+
# via config file
|
|
161
|
+
# "logDir": "~/.ecode/logs"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Each session writes a timestamped `.jsonl` file. Each line is a JSON object with `role`, `content`, and `timestamp`.
|
|
72
165
|
|
|
73
166
|
## Requirements
|
|
74
167
|
|
package/dist/index.js
CHANGED
|
@@ -96,7 +96,7 @@ function loadConfig() {
|
|
|
96
96
|
|
|
97
97
|
// src/ui/App.tsx
|
|
98
98
|
import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3, useMemo } from "react";
|
|
99
|
-
import { Box as Box5, useInput as useInput2, useStdout } from "ink";
|
|
99
|
+
import { Box as Box5, useInput as useInput2, useStdout, useStdin } from "ink";
|
|
100
100
|
|
|
101
101
|
// src/llm.ts
|
|
102
102
|
import OpenAI from "openai";
|
|
@@ -870,10 +870,32 @@ function dismiss(state) {
|
|
|
870
870
|
return { ...state, dismissed: true };
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
+
// src/ui/mouseInput.ts
|
|
874
|
+
var SGR_MOUSE_RE = /^\x1b\[<(\d+);\d+;\d+[Mm]/;
|
|
875
|
+
function parseMouseScroll(data) {
|
|
876
|
+
const s = typeof data === "string" ? data : data.toString("binary");
|
|
877
|
+
if (!s) return null;
|
|
878
|
+
const sgrMatch = SGR_MOUSE_RE.exec(s);
|
|
879
|
+
if (sgrMatch) {
|
|
880
|
+
const cb = parseInt(sgrMatch[1], 10);
|
|
881
|
+
if (cb === 64) return { direction: "up" };
|
|
882
|
+
if (cb === 65) return { direction: "down" };
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
if (s.length >= 6 && s.charCodeAt(0) === 27 && s[1] === "[" && s[2] === "M") {
|
|
886
|
+
const buttonByte = s.charCodeAt(3);
|
|
887
|
+
if (buttonByte === 96) return { direction: "up" };
|
|
888
|
+
if (buttonByte === 97) return { direction: "down" };
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
|
|
873
894
|
// src/ui/App.tsx
|
|
874
895
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
875
896
|
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2, llmClient }) {
|
|
876
897
|
const { stdout } = useStdout();
|
|
898
|
+
const { stdin } = useStdin();
|
|
877
899
|
const historyMaxHeight = Math.max(5, (stdout?.rows ?? 24) - 4);
|
|
878
900
|
const [messages, setMessages] = useState3([]);
|
|
879
901
|
const [status, setStatus] = useState3("idle");
|
|
@@ -899,6 +921,8 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
899
921
|
0
|
|
900
922
|
);
|
|
901
923
|
}, [messages, expandTools, stdout?.columns]);
|
|
924
|
+
const totalLinesRef = useRef2(totalLines);
|
|
925
|
+
totalLinesRef.current = totalLines;
|
|
902
926
|
const pendingConfirmRef = useRef2(null);
|
|
903
927
|
const llmRef = useRef2(llmClient ?? createLLMClient(config2));
|
|
904
928
|
const inputRef = useRef2(null);
|
|
@@ -927,6 +951,24 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
927
951
|
}
|
|
928
952
|
loggedCountRef.current = messages.length;
|
|
929
953
|
}, [messages]);
|
|
954
|
+
useEffect3(() => {
|
|
955
|
+
if (!stdin || !stdout) return;
|
|
956
|
+
stdout.write("\x1B[?1000h\x1B[?1006h");
|
|
957
|
+
const onMouseData = (data) => {
|
|
958
|
+
const event = parseMouseScroll(data);
|
|
959
|
+
if (!event) return;
|
|
960
|
+
if (event.direction === "up") {
|
|
961
|
+
setScrollOffset((prev) => Math.min(prev + 3, Math.max(0, totalLinesRef.current - 1)));
|
|
962
|
+
} else {
|
|
963
|
+
setScrollOffset((prev) => Math.max(0, prev - 3));
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
stdin.on("data", onMouseData);
|
|
967
|
+
return () => {
|
|
968
|
+
stdout.write("\x1B[?1000l\x1B[?1006l");
|
|
969
|
+
stdin.off("data", onMouseData);
|
|
970
|
+
};
|
|
971
|
+
}, [stdin, stdout]);
|
|
930
972
|
useInput2((input, key) => {
|
|
931
973
|
const skillList = registry2?.list() ?? [];
|
|
932
974
|
const suggestions = computeSuggestions(skillList, acState);
|