shmakk 1.2.4 → 1.2.5

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 (51) hide show
  1. package/.env.example +11 -0
  2. package/README.md +75 -1
  3. package/docs/index.html +154 -16
  4. package/docs/mcp.md +78 -0
  5. package/docs/ssh.md +82 -0
  6. package/docs/vibedit-analysis.md +375 -0
  7. package/docs/vim.md +110 -0
  8. package/docs/voice.md +4 -0
  9. package/package.json +9 -5
  10. package/scripts/test-vibedit.js +45 -0
  11. package/scripts/vibedit-demo.sh +52 -0
  12. package/skills/shmakk-skill-creator.md +269 -0
  13. package/src/_check.js +7 -0
  14. package/src/_check_schema.js +5 -0
  15. package/src/_cleanup.js +18 -0
  16. package/src/_fix.js +9 -0
  17. package/src/_test_import.js +15 -0
  18. package/src/agent.js +11 -4
  19. package/src/browser-daemon.js +209 -0
  20. package/src/browser.js +10 -0
  21. package/src/cli/browserDaemon.js +60 -0
  22. package/src/cli/connectBrowser.js +137 -0
  23. package/src/cli.js +235 -8
  24. package/src/completions.js +8 -0
  25. package/src/control.js +273 -1
  26. package/src/core/browserConnector.js +523 -0
  27. package/src/electron.js +305 -0
  28. package/src/endpoints.js +74 -9
  29. package/src/index.js +24 -1
  30. package/src/llm.js +501 -61
  31. package/src/mobile.js +307 -0
  32. package/src/notify.js +51 -3
  33. package/src/orchestrator.js +35 -1
  34. package/src/pty.js +11 -6
  35. package/src/review.js +45 -11
  36. package/src/self-commands.js +153 -0
  37. package/src/session-convert.js +508 -0
  38. package/src/session-search.js +31 -0
  39. package/src/session.js +384 -46
  40. package/src/skills/browserActions.ts +984 -0
  41. package/src/skills.js +451 -24
  42. package/src/system-prompt.js +31 -25
  43. package/src/tools.js +81 -0
  44. package/src/vibedit/control.js +534 -0
  45. package/src/vibedit/electron.js +108 -0
  46. package/src/vibedit/files.js +171 -0
  47. package/src/vibedit/index.js +298 -0
  48. package/src/vibedit/overlay.js +1482 -0
  49. package/src/vibedit/prompts.js +245 -0
  50. package/src/vibedit/state.js +32 -0
  51. package/src/vim.js +410 -0
@@ -126,6 +126,34 @@ function makeSessionId() {
126
126
  return `${date}-${rand}`;
127
127
  }
128
128
 
