shmakk 1.2.3 → 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.
- package/.env.example +11 -0
- package/README.md +75 -1
- package/docs/index.html +154 -16
- package/docs/mcp.md +78 -0
- package/docs/ssh.md +82 -0
- package/docs/vibedit-analysis.md +375 -0
- package/docs/vim.md +110 -0
- package/docs/voice.md +4 -0
- package/package.json +9 -5
- package/scripts/test-vibedit.js +45 -0
- package/scripts/vibedit-demo.sh +52 -0
- package/skills/shmakk-skill-creator.md +269 -0
- package/src/_check.js +7 -0
- package/src/_check_schema.js +5 -0
- package/src/_cleanup.js +18 -0
- package/src/_fix.js +9 -0
- package/src/_test_import.js +15 -0
- package/src/agent.js +11 -4
- package/src/browser-daemon.js +209 -0
- package/src/browser.js +10 -0
- package/src/cli/browserDaemon.js +60 -0
- package/src/cli/connectBrowser.js +137 -0
- package/src/cli.js +235 -8
- package/src/completions.js +8 -0
- package/src/control.js +273 -1
- package/src/core/browserConnector.js +523 -0
- package/src/correction.js +6 -0
- package/src/electron.js +305 -0
- package/src/endpoints.js +74 -9
- package/src/index.js +24 -1
- package/src/llm.js +501 -61
- package/src/mobile.js +307 -0
- package/src/notify.js +51 -3
- package/src/orchestrator.js +35 -1
- package/src/pty.js +11 -6
- package/src/review.js +45 -11
- package/src/self-commands.js +153 -0
- package/src/session-convert.js +508 -0
- package/src/session-search.js +31 -0
- package/src/session.js +392 -46
- package/src/skills/browserActions.ts +984 -0
- package/src/skills.js +451 -24
- package/src/system-prompt.js +31 -25
- package/src/tools.js +81 -0
- package/src/vibedit/control.js +534 -0
- package/src/vibedit/electron.js +108 -0
- package/src/vibedit/files.js +171 -0
- package/src/vibedit/index.js +298 -0
- package/src/vibedit/overlay.js +1482 -0
- package/src/vibedit/prompts.js +245 -0
- package/src/vibedit/state.js +32 -0
- package/src/vim.js +410 -0
package/src/session-search.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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;
|
|
@@ -613,6 +839,14 @@ async function runOneSession(opts, registerSession) {
|
|
|
613
839
|
lastCommand = null;
|
|
614
840
|
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
615
841
|
|
|
842
|
+
// No command was tracked — precmd can fire at shell startup (especially
|
|
843
|
+
// in zsh) before any command executes. There's nothing to correct or
|
|
844
|
+
// route to the agent.
|
|
845
|
+
if (!lastCmd) {
|
|
846
|
+
discardPending();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
616
850
|
// ── Self-command detection (FIRST — before ANY other processing) ──
|
|
617
851
|
// Self-commands are pure local execution. They MUST bypass:
|
|
618
852
|
// - the noAi early-return (they don't need an LLM)
|
|
@@ -665,6 +899,109 @@ async function runOneSession(opts, registerSession) {
|
|
|
665
899
|
return;
|
|
666
900
|
}
|
|
667
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
|
+
|
|
668
1005
|
if (selfCmd.confirm) {
|
|
669
1006
|
const go = await ask(`Run ${selfCmd.action}?`, true, { onCancel: () => {} });
|
|
670
1007
|
if (!go) { session.childWrite('\r'); return; }
|
|
@@ -675,6 +1012,7 @@ async function runOneSession(opts, registerSession) {
|
|
|
675
1012
|
HELP_SUMMARY,
|
|
676
1013
|
HELP_SESSION_SUMMARY,
|
|
677
1014
|
setColors: (v) => { colorsEnabled = v; },
|
|
1015
|
+
setVoiceMode,
|
|
678
1016
|
});
|
|
679
1017
|
session.childWrite('\r');
|
|
680
1018
|
return;
|
|
@@ -757,6 +1095,7 @@ async function runOneSession(opts, registerSession) {
|
|
|
757
1095
|
`- Reason: ${decision.reason || 'deterministic match'}`,
|
|
758
1096
|
'',
|
|
759
1097
|
].join('\r\n')),
|
|
1098
|
+
notifyBody: cmd,
|
|
760
1099
|
});
|
|
761
1100
|
if (go) { correctionOrigin = cmd; audit.append({ kind: 'correction-run', proposed: decision.proposed }); session.childWrite(decision.proposed + '\r'); }
|
|
762
1101
|
return;
|
|
@@ -782,6 +1121,7 @@ async function runOneSession(opts, registerSession) {
|
|
|
782
1121
|
`- Reason: ${decision.reason || 'deterministic match'}`,
|
|
783
1122
|
'',
|
|
784
1123
|
].join('\r\n')),
|
|
1124
|
+
notifyBody: cmd,
|
|
785
1125
|
});
|
|
786
1126
|
if (go) { correctionOrigin = cmd; audit.append({ kind: 'correction-run', proposed: decision.proposed }); session.childWrite(decision.proposed + '\r'); }
|
|
787
1127
|
return;
|
|
@@ -910,8 +1250,11 @@ async function runOneSession(opts, registerSession) {
|
|
|
910
1250
|
}
|
|
911
1251
|
|
|
912
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;
|
|
913
1256
|
try {
|
|
914
|
-
const updated = await runAgent({ ...agentOpts, input:
|
|
1257
|
+
const updated = await runAgent({ ...agentOpts, input: fullTaskInput, history });
|
|
915
1258
|
history = trimHistory(updated || history);
|
|
916
1259
|
lastUpdated = updated;
|
|
917
1260
|
plan.tasks[i].status = 'completed';
|
|
@@ -993,9 +1336,12 @@ async function runOneSession(opts, registerSession) {
|
|
|
993
1336
|
const taskIndicator = routing.indicator
|
|
994
1337
|
? `\x1b[36m[shmakk task · ${routing.indicator}] (Ctrl-C to interrupt)\x1b[0m\r\n`
|
|
995
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;
|
|
996
1342
|
out(taskIndicator);
|
|
997
1343
|
try {
|
|
998
|
-
const updated = await runAgent({ ...agentOpts, input:
|
|
1344
|
+
const updated = await runAgent({ ...agentOpts, input: fullCmd, history });
|
|
999
1345
|
history = trimHistory(updated || history);
|
|
1000
1346
|
|
|
1001
1347
|
// TTS: speak the agent's response aloud if --tts is active
|