ccx-relay 1.9.0 → 2.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/bin/ccx-init.js +164 -70
- package/bin/ccx.js +16 -8
- package/package.json +1 -1
- package/src/gemini.js +4 -4
- package/src/input.js +5 -1
package/bin/ccx-init.js
CHANGED
|
@@ -7,128 +7,222 @@ import { enhance, listModels, RateLimitError } from '../src/gemini.js';
|
|
|
7
7
|
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// ── ANSI ──────────────────────────────────────────────────────────────────────
|
|
11
|
+
const c = {
|
|
12
|
+
reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
|
|
13
|
+
green: '\x1b[32m', red: '\x1b[31m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
14
|
+
};
|
|
15
|
+
const s = {
|
|
16
|
+
active: `${c.cyan}◆${c.reset}`, done: `${c.gray}◇${c.reset}`,
|
|
17
|
+
pipe: `${c.gray}│${c.reset}`, end: `${c.gray}└${c.reset}`,
|
|
18
|
+
ok: `${c.green}✓${c.reset}`, err: `${c.red}✗${c.reset}`,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
22
|
+
function banner() {
|
|
23
|
+
const title = ' ccx-relay · prompt relay setup ';
|
|
24
|
+
const bar = '─'.repeat(title.length);
|
|
25
|
+
console.log(`\n ╭${bar}╮`);
|
|
26
|
+
console.log(` │${title}│`);
|
|
27
|
+
console.log(` ╰${bar}╯\n`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Primitives ────────────────────────────────────────────────────────────────
|
|
31
|
+
const pipe = () => console.log(s.pipe);
|
|
32
|
+
const info = (m) => console.log(`${s.pipe} ${c.dim}${m}${c.reset}`);
|
|
33
|
+
const good = (m) => console.log(`${s.pipe} ${s.ok} ${m}`);
|
|
34
|
+
const bad = (m) => console.log(`${s.pipe} ${s.err} ${c.red}${m}${c.reset}`);
|
|
35
|
+
const head = (label) => console.log(`${s.active} ${c.bold}${label}${c.reset}`);
|
|
36
|
+
|
|
37
|
+
// ── Hidden input (●●● masking) ────────────────────────────────────────────────
|
|
38
|
+
async function promptHidden() {
|
|
11
39
|
return new Promise(resolve => {
|
|
12
|
-
process.stdout.write(
|
|
40
|
+
process.stdout.write(`${s.pipe} `);
|
|
13
41
|
const rl = createInterface({ input: process.stdin, output: null, terminal: false });
|
|
14
42
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
15
43
|
let input = '';
|
|
16
44
|
process.stdin.on('data', function onData(buf) {
|
|
17
45
|
for (let i = 0; i < buf.length; i++) {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
46
|
+
const b = buf[i];
|
|
47
|
+
if (b === 0x0d || b === 0x0a) {
|
|
20
48
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
21
49
|
process.stdout.write('\n');
|
|
22
50
|
process.stdin.removeListener('data', onData);
|
|
23
51
|
rl.close();
|
|
24
52
|
resolve(input);
|
|
25
53
|
return;
|
|
26
|
-
} else if (
|
|
54
|
+
} else if (b === 0x7f || b === 0x08) {
|
|
27
55
|
if (input.length > 0) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
|
|
28
|
-
} else if (
|
|
56
|
+
} else if (b === 0x03) {
|
|
29
57
|
process.exit(0);
|
|
30
|
-
} else if (
|
|
31
|
-
input += String.fromCharCode(
|
|
32
|
-
process.stdout.write(
|
|
58
|
+
} else if (b >= 0x20) {
|
|
59
|
+
input += String.fromCharCode(b);
|
|
60
|
+
process.stdout.write(`${c.dim}●${c.reset}`);
|
|
33
61
|
}
|
|
34
62
|
}
|
|
35
63
|
});
|
|
36
64
|
});
|
|
37
65
|
}
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
// ── Plain prompt ──────────────────────────────────────────────────────────────
|
|
68
|
+
async function ask(question, defaultVal = '') {
|
|
69
|
+
return new Promise(resolve => {
|
|
70
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
71
|
+
rl.question(question, answer => {
|
|
72
|
+
rl.close();
|
|
73
|
+
resolve(answer.trim() || defaultVal);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
47
76
|
}
|
|
48
77
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
// ── Spinner ───────────────────────────────────────────────────────────────────
|
|
79
|
+
const FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
80
|
+
function spin(msg) {
|
|
81
|
+
let f = 0;
|
|
82
|
+
process.stdout.write(`${s.pipe} `);
|
|
83
|
+
const id = setInterval(() => {
|
|
84
|
+
process.stdout.write(`\r${s.pipe} ${c.dim}${FRAMES[f++ % FRAMES.length]}${c.reset} ${msg}`);
|
|
85
|
+
}, 80);
|
|
86
|
+
return () => { clearInterval(id); process.stdout.write('\r\x1b[2K'); };
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
89
|
+
// ── Model metadata ────────────────────────────────────────────────────────────
|
|
90
|
+
const MODEL_META = {
|
|
91
|
+
'llama-3.1-8b-instant': { note: 'fastest', quota: '14,400/day' },
|
|
92
|
+
'llama-3.3-70b-versatile': { note: 'smarter', quota: ' 1,000/day' },
|
|
93
|
+
'gemma2-9b-it': { note: 'balanced', quota: '14,400/day' },
|
|
94
|
+
'llama3-8b-8192': { note: 'classic', quota: '14,400/day' },
|
|
95
|
+
'llama-3.1-70b-versatile': { note: 'large', quota: ' 1,000/day' },
|
|
96
|
+
'gemini-2.5-flash': { note: 'latest', quota: 'limited' },
|
|
97
|
+
'gemini-2.0-flash-lite': { note: 'fast', quota: 'limited' },
|
|
98
|
+
'gemini-1.5-flash': { note: 'stable', quota: '1,500/day' },
|
|
99
|
+
'gemini-2.5-pro': { note: 'powerful', quota: 'limited' },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function modelRow(m, i, isDefault) {
|
|
103
|
+
const meta = MODEL_META[m] || { note: '', quota: '' };
|
|
104
|
+
const num = `${i + 1}`.padStart(2);
|
|
105
|
+
const name = m.padEnd(34);
|
|
106
|
+
const note = meta.note.padEnd(10);
|
|
107
|
+
const tag = isDefault ? ` ${c.dim}← default${c.reset}` : '';
|
|
108
|
+
return `${s.pipe} ${c.dim}${num}${c.reset} ${name}${c.dim}${note} ${meta.quota}${c.reset}${tag}`;
|
|
67
109
|
}
|
|
68
110
|
|
|
111
|
+
// ── Wizard ────────────────────────────────────────────────────────────────────
|
|
69
112
|
async function runWizard() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
banner();
|
|
114
|
+
|
|
115
|
+
// ── Key ──
|
|
116
|
+
head('API key');
|
|
117
|
+
info('Groq (gsk_...) → 14,400 req/day free ← recommended');
|
|
118
|
+
info('Gemini (AIza...) → limited free tier');
|
|
119
|
+
pipe();
|
|
120
|
+
|
|
121
|
+
const key = await promptHidden();
|
|
122
|
+
if (!key) { bad('No key entered.'); process.exit(1); }
|
|
123
|
+
const provider = key.startsWith('gsk_') ? 'Groq' : 'Gemini';
|
|
124
|
+
pipe();
|
|
125
|
+
|
|
126
|
+
// ── Validate ──
|
|
127
|
+
head(`Validating ${provider} key`);
|
|
128
|
+
const stopValidate = spin('Connecting...');
|
|
75
129
|
const testModel = key.startsWith('gsk_') ? 'llama-3.1-8b-instant' : 'gemini-2.5-flash';
|
|
76
|
-
process.stdout.write(' Testing key... ');
|
|
77
130
|
try {
|
|
78
131
|
await enhance('hello', { geminiApiKey: key, geminiModel: testModel, timeoutSeconds: 10 });
|
|
79
|
-
|
|
132
|
+
stopValidate();
|
|
133
|
+
good(`${provider} key valid`);
|
|
80
134
|
} catch (err) {
|
|
81
135
|
if (err instanceof RateLimitError) {
|
|
82
|
-
|
|
136
|
+
stopValidate();
|
|
137
|
+
good(`${provider} key valid ${c.dim}(quota hit — authenticated)${c.reset}`);
|
|
83
138
|
} else {
|
|
84
|
-
|
|
139
|
+
stopValidate();
|
|
140
|
+
bad(`Invalid key — ${err.message}`);
|
|
141
|
+
console.log(`${s.end} Run ${c.bold}ccx-init${c.reset} again with a valid key.\n`);
|
|
85
142
|
process.exit(1);
|
|
86
143
|
}
|
|
87
144
|
}
|
|
145
|
+
pipe();
|
|
88
146
|
|
|
89
|
-
//
|
|
147
|
+
// ── Models ──
|
|
148
|
+
head('Model');
|
|
149
|
+
const stopFetch = spin('Fetching available models...');
|
|
90
150
|
let models;
|
|
91
|
-
process.stdout.write(' Fetching available models... ');
|
|
92
151
|
try {
|
|
93
152
|
const all = await listModels(key);
|
|
94
|
-
models = all.slice(0,
|
|
95
|
-
if (models.length
|
|
96
|
-
|
|
153
|
+
models = all.slice(0, 8);
|
|
154
|
+
if (!models.length) throw new Error('empty');
|
|
155
|
+
stopFetch();
|
|
97
156
|
} catch (_) {
|
|
157
|
+
stopFetch();
|
|
98
158
|
models = key.startsWith('gsk_')
|
|
99
|
-
? ['llama-3.1-8b-instant', 'llama-3.3-70b-versatile', 'gemma2-9b-it']
|
|
100
|
-
: ['gemini-2.5-flash', 'gemini-1.5-flash', 'gemini-2.5-pro'];
|
|
101
|
-
console.log('(using defaults)');
|
|
159
|
+
? ['llama-3.1-8b-instant', 'llama-3.3-70b-versatile', 'gemma2-9b-it', 'llama3-8b-8192']
|
|
160
|
+
: ['gemini-2.5-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash', 'gemini-2.5-pro'];
|
|
102
161
|
}
|
|
103
162
|
|
|
104
|
-
|
|
163
|
+
models.forEach((m, i) => console.log(modelRow(m, i, i === 0)));
|
|
164
|
+
pipe();
|
|
105
165
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
models.
|
|
109
|
-
console.log(` ${i + 1}) ${m}${i === 0 ? ' (default)' : ''}`);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const modelChoice = await prompt(rl, ' Choice [1]: ');
|
|
113
|
-
const idx = parseInt(modelChoice.trim(), 10);
|
|
114
|
-
const modelIndex = (!idx || idx < 1 || idx > models.length) ? 0 : idx - 1;
|
|
166
|
+
const modelInput = await ask(`${s.pipe} Choice ${c.dim}[1]${c.reset}: `, '1');
|
|
167
|
+
const mIdx = parseInt(modelInput, 10);
|
|
168
|
+
const modelIndex = (!mIdx || mIdx < 1 || mIdx > models.length) ? 0 : mIdx - 1;
|
|
115
169
|
const model = models[modelIndex];
|
|
170
|
+
good(model);
|
|
171
|
+
pipe();
|
|
172
|
+
|
|
173
|
+
// ── Marker ──
|
|
174
|
+
head('Trigger marker');
|
|
175
|
+
info('Type at end of your prompt + Enter to enhance (Alt+M also works anywhere)');
|
|
176
|
+
const marker = await ask(`${s.pipe} Default ${c.dim}[;;]${c.reset}: `, ';;');
|
|
177
|
+
pipe();
|
|
178
|
+
|
|
179
|
+
// ── Timeout ──
|
|
180
|
+
head('Request timeout');
|
|
181
|
+
info('Seconds before giving up and restoring original text');
|
|
182
|
+
const timeoutInput = await ask(`${s.pipe} Default ${c.dim}[8]${c.reset}: `, '8');
|
|
183
|
+
const timeoutSeconds = parseInt(timeoutInput, 10) || 8;
|
|
184
|
+
pipe();
|
|
185
|
+
|
|
186
|
+
// ── Save ──
|
|
187
|
+
save({ geminiApiKey: key, geminiModel: model, marker, timeoutSeconds });
|
|
188
|
+
const path = configPath();
|
|
189
|
+
console.log(`${s.end} ${s.ok} ${c.bold}${c.green}Config saved${c.reset}`);
|
|
190
|
+
console.log(` ${c.dim}${path}${c.reset}`);
|
|
191
|
+
console.log(`\n Run ${c.bold}ccx claude${c.reset} to start\n`);
|
|
192
|
+
}
|
|
116
193
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
194
|
+
// ── Sub-commands ──────────────────────────────────────────────────────────────
|
|
195
|
+
function maskKey(key) {
|
|
196
|
+
if (!key) return '(not set)';
|
|
197
|
+
return key.slice(0, 6) + '****...';
|
|
198
|
+
}
|
|
122
199
|
|
|
123
|
-
|
|
200
|
+
function showConfig() {
|
|
201
|
+
const cfg = load();
|
|
202
|
+
const path = configPath();
|
|
203
|
+
const provider = cfg.geminiApiKey?.startsWith('gsk_') ? 'Groq' : 'Gemini';
|
|
204
|
+
banner();
|
|
205
|
+
console.log(`${s.pipe} ${c.dim}Path${c.reset} ${path}`);
|
|
206
|
+
console.log(`${s.pipe} ${c.dim}Provider${c.reset} ${provider}`);
|
|
207
|
+
console.log(`${s.pipe} ${c.dim}Key${c.reset} ${maskKey(cfg.geminiApiKey)}`);
|
|
208
|
+
console.log(`${s.pipe} ${c.dim}Model${c.reset} ${cfg.geminiModel}`);
|
|
209
|
+
console.log(`${s.pipe} ${c.dim}Marker${c.reset} ${cfg.marker}`);
|
|
210
|
+
console.log(`${s.end} ${c.dim}Timeout${c.reset} ${cfg.timeoutSeconds}s\n`);
|
|
211
|
+
}
|
|
124
212
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
213
|
+
function resetConfig() {
|
|
214
|
+
const path = configPath();
|
|
215
|
+
if (existsSync(path)) {
|
|
216
|
+
rmSync(path);
|
|
217
|
+
console.log(`${s.ok} Config reset. Run ${c.bold}ccx-init${c.reset} to set up again.`);
|
|
218
|
+
} else {
|
|
219
|
+
console.log('No config file found — nothing to reset.');
|
|
220
|
+
}
|
|
128
221
|
}
|
|
129
222
|
|
|
223
|
+
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
130
224
|
if (args.length === 0) {
|
|
131
|
-
runWizard();
|
|
225
|
+
runWizard().catch(err => { console.error(err.message); process.exit(1); });
|
|
132
226
|
} else if (args[0] === '--show') {
|
|
133
227
|
showConfig();
|
|
134
228
|
} else if (args[0] === '--reset') {
|
package/bin/ccx.js
CHANGED
|
@@ -116,6 +116,20 @@ process.stdout.on('resize', () => {
|
|
|
116
116
|
process.stdin.setRawMode(true);
|
|
117
117
|
process.stdin.resume();
|
|
118
118
|
|
|
119
|
+
function eraseInput(line, cursor) {
|
|
120
|
+
const parts = line.split('\n');
|
|
121
|
+
const extraLines = parts.length - 1;
|
|
122
|
+
if (extraLines === 0) {
|
|
123
|
+
const charsAfterCursor = line.length - cursor;
|
|
124
|
+
if (charsAfterCursor > 0) ptyProcess.write(`\x1b[${charsAfterCursor}C`);
|
|
125
|
+
ptyProcess.write(Buffer.alloc(line.length, 0x7f));
|
|
126
|
+
} else {
|
|
127
|
+
ptyProcess.write('\x1b[2K');
|
|
128
|
+
for (let i = 0; i < extraLines; i++) ptyProcess.write('\x1b[1A\x1b[2K');
|
|
129
|
+
ptyProcess.write('\x1b[G');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
const handler = createInputHandler({
|
|
120
134
|
marker: config.marker,
|
|
121
135
|
|
|
@@ -135,20 +149,14 @@ const handler = createInputHandler({
|
|
|
135
149
|
else if (err instanceof TimeoutError) msg = `Timed out after ${config.timeoutSeconds}s — original restored`;
|
|
136
150
|
else if (err instanceof NetworkError) msg = 'No connection — original restored';
|
|
137
151
|
await spinnerStop('error', msg);
|
|
138
|
-
|
|
139
|
-
const charsAfterCursor = line.length - cursor;
|
|
140
|
-
if (charsAfterCursor > 0) ptyProcess.write(`\x1b[${charsAfterCursor}C`);
|
|
141
|
-
ptyProcess.write(Buffer.alloc(line.length, 0x7f));
|
|
152
|
+
eraseInput(line, cursor);
|
|
142
153
|
ptyProcess.write(toSend);
|
|
143
154
|
handler.setBusy(false);
|
|
144
155
|
handler.setLine(toSend);
|
|
145
156
|
return;
|
|
146
157
|
}
|
|
147
158
|
await spinnerStop('success', 'Enhanced');
|
|
148
|
-
|
|
149
|
-
const charsAfterCursor = line.length - cursor;
|
|
150
|
-
if (charsAfterCursor > 0) ptyProcess.write(`\x1b[${charsAfterCursor}C`);
|
|
151
|
-
ptyProcess.write(Buffer.alloc(line.length, 0x7f));
|
|
159
|
+
eraseInput(line, cursor);
|
|
152
160
|
ptyProcess.write(improved);
|
|
153
161
|
handler.setBusy(false);
|
|
154
162
|
handler.setLine(improved);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "ccx-relay",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "2.1.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": {
|
package/src/gemini.js
CHANGED
|
@@ -7,11 +7,11 @@ export class NetworkError extends Error {}
|
|
|
7
7
|
const SYSTEM_PROMPT =
|
|
8
8
|
'Rewrite the following text to be grammatically correct and clearer, ' +
|
|
9
9
|
'while preserving its original intent and meaning exactly. ' +
|
|
10
|
-
'This is
|
|
11
|
-
'
|
|
10
|
+
'This is text typed into a terminal prompt — it may be one line or multiple lines. ' +
|
|
11
|
+
'Respond with EXACTLY ONE rewritten version preserving the same line structure and count. ' +
|
|
12
12
|
'Do not offer multiple options or alternatives. Do not add headings, bullet ' +
|
|
13
13
|
'points, markdown formatting, quotes, explanations, or any commentary. ' +
|
|
14
|
-
'Output must contain nothing but the rewritten
|
|
14
|
+
'Output must contain nothing but the rewritten text itself.';
|
|
15
15
|
|
|
16
16
|
function isGroq(apiKey) { return apiKey.startsWith('gsk_'); }
|
|
17
17
|
|
|
@@ -78,7 +78,7 @@ async function enhanceGroq(text, config, context) {
|
|
|
78
78
|
{ role: 'system', content: SYSTEM_PROMPT },
|
|
79
79
|
{ role: 'user', content: userContent },
|
|
80
80
|
],
|
|
81
|
-
max_tokens:
|
|
81
|
+
max_tokens: 2048,
|
|
82
82
|
}),
|
|
83
83
|
});
|
|
84
84
|
|
package/src/input.js
CHANGED
|
@@ -55,7 +55,11 @@ export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough,
|
|
|
55
55
|
if (seq !== null) {
|
|
56
56
|
if (seq.kd === 1) {
|
|
57
57
|
if (seq.vk === 13) {
|
|
58
|
-
if (
|
|
58
|
+
if (seq.cs & 0x0010) {
|
|
59
|
+
// Shift+Enter: append newline, accumulate multi-line buffer
|
|
60
|
+
lineBuffer += '\n';
|
|
61
|
+
cursor = lineBuffer.length;
|
|
62
|
+
} else if (lineBuffer.endsWith(marker) && lineBuffer.trim().length > marker.length) {
|
|
59
63
|
onEnhance(lineBuffer, cursor);
|
|
60
64
|
return;
|
|
61
65
|
} else {
|