voicecc 1.1.20 → 1.1.22

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.
@@ -118,23 +118,19 @@ function spawnLoginProcess(): Promise<string> {
118
118
 
119
119
  let output = "";
120
120
  let resolved = false;
121
- let step = 0;
122
-
123
- let loginSent = false;
124
- let enterDebounce: ReturnType<typeof setTimeout> | null = null;
121
+ let enterCount = 0;
125
122
 
126
123
  child.onData((data: string) => {
127
124
  output += data;
128
125
  const clean = data.replace(/\x1b\[[^m]*m/g, "").replace(/\r/g, "").trim();
129
- if (clean) console.debug(`[auth/login] step=${step} loginSent=${loginSent} data: ${clean.slice(0, 200)}`);
126
+ if (clean) console.debug(`[auth/login] data: ${clean.slice(0, 200)}`);
130
127
 
131
128
  if (resolved) return;
132
129
 
133
- // ---- Always check for OAuth URL (can appear during setup or after /login) ----
130
+ // Check for OAuth URL on every data event
134
131
  const urlMatch = output.match(/(https:\/\/claude\.ai\/oauth\/authorize\S+)/);
135
132
  if (urlMatch) {
136
133
  resolved = true;
137
- step = 4;
138
134
  console.debug("[auth/login] URL captured");
139
135
  pendingLogin = {
140
136
  pty: child,
@@ -145,26 +141,41 @@ function spawnLoginProcess(): Promise<string> {
145
141
  return;
146
142
  }
147
143
 
148
- // ---- Dismiss any selection prompt (❯) by pressing Enter ----
149
- // Handles: trust, theme picker, login method, or any other setup prompt.
150
- if (/❯/.test(data)) {
151
- if (enterDebounce) clearTimeout(enterDebounce);
152
- enterDebounce = setTimeout(() => {
153
- console.debug("[auth/login] pressing Enter on selection prompt");
154
- child.write("\r");
155
- }, 800);
156
- }
157
-
158
- // ---- Detect the main REPL prompt and send /login ----
159
- // Sparkle animation means we're in the main REPL, not setup.
160
- if (!loginSent && /[✻✽✶✢]/.test(data)) {
161
- loginSent = true;
162
- console.debug("[auth/login] main prompt ready, sending /login");
163
- setTimeout(() => child.write("/login\r"), 1000);
144
+ // Detect REPL prompt means user is already authenticated.
145
+ // The REPL shows "◐ medium · /effort" or similar model/effort indicator.
146
+ const stripped = output.replace(/\x1b\[[^m]*m/g, "");
147
+ if (enterCount >= 1 && !resolved) {
148
+ const replIndicators = [
149
+ /\/effort/, // effort command shown in REPL status
150
+ /◐.*medium/, // model indicator in REPL
151
+ /\$\d+\.\d+/, // cost like $0.00
152
+ /what can i help/i,
153
+ ];
154
+ for (const pattern of replIndicators) {
155
+ if (pattern.test(stripped)) {
156
+ resolved = true;
157
+ console.debug("[auth/login] REPL detected — already authenticated");
158
+ child.kill();
159
+ reject(new Error("ALREADY_AUTHENTICATED"));
160
+ return;
161
+ }
162
+ }
164
163
  }
165
164
  });
166
165
 
166
+ // Press Enter every 2s to advance through setup prompts
167
+ // (trust, theme, login method, etc.) until the OAuth URL appears.
168
+ // Some prompts render ❯ via ANSI cursor positioning which is
169
+ // impossible to detect reliably, so periodic Enter is simplest.
170
+ const enterInterval = setInterval(() => {
171
+ if (resolved) { clearInterval(enterInterval); return; }
172
+ enterCount++;
173
+ console.debug(`[auth/login] pressing Enter (periodic #${enterCount})`);
174
+ child.write("\r");
175
+ }, 2000);
176
+
167
177
  child.onExit(({ exitCode }: { exitCode: number }) => {
178
+ clearInterval(enterInterval);
168
179
  console.debug("[auth/login] PTY exited with code", exitCode);
169
180
  if (!resolved) {
170
181
  resolved = true;
@@ -178,10 +189,11 @@ function spawnLoginProcess(): Promise<string> {
178
189
  // Timeout
179
190
  setTimeout(() => {
180
191
  if (!resolved) {
192
+ clearInterval(enterInterval);
181
193
  resolved = true;
182
194
  child.kill();
183
195
  const clean = output.replace(/\x1b\[[^m]*m/g, "").slice(-500);
184
- reject(new Error(`Login timed out at step ${step}. Last output: ${clean}`));
196
+ reject(new Error(`Login timed out. Last output: ${clean}`));
185
197
  }
186
198
  }, LOGIN_TIMEOUT_MS);
187
199
  });
@@ -289,11 +301,21 @@ export function authRoutes(): Hono {
289
301
  * Spawns interactive claude via PTY, navigates to /login, returns URL.
290
302
  */
291
303
  app.post("/oauth/start", async (c) => {
304
+ // Pre-check: if already authenticated, don't spawn PTY
305
+ const status = await getAuthStatus();
306
+ if (status.authenticated) {
307
+ return c.json({ error: "Already authenticated", alreadyAuthenticated: true }, 400);
308
+ }
309
+
292
310
  try {
293
311
  const url = await spawnLoginProcess();
294
312
  return c.json({ url });
295
313
  } catch (err) {
296
- return c.json({ error: (err as Error).message }, 500);
314
+ const msg = (err as Error).message;
315
+ if (msg === "ALREADY_AUTHENTICATED") {
316
+ return c.json({ error: "Already authenticated", alreadyAuthenticated: true }, 400);
317
+ }
318
+ return c.json({ error: msg }, 500);
297
319
  }
298
320
  });
299
321
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicecc",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
4
4
  "description": "Voice mode plugin for Claude Code -- hands-free interaction via ElevenLabs STT/TTS and VAD",
5
5
  "type": "module",
6
6
  "bin": {