ccx-relay 2.3.0 → 2.5.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.
Files changed (2) hide show
  1. package/bin/ccx.js +60 -57
  2. package/package.json +1 -1
package/bin/ccx.js CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
  import { load } from '../src/config.js';
5
5
  import { enhance, RateLimitError, AuthError, ModelError, TimeoutError, NetworkError } from '../src/gemini.js';
6
- import { start as spinnerStart, stop as spinnerStop } from '../src/ui.js';
7
- import { openPopup } from '../src/popup.js';
6
+ import { start as spinnerStart, stop as spinnerStop, clear as spinnerClear } from '../src/ui.js';
7
+ import { createInputHandler } from '../src/input.js';
8
8
 
9
9
  const argv = process.argv.slice(2);
10
10
 
@@ -42,8 +42,8 @@ if (argv[0] === 'completion') {
42
42
  process.exit(1);
43
43
  }
44
44
  process.stdout.write(scripts[shell] + '\n');
45
- if (shell === 'bash') process.stdout.write('# eval "$(ccx completion bash)"\n');
46
- else if (shell === 'zsh') process.stdout.write('# eval "$(ccx completion zsh)"\n');
45
+ if (shell === 'bash') process.stdout.write('# eval "$(ccx completion bash)"\n');
46
+ else if (shell === 'zsh') process.stdout.write('# eval "$(ccx completion zsh)"\n');
47
47
  else if (shell === 'powershell') process.stdout.write('# Invoke-Expression (ccx completion powershell)\n');
48
48
  process.exit(0);
49
49
  }
@@ -111,68 +111,71 @@ process.stdout.on('resize', () => {
111
111
  ptyProcess.resize(process.stdout.columns, process.stdout.rows);
112
112
  });
113
113
 
114
- // ── Alt+M detection ────────────────────────────────────────────────────────────
115
- // Returns true if this chunk is an Alt+M keypress (standard or win32-input-mode).
116
- function isAltM(chunk) {
117
- // Standard: ESC m (0x1b 0x6d)
118
- if (chunk.length >= 2 && chunk[0] === 0x1b && chunk[1] === 0x6d) return true;
119
- // Win32 input mode: ESC [ vk;sc;uc;kd;cs;rc _ where vk=77 (M), kd=1 (keydown), Alt bit in cs
120
- if (chunk.length >= 2 && chunk[0] === 0x1b && chunk[1] === 0x5b) {
121
- const m = chunk.toString('ascii').match(/^\x1b\[(\d+);\d+;\d+;(\d+);(\d+);\d+_/);
122
- if (m) {
123
- const vk = +m[1], kd = +m[2], cs = +m[3];
124
- if (vk === 77 && kd === 1 && (cs & 0x0003) && !(cs & 0x000c)) return true;
125
- }
114
+ // ── Erase current input via readline shortcuts ────────────────────────────────
115
+ // Ctrl+E (go to end of line) + Ctrl+U (kill to start) = clear entire line.
116
+ // Works regardless of visual wrapping — Claude Code's own readline handles it.
117
+ // For multi-line: Backspace through the join, then kill each previous line.
118
+ function eraseInput(line) {
119
+ const lineCount = line.split('\n').length;
120
+ ptyProcess.write('\x05\x15'); // Ctrl+E + Ctrl+U: clear last (or only) line
121
+ for (let i = 1; i < lineCount; i++) {
122
+ ptyProcess.write('\x7f\x05\x15'); // Backspace join + Ctrl+E + Ctrl+U
126
123
  }
127
- return false;
128
124
  }
129
125
 
130
- // ── Enhancement flow ───────────────────────────────────────────────────────────
131
- let busy = false;
132
-
133
- async function handleEnhance() {
134
- busy = true;
135
-
136
- const text = await openPopup();
137
- if (!text) { busy = false; return; }
138
-
139
- spinnerStart();
140
- let improved;
141
- try {
142
- const context = contextLines.length > 0 ? contextLines.join('\n') : null;
143
- improved = await enhance(text, config, context);
144
- } catch (err) {
145
- let msg = 'Enhancement failed';
146
- if (err instanceof RateLimitError) msg = 'Quota exceeded';
147
- else if (err instanceof AuthError) msg = 'Invalid key — run ccx-init';
148
- else if (err instanceof ModelError) msg = 'Model not found — run ccx-init';
149
- else if (err instanceof TimeoutError) msg = `Timed out after ${config.timeoutSeconds}s`;
150
- else if (err instanceof NetworkError) msg = 'No connection';
151
- await spinnerStop('error', msg);
152
- busy = false;
153
- return;
154
- }
126
+ // ── Input handler ──────────────────────────────────────────────────────────────
127
+ process.stdin.setRawMode(true);
128
+ process.stdin.resume();
129
+
130
+ const handler = createInputHandler({
131
+ marker: config.marker,
132
+
133
+ onEnhance: async (line, cursor) => {
134
+ const toSend = line.endsWith(config.marker)
135
+ ? line.slice(0, -config.marker.length)
136
+ : line;
137
+
138
+ handler.setBusy(true);
139
+ spinnerStart();
140
+
141
+ let improved;
142
+ try {
143
+ const context = contextLines.length > 0 ? contextLines.join('\n') : null;
144
+ improved = await enhance(toSend, config, context);
145
+ } catch (err) {
146
+ let msg = 'Enhancement failed';
147
+ if (err instanceof RateLimitError) msg = 'Quota exceeded';
148
+ else if (err instanceof AuthError) msg = 'Invalid key — run ccx-init';
149
+ else if (err instanceof ModelError) msg = 'Model not found — run ccx-init';
150
+ else if (err instanceof TimeoutError) msg = `Timed out after ${config.timeoutSeconds}s — original restored`;
151
+ else if (err instanceof NetworkError) msg = 'No connection — original restored';
152
+ await spinnerStop('error', msg);
153
+ eraseInput(line);
154
+ ptyProcess.write(toSend);
155
+ handler.setBusy(false);
156
+ handler.setLine(toSend);
157
+ return;
158
+ }
155
159
 
156
- await spinnerStop('success', 'Enhanced');
160
+ await spinnerStop('success', 'Enhanced');
161
+ eraseInput(line, cursor);
162
+ ptyProcess.write(improved);
163
+ handler.setBusy(false);
164
+ handler.setLine(improved);
165
+ },
157
166
 
158
- // Clear any existing text in Claude Code's input line, then inject enhanced text
159
- ptyProcess.write('\x15'); // Ctrl+U = kill line (clears Claude Code's current input)
160
- // Small pause so Claude Code processes the Ctrl+U before we type the new text
161
- await new Promise(r => setTimeout(r, 30));
162
- ptyProcess.write(improved);
163
- busy = false;
164
- }
167
+ onSubmit: () => {},
165
168
 
166
- // ── Stdin routing ──────────────────────────────────────────────────────────────
167
- process.stdin.setRawMode(true);
168
- process.stdin.resume();
169
+ onPassthrough: (chunk) => ptyProcess.write(chunk),
169
170
 
170
- process.stdin.on('data', chunk => {
171
- if (busy) return; // swallow input during enhancement
172
- if (isAltM(chunk)) { handleEnhance(); return; }
173
- ptyProcess.write(chunk);
171
+ onCtrlC: () => {
172
+ spinnerClear();
173
+ handler.reset();
174
+ },
174
175
  });
175
176
 
177
+ process.stdin.on('data', chunk => handler.processChunk(chunk));
178
+
176
179
  // ── Cleanup ────────────────────────────────────────────────────────────────────
177
180
  function cleanupAndExit(code) {
178
181
  try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch (_) {}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "ccx-relay",
4
- "version": "2.3.0",
4
+ "version": "2.5.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": {