129
+ // ── Session resume ────────────────────────────────────────────────────────
130
+
131
+ // Returns the most recent active session for a workspace, or null if none.
132
+ // An "active" session has ended_at IS NULL. No time or PID check; sessions
133
+ // live until explicitly ended (by process exit or --new-session).
134
+ function findActiveSession(workspace) {
135
+ if (!workspace) return null;
136
+ const db = getDB();
137
+ if (!db) return null;
138
+ try {
139
+ return db.prepare(
140
+ 'SELECT * FROM sessions WHERE workspace = ? AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1'
141
+ ).get(workspace) || null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ // Update the PID for an existing session (called on resume, so the current
148
+ // process becomes the session's recorded owner).
149
+ function updateSessionPid(sessionId, pid) {
150
+ const db = getDB();
151
+ if (!db) return;
152
+ try {
153
+ db.prepare('UPDATE sessions SET pid = ? WHERE id = ?').run(pid, sessionId);
154
+ } catch {}
155
+ }
156
+
129
157
  // ── Recording (called from session/agent code) ───────────────────────────
130
158
 
131
159
  function recordSessionStart({ sessionId, workspace, pid }) {
@@ -411,6 +439,8 @@ function isAvailable() {
411
439
  module.exports = {
412
440
  isAvailable,
413
441
  makeSessionId,
442
+ findActiveSession,
443
+ updateSessionPid,
414
444
  recordSessionStart,
415
445
  recordSessionEnd,
416
446
  recordTurn,
@@ -424,4 +454,5 @@ module.exports = {
424
454
  dbStats,
425
455
  closeDB,
426
456
  dbPath,
457
+ getDB,
427
458
  };
package/src/session.js CHANGED
@@ -11,7 +11,6 @@ const { clearIndex } = require('./workspace-index');
11
11
  const { loadGlossary } = require('./glossary');
12
12
  const { isConfigured } = require('./llm');
13
13
  const { makePrompter, decisionBanner } = require('./review');
14
- const { notify } = require('./notify');
15
14
  const { workspaceWarning } = require('./safety');
16
15
  const { createMCPManager } = require('./mcp-client');
17
16
  const { clearEdits } = require('./edit-tracker');
@@ -21,8 +20,10 @@ const { addPlanTasks, markTaskComplete, markTaskSkipped } = require('./task-file
21
20
  const { captureGitSha, runPostPlanReview } = require('./code-reviewer');
22
21
  const sessionSearch = require('./session-search');
23
22
  const { HELP, HELP_SUMMARY, HELP_SESSION_SUMMARY } = require('./cli');
23
+ const { vibeditState } = require('./vibedit/state');
24
24
  const audit = require('./audit');
25
25
  const { setMaxListeners } = require('events');
26
+ const { prepareVimEnvironment } = require('./vim');
26
27
 
27
28
  // Lazy-loaded voice service — only required when --voice is active
28
29
  let voiceService = null;
@@ -156,14 +157,51 @@ function makeToolConfirm(opts, ask, out, getAbort) {
156
157
  const ok = await ask('Run?', wouldAuto, {
157
158
  onCancel: getAbort,
158
159
  onWhy: showWhy,
160
+ notifyBody: description,
159
161
  });
160
162
  audit.append({ kind: ok ? 'tool-allowed' : 'tool-declined', name, args });
161
163
  return ok;
162
164
  };
163
165
  }
164
166
 
167
+ // Check all workspace roots for pending vibedit specs. If found, read the
168
+ // spec, delete the signal file, and return formatted injection text.
169
+ function drainPendingVibeditSpecs(roots) {
170
+ const specs = [];
171
+ for (const root of roots) {
172
+ const signalFile = vibeditState(root).pendingSpecFile;
173
+ try {
174
+ if (require('fs').existsSync(signalFile)) {
175
+ const specPath = require('fs').readFileSync(signalFile, 'utf8').trim();
176
+ if (specPath && require('fs').existsSync(specPath)) {
177
+ const raw = require('fs').readFileSync(specPath, 'utf8');
178
+ require('fs').unlinkSync(signalFile);
179
+ specs.push({ path: specPath, content: raw });
180
+ }
181
+ }
182
+ } catch {}
183
+ }
184
+ if (specs.length === 0) return null;
185
+
186
+ let text = 'Implement the following vibedit specification(s). ';
187
+ text += 'These were produced by the visual browser editor where the user made live changes and clicked Save. ';
188
+ text += 'The spec describes what the user wants. You are shmakk PM: read the spec, figure out all files that need changes (may span frontend AND backend), and make the edits.\n\n';
189
+ for (const s of specs) {
190
+ text += `--- VIBEDIT SPEC (from ${s.path}) ---\n${s.content}\n\n`;
191
+ }
192
+ return text;
193
+ }
194
+
165
195
  async function runOneSession(opts, registerSession) {
166
- const session = startSession({ debug: opts.debug, voiceEnabled: !!opts.voice, shellOverride: opts.shell });
196
+ const vimShim = prepareVimEnvironment(opts.vim || 'vim');
197
+ const session = startSession({
198
+ debug: opts.debug,
199
+ voiceEnabled: !!opts.voice && !opts.sts,
200
+ stsEnabled: !!opts.sts,
201
+ shellOverride: opts.shell,
202
+ extraEnv: vimShim.env,
203
+ cleanup: vimShim.cleanup,
204
+ });
167
205
  let colorsEnabled = opts.colors !== false;
168
206
  let markdownEnabled = opts.markdown !== false;
169
207
  const out = (s) => session.stdoutWrite(colorsEnabled ? s : stripAnsi(s));
@@ -176,9 +214,18 @@ async function runOneSession(opts, registerSession) {
176
214
  let cwd = pinnedWorkspace || process.cwd();
177
215
 
178
216
  function currentRoots() {
179
- if (!pinnedWorkspace) return [require('path').resolve(cwd)];
217
+ const tmp = '/tmp';
218
+ if (!pinnedWorkspace) {
219
+ const r = [require('path').resolve(cwd)];
220
+ if (tmp !== r[0] && !r[0].startsWith(tmp + '/')) r.push(tmp);
221
+ return r;
222
+ }
180
223
  const c = require('path').resolve(cwd);
181
- return c === pinnedWorkspace ? [pinnedWorkspace] : [pinnedWorkspace, c];
224
+ const r = c === pinnedWorkspace ? [pinnedWorkspace] : [pinnedWorkspace, c];
225
+ if (tmp !== r[0] && !r[0].startsWith(tmp + '/')) {
226
+ if (r.length < 2 || (tmp !== r[1] && !r[1].startsWith(tmp + '/'))) r.push(tmp);
227
+ }
228
+ return r;
182
229
  }
183
230
 
184
231
  // ── MCP server setup ──
@@ -197,10 +244,30 @@ async function runOneSession(opts, registerSession) {
197
244
  }
198
245
  // Generate a session ID so all turns/files this session produces can be
199
246
  // joined together in the search DB. Persists in env so subagents can tag.
200
- const sessionId = sessionSearch.makeSessionId();
247
+ // Resume an existing session for this workspace if available and the
248
+ // user hasn't explicitly requested a fresh session via --new-session.
249
+ let sessionId = null;
250
+ let resumed = false;
251
+ if (cwd) {
252
+ const existing = sessionSearch.findActiveSession(cwd);
253
+ if (existing && !opts.newSession) {
254
+ sessionId = existing.id;
255
+ sessionSearch.updateSessionPid(sessionId, process.pid);
256
+ resumed = true;
257
+ } else if (existing && opts.newSession) {
258
+ // Force a new session: end the old one first
259
+ sessionSearch.recordSessionEnd({ sessionId: existing.id });
260
+ }
261
+ }
262
+ if (!sessionId) {
263
+ sessionId = sessionSearch.makeSessionId();
264
+ }
201
265
  process.env.SHMAKK_SESSION_ID = sessionId;
202
266
  audit.append({ kind: 'session-start', sessionId, workspace: cwd, pinnedWorkspace, review: !!opts.review, pid: process.pid });
203
267
  sessionSearch.recordSessionStart({ sessionId, workspace: cwd, pid: process.pid });
268
+ if (resumed) {
269
+ out(`\x1b[2m[shmakk] resumed session ${sessionId}\x1b[0m\r\n`);
270
+ }
204
271
 
205
272
  // Incremental audit-log index catch-up — runs once at session start, async,
206
273
  // never blocks the user. Pulls in any sessions/turns persisted by other
@@ -374,6 +441,7 @@ async function runOneSession(opts, registerSession) {
374
441
  HELP_SUMMARY,
375
442
  HELP_SESSION_SUMMARY,
376
443
  setColors: (v) => { colorsEnabled = v; },
444
+ setVoiceMode,
377
445
  });
378
446
  return;
379
447
  }
@@ -471,12 +539,72 @@ async function runOneSession(opts, registerSession) {
471
539
  // ── Continuous voice loop (--sts always-on) ──
472
540
  // When --sts is active, runs a background loop: listen → transcribe → inject.
473
541
  // No hotkey needed — just speak and pause.
542
+
543
+ // ── Vibedit spec trigger ──
544
+ // Called from the vibedit onSpec callback when the user clicks Save in the
545
+ // overlay. Drains pending specs, runs the agent immediately, and updates
546
+ // history/session search — just like the normal command loop.
547
+ let specRunBusy = false;
548
+ async function runVibeditSpecNow(spec, specPath) {
549
+ if (specRunBusy) {
550
+ // Spec is already saved to the pending signal file. The currently
551
+ // running agent (or the next user command) will pick it up.
552
+ out(dim('[shmakk vibedit] spec queued — agent busy, will apply next', colorsEnabled) + '\r\n');
553
+ return;
554
+ }
555
+ if (!isConfigured()) {
556
+ out('\r\n\x1b[33m[shmakk vibedit] LLM not configured — spec saved but cannot apply yet\x1b[0m\r\n');
557
+ session.childWrite('\r');
558
+ return;
559
+ }
560
+ specRunBusy = true;
561
+ try {
562
+ await withAI(async (ctrl) => {
563
+ const specInjection = drainPendingVibeditSpecs(currentRoots());
564
+ if (!specInjection) {
565
+ out('\r\n\x1b[33m[shmakk vibedit] no pending specs found\x1b[0m\r\n');
566
+ return;
567
+ }
568
+ out('\x1b[36m[shmakk vibedit] applying spec immediately... (Ctrl-C to interrupt)\x1b[0m\r\n');
569
+ out(dim(`[shmakk vibedit] spec: ${spec.summary || '(no summary)'}`, colorsEnabled) + '\r\n');
570
+ try {
571
+ const updated = await runAgent({
572
+ roots: currentRoots(),
573
+ glossary,
574
+ confirmTool: makeToolConfirm(opts, ask, out, () => ctrl.abort()),
575
+ write: out,
576
+ signal: ctrl.signal,
577
+ profile: opts.profile || 'balanced',
578
+ colors: colorsEnabled,
579
+ markdown: markdownEnabled,
580
+ mcpManager,
581
+ input: specInjection,
582
+ history,
583
+ });
584
+ history = trimHistory(updated || history);
585
+ out(dim('\r\n[shmakk vibedit] spec applied', colorsEnabled) + '\r\n');
586
+ } catch (e) {
587
+ if (isAbortError(e)) {
588
+ // already signaled
589
+ } else {
590
+ out(`\r\n\x1b[31m[shmakk vibedit] error applying spec: ${e.message}\x1b[0m\r\n`);
591
+ }
592
+ }
593
+ });
594
+ } finally {
595
+ specRunBusy = false;
596
+ }
597
+ }
474
598
  // Pauses while TTS is speaking to avoid feedback loop.
475
- if (opts.sts) {
599
+ let stsLoopStarted = false;
600
+ function startStsLoop() {
601
+ if (stsLoopStarted) return true;
476
602
  const vs = getVoiceService();
477
603
  if (!vs.isAvailable()) {
478
604
  out('\r\n\x1b[33m[shmakk] no audio recorder found. Install sox.\x1b[0m\r\n');
605
+ return false;
479
606
  } else {
607
+ stsLoopStarted = true;
480
608
  // Preload STT model in background so first transcription doesn't lag
481
609
  try { vs.preloadSTT(); } catch {}
482
610
  let voiceLoopActive = true;
@@ -525,56 +653,99 @@ async function runOneSession(opts, registerSession) {
525
653
  if (!session._stsFlags) session._stsFlags = {};
526
654
  session._stsFlags.setTtsSpeaking = (v) => { ttsSpeaking = v; if (!v) ttsStoppedAt = Date.now(); };
527
655
  // Let the global Ctrl+C handler stop the STS loop on double-press.
528
- session._stsFlags.stopLoop = () => { voiceLoopActive = false; };
656
+ session._stsFlags.stopLoop = () => { voiceLoopActive = false; stsLoopStarted = false; };
657
+ // Pause/resume the loop externally (used by push-to-talk interrupt).
658
+ session._stsFlags.setVoiceBusy = (v) => { voiceBusy = v; };
659
+ return true;
660
+ }
661
+ }
662
+
663
+ if (opts.sts) startStsLoop();
664
+
665
+ function stopStsLoop() {
666
+ if (session._stsFlags?.stopLoop) session._stsFlags.stopLoop();
667
+ }
668
+
669
+ function stopRecorder() {
670
+ try { getVoiceService()._killRecorder(); } catch {}
671
+ }
672
+
673
+ function stopTts() {
674
+ try { getVoiceService()._killTts(); } catch {}
675
+ try { getTTSService().stopSpeaking(); } catch {}
676
+ }
677
+
678
+ function setVoiceMode(mode, enabled) {
679
+ const on = !!enabled;
680
+ if (mode === 'stt') {
681
+ if (on) {
682
+ stopStsLoop();
683
+ opts.stt = true;
684
+ opts.voice = true;
685
+ opts.sts = false;
686
+ session.setVoiceEnabled(true);
687
+ } else {
688
+ opts.stt = false;
689
+ if (!opts.sts) opts.voice = false;
690
+ if (!opts.sts) session.setVoiceEnabled(false);
691
+ stopRecorder();
692
+ }
693
+ return;
694
+ }
695
+ if (mode === 'tts') {
696
+ if (on) {
697
+ opts.tts = true;
698
+ } else {
699
+ opts.tts = false;
700
+ stopTts();
701
+ }
702
+ return;
703
+ }
704
+ if (mode === 'sts') {
705
+ if (on) {
706
+ stopRecorder();
707
+ stopTts();
708
+ opts.sts = true;
709
+ opts.voice = true;
710
+ opts.tts = true;
711
+ opts.stt = false;
712
+ session.setVoiceEnabled(true);
713
+ startStsLoop();
714
+ } else {
715
+ opts.sts = false;
716
+ opts.voice = !!opts.stt;
717
+ opts.tts = false;
718
+ session.setVoiceEnabled(!!opts.stt);
719
+ stopStsLoop();
720
+ stopRecorder();
721
+ stopTts();
722
+ }
529
723
  }
530
724
  }
531
725
 
532
726
  // ── Voice input handler (Ctrl+O hotkey) ──
533
727
  // Only active when --voice/--stt is passed without --sts.
534
728
  let voiceInProgress = false;
535
- if (opts.voice && !opts.sts) {
536
- const voiceWarned = { mic: false };
537
- session.ev.on('voice', async () => {
538
- if (voiceInProgress) return;
729
+ const voiceWarned = { mic: false };
730
+ session.ev.on('voice', async () => {
731
+ if (!opts.voice || voiceInProgress) return;
732
+
733
+ // ── STS push-to-talk interrupt ──
734
+ // User hits Ctrl+O while shmakk is talking (or idle) in STS mode.
735
+ // Kill TTS + ongoing recording, record PTT clip, run it, then resume.
736
+ if (opts.sts) {
539
737
  voiceInProgress = true;
540
738
  try {
541
739
  const vs = getVoiceService();
542
- if (!voiceWarned.mic) {
543
- if (!vs.isAvailable()) {
544
- out('\r\n\x1b[33m[shmakk voice] no microphone found. Install sox/arecord.\x1b[0m\r\n');
545
- voiceInProgress = false;
546
- return;
547
- }
548
- voiceWarned.mic = true;
549
- }
550
- // Show recording indicator — stays visible until transcription starts
551
- out('\r\n\x1b[36m🎤 [shmakk] Listening... (speak now, stops on silence)\x1b[0m');
552
- // Use a handler on the stdin stack so Ctrl-C aborts recording
553
- let recordingDone = false;
554
- const release = session.captureStdin((data) => {
555
- for (let i = 0; i < data.length; i++) {
556
- if (data[i] === 0x03 || data[i] === 0x0f || findCtrlC(data) !== -1) {
557
- recordingDone = true;
558
- // Kill the recorder process immediately
559
- try { vs._killRecorder(); } catch {}
560
- release();
561
- return;
562
- }
563
- }
564
- session.childWrite(data);
565
- });
740
+ const wasBusy = session._stsFlags?.setVoiceBusy ? true : false;
741
+ try { fullVoiceTeardown(); } catch {}
742
+ if (session._stsFlags?.setVoiceBusy) session._stsFlags.setVoiceBusy(true);
743
+ out('\r\n\x1b[36m[shmakk voice] push-to-talk (speak now, stops on silence)\x1b[0m');
566
744
  const text = await vs.recordAndTranscribe({
567
745
  maxDurationSec: parseInt(opts.voiceMaxDuration || process.env.SHMAKK_VOICE_MAX_SEC || '10', 10),
568
746
  language: opts.voiceLanguage || process.env.SHMAKK_VOICE_LANGUAGE,
569
- onStart: () => {},
570
- onStop: () => {
571
- recordingDone = true;
572
- try { release(); } catch {}
573
- },
574
747
  });
575
748
  if (text) {
576
- // Route to the agent, not the shell, so the correction engine
577
- // doesn't try to turn transcripts into commands.
578
749
  await runVoiceAsTask(text);
579
750
  } else {
580
751
  out('\r\x1b[33m[shmakk] no speech detected\x1b[0m\r\n');
@@ -583,10 +754,65 @@ async function runOneSession(opts, registerSession) {
583
754
  out(`\r\x1b[31m[shmakk voice] ${err.message}\x1b[0m\r\n`);
584
755
  if (opts.debug) out(`\r\x1b[33m${err.stack}\x1b[0m\r\n`);
585
756
  } finally {
757
+ if (session._stsFlags?.setVoiceBusy) session._stsFlags.setVoiceBusy(false);
586
758
  voiceInProgress = false;
587
759
  }
588
- });
589
- }
760
+ return;
761
+ }
762
+
763
+ // ── STT push-to-talk (hotkey-driven voice input) ──
764
+ voiceInProgress = true;
765
+ try {
766
+ const vs = getVoiceService();
767
+ if (!voiceWarned.mic) {
768
+ if (!vs.isAvailable()) {
769
+ out('\r\n\x1b[33m[shmakk voice] no microphone found. Install sox/arecord.\x1b[0m\r\n');
770
+ voiceInProgress = false;
771
+ return;
772
+ }
773
+ voiceWarned.mic = true;
774
+ }
775
+ // Show recording indicator — stays visible until transcription starts
776
+ out('\r\n\x1b[36m[shmakk voice] Listening... (speak now, stops on silence)\x1b[0m');
777
+ session.setVoiceEnabled(false);
778
+ // Use a handler on the stdin stack so Ctrl-C aborts recording
779
+ let recordingDone = false;
780
+ const release = session.captureStdin((data) => {
781
+ for (let i = 0; i < data.length; i++) {
782
+ if (data[i] === 0x03 || data[i] === 0x0f || findCtrlC(data) !== -1) {
783
+ recordingDone = true;
784
+ // Kill the recorder process immediately
785
+ try { vs._killRecorder(); } catch {}
786
+ release();
787
+ return;
788
+ }
789
+ }
790
+ session.childWrite(data);
791
+ });
792
+ const text = await vs.recordAndTranscribe({
793
+ maxDurationSec: parseInt(opts.voiceMaxDuration || process.env.SHMAKK_VOICE_MAX_SEC || '10', 10),
794
+ language: opts.voiceLanguage || process.env.SHMAKK_VOICE_LANGUAGE,
795
+ onStart: () => {},
796
+ onStop: () => {
797
+ recordingDone = true;
798
+ try { release(); } catch {}
799
+ },
800
+ });
801
+ if (text) {
802
+ // Route to the agent, not the shell, so the correction engine
803
+ // doesn't try to turn transcripts into commands.
804
+ await runVoiceAsTask(text);
805
+ } else {
806
+ out('\r\x1b[33m[shmakk] no speech detected\x1b[0m\r\n');
807
+ }
808
+ } catch (err) {
809
+ out(`\r\x1b[31m[shmakk voice] ${err.message}\x1b[0m\r\n`);
810
+ if (opts.debug) out(`\r\x1b[33m${err.stack}\x1b[0m\r\n`);
811
+ } finally {
812
+ voiceInProgress = false;
813
+ session.setVoiceEnabled(!!opts.stt && !opts.sts);
814
+ }
815
+ });
590
816
 
591
817
  session.ev.on('command', (c) => {
592
818
  lastCommand = c;
@@ -673,6 +899,109 @@ async function runOneSession(opts, registerSession) {
673
899
  return;
674
900
  }
675
901
 
902
+ // ── Vibedit: start in-browser overlay chat + recorder ───────────
903
+ if (selfCmd.action === 'vibedit' && selfCmd.arg) {
904
+ // The arg is the app URL (e.g. "http://localhost:3714")
905
+ // Strip any surrounding whitespace or quotes
906
+ const arg = selfCmd.arg.trim().replace(/^['"]|['"]$/g, '');
907
+ // If arg looks like a URL or file path, use it directly
908
+ let appUrl;
909
+ const _fs = require('fs');
910
+ if (/^https?:\/\//.test(arg) || (_fs.existsSync(arg) && (_fs.statSync(arg).isFile() || _fs.statSync(arg).isDirectory()))) {
911
+ appUrl = arg;
912
+ } else {
913
+ // Treat as a natural language request for the visual editing workflow
914
+ out(`\r\n\x1b[33m[shmakk vibedit] Natural language request: ${arg}\x1b[0m\r\n`);
915
+ out(`\x1b[33m[shmakk vibedit] To start the overlay, use: /vibedit http://localhost:<port>\x1b[0m\r\n`);
916
+ session.childWrite('\r');
917
+ return;
918
+ }
919
+
920
+ try {
921
+ const { startVibedit } = require('./vibedit');
922
+ const projectDir = currentRoots()[0] || process.cwd();
923
+ out(`\x1b[36m[shmakk vibedit] Starting overlay on ${appUrl}...\x1b[0m\r\n`);
924
+ out(`\x1b[36m[shmakk vibedit] A browser window will open with the chat panel (bottom-right puck).\x1b[0m\r\n`);
925
+ out(`\x1b[36m[shmakk vibedit] Close the browser window to shut down this vibedit.\x1b[0m\r\n`);
926
+
927
+ const vibedit = await startVibedit({
928
+ projectDir,
929
+ appUrl,
930
+ onSpec: (spec, specPath) => {
931
+ out(`\r\n\x1b[36m[shmakk vibedit] Spec saved!\x1b[0m\r\n`);
932
+ out(dim(`[shmakk vibedit] spec: ${spec.summary || '(no summary)'}\x1b[0m\r\n`, colorsEnabled));
933
+ runVibeditSpecNow(spec, specPath);
934
+ },
935
+ });
936
+
937
+ if (!vibedit) {
938
+ session.childWrite('\r');
939
+ return;
940
+ }
941
+
942
+ // vibedit runs until the browser closes or Ctrl-C
943
+ // Don't block the session - let the user keep typing commands
944
+ // while vibedit runs in the background
945
+ out(dim(`[shmakk vibedit] overlay active on control port ${vibedit.port}\r\n`, colorsEnabled));
946
+
947
+ // Store for cleanup on exit
948
+ if (!session._vibeditInstances) session._vibeditInstances = [];
949
+ session._vibeditInstances.push(vibedit);
950
+ } catch (e) {
951
+ if (e.code === 'MODULE_NOT_FOUND' && e.message.includes('playwright')) {
952
+ out(`\r\n\x1b[31m[shmakk vibedit] Playwright not installed. Run: npm install playwright\x1b[0m\r\n`);
953
+ } else {
954
+ out(`\r\n\x1b[31m[shmakk vibedit] error: ${e.message}\x1b[0m\r\n`);
955
+ }
956
+ }
957
+ session.childWrite('\r');
958
+ return;
959
+ }
960
+
961
+ if (selfCmd.action === 'vibedit-electron') {
962
+ const arg = (selfCmd.arg || '').trim().replace(/^['"]|['"]$/g, '');
963
+ let debugPort = 9222;
964
+ const portRe = /--port[= ](\d+)/i;
965
+ const portM = arg.match(portRe);
966
+ if (portM) debugPort = parseInt(portM[1], 10);
967
+ const projectDir = currentRoots()[0] || process.cwd();
968
+
969
+ try {
970
+ const { startVibeditElectron } = require('./vibedit/electron');
971
+ out(`\r\n\x1b[36m[shmakk vibedit electron] Connecting to Electron on port ${debugPort}...\x1b[0m\r\n`);
972
+ out(`\x1b[36m[shmakk vibedit electron] The overlay will appear in the Electron app window.\x1b[0m\r\n`);
973
+
974
+ const vibedit = await startVibeditElectron({
975
+ projectDir,
976
+ debugPort,
977
+ onSpec: (spec, specPath) => {
978
+ out(`\r\n\x1b[36m[shmakk vibedit] Spec saved!\x1b[0m\r\n`);
979
+ out(dim(`[shmakk vibedit] spec: ${spec.summary || '(no summary)'}\x1b[0m\r\n`, colorsEnabled));
980
+ runVibeditSpecNow(spec, specPath);
981
+ },
982
+ });
983
+
984
+ if (!vibedit) {
985
+ session.childWrite('\r');
986
+ return;
987
+ }
988
+
989
+ out(dim(`[shmakk vibedit electron] overlay active on control port ${vibedit.port}\r\n`, colorsEnabled));
990
+
991
+ if (!session._vibeditInstances) session._vibeditInstances = [];
992
+ session._vibeditInstances.push(vibedit);
993
+ } catch (e) {
994
+ if (e.code === 'MODULE_NOT_FOUND' && e.message.includes('playwright')) {
995
+ out(`\r\n\x1b[31m[shmakk vibedit electron] Playwright not installed. Run: npm install playwright\x1b[0m\r\n`);
996
+ } else {
997
+ out(`\r\n\x1b[31m[shmakk vibedit electron] error: ${e.message}\x1b[0m\r\n`);
998
+ }
999
+ }
1000
+ session.childWrite('\r');
1001
+ return;
1002
+ }
1003
+
1004
+
676
1005
  if (selfCmd.confirm) {
677
1006
  const go = await ask(`Run ${selfCmd.action}?`, true, { onCancel: () => {} });
678
1007
  if (!go) { session.childWrite('\r'); return; }
@@ -683,6 +1012,7 @@ async function runOneSession(opts, registerSession) {
683
1012
  HELP_SUMMARY,
684
1013
  HELP_SESSION_SUMMARY,
685
1014
  setColors: (v) => { colorsEnabled = v; },
1015
+ setVoiceMode,
686
1016
  });
687
1017
  session.childWrite('\r');
688
1018
  return;
@@ -765,6 +1095,7 @@ async function runOneSession(opts, registerSession) {
765
1095
  `- Reason: ${decision.reason || 'deterministic match'}`,
766
1096
  '',
767
1097
  ].join('\r\n')),
1098
+ notifyBody: cmd,
768
1099
  });
769
1100
  if (go) { correctionOrigin = cmd; audit.append({ kind: 'correction-run', proposed: decision.proposed }); session.childWrite(decision.proposed + '\r'); }
770
1101
  return;
@@ -790,6 +1121,7 @@ async function runOneSession(opts, registerSession) {
790
1121
  `- Reason: ${decision.reason || 'deterministic match'}`,
791
1122
  '',
792
1123
  ].join('\r\n')),
1124
+ notifyBody: cmd,
793
1125
  });
794
1126
  if (go) { correctionOrigin = cmd; audit.append({ kind: 'correction-run', proposed: decision.proposed }); session.childWrite(decision.proposed + '\r'); }
795
1127
  return;
@@ -918,8 +1250,11 @@ async function runOneSession(opts, registerSession) {
918
1250
  }
919
1251
 
920
1252
  const taskInput = `[Task ${i + 1} of ${plan.tasks.length}: ${task.title}]\n${task.description}\n\nOverall goal: ${plan.title}\n\nOriginal request: ${cmd}`;
1253
+ // Inject any pending vibedit specs
1254
+ const specInjection = drainPendingVibeditSpecs(currentRoots());
1255
+ const fullTaskInput = specInjection ? `${specInjection}\n\n---\n\n${taskInput}` : taskInput;
921
1256
  try {
922
- const updated = await runAgent({ ...agentOpts, input: taskInput, history });
1257
+ const updated = await runAgent({ ...agentOpts, input: fullTaskInput, history });
923
1258
  history = trimHistory(updated || history);
924
1259
  lastUpdated = updated;
925
1260
  plan.tasks[i].status = 'completed';
@@ -1001,9 +1336,12 @@ async function runOneSession(opts, registerSession) {
1001
1336
  const taskIndicator = routing.indicator
1002
1337
  ? `\x1b[36m[shmakk task · ${routing.indicator}] (Ctrl-C to interrupt)\x1b[0m\r\n`
1003
1338
  : '\x1b[36m[shmakk task] (Ctrl-C to interrupt)\x1b[0m\r\n';
1339
+ // Inject any pending vibedit specs before running the agent
1340
+ const specInjection = drainPendingVibeditSpecs(currentRoots());
1341
+ const fullCmd = specInjection ? `${specInjection}\n\n---\n\nUser also typed: ${cmd}` : cmd;
1004
1342
  out(taskIndicator);
1005
1343
  try {
1006
- const updated = await runAgent({ ...agentOpts, input: cmd, history });
1344
+ const updated = await runAgent({ ...agentOpts, input: fullCmd, history });
1007
1345
  history = trimHistory(updated || history);
1008
1346
 
1009
1347
  // TTS: speak the agent's response aloud if --tts is active