ccx-relay 1.7.1 → 1.9.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 +0 -5
- package/bin/ccx-init.js +4 -2
- package/bin/ccx.js +5 -40
- package/package.json +1 -1
- package/src/input.js +7 -49
package/README.md
CHANGED
|
@@ -78,11 +78,6 @@ While the wrapped process is running:
|
|
|
78
78
|
- Review the rewritten line, then press **Enter** to submit it to the child process.
|
|
79
79
|
- A plain line with no trigger submits immediately on Enter, exactly as if `ccx` weren't there.
|
|
80
80
|
- Arrow keys, Ctrl+C, and other control sequences pass through untouched.
|
|
81
|
-
- **Multi-line prompts:** Shift+Enter adds a line break without submitting (same as in a plain Claude Code session).
|
|
82
|
-
`ccx` tracks the whole multi-line prompt internally, so the marker/Alt+M trigger enhances the entire thing, and a
|
|
83
|
-
multi-line rewrite gets spliced back in as multiple soft-broken lines rather than being submitted early.
|
|
84
|
-
- Typing ahead while a spinner is running (e.g. mashing Alt+M again) is queued and replayed once the enhancement
|
|
85
|
-
finishes, instead of leaking into the child mid-rewrite. Ctrl+C still interrupts immediately even while enhancing.
|
|
86
81
|
|
|
87
82
|
### Changing the marker
|
|
88
83
|
|
package/bin/ccx-init.js
CHANGED
|
@@ -91,11 +91,13 @@ async function runWizard() {
|
|
|
91
91
|
process.stdout.write(' Fetching available models... ');
|
|
92
92
|
try {
|
|
93
93
|
const all = await listModels(key);
|
|
94
|
-
models = all.
|
|
94
|
+
models = all.slice(0, 10);
|
|
95
95
|
if (models.length === 0) throw new Error('empty');
|
|
96
96
|
console.log(`\x1b[32m✓\x1b[0m ${models.length} found`);
|
|
97
97
|
} catch (_) {
|
|
98
|
-
models =
|
|
98
|
+
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'];
|
|
99
101
|
console.log('(using defaults)');
|
|
100
102
|
}
|
|
101
103
|
|
package/bin/ccx.js
CHANGED
|
@@ -116,40 +116,10 @@ process.stdout.on('resize', () => {
|
|
|
116
116
|
process.stdin.setRawMode(true);
|
|
117
117
|
process.stdin.resume();
|
|
118
118
|
|
|
119
|
-
// Windows Console API CONTROL_KEY_STATE flag (see README's win32-input-mode notes;
|
|
120
|
-
// must match SHIFT_PRESSED in src/input.js).
|
|
121
|
-
const SHIFT_PRESSED = 0x0010;
|
|
122
|
-
|
|
123
|
-
function win32KeyEvent(vk, uc, keyDown, cs) {
|
|
124
|
-
return `\x1b[${vk};0;${uc};${keyDown ? 1 : 0};${cs};1_`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// A raw '\r'/'\n' byte written into the pty reads to the wrapped app as a plain
|
|
128
|
-
// Enter keypress, which submits instead of inserting a line break — so a
|
|
129
|
-
// multi-line rewrite (or the original multi-line prompt, on restore-after-error)
|
|
130
|
-
// would get cut off mid-line and the remainder typed as a new, separately
|
|
131
|
-
// submitted line. Encode each embedded newline as a synthetic Shift+Enter
|
|
132
|
-
// win32-input-mode sequence instead, matching how a real Shift+Enter keypress
|
|
133
|
-
// already arrives (see the SHIFT_PRESSED branch in src/input.js).
|
|
134
|
-
function writeTextPreservingLines(text) {
|
|
135
|
-
const lines = text.split('\n');
|
|
136
|
-
lines.forEach((line, idx) => {
|
|
137
|
-
if (line) ptyProcess.write(line);
|
|
138
|
-
if (idx < lines.length - 1) {
|
|
139
|
-
if (isWindows) {
|
|
140
|
-
ptyProcess.write(win32KeyEvent(13, 13, true, SHIFT_PRESSED));
|
|
141
|
-
ptyProcess.write(win32KeyEvent(13, 13, false, SHIFT_PRESSED));
|
|
142
|
-
} else {
|
|
143
|
-
ptyProcess.write('\n');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
119
|
const handler = createInputHandler({
|
|
150
120
|
marker: config.marker,
|
|
151
121
|
|
|
152
|
-
onEnhance: async (line, cursor
|
|
122
|
+
onEnhance: async (line, cursor) => {
|
|
153
123
|
const toSend = line.endsWith(config.marker) ? line.slice(0, -config.marker.length) : line;
|
|
154
124
|
handler.setBusy(true);
|
|
155
125
|
spinnerStart();
|
|
@@ -158,10 +128,6 @@ const handler = createInputHandler({
|
|
|
158
128
|
const context = contextLines.length > 0 ? contextLines.join('\n') : null;
|
|
159
129
|
improved = await enhance(toSend, config, context);
|
|
160
130
|
} catch (err) {
|
|
161
|
-
// The user may have Ctrl+C'd out (or otherwise moved on) while this was
|
|
162
|
-
// in flight — reset() bumps the epoch, so an old token here means
|
|
163
|
-
// whatever we'd write back no longer belongs on the current line.
|
|
164
|
-
if (!handler.isCurrent(token)) return;
|
|
165
131
|
let msg = 'Enhancement failed';
|
|
166
132
|
if (err instanceof RateLimitError) msg = 'Quota exceeded';
|
|
167
133
|
else if (err instanceof AuthError) msg = 'Invalid key — run ccx-init';
|
|
@@ -173,20 +139,19 @@ const handler = createInputHandler({
|
|
|
173
139
|
const charsAfterCursor = line.length - cursor;
|
|
174
140
|
if (charsAfterCursor > 0) ptyProcess.write(`\x1b[${charsAfterCursor}C`);
|
|
175
141
|
ptyProcess.write(Buffer.alloc(line.length, 0x7f));
|
|
176
|
-
|
|
177
|
-
handler.setLine(toSend);
|
|
142
|
+
ptyProcess.write(toSend);
|
|
178
143
|
handler.setBusy(false);
|
|
144
|
+
handler.setLine(toSend);
|
|
179
145
|
return;
|
|
180
146
|
}
|
|
181
|
-
if (!handler.isCurrent(token)) return;
|
|
182
147
|
await spinnerStop('success', 'Enhanced');
|
|
183
148
|
// Move cursor to end of line then erase, write improved
|
|
184
149
|
const charsAfterCursor = line.length - cursor;
|
|
185
150
|
if (charsAfterCursor > 0) ptyProcess.write(`\x1b[${charsAfterCursor}C`);
|
|
186
151
|
ptyProcess.write(Buffer.alloc(line.length, 0x7f));
|
|
187
|
-
|
|
188
|
-
handler.setLine(improved);
|
|
152
|
+
ptyProcess.write(improved);
|
|
189
153
|
handler.setBusy(false);
|
|
154
|
+
handler.setLine(improved);
|
|
190
155
|
},
|
|
191
156
|
|
|
192
157
|
onSubmit: (_line) => {},
|
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.9.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/input.js
CHANGED
|
@@ -21,42 +21,19 @@ function parseCsiSeq(chunk, i) {
|
|
|
21
21
|
return { consumed: j + 1 - i, params, final };
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Windows Console API CONTROL_KEY_STATE flag (see README's win32-input-mode notes).
|
|
25
|
-
const SHIFT_PRESSED = 0x0010;
|
|
26
|
-
|
|
27
24
|
export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough, onCtrlC }) {
|
|
28
25
|
let lineBuffer = '';
|
|
29
26
|
let cursor = 0;
|
|
30
27
|
let busy = false;
|
|
31
|
-
let epoch = 0;
|
|
32
|
-
let pendingWhileBusy = [];
|
|
33
28
|
|
|
34
29
|
function reset() {
|
|
35
30
|
lineBuffer = '';
|
|
36
31
|
cursor = 0;
|
|
37
32
|
busy = false;
|
|
38
|
-
pendingWhileBusy = [];
|
|
39
|
-
epoch++;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Invalidated by reset() (e.g. Ctrl+C) so a still-in-flight enhance that
|
|
43
|
-
// resolves afterward can tell its result is stale and discard it instead of
|
|
44
|
-
// writing over whatever the user has moved on to.
|
|
45
|
-
function isCurrent(token) {
|
|
46
|
-
return token === epoch;
|
|
47
33
|
}
|
|
48
34
|
|
|
49
35
|
function setBusy(b) {
|
|
50
|
-
const wasBusy = busy;
|
|
51
36
|
busy = b;
|
|
52
|
-
// Replay input that arrived mid-enhancement now that the line has been
|
|
53
|
-
// updated — see processChunk's busy branch for why it's queued instead
|
|
54
|
-
// of forwarded live.
|
|
55
|
-
if (wasBusy && !b && pendingWhileBusy.length > 0) {
|
|
56
|
-
const queued = pendingWhileBusy;
|
|
57
|
-
pendingWhileBusy = [];
|
|
58
|
-
for (const buf of queued) processChunk(buf);
|
|
59
|
-
}
|
|
60
37
|
}
|
|
61
38
|
|
|
62
39
|
// Sync internal buffer after external rewrite (e.g. after enhancement)
|
|
@@ -66,21 +43,7 @@ export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough,
|
|
|
66
43
|
}
|
|
67
44
|
|
|
68
45
|
function processChunk(chunk) {
|
|
69
|
-
if (busy) {
|
|
70
|
-
// A lone Ctrl+C still gets through immediately so the wrapped app (and
|
|
71
|
-
// the user) aren't stuck waiting on a hung enhance call. Everything
|
|
72
|
-
// else is queued rather than forwarded raw: forwarding live would let
|
|
73
|
-
// e.g. a mashed Alt+M reach the child unintercepted, silently editing
|
|
74
|
-
// its buffer in a way ccx's lineBuffer/cursor snapshot never sees —
|
|
75
|
-
// desyncing the erase-and-replace math once the enhance completes.
|
|
76
|
-
if (chunk.length === 1 && chunk[0] === 0x03) {
|
|
77
|
-
onCtrlC();
|
|
78
|
-
onPassthrough(chunk);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
pendingWhileBusy.push(Buffer.from(chunk));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
46
|
+
if (busy) { onPassthrough(chunk); return; }
|
|
84
47
|
|
|
85
48
|
let i = 0;
|
|
86
49
|
while (i < chunk.length) {
|
|
@@ -92,13 +55,8 @@ export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough,
|
|
|
92
55
|
if (seq !== null) {
|
|
93
56
|
if (seq.kd === 1) {
|
|
94
57
|
if (seq.vk === 13) {
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
// multi-line input, not a submit — keep composing.
|
|
98
|
-
lineBuffer = lineBuffer.slice(0, cursor) + '\n' + lineBuffer.slice(cursor);
|
|
99
|
-
cursor++;
|
|
100
|
-
} else if (lineBuffer.endsWith(marker) && lineBuffer.trim().length > marker.length) {
|
|
101
|
-
onEnhance(lineBuffer, cursor, epoch);
|
|
58
|
+
if (lineBuffer.endsWith(marker) && lineBuffer.trim().length > marker.length) {
|
|
59
|
+
onEnhance(lineBuffer, cursor);
|
|
102
60
|
return;
|
|
103
61
|
} else {
|
|
104
62
|
onSubmit(lineBuffer);
|
|
@@ -119,7 +77,7 @@ export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough,
|
|
|
119
77
|
} else if (seq.vk === 35) {
|
|
120
78
|
cursor = lineBuffer.length;
|
|
121
79
|
} else if (seq.vk === 77 && (seq.cs & (0x0001 | 0x0002)) && !(seq.cs & (0x0004 | 0x0008))) {
|
|
122
|
-
if (lineBuffer.length > 0) { onEnhance(lineBuffer, cursor
|
|
80
|
+
if (lineBuffer.length > 0) { onEnhance(lineBuffer, cursor); return; }
|
|
123
81
|
}
|
|
124
82
|
}
|
|
125
83
|
onPassthrough(chunk.slice(i, i + seq.consumed));
|
|
@@ -130,7 +88,7 @@ export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough,
|
|
|
130
88
|
// 2. Alt+M (ESC m)
|
|
131
89
|
if (i + 1 < chunk.length && chunk[i + 1] === 0x6d) {
|
|
132
90
|
if (lineBuffer.length > 0) {
|
|
133
|
-
onEnhance(lineBuffer, cursor
|
|
91
|
+
onEnhance(lineBuffer, cursor);
|
|
134
92
|
return;
|
|
135
93
|
} else {
|
|
136
94
|
onPassthrough(chunk.slice(i, i + 2));
|
|
@@ -158,7 +116,7 @@ export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough,
|
|
|
158
116
|
return;
|
|
159
117
|
} else if (byte === 0x0d) {
|
|
160
118
|
if (lineBuffer.endsWith(marker) && lineBuffer.trim().length > marker.length) {
|
|
161
|
-
onEnhance(lineBuffer, cursor
|
|
119
|
+
onEnhance(lineBuffer, cursor);
|
|
162
120
|
return;
|
|
163
121
|
} else {
|
|
164
122
|
onSubmit(lineBuffer);
|
|
@@ -189,5 +147,5 @@ export function createInputHandler({ marker, onEnhance, onSubmit, onPassthrough,
|
|
|
189
147
|
}
|
|
190
148
|
}
|
|
191
149
|
|
|
192
|
-
return { processChunk, setBusy, reset, setLine
|
|
150
|
+
return { processChunk, setBusy, reset, setLine };
|
|
193
151
|
}
|