ccx-relay 1.7.1 → 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 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.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, token) => {
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
- writeTextPreservingLines(toSend);
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
- writeTextPreservingLines(improved);
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.7.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": {
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 (seq.cs & SHIFT_PRESSED) {
96
- // Shift+Enter: soft line break in the wrapped app's own
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, epoch); return; }
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, epoch);
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, epoch);
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, isCurrent };
150
+ return { processChunk, setBusy, reset, setLine };
193
151
  }