myrlin-workbook 0.9.11 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
4
4
  "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -9568,11 +9568,14 @@ class CWMApp {
9568
9568
  const text = accumulatedTranscript.trim();
9569
9569
  if (text) {
9570
9570
  const currentTp = this.terminalPanes[slotIdx];
9571
- if (currentTp && currentTp.ws && currentTp.ws.readyState === WebSocket.OPEN) {
9572
- currentTp.ws.send(JSON.stringify({ type: 'input', data: text + '\n' }));
9573
- this.showToast('Voice input sent', 'success');
9574
- } else {
9571
+ if (!currentTp || !currentTp.ws || currentTp.ws.readyState !== WebSocket.OPEN) {
9575
9572
  this.showToast('Terminal not connected, voice input discarded', 'warning');
9573
+ } else {
9574
+ // Clean up punctuation and grammar before sending
9575
+ this._punctuateVoiceText(text).then(cleaned => {
9576
+ currentTp.ws.send(JSON.stringify({ type: 'input', data: cleaned + '\n' }));
9577
+ this.showToast('Voice input sent', 'success');
9578
+ });
9576
9579
  }
9577
9580
  }
9578
9581
  } else {
@@ -9633,6 +9636,31 @@ class CWMApp {
9633
9636
  }
9634
9637
  }
9635
9638
 
9639
+ /**
9640
+ * Add punctuation and grammar to raw voice transcription text.
9641
+ * Tries the server-side AI punctuation endpoint (uses Anthropic API key if configured).
9642
+ * Falls back to basic rule-based cleanup if the API is unavailable.
9643
+ * @param {string} rawText - Raw speech-to-text output without punctuation
9644
+ * @returns {Promise<string>} Cleaned text with punctuation and capitalization
9645
+ */
9646
+ async _punctuateVoiceText(rawText) {
9647
+ if (!rawText || !rawText.trim()) return rawText;
9648
+
9649
+ // Try AI-powered punctuation via the server
9650
+ try {
9651
+ const data = await this.api('POST', '/api/ai/punctuate', { text: rawText });
9652
+ if (data && data.text) return data.text;
9653
+ } catch (_) {
9654
+ // API unavailable or no key configured, fall through to rule-based
9655
+ }
9656
+
9657
+ // Rule-based fallback: capitalize first letter, add period at end
9658
+ let text = rawText.trim();
9659
+ text = text.charAt(0).toUpperCase() + text.slice(1);
9660
+ if (!/[.!?]$/.test(text)) text += '.';
9661
+ return text;
9662
+ }
9663
+
9636
9664
  /**
9637
9665
  * Swap two terminal panes in the grid.
9638
9666
  * Swaps the xterm DOM nodes and the terminalPanes array entries.
@@ -257,6 +257,7 @@ class TerminalPane {
257
257
  this._writeBuf = '';
258
258
  this._activitySample = '';
259
259
  this._writeRaf = null;
260
+ this._pasteHandled = false;
260
261
  }
261
262
 
262
263
  _log(msg) {
@@ -400,10 +401,17 @@ class TerminalPane {
400
401
  const xtermTextarea = container.querySelector('.xterm-helper-textarea');
401
402
  if (xtermTextarea) {
402
403
  xtermTextarea.addEventListener('beforeinput', (e) => {
403
- // Block paste events - we handle Ctrl+V/Cmd+V ourselves via
404
- // pasteFromClipboard() to avoid xterm.js onData firing twice
404
+ // Intercept paste-via-beforeinput to prevent xterm.js onData double-send.
405
+ // Extract the pasted text and send it through our WebSocket instead.
406
+ // Set _pasteHandled flag so the paste event handler doesn't double-send.
405
407
  if (e.inputType === 'insertFromPaste') {
406
408
  e.preventDefault();
409
+ this._pasteHandled = true;
410
+ const text = e.data || (e.dataTransfer && e.dataTransfer.getData('text/plain')) || '';
411
+ if (text && this.ws && this.ws.readyState === WebSocket.OPEN) {
412
+ const bracketedText = '\x1b[200~' + text + '\x1b[201~';
413
+ this.ws.send(JSON.stringify({ type: 'input', data: bracketedText }));
414
+ }
407
415
  return;
408
416
  }
409
417
 
@@ -426,10 +434,22 @@ class TerminalPane {
426
434
  }
427
435
  }, { capture: true });
428
436
 
429
- // Also block native paste event as a fallback
437
+ // Intercept native paste events (right-click > Paste, Edit menu, touch-paste)
438
+ // and route them through our WebSocket instead of letting xterm.js double-send.
439
+ // Ctrl+V/Cmd+V is handled by the custom key handler. beforeinput may have
440
+ // already handled this paste (sets _pasteHandled), so check the flag first.
430
441
  xtermTextarea.addEventListener('paste', (e) => {
431
442
  e.preventDefault();
432
443
  e.stopPropagation();
444
+ if (this._pasteHandled) {
445
+ this._pasteHandled = false;
446
+ return;
447
+ }
448
+ const text = (e.clipboardData || window.clipboardData || '').getData('text');
449
+ if (text && this.ws && this.ws.readyState === WebSocket.OPEN) {
450
+ const bracketedText = '\x1b[200~' + text + '\x1b[201~';
451
+ this.ws.send(JSON.stringify({ type: 'input', data: bracketedText }));
452
+ }
433
453
  }, { capture: true });
434
454
  }
435
455
 
package/src/web/server.js CHANGED
@@ -2363,6 +2363,57 @@ app.put('/api/keys/anthropic', requireAuth, (req, res) => {
2363
2363
  return res.json({ success: true, configured: !!key, masked });
2364
2364
  });
2365
2365
 
2366
+ // ──────────────────────────────────────────────────────────
2367
+ // AI-POWERED VOICE PUNCTUATION
2368
+ // ──────────────────────────────────────────────────────────
2369
+
2370
+ /**
2371
+ * POST /api/ai/punctuate
2372
+ * Adds punctuation, capitalization, and grammar to raw voice dictation text.
2373
+ * Uses Claude Haiku for fast, low-cost cleanup. Returns 400 if no API key.
2374
+ * Body: { text: "raw voice text without punctuation" }
2375
+ * Returns: { text: "Cleaned text with proper punctuation." }
2376
+ */
2377
+ app.post('/api/ai/punctuate', requireAuth, async (req, res) => {
2378
+ const { text } = req.body || {};
2379
+ if (!text || typeof text !== 'string' || text.trim().length < 2) {
2380
+ return res.status(400).json({ error: 'Text is required (at least 2 characters)' });
2381
+ }
2382
+
2383
+ const store = getStore();
2384
+ const apiKey = (store.getState().settings || {}).anthropicApiKey || '';
2385
+ if (!apiKey) {
2386
+ return res.status(400).json({ error: 'No Anthropic API key configured' });
2387
+ }
2388
+
2389
+ try {
2390
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
2391
+ method: 'POST',
2392
+ headers: {
2393
+ 'Content-Type': 'application/json',
2394
+ 'x-api-key': apiKey,
2395
+ 'anthropic-version': '2023-06-01',
2396
+ },
2397
+ body: JSON.stringify({
2398
+ model: 'claude-haiku-4-5-20251001',
2399
+ max_tokens: 1024,
2400
+ system: `You are a punctuation and grammar fixer for voice dictation. Given raw speech-to-text output, add proper punctuation (periods, commas, question marks, exclamation points), capitalization, and fix obvious grammar issues. Keep the original meaning and wording intact. Do NOT add, remove, or rephrase words. Do NOT add quotes around the text. Return ONLY the corrected text, nothing else.`,
2401
+ messages: [{ role: 'user', content: text.trim() }],
2402
+ }),
2403
+ });
2404
+
2405
+ if (!response.ok) {
2406
+ return res.status(502).json({ error: `Claude API returned ${response.status}` });
2407
+ }
2408
+
2409
+ const data = await response.json();
2410
+ const cleaned = (data.content && data.content[0] && data.content[0].text) || text;
2411
+ return res.json({ text: cleaned.trim() });
2412
+ } catch (err) {
2413
+ return res.status(502).json({ error: 'Failed to reach Claude API: ' + err.message });
2414
+ }
2415
+ });
2416
+
2366
2417
  // ──────────────────────────────────────────────────────────
2367
2418
  // AI-POWERED SESSION FINDER
2368
2419
  // ──────────────────────────────────────────────────────────