ccx-relay 1.7.0 → 1.8.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 +19 -23
- package/bin/ccx-init.js +2 -1
- package/package.json +2 -3
- package/src/gemini.js +71 -4
- package/.env.example +0 -12
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ npm install -g ccx-relay
|
|
|
38
38
|
Run the one-time setup wizard:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
ccx
|
|
41
|
+
ccx-init
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
This will prompt for your Gemini API key (get one at https://aistudio.google.com/apikey), validate it, let you pick a model, and save config to your user directory (`%APPDATA%\ccx\config.json` on Windows, `~/.config/ccx/config.json` on macOS/Linux). Config survives npm upgrades.
|
|
@@ -46,13 +46,13 @@ This will prompt for your Gemini API key (get one at https://aistudio.google.com
|
|
|
46
46
|
Other init commands:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
ccx
|
|
50
|
-
ccx
|
|
49
|
+
ccx-init --show # print current config (key masked)
|
|
50
|
+
ccx-init --reset # wipe config and start over
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### Environment variable override
|
|
54
54
|
|
|
55
|
-
You can skip `ccx
|
|
55
|
+
You can skip `ccx-init` and set env vars directly (useful for CI):
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
export GEMINI_API_KEY=AIza...
|
|
@@ -81,14 +81,14 @@ While the wrapped process is running:
|
|
|
81
81
|
|
|
82
82
|
### Changing the marker
|
|
83
83
|
|
|
84
|
-
Change `CCX_MARKER` via `ccx
|
|
84
|
+
Change `CCX_MARKER` via `ccx-init` or env var (default `;;`). No source changes needed.
|
|
85
85
|
|
|
86
86
|
## Testing it locally
|
|
87
87
|
|
|
88
88
|
1. `npm install`
|
|
89
|
-
2. `npm
|
|
89
|
+
2. `npm test` — runs the unit test suite (`node --test`) covering config loading, the Gemini client, and keystroke handling.
|
|
90
90
|
3. `npm link`
|
|
91
|
-
4.
|
|
91
|
+
4. `ccx-init` — set up your real `GEMINI_API_KEY` (this validates the key and lets you pick a model live).
|
|
92
92
|
5. Sanity-check the relay mechanics with a safe command first, before pointing it at `claude`:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
@@ -97,9 +97,9 @@ Change `CCX_MARKER` via `ccx init` or env var (default `;;`). No source changes
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
You should see a completely normal shell. Type a sentence with a typo, e.g. `pls fix teh bug in server.js;;`,
|
|
100
|
-
then press **Enter**. You should see
|
|
101
|
-
(e.g. `Please fix the bug in server.js`) with nothing submitted yet. Press Enter
|
|
102
|
-
shell command.
|
|
100
|
+
then press **Enter**. You should see a spinner flash briefly (`⠋ Enhancing prompt...`), then `✓ Enhanced`, then
|
|
101
|
+
the line rewritten in place (e.g. `Please fix the bug in server.js`) with nothing submitted yet. Press Enter
|
|
102
|
+
again to run it as a normal shell command.
|
|
103
103
|
|
|
104
104
|
6. Once the mechanics check out, run it against the real target:
|
|
105
105
|
|
|
@@ -109,22 +109,18 @@ Change `CCX_MARKER` via `ccx init` or env var (default `;;`). No source changes
|
|
|
109
109
|
|
|
110
110
|
## Known limitations
|
|
111
111
|
|
|
112
|
-
- **
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
detected and forwarded to the child untouched so navigation still *works* visually, but `ccx`'s internal buffer
|
|
116
|
-
does not move its own "cursor" — it only appends on printable input and pops on backspace. If you arrow back into
|
|
117
|
-
the middle of a line and start editing there, the buffer sent to Gemini (and the backspace count used to erase the
|
|
118
|
-
line) can drift out of sync with what's actually on screen. Worth testing carefully before relying on it for
|
|
119
|
-
heavily-edited multi-line pastes.
|
|
112
|
+
- **Cursor position is tracked, including mid-line edits.** Left/Right/Home/End move an internal cursor that stays
|
|
113
|
+
in sync with inserts, backspace-at-cursor, and both plain-byte and win32-input-mode key encodings — so arrowing
|
|
114
|
+
back into the middle of a line and editing there is expected to work, including for the enhance trigger.
|
|
120
115
|
- **Byte-wise, not codepoint-wise.** Input is processed one byte at a time; multi-byte UTF-8 characters (accents,
|
|
121
116
|
emoji, non-Latin scripts) may not round-trip through the buffer correctly, though they are still forwarded to the
|
|
122
|
-
child untouched when you're not
|
|
123
|
-
- **Status
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
child untouched when you're not triggering enhancement.
|
|
118
|
+
- **Status bar display is best-effort.** `ccx` draws the spinner/result to the last row of the terminal via ANSI
|
|
119
|
+
save/restore-cursor sequences. This works in standard ANSI/VT terminals (including the VS Code integrated
|
|
120
|
+
terminal) but isn't guaranteed on every terminal emulator, and a terminal resize mid-animation could leave a
|
|
121
|
+
stray line if the row count changes between draws.
|
|
126
122
|
- **The marker itself is reserved.** If you genuinely need a prompt to end with `;;` literally, either add a
|
|
127
|
-
trailing space after it or change
|
|
123
|
+
trailing space after it or change the marker to something you don't otherwise type (via `ccx-init` or `CCX_MARKER`).
|
|
128
124
|
|
|
129
125
|
## Windows-specific notes
|
|
130
126
|
|
package/bin/ccx-init.js
CHANGED
|
@@ -72,9 +72,10 @@ async function runWizard() {
|
|
|
72
72
|
const key = await promptHidden(' API key: ');
|
|
73
73
|
if (!key) { console.log('No key entered. Aborting.'); process.exit(1); }
|
|
74
74
|
|
|
75
|
+
const testModel = key.startsWith('gsk_') ? 'llama-3.1-8b-instant' : 'gemini-2.5-flash';
|
|
75
76
|
process.stdout.write(' Testing key... ');
|
|
76
77
|
try {
|
|
77
|
-
await enhance('hello', { geminiApiKey: key, geminiModel:
|
|
78
|
+
await enhance('hello', { geminiApiKey: key, geminiModel: testModel, timeoutSeconds: 10 });
|
|
78
79
|
console.log('\x1b[32m✓ Valid\x1b[0m');
|
|
79
80
|
} catch (err) {
|
|
80
81
|
if (err instanceof RateLimitError) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "ccx-relay",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.0",
|
|
5
5
|
"description": "Transparent PTY wrapper for Claude Code (or any CLI) that lets you refine your prompt with Gemini AI before submitting it, without leaving your terminal session.",
|
|
6
6
|
"main": "bin/ccx.js",
|
|
7
7
|
"bin": {
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
13
13
|
"src",
|
|
14
|
-
"README.md"
|
|
15
|
-
".env.example"
|
|
14
|
+
"README.md"
|
|
16
15
|
],
|
|
17
16
|
"scripts": {
|
|
18
17
|
"test": "node --test test/**/*.test.js",
|
package/src/gemini.js
CHANGED
|
@@ -13,13 +13,25 @@ const SYSTEM_PROMPT =
|
|
|
13
13
|
'points, markdown formatting, quotes, explanations, or any commentary. ' +
|
|
14
14
|
'Output must contain nothing but the rewritten line itself.';
|
|
15
15
|
|
|
16
|
+
function isGroq(apiKey) { return apiKey.startsWith('gsk_'); }
|
|
17
|
+
|
|
16
18
|
function buildAuthHeaders(apiKey) {
|
|
17
|
-
return apiKey
|
|
19
|
+
return isGroq(apiKey)
|
|
18
20
|
? { 'Authorization': `Bearer ${apiKey}` }
|
|
19
21
|
: { 'x-goog-api-key': apiKey };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export async function listModels(apiKey) {
|
|
25
|
+
if (isGroq(apiKey)) {
|
|
26
|
+
return [
|
|
27
|
+
'llama-3.1-8b-instant',
|
|
28
|
+
'llama-3.3-70b-versatile',
|
|
29
|
+
'gemma2-9b-it',
|
|
30
|
+
'llama3-8b-8192',
|
|
31
|
+
'llama-3.1-70b-versatile',
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
const controller = new AbortController();
|
|
24
36
|
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
25
37
|
try {
|
|
@@ -43,7 +55,56 @@ export async function listModels(apiKey) {
|
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
|
|
58
|
+
async function enhanceGroq(text, config, context) {
|
|
59
|
+
const { geminiApiKey: apiKey, geminiModel: model, timeoutSeconds } = config;
|
|
60
|
+
const url = 'https://api.groq.com/openai/v1/chat/completions';
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
let timeoutId;
|
|
63
|
+
|
|
64
|
+
const userContent = context
|
|
65
|
+
? `Recent terminal context:\n${context}\n\nText:\n${text}`
|
|
66
|
+
: text;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
model,
|
|
77
|
+
messages: [
|
|
78
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
79
|
+
{ role: 'user', content: userContent },
|
|
80
|
+
],
|
|
81
|
+
max_tokens: 256,
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
if (response.status === 429) throw new RateLimitError('Quota exceeded');
|
|
87
|
+
else if (response.status === 401 || response.status === 403) throw new AuthError('Invalid key — run ccx-init');
|
|
88
|
+
else if (response.status === 404) throw new ModelError('Model not found — run ccx-init');
|
|
89
|
+
else throw new NetworkError(`API error ${response.status}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
const result = data.choices[0].message.content.trim();
|
|
94
|
+
if (!result) throw new NetworkError('Empty response');
|
|
95
|
+
return result;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err.name === 'AbortError') throw new TimeoutError(`Timed out after ${timeoutSeconds}s`);
|
|
98
|
+
if (err instanceof RateLimitError || err instanceof AuthError ||
|
|
99
|
+
err instanceof ModelError || err instanceof NetworkError ||
|
|
100
|
+
err instanceof TimeoutError) throw err;
|
|
101
|
+
throw new NetworkError('No connection');
|
|
102
|
+
} finally {
|
|
103
|
+
clearTimeout(timeoutId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function enhanceGemini(text, config, context) {
|
|
47
108
|
const { geminiApiKey, geminiModel, timeoutSeconds } = config;
|
|
48
109
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent`;
|
|
49
110
|
const controller = new AbortController();
|
|
@@ -66,8 +127,8 @@ export async function enhance(text, config, context = null) {
|
|
|
66
127
|
if (!response.ok) {
|
|
67
128
|
const body = await response.text();
|
|
68
129
|
if (response.status === 429) throw new RateLimitError('Quota exceeded');
|
|
69
|
-
else if (response.status === 401 || response.status === 403) throw new AuthError('Invalid key — run ccx
|
|
70
|
-
else if (response.status === 404) throw new ModelError('Model not found — run ccx
|
|
130
|
+
else if (response.status === 401 || response.status === 403) throw new AuthError('Invalid key — run ccx-init');
|
|
131
|
+
else if (response.status === 404) throw new ModelError('Model not found — run ccx-init');
|
|
71
132
|
else throw new NetworkError(`API error ${response.status}: ${body.substring(0, 200)}`);
|
|
72
133
|
}
|
|
73
134
|
|
|
@@ -85,3 +146,9 @@ export async function enhance(text, config, context = null) {
|
|
|
85
146
|
clearTimeout(timeoutId);
|
|
86
147
|
}
|
|
87
148
|
}
|
|
149
|
+
|
|
150
|
+
export async function enhance(text, config, context = null) {
|
|
151
|
+
return isGroq(config.geminiApiKey)
|
|
152
|
+
? enhanceGroq(text, config, context)
|
|
153
|
+
: enhanceGemini(text, config, context);
|
|
154
|
+
}
|
package/.env.example
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Google Gemini API key (required).
|
|
2
|
-
# Get one at https://aistudio.google.com/apikey
|
|
3
|
-
GEMINI_API_KEY=
|
|
4
|
-
|
|
5
|
-
# Gemini model to use for prompt rewriting (optional).
|
|
6
|
-
# Default: gemini-3.5-flash
|
|
7
|
-
GEMINI_MODEL=gemini-3.5-flash
|
|
8
|
-
|
|
9
|
-
# Trailing marker that triggers "enhance current line" when you press Enter
|
|
10
|
-
# (optional). Type your prompt, append this marker, then press Enter once to
|
|
11
|
-
# enhance (not submit) - press Enter again to actually submit. Default: ;;
|
|
12
|
-
CCX_MARKER=;;
|