osborn 0.8.12 → 0.8.13

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/dist/claude-auth.js +51 -13
  2. package/package.json +1 -1
@@ -74,6 +74,26 @@ function resolveClaudePath() {
74
74
  // Constants
75
75
  // ─────────────────────────────────────────
76
76
  const CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
77
+ /**
78
+ * Strip ALL ANSI escape sequences from a string — CSI (including private
79
+ * prefixes like `?`), OSC (both BEL and ST terminators), and lone ESC bytes.
80
+ *
81
+ * This is a superset of the common `/\x1B\[[0-9;]*[A-Za-z]/g` pattern, which
82
+ * misses private-prefix modes like `\x1B[?2026h` and string terminators like
83
+ * `\x1B\\`. Claude's Ink UI uses these extensively and they leak into error
84
+ * messages and URLs when we only strip the basic CSI form.
85
+ */
86
+ function stripAnsi(text) {
87
+ return text
88
+ // Full CSI: ESC [ <intermediates 0x20–0x3F> <final 0x40–0x7E>
89
+ .replace(/\x1B\[[\x20-\x3F]*[\x40-\x7E]/g, '')
90
+ // OSC terminated by BEL (0x07) — ESC ] <content> BEL
91
+ .replace(/\x1B\][^\x07]*\x07/g, '')
92
+ // OSC terminated by ST (ESC \) — ESC ] <content> ESC \
93
+ .replace(/\x1B\][^\x1B]*\x1B\\/g, '')
94
+ // Any remaining lone ESC bytes (e.g. ESC \ string terminator used standalone)
95
+ .replace(/\x1B/g, '');
96
+ }
77
97
  // URL matching: strip all whitespace first (like claudebox), then match
78
98
  // Handles Ink UI wrapping URLs across multiple lines
79
99
  const URL_REGEX = /https:\/\/claude\.(com|ai)\/cai\/oauth\/authorize[^\s]*/;
@@ -168,18 +188,20 @@ export async function checkClaudeAuthStatus() {
168
188
  * accepts.
169
189
  */
170
190
  function extractOAuthUrl(text) {
171
- // Replace ANSI control sequences AND lone ESC bytes with a NUL sentinel.
172
- // Why NUL and not a space: we need to strip all whitespace next (to
173
- // unwrap URLs that Ink wrapped across terminal lines), and if we used
174
- // a space it would be eaten by the whitespace strip then text on
175
- // either side of the control sequence would fuse into the URL. NUL
176
- // survives the whitespace strip and acts as a hard boundary the
177
- // tail-cut logic below can detect.
191
+ // Replace ALL ANSI control sequences with a NUL sentinel. NUL (not a
192
+ // space) because we strip all whitespace next to unwrap URLs that Ink
193
+ // split across terminal lines a space would vanish and text on either
194
+ // side of a control sequence would fuse into the URL. NUL survives the
195
+ // strip and acts as a hard boundary the tail-cut below can detect.
196
+ //
197
+ // Uses the same patterns as stripAnsi() but replaces with SENTINEL
198
+ // instead of '' so boundaries are preserved.
178
199
  const SENTINEL = '\x00';
179
200
  const noAnsi = text
180
- .replace(/\x1B\[[0-9;]*[A-Za-z]/g, SENTINEL)
181
- .replace(/\x1B\][^\x07]*\x07/g, SENTINEL)
182
- .replace(/\x1B/g, SENTINEL);
201
+ .replace(/\x1B\[[\x20-\x3F]*[\x40-\x7E]/g, SENTINEL) // Full CSI (incl. private-prefix ?/</>/=)
202
+ .replace(/\x1B\][^\x07]*\x07/g, SENTINEL) // OSC terminated by BEL
203
+ .replace(/\x1B\][^\x1B]*\x1B\\/g, SENTINEL) // OSC terminated by ST (ESC \)
204
+ .replace(/\x1B/g, SENTINEL); // Lone ESC bytes
183
205
  // Strip all whitespace (claudebox pattern: strings.Join(strings.Fields(pane), ""))
184
206
  // to unwrap URLs split across terminal lines. NUL sentinels survive.
185
207
  const stripped = noAnsi.replace(/\s+/g, '');
@@ -223,7 +245,23 @@ export function runClaudeAuthFlow(callbacks) {
223
245
  const handle = {
224
246
  submitCode: (code) => {
225
247
  if (procRef) {
226
- const trimmed = code.trim();
248
+ let trimmed = code.trim();
249
+ // User may paste the full callback URL instead of just the code:
250
+ // http://localhost:38719/callback?code=ZIgFd5nApQMR7...&state=TSLp6...
251
+ // Extract the bare `code` value so the CLI accepts it.
252
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
253
+ try {
254
+ const u = new URL(trimmed);
255
+ const extracted = u.searchParams.get('code');
256
+ if (extracted) {
257
+ console.log(`🔑 Extracted code from pasted callback URL (${extracted.length} chars)`);
258
+ trimmed = extracted;
259
+ }
260
+ }
261
+ catch {
262
+ // Not a valid URL, use as-is
263
+ }
264
+ }
227
265
  console.log(`🔑 Submitting auth code to Claude CLI (${trimmed.length} chars)`);
228
266
  // Ink reads raw keypresses. Write in chunks to simulate typing.
229
267
  const CHUNK_SIZE = 10;
@@ -274,7 +312,7 @@ export function runClaudeAuthFlow(callbacks) {
274
312
  }
275
313
  }, AUTH_TIMEOUT_MS);
276
314
  proc.onData((data) => {
277
- const clean = data.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '')
315
+ const clean = stripAnsi(data)
278
316
  .replace(/\x1B\][^\x07]*\x07/g, '');
279
317
  fullBuffer += clean;
280
318
  recentBuffer += clean;
@@ -332,7 +370,7 @@ export function runClaudeAuthFlow(callbacks) {
332
370
  }
333
371
  // Detect errors
334
372
  if (/OAuth error|Invalid code|expired/i.test(recentBuffer)) {
335
- const errMsg = recentBuffer.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '').trim().substring(0, 200);
373
+ const errMsg = stripAnsi(recentBuffer).trim().substring(0, 200);
336
374
  console.log('⚠️ Claude auth error:', errMsg);
337
375
  callbacks.onError(errMsg);
338
376
  recentBuffer = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.8.12",
3
+ "version": "0.8.13",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {