omnius 1.0.253 → 1.0.255

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/dist/index.js CHANGED
@@ -564799,7 +564799,7 @@ ${parts.join("\n")}
564799
564799
  disablePersistentMemory: options2?.disablePersistentMemory ?? false,
564800
564800
  disableCodebaseMap: options2?.disableCodebaseMap ?? false,
564801
564801
  sessionId: options2?.sessionId ?? "",
564802
- streamEnabled: options2?.streamEnabled ?? false,
564802
+ streamEnabled: options2?.streamEnabled ?? true,
564803
564803
  thinking: options2?.thinking ?? false,
564804
564804
  toolProfile: options2?.toolProfile ?? void 0,
564805
564805
  bruteForce: options2?.bruteForce ?? true,
@@ -669686,413 +669686,1309 @@ var init_chat_session = __esm({
669686
669686
  }
669687
669687
  });
669688
669688
 
669689
- // packages/cli/src/api/command-passthrough.ts
669690
- var command_passthrough_exports = {};
669691
- __export(command_passthrough_exports, {
669692
- runCommand: () => runCommand
669689
+ // packages/cli/src/tui/voicechat.ts
669690
+ var voicechat_exports = {};
669691
+ __export(voicechat_exports, {
669692
+ VoiceChatSession: () => VoiceChatSession
669693
669693
  });
669694
- function stripAnsi5(s2) {
669695
- return s2.replace(/\x1B(?:\[[\d;?]*[a-zA-Z]|\][^\x07\x1B]*[\x07\x1B]?|[@-Z\\-_])/g, "");
669696
- }
669697
- async function runCommand(input, opts) {
669698
- const start2 = Date.now();
669699
- const trimmed = input.trim();
669700
- const slashCmd = trimmed.startsWith("/") ? trimmed : "/" + trimmed;
669701
- const [rawCmd, ...rest] = slashCmd.slice(1).split(/\s+/);
669702
- const cmdName = rawCmd ?? "";
669703
- const argsStr = rest.join(" ");
669704
- const release = await acquireLock2();
669705
- try {
669706
- const quick2 = buildNonInteractiveSummary(cmdName, argsStr, opts?.config);
669707
- if (quick2) {
669708
- return {
669709
- ok: true,
669710
- command: cmdName,
669711
- args: argsStr,
669712
- kind: "handled",
669713
- output: quick2,
669714
- ansi: quick2,
669715
- durationMs: Date.now() - start2
669716
- };
669717
- }
669718
- const buf = [];
669719
- setContentWriteHook({
669720
- begin: () => {
669721
- },
669722
- end: () => {
669723
- },
669724
- redirect: () => (text2) => {
669725
- buf.push(text2);
669726
- }
669727
- });
669728
- const origWrite = process.stdout.write.bind(process.stdout);
669729
- process.stdout.write = function(chunk, ...rest2) {
669730
- if (typeof chunk === "string") buf.push(chunk);
669731
- else if (chunk instanceof Buffer) buf.push(chunk.toString("utf-8"));
669732
- const cb = rest2.find((r2) => typeof r2 === "function");
669733
- if (cb) cb();
669734
- return true;
669735
- };
669736
- let kind = "handled";
669737
- let errMsg;
669738
- try {
669739
- const ctx3 = buildSyntheticContext(opts?.config, opts?.repoRoot);
669740
- kind = await handleSlashCommand(slashCmd, ctx3);
669741
- } catch (e2) {
669742
- kind = "error";
669743
- errMsg = e2 instanceof Error ? e2.message : String(e2);
669744
- buf.push(`
669745
- [error] ${errMsg}
669746
- `);
669747
- } finally {
669748
- process.stdout.write = origWrite;
669749
- setContentWriteHook({ begin: () => {
669750
- }, end: () => {
669751
- } });
669752
- }
669753
- const ansi5 = buf.join("");
669754
- return {
669755
- ok: kind !== "error" && kind !== "not_a_command",
669756
- command: cmdName,
669757
- args: argsStr,
669758
- kind,
669759
- output: stripAnsi5(ansi5).trim(),
669760
- ansi: ansi5,
669761
- durationMs: Date.now() - start2,
669762
- error: errMsg
669763
- };
669764
- } finally {
669765
- release();
669766
- }
669694
+ import { EventEmitter as EventEmitter12 } from "node:events";
669695
+ function clamp0114(x) {
669696
+ return x < 0 ? 0 : x > 1 ? 1 : x;
669767
669697
  }
669768
- function buildNonInteractiveSummary(cmdName, _args, config) {
669769
- const cfg = config ?? loadConfig();
669770
- if (cmdName === "setup" || cmdName === "wizard") {
669771
- return [
669772
- "omnius setup",
669773
- "",
669774
- "The setup wizard is an interactive terminal flow. In the GUI command bridge it is summarized instead of opening prompts, installing software, starting Ollama, or pulling models.",
669775
- "",
669776
- `Current backend: ${cfg.backendType ?? "ollama"}`,
669777
- `Current endpoint: ${cfg.backendUrl ?? "http://127.0.0.1:11434"}`,
669778
- `Current model: ${cfg.model ?? "qwen3.5:latest"}`,
669779
- "",
669780
- "Available non-interactive setup actions:",
669781
- " /endpoint <url> Set or inspect the inference endpoint.",
669782
- " /model <name> Set the active model directly.",
669783
- " /models Show model-selection guidance.",
669784
- " /config Inspect persisted configuration.",
669785
- " /doctor Run diagnostics from the terminal if deeper repair is needed.",
669786
- "",
669787
- "Open a terminal and run `omnius`, then use /setup for the full guided wizard."
669788
- ].join("\n");
669789
- }
669790
- if (cmdName === "models") {
669791
- return [
669792
- "omnius models",
669793
- "",
669794
- "The model picker is interactive in the TUI. The GUI bridge does not probe remote model endpoints here, so it cannot hang on a stale backend.",
669795
- "",
669796
- `Active model: ${cfg.model ?? "qwen3.5:latest"}`,
669797
- `Endpoint: ${cfg.backendUrl ?? "http://127.0.0.1:11434"}`,
669798
- `Backend: ${cfg.backendType ?? "ollama"}`,
669799
- "",
669800
- "Use /model <name> to set a model directly, /endpoint to switch providers, or open the TUI for the searchable model picker."
669801
- ].join("\n");
669802
- }
669803
- return null;
669698
+ function alnumRatio(s2) {
669699
+ if (!s2) return 0;
669700
+ const al = (s2.match(/[\p{L}\p{N}]/gu) || []).length;
669701
+ return al / s2.length;
669804
669702
  }
669805
- function acquireLock2() {
669806
- let release;
669807
- const next = new Promise((res) => {
669808
- release = res;
669809
- });
669810
- const prev = _passthroughLock;
669811
- _passthroughLock = next;
669812
- return prev.then(() => release);
669703
+ function wordCount(s2) {
669704
+ const words = s2.trim().match(/[\p{L}\p{N}][\p{L}\p{N}'’_-]*/gu);
669705
+ return words ? words.length : 0;
669813
669706
  }
669814
- function buildSyntheticContext(config, repoRoot) {
669815
- const cfg = config ?? loadConfig();
669816
- const root = repoRoot ?? process.cwd();
669817
- let colorsEnabled = loadProjectSettings(root).colors ?? loadGlobalSettings().colors ?? true;
669818
- let selfModifyEnabled = loadProjectSettings(root).selfModify ?? loadGlobalSettings().selfModify ?? false;
669819
- return {
669820
- config: cfg,
669821
- repoRoot: root,
669822
- rl: makeRejectingReadline(),
669823
- setModel: (_model) => {
669824
- },
669825
- setVerbose: (_verbose) => {
669826
- },
669827
- setEndpoint: (_url, _t, _k) => {
669828
- },
669829
- deactivateStatusBar: () => {
669830
- },
669831
- disableMouse: () => {
669832
- },
669833
- enableMouse: () => {
669834
- },
669835
- isMouseEnabled: () => false,
669836
- lockFooter: () => {
669837
- },
669838
- unlockFooter: () => {
669839
- },
669840
- stopBanner: () => {
669841
- },
669842
- killEphemeral: () => {
669843
- },
669844
- setKeyPool: (_keys) => {
669845
- },
669846
- clearScreen: () => {
669847
- },
669848
- newSession: () => {
669849
- },
669850
- refreshBanner: () => {
669851
- },
669852
- exit: () => {
669853
- renderError("/quit and /exit are TUI-only — close the browser tab to end the GUI session.");
669854
- },
669855
- voiceToggle: async () => {
669856
- renderError(`voice ${TUI_ONLY_HINT} — use the GUI voice button instead.`);
669857
- return "voice not available in GUI";
669858
- },
669859
- voiceSetModel: async (_id2) => {
669860
- renderError(`voice model ${TUI_ONLY_HINT}`);
669861
- return "";
669862
- },
669863
- getColors: () => colorsEnabled,
669864
- setColors: (enabled2) => {
669865
- colorsEnabled = enabled2;
669866
- },
669867
- getSelfModify: () => selfModifyEnabled,
669868
- setSelfModify: (enabled2) => {
669869
- selfModifyEnabled = enabled2;
669870
- },
669871
- saveSettings: (settings) => {
669872
- saveProjectSettings(root, settings);
669873
- saveGlobalSettings(settings);
669874
- },
669875
- saveLocalSettings: (settings) => {
669876
- saveProjectSettings(root, settings);
669707
+ function repeatingCharPenalty(s2) {
669708
+ let maxRun = 1, cur = 1;
669709
+ for (let i2 = 1; i2 < s2.length; i2++) {
669710
+ if (s2[i2] === s2[i2 - 1]) cur++;
669711
+ else {
669712
+ if (cur > maxRun) maxRun = cur;
669713
+ cur = 1;
669877
669714
  }
669878
- };
669879
- }
669880
- function makeRejectingReadline() {
669881
- const reject = () => {
669882
- throw new Error(
669883
- "interactive prompts are not supported via the GUI command bridge — run this command from the TUI (open a terminal and type `omnius`)."
669884
- );
669885
- };
669886
- const noop2 = () => {
669887
- };
669888
- return {
669889
- question: (_q, _cb) => reject(),
669890
- close: noop2,
669891
- write: noop2,
669892
- on: () => noop2,
669893
- once: () => noop2,
669894
- off: () => noop2,
669895
- removeListener: () => noop2,
669896
- pause: noop2,
669897
- resume: noop2,
669898
- setPrompt: noop2,
669899
- prompt: noop2,
669900
- line: "",
669901
- cursor: 0,
669902
- terminal: false,
669903
- input: { isTTY: false, on: noop2, once: noop2, removeListener: noop2, pause: noop2, resume: noop2 },
669904
- output: { write: noop2, columns: 80, rows: 24, isTTY: false }
669905
- };
669906
- }
669907
- var _passthroughLock, TUI_ONLY_HINT;
669908
- var init_command_passthrough = __esm({
669909
- "packages/cli/src/api/command-passthrough.ts"() {
669910
- "use strict";
669911
- init_render();
669912
- init_commands();
669913
- init_config();
669914
- init_omnius_directory();
669915
- _passthroughLock = Promise.resolve();
669916
- TUI_ONLY_HINT = "(this command is TUI-only — no-op in GUI)";
669917
669715
  }
669918
- });
669919
-
669920
- // packages/cli/src/api/projects.ts
669921
- var projects_exports = {};
669922
- __export(projects_exports, {
669923
- _resetCurrentProject: () => _resetCurrentProject,
669924
- getCurrentProject: () => getCurrentProject,
669925
- listProjects: () => listProjects,
669926
- registerProject: () => registerProject,
669927
- renameProject: () => renameProject,
669928
- setCurrentProject: () => setCurrentProject,
669929
- unregisterProject: () => unregisterProject
669930
- });
669931
- import { readFileSync as readFileSync112, writeFileSync as writeFileSync74, mkdirSync as mkdirSync84, existsSync as existsSync135, statSync as statSync48, renameSync as renameSync11 } from "node:fs";
669932
- import { homedir as homedir47 } from "node:os";
669933
- import { basename as basename36, join as join148, resolve as resolve59 } from "node:path";
669934
- import { randomUUID as randomUUID18 } from "node:crypto";
669935
- function readAll2() {
669936
- try {
669937
- if (!existsSync135(PROJECTS_FILE)) return { projects: [], schemaVersion: 1 };
669938
- const raw = readFileSync112(PROJECTS_FILE, "utf8");
669939
- const parsed = JSON.parse(raw);
669940
- if (!parsed || !Array.isArray(parsed.projects)) return { projects: [], schemaVersion: 1 };
669941
- return { projects: parsed.projects, schemaVersion: 1 };
669942
- } catch {
669943
- return { projects: [], schemaVersion: 1 };
669716
+ if (cur > maxRun) maxRun = cur;
669717
+ return Math.min(1, Math.max(0, (maxRun - 3) / 10));
669718
+ }
669719
+ function computeSignalFromText(text2, confidence2) {
669720
+ const t2 = text2.trim();
669721
+ if (!t2) return 0;
669722
+ if (NOISE_ONLY_RE.test(t2)) return 0.05;
669723
+ const len = t2.length;
669724
+ const wc = wordCount(t2);
669725
+ const alpha = alnumRatio(t2);
669726
+ let score = 0;
669727
+ if (wc >= 6 && alpha >= 0.6) score = 0.85;
669728
+ else if (wc >= 3 && alpha >= 0.5) score = 0.7;
669729
+ else if (wc >= 2 && alpha >= 0.4) score = 0.5;
669730
+ else if (wc >= 1 && alpha >= 0.3 && len >= 4) score = 0.35;
669731
+ else score = 0.15;
669732
+ score -= repeatingCharPenalty(t2) * 0.4;
669733
+ if (typeof confidence2 === "number" && !Number.isNaN(confidence2)) {
669734
+ score = 0.7 * score + 0.3 * clamp0114(confidence2);
669944
669735
  }
669736
+ return clamp0114(score);
669945
669737
  }
669946
- function writeAll(file) {
669947
- mkdirSync84(OMNIUS_DIR3, { recursive: true });
669948
- const tmp = `${PROJECTS_FILE}.${randomUUID18().slice(0, 8)}.tmp`;
669949
- writeFileSync74(tmp, JSON.stringify(file, null, 2), "utf8");
669950
- renameSync11(tmp, PROJECTS_FILE);
669738
+ function truncateForLog(s2, n2) {
669739
+ return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "…";
669951
669740
  }
669952
- function listProjects() {
669953
- const { projects } = readAll2();
669954
- const alive = [];
669955
- for (const p2 of projects) {
669741
+ function extractToolJson(text2) {
669742
+ const lines = text2.split(/\r?\n/);
669743
+ for (const line of lines) {
669744
+ const t2 = line.trim();
669745
+ if (!t2.startsWith("{") || !t2.endsWith("}")) continue;
669956
669746
  try {
669957
- if (statSync48(p2.root).isDirectory()) alive.push(p2);
669747
+ const obj = JSON.parse(t2);
669748
+ if (typeof obj.tool === "string") {
669749
+ const name10 = obj.tool;
669750
+ const args = obj.args && typeof obj.args === "object" ? obj.args : {};
669751
+ return { name: name10, args };
669752
+ }
669958
669753
  } catch {
669959
669754
  }
669960
669755
  }
669961
- alive.sort((a2, b) => b.lastSeen - a2.lastSeen);
669962
- return alive;
669963
- }
669964
- function registerProject(root, pid) {
669965
- const canonical = resolve59(root);
669966
- const now = Date.now();
669967
- const file = readAll2();
669968
- const existing = file.projects.find((p2) => p2.root === canonical);
669969
- let entry;
669970
- if (existing) {
669971
- entry = {
669972
- ...existing,
669973
- lastSeen: now,
669974
- pid: pid ?? existing.pid,
669975
- omniusDir: join148(canonical, ".omnius")
669976
- };
669977
- file.projects = file.projects.map((p2) => p2.root === canonical ? entry : p2);
669978
- } else {
669979
- entry = {
669980
- root: canonical,
669981
- name: basename36(canonical) || canonical,
669982
- firstSeen: now,
669983
- lastSeen: now,
669984
- pid: pid ?? null,
669985
- omniusDir: join148(canonical, ".omnius")
669986
- };
669987
- file.projects.push(entry);
669988
- }
669989
- writeAll(file);
669990
- return entry;
669991
- }
669992
- function unregisterProject(root) {
669993
- const canonical = resolve59(root);
669994
- const file = readAll2();
669995
- const before = file.projects.length;
669996
- file.projects = file.projects.filter((p2) => p2.root !== canonical);
669997
- if (file.projects.length === before) return false;
669998
- writeAll(file);
669999
- return true;
670000
- }
670001
- function renameProject(root, name10) {
670002
- const canonical = resolve59(root);
670003
- const file = readAll2();
670004
- const idx = file.projects.findIndex((p2) => p2.root === canonical);
670005
- if (idx < 0) return null;
670006
- const next = { ...file.projects[idx], name: name10.trim() || file.projects[idx].name };
670007
- file.projects[idx] = next;
670008
- writeAll(file);
670009
- return next;
669756
+ return null;
670010
669757
  }
670011
- function getCurrentProject() {
670012
- if (!currentRoot) {
669758
+ function extractToolJsonLoose(text2) {
669759
+ const stripped = text2.replace(/```[a-zA-Z]*|```/g, "\n");
669760
+ const exact = extractToolJson(stripped);
669761
+ if (exact) return exact;
669762
+ const match = stripped.match(/[\{][\s\S]*[\}]/);
669763
+ if (match) {
670013
669764
  try {
670014
- if (existsSync135(CURRENT_FILE)) {
670015
- const persisted = readFileSync112(CURRENT_FILE, "utf8").trim();
670016
- if (persisted) currentRoot = persisted;
669765
+ const obj = JSON.parse(match[0]);
669766
+ if (typeof obj.tool === "string") {
669767
+ const args = obj.args && typeof obj.args === "object" ? obj.args : {};
669768
+ return { name: obj.tool, args };
670017
669769
  }
670018
669770
  } catch {
670019
669771
  }
670020
669772
  }
670021
- if (!currentRoot) return null;
670022
- const all2 = listProjects();
670023
- return all2.find((p2) => p2.root === currentRoot) ?? null;
670024
- }
670025
- function setCurrentProject(root) {
670026
- const canonical = resolve59(root);
670027
- const entry = listProjects().find((p2) => p2.root === canonical);
670028
- if (!entry) return null;
670029
- currentRoot = canonical;
670030
- try {
670031
- mkdirSync84(OMNIUS_DIR3, { recursive: true });
670032
- writeFileSync74(CURRENT_FILE, `${canonical}
670033
- `, "utf8");
670034
- } catch {
670035
- }
670036
- return entry;
669773
+ return null;
670037
669774
  }
670038
- function _resetCurrentProject() {
670039
- currentRoot = null;
669775
+ function stripToolJsonLines(text2) {
669776
+ const lines = text2.split(/\r?\n/);
669777
+ const kept = lines.filter((l2) => {
669778
+ const t2 = l2.trim();
669779
+ if (!t2.startsWith("{") || !t2.endsWith("}")) return true;
669780
+ try {
669781
+ const obj = JSON.parse(t2);
669782
+ return !(typeof obj.tool === "string");
669783
+ } catch {
669784
+ return true;
669785
+ }
669786
+ });
669787
+ return kept.join("\n").trim();
670040
669788
  }
670041
- var OMNIUS_DIR3, PROJECTS_FILE, CURRENT_FILE, currentRoot;
670042
- var init_projects = __esm({
670043
- "packages/cli/src/api/projects.ts"() {
669789
+ var VAD_SILENCE_MS, MAX_SEGMENT_MS, MAX_CONTEXT_TURNS, SYSTEM_PROMPT2, MIN_SIGNAL_SCORE, NOISE_ONLY_RE, VoiceChatSession;
669790
+ var init_voicechat = __esm({
669791
+ "packages/cli/src/tui/voicechat.ts"() {
670044
669792
  "use strict";
670045
- OMNIUS_DIR3 = join148(homedir47(), ".omnius");
670046
- PROJECTS_FILE = join148(OMNIUS_DIR3, "projects.json");
670047
- CURRENT_FILE = join148(OMNIUS_DIR3, "current-project");
670048
- currentRoot = null;
670049
- }
670050
- });
669793
+ VAD_SILENCE_MS = 3e3;
669794
+ MAX_SEGMENT_MS = 6500;
669795
+ MAX_CONTEXT_TURNS = 20;
669796
+ SYSTEM_PROMPT2 = `You are a voice assistant having a live spoken conversation. Keep responses extremely brief — 1-2 sentences max. You're speaking aloud, not writing. Be conversational, direct, and helpful. Don't use markdown or formatting — just natural speech.
670051
669797
 
670052
- // packages/cli/src/tui/mouse-filter.ts
670053
- var mouse_filter_exports = {};
670054
- __export(mouse_filter_exports, {
670055
- MouseFilterStream: () => MouseFilterStream
670056
- });
670057
- import { Transform } from "node:stream";
670058
- var MouseFilterStream;
670059
- var init_mouse_filter = __esm({
670060
- "packages/cli/src/tui/mouse-filter.ts"() {
670061
- "use strict";
670062
- MouseFilterStream = class extends Transform {
670063
- buffer = "";
670064
- onScroll = null;
670065
- onActivity = null;
670066
- onPointer = null;
670067
- onKeyboard = null;
670068
- flushTimer = null;
670069
- expectPrefixlessMouseUntil = 0;
670070
- constructor(scrollHandler, activityHandler, pointerHandler, keyboardHandler) {
669798
+ Rules:
669799
+ - Never invent environment facts (cwd, OS, specs, repo state). If you need a precise fact from the main agent, request a tool by outputting on a single line EXACTLY one JSON object: {"tool": string, "args": object} and nothing else. Then wait for the tool result before answering.
669800
+ - You may also request to relay a user task to the main agent by emitting {"tool":"voice_to_main","args":{"message":"...","start":true}}.
669801
+ - Prefer tools for factual queries; otherwise, answer directly with a short reply.`;
669802
+ MIN_SIGNAL_SCORE = 0.4;
669803
+ NOISE_ONLY_RE = /^(?:[.·…\s,;:!?\-–—_()\[\]{}"'`]+|(?:uh|um|erm|hmm|mm+|uhh+|umm+)[\s.!?]*)+$/i;
669804
+ VoiceChatSession = class extends EventEmitter12 {
669805
+ voice;
669806
+ listen;
669807
+ backendUrl;
669808
+ model;
669809
+ apiKey;
669810
+ runner;
669811
+ toolRelay = null;
669812
+ verbose = false;
669813
+ debugSnr = false;
669814
+ heuristicsEnabled = true;
669815
+ toolCatalogNote = null;
669816
+ // State machine
669817
+ _state = "IDLE";
669818
+ active = false;
669819
+ // Conversation context — own turns, separate from main agent
669820
+ context = [];
669821
+ turnCount = 0;
669822
+ // Transcripts — separate logs for user<->voice and relay voice<->main
669823
+ voiceTranscript = [];
669824
+ relayTranscript = [];
669825
+ // VAD segment capture
669826
+ captureBuffer = "";
669827
+ captureStartTime = 0;
669828
+ silenceTimer = null;
669829
+ maxSegmentTimer = null;
669830
+ lastSignalScore = null;
669831
+ // Abort control for inference
669832
+ abortController = null;
669833
+ // Callbacks
669834
+ onStatus;
669835
+ onUserSpeech;
669836
+ onPartialTranscript;
669837
+ onAgentSpeech;
669838
+ onStateChange;
669839
+ // Bound handlers for cleanup
669840
+ _onTranscript = null;
669841
+ _onError = null;
669842
+ _retryMicTimer = null;
669843
+ constructor(opts) {
670071
669844
  super();
670072
- this.onScroll = scrollHandler;
670073
- this.onActivity = activityHandler ?? null;
670074
- this.onPointer = pointerHandler ?? null;
670075
- this.onKeyboard = keyboardHandler ?? null;
670076
- }
670077
- _transform(chunk, _encoding, callback) {
670078
- this.buffer += chunk.toString();
670079
- this.processBuffer(callback);
670080
- }
670081
- processBuffer(callback) {
670082
- let output = "";
670083
- let i2 = 0;
670084
- while (i2 < this.buffer.length) {
670085
- const remaining = this.buffer.slice(i2);
670086
- const prefixlessMouse = this.matchPrefixlessSgrMouse(remaining);
670087
- if (prefixlessMouse) {
670088
- this.handleSgrMouse(prefixlessMouse);
670089
- i2 += prefixlessMouse.raw.length;
670090
- continue;
670091
- }
670092
- if (this.looksLikePartialPrefixlessSgrMouse(remaining)) {
670093
- break;
670094
- }
670095
- if (this.buffer[i2] === "\x1B") {
669845
+ this.voice = opts.voice;
669846
+ this.listen = opts.listen;
669847
+ this.backendUrl = opts.backendUrl.replace(/\/+$/, "");
669848
+ this.model = opts.model;
669849
+ this.apiKey = opts.apiKey ?? "";
669850
+ this.runner = opts.runner ?? null;
669851
+ this.toolRelay = opts.toolRelay ?? null;
669852
+ this.verbose = Boolean(opts.verbose);
669853
+ this.debugSnr = Boolean(opts.debugSnr);
669854
+ this.heuristicsEnabled = opts.heuristicsEnabled !== false;
669855
+ if (typeof opts.vadSilenceMs === "number" && opts.vadSilenceMs > 0) {
669856
+ this._vadSilenceMs = Math.floor(opts.vadSilenceMs);
669857
+ }
669858
+ this.onStatus = opts.onStatus ?? (() => {
669859
+ });
669860
+ this.onUserSpeech = opts.onUserSpeech ?? (() => {
669861
+ });
669862
+ this.onPartialTranscript = opts.onPartialTranscript ?? (() => {
669863
+ });
669864
+ this.onAgentSpeech = opts.onAgentSpeech ?? (() => {
669865
+ });
669866
+ this.onStateChange = opts.onStateChange ?? (() => {
669867
+ });
669868
+ }
669869
+ get state() {
669870
+ return this._state;
669871
+ }
669872
+ get isActive() {
669873
+ return this.active;
669874
+ }
669875
+ // ---------------------------------------------------------------------------
669876
+ // State transitions
669877
+ // ---------------------------------------------------------------------------
669878
+ setState(next) {
669879
+ if (this._state === next) return;
669880
+ const prev = this._state;
669881
+ this._state = next;
669882
+ this.onStateChange(next);
669883
+ this.emit("stateChange", { from: prev, to: next });
669884
+ }
669885
+ // ---------------------------------------------------------------------------
669886
+ // Start / Stop
669887
+ // ---------------------------------------------------------------------------
669888
+ async start() {
669889
+ if (this.active) return;
669890
+ if (!this.voice.enabled || !this.voice.ready) {
669891
+ this.onStatus("Enabling voice engine...");
669892
+ await this.voice.toggle();
669893
+ }
669894
+ this.active = true;
669895
+ this.context = [{ role: "system", content: SYSTEM_PROMPT2 }];
669896
+ if (this.toolRelay) {
669897
+ this.toolCatalogNote = `Available tools (emit one-line JSON: {"tool":string,"args":object}):
669898
+ - voice_env{} → environment facts (cwd, os, cpu, mem)
669899
+ - voice_status{} → main-agent status
669900
+ - voice_list_files{dir?: string='.'} → list directory (bounded)
669901
+ - voice_read_file{path: string, max?: number=2048} → read file snippet
669902
+ - voice_to_main{message: string, start?: boolean=true} → relay/start task`;
669903
+ this.context.push({ role: "system", content: this.toolCatalogNote });
669904
+ }
669905
+ this.turnCount = 0;
669906
+ if (this.verbose) this.onStatus("VoiceChat active — LISTENING");
669907
+ this._onTranscript = (...args) => {
669908
+ let text2;
669909
+ let isFinal;
669910
+ let snr;
669911
+ let confidence2;
669912
+ if (typeof args[0] === "object" && args[0] !== null) {
669913
+ const evt = args[0];
669914
+ text2 = evt.text ?? "";
669915
+ isFinal = evt.isFinal ?? false;
669916
+ snr = evt.snr;
669917
+ confidence2 = evt.confidence;
669918
+ } else {
669919
+ text2 = String(args[0] ?? "");
669920
+ isFinal = Boolean(args[1]);
669921
+ }
669922
+ if (!text2.trim()) return;
669923
+ this.handleTranscript(text2.trim(), isFinal, snr, confidence2);
669924
+ };
669925
+ this._onError = (err) => {
669926
+ const msg = err instanceof Error ? err.message : String(err);
669927
+ this.onStatus(`ASR error (voicechat continues without mic): ${msg.slice(0, 80)}`);
669928
+ if (this.active && !this._retryMicTimer) {
669929
+ this._retryMicTimer = setTimeout(async () => {
669930
+ this._retryMicTimer = null;
669931
+ if (!this.active) return;
669932
+ try {
669933
+ await this.listen.stop().catch(() => {
669934
+ });
669935
+ await this.listen.start();
669936
+ if (this.verbose) this.onStatus("Mic auto-recovered — LISTENING");
669937
+ } catch {
669938
+ }
669939
+ }, 1e3);
669940
+ }
669941
+ };
669942
+ this.listen.on("transcript", this._onTranscript);
669943
+ this.listen.on("error", this._onError);
669944
+ try {
669945
+ await this.listen.start();
669946
+ this.setState("LISTENING");
669947
+ if (this.verbose) this.onStatus("Mic active — LISTENING for speech...");
669948
+ } catch (err) {
669949
+ this.onStatus(`Mic failed: ${err instanceof Error ? err.message : String(err)}. VoiceChat active without mic.`);
669950
+ this.setState("LISTENING");
669951
+ }
669952
+ }
669953
+ async stop() {
669954
+ if (!this.active) return;
669955
+ this.active = false;
669956
+ if (this.abortController) {
669957
+ this.abortController.abort();
669958
+ this.abortController = null;
669959
+ }
669960
+ if (this.silenceTimer) {
669961
+ clearTimeout(this.silenceTimer);
669962
+ this.silenceTimer = null;
669963
+ }
669964
+ if (this.maxSegmentTimer) {
669965
+ clearTimeout(this.maxSegmentTimer);
669966
+ this.maxSegmentTimer = null;
669967
+ }
669968
+ if (this.captureBuffer.trim() && (this._state === "CAPTURING" || this._state === "TRANSCRIBING")) {
669969
+ this.finalizeSegment();
669970
+ }
669971
+ if (this._onTranscript) {
669972
+ this.listen.removeAllListeners("transcript");
669973
+ this._onTranscript = null;
669974
+ }
669975
+ if (this._onError) {
669976
+ this.listen.removeAllListeners("error");
669977
+ this._onError = null;
669978
+ }
669979
+ try {
669980
+ await this.listen.stop();
669981
+ } catch {
669982
+ }
669983
+ this.setState("IDLE");
669984
+ if (this.verbose) this.onStatus("VoiceChat ended");
669985
+ this.emit("stopped");
669986
+ }
669987
+ // ---------------------------------------------------------------------------
669988
+ // Transcript handling — VAD-style segment capture (Voryn pattern)
669989
+ // ---------------------------------------------------------------------------
669990
+ handleTranscript(text2, isFinal, snr, confidence2) {
669991
+ if (!this.active) return;
669992
+ if (this._state !== "LISTENING" && this._state !== "CAPTURING") {
669993
+ return;
669994
+ }
669995
+ if (this._state === "LISTENING") {
669996
+ this.setState("CAPTURING");
669997
+ this.captureBuffer = "";
669998
+ this.captureStartTime = Date.now();
669999
+ this.maxSegmentTimer = setTimeout(() => {
670000
+ if (this._state === "CAPTURING") {
670001
+ this.finalizeSegment();
670002
+ }
670003
+ }, MAX_SEGMENT_MS);
670004
+ }
670005
+ this.captureBuffer = text2;
670006
+ this.lastSignalScore = typeof snr === "number" && !Number.isNaN(snr) ? clamp0114(snr) : computeSignalFromText(text2, confidence2);
670007
+ this.emit("snr", { score: this.lastSignalScore });
670008
+ this.onPartialTranscript(text2);
670009
+ if (this.silenceTimer) clearTimeout(this.silenceTimer);
670010
+ const waitMs = this._vadSilenceMs ?? VAD_SILENCE_MS;
670011
+ this.silenceTimer = setTimeout(() => {
670012
+ if (this._state === "CAPTURING") {
670013
+ this.finalizeSegment();
670014
+ }
670015
+ }, waitMs);
670016
+ }
670017
+ // ---------------------------------------------------------------------------
670018
+ // Segment finalization → Transcribing → Thinking → Speaking
670019
+ // ---------------------------------------------------------------------------
670020
+ finalizeSegment() {
670021
+ const text2 = this.captureBuffer.trim();
670022
+ if (this.silenceTimer) {
670023
+ clearTimeout(this.silenceTimer);
670024
+ this.silenceTimer = null;
670025
+ }
670026
+ if (this.maxSegmentTimer) {
670027
+ clearTimeout(this.maxSegmentTimer);
670028
+ this.maxSegmentTimer = null;
670029
+ }
670030
+ this.captureBuffer = "";
670031
+ if (!text2) {
670032
+ this.setState("LISTENING");
670033
+ return;
670034
+ }
670035
+ const score = this.lastSignalScore ?? computeSignalFromText(text2);
670036
+ if (score < MIN_SIGNAL_SCORE || NOISE_ONLY_RE.test(text2)) {
670037
+ if (this.debugSnr) this.onStatus(`Ignoring low-signal utterance (SNR:${score.toFixed(2)}): ${truncateForLog(text2, 48)}`);
670038
+ this.emit("snrFiltered", { score, text: text2 });
670039
+ this.setState("LISTENING");
670040
+ this.captureBuffer = "";
670041
+ this.lastSignalScore = null;
670042
+ return;
670043
+ }
670044
+ this.setState("TRANSCRIBING");
670045
+ this.onUserSpeech(text2);
670046
+ this.voiceTranscript.push({ role: "user", content: text2, ts: Date.now() });
670047
+ this.context.push({ role: "user", content: text2 });
670048
+ this.turnCount++;
670049
+ if (this.runner) {
670050
+ try {
670051
+ this.runner.injectUserMessage(`[VOICECHAT] ${text2}`);
670052
+ } catch {
670053
+ }
670054
+ }
670055
+ while (this.context.length > MAX_CONTEXT_TURNS + 1) {
670056
+ this.context.splice(1, 1);
670057
+ }
670058
+ this.think();
670059
+ }
670060
+ // ---------------------------------------------------------------------------
670061
+ // Direct Ollama inference (not through main agent runner)
670062
+ // ---------------------------------------------------------------------------
670063
+ async think() {
670064
+ if (!this.active) return;
670065
+ this.setState("THINKING");
670066
+ if (this.verbose) this.onStatus("Thinking...");
670067
+ this.abortController = new AbortController();
670068
+ try {
670069
+ if (this.toolRelay?.contextSnapshot) {
670070
+ try {
670071
+ const snap = await Promise.resolve(this.toolRelay.contextSnapshot());
670072
+ if (snap && snap.trim()) {
670073
+ this.context.push({ role: "system", content: `Context snapshot (read-only):
670074
+ ${snap.trim()}` });
670075
+ }
670076
+ } catch {
670077
+ }
670078
+ }
670079
+ const lastUser = [...this.context].reverse().find((m2) => m2.role === "user")?.content || "";
670080
+ let preAnswered = false;
670081
+ if (this.heuristicsEnabled && this.toolRelay && lastUser) {
670082
+ const lower = lastUser.toLowerCase();
670083
+ const wantList = /(list|show|explore|browse|what's in|whats in|contents).*(dir|directory|folder|files)/.test(lower);
670084
+ const wantEnv = /(what\s+dir|cwd|current\s+dir|working\s+directory|where\s+are\s+you)/.test(lower);
670085
+ const readMatch = lastUser.match(/(?:read|open|show)\s+file\s+([\w./\\-]+)\b/i);
670086
+ const toMainMatch = lastUser.match(/^(?:start|run|do)\s+(.{5,})$/i);
670087
+ try {
670088
+ if (wantEnv) {
670089
+ const out = await this.toolRelay.call("voice_env", {});
670090
+ this.context.push({ role: "system", content: `Tool voice_env result (authoritative):
670091
+ ${out}` });
670092
+ preAnswered = true;
670093
+ } else if (wantList) {
670094
+ const out = await this.toolRelay.call("voice_list_files", { dir: "." });
670095
+ this.context.push({ role: "system", content: `Tool voice_list_files result (authoritative):
670096
+ ${out}` });
670097
+ preAnswered = true;
670098
+ } else if (readMatch) {
670099
+ const out = await this.toolRelay.call("voice_read_file", { path: readMatch[1], max: 1024 });
670100
+ this.context.push({ role: "system", content: `Tool voice_read_file result (authoritative):
670101
+ ${out}` });
670102
+ preAnswered = true;
670103
+ } else if (toMainMatch) {
670104
+ const msg = toMainMatch[1].trim();
670105
+ const out = await this.toolRelay.call("voice_to_main", { message: msg, start: true });
670106
+ this.relayTranscript.push({ dir: "toMain", content: msg, ts: Date.now() });
670107
+ this.context.push({ role: "system", content: `Tool voice_to_main result (authoritative):
670108
+ ${out}` });
670109
+ preAnswered = true;
670110
+ }
670111
+ } catch {
670112
+ }
670113
+ }
670114
+ let response = "";
670115
+ for (let i2 = 0; i2 < 3; i2++) {
670116
+ response = await this.streamOllamaInference(this.abortController.signal);
670117
+ if (!this.toolRelay) break;
670118
+ const toolReq = extractToolJsonLoose(response);
670119
+ if (!toolReq) break;
670120
+ const { name: name10, args } = toolReq;
670121
+ let toolOutput = "";
670122
+ try {
670123
+ toolOutput = await this.toolRelay.call(name10, args);
670124
+ } catch (e2) {
670125
+ toolOutput = `Tool ${name10} failed: ${e2 instanceof Error ? e2.message : String(e2)}`;
670126
+ }
670127
+ if (name10 === "voice_to_main") {
670128
+ const msg = typeof args?.message === "string" ? String(args.message) : "";
670129
+ if (msg) this.relayTranscript.push({ dir: "toMain", content: msg, ts: Date.now() });
670130
+ }
670131
+ this.context.push({ role: "system", content: `Tool ${name10} result (authoritative):
670132
+ ${toolOutput}` });
670133
+ }
670134
+ if (!this.active) return;
670135
+ if (this.heuristicsEnabled && this.toolRelay && /\b(can't|cannot)\b/i.test(response) && this.toolCatalogNote) {
670136
+ this.context.push({ role: "system", content: `You have tools. Use them. ${this.toolCatalogNote}` });
670137
+ response = await this.streamOllamaInference(this.abortController.signal);
670138
+ }
670139
+ if (response.trim()) {
670140
+ const finalSpoken = stripToolJsonLines(response.trim());
670141
+ this.context.push({ role: "assistant", content: finalSpoken });
670142
+ this.setState("SPEAKING");
670143
+ this.onAgentSpeech(finalSpoken);
670144
+ try {
670145
+ this.listen.pause();
670146
+ } catch {
670147
+ }
670148
+ this.voice.speak(finalSpoken);
670149
+ this.voiceTranscript.push({ role: "assistant", content: finalSpoken, ts: Date.now() });
670150
+ this.voiceTranscript.push({ role: "assistant", content: response.trim(), ts: Date.now() });
670151
+ if (this.runner) {
670152
+ this.injectSummary();
670153
+ }
670154
+ if (typeof this.voice.waitUntilIdle === "function") {
670155
+ try {
670156
+ await this.voice.waitUntilIdle();
670157
+ } catch {
670158
+ }
670159
+ } else {
670160
+ const estimatedMs = Math.max(1500, response.length / 5 * (6e4 / 150));
670161
+ await new Promise((r2) => setTimeout(r2, estimatedMs));
670162
+ }
670163
+ }
670164
+ } catch (err) {
670165
+ if (!this.active) return;
670166
+ const msg = err instanceof Error ? err.message : String(err);
670167
+ if (!msg.includes("abort")) {
670168
+ this.onStatus(`Inference error: ${msg.slice(0, 100)}`);
670169
+ }
670170
+ } finally {
670171
+ this.abortController = null;
670172
+ }
670173
+ if (this.active) {
670174
+ try {
670175
+ await this.listen.resume();
670176
+ } catch {
670177
+ }
670178
+ this.setState("LISTENING");
670179
+ if (this.verbose) this.onStatus("LISTENING...");
670180
+ }
670181
+ }
670182
+ /**
670183
+ * Stream inference. Tries native Ollama /api/chat first (supports think:false
670184
+ * for reasoning models), falls back to OpenAI-compat /v1/chat/completions.
670185
+ */
670186
+ async streamOllamaInference(signal) {
670187
+ const baseUrl = this.backendUrl.replace(/\/v1\/?$/, "");
670188
+ const headers = { "Content-Type": "application/json" };
670189
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
670190
+ try {
670191
+ const nativeBody = JSON.stringify({
670192
+ model: this.model,
670193
+ messages: this.context,
670194
+ stream: true,
670195
+ think: false,
670196
+ // Disable reasoning — voice chat needs fast, direct responses
670197
+ options: { temperature: 0.7, num_predict: 256 }
670198
+ });
670199
+ const res2 = await fetch(`${baseUrl}/api/chat`, {
670200
+ method: "POST",
670201
+ headers,
670202
+ body: nativeBody,
670203
+ signal
670204
+ });
670205
+ if (res2.ok) {
670206
+ return await this.parseOllamaNativeStream(res2, signal);
670207
+ }
670208
+ } catch (err) {
670209
+ const msg = err instanceof Error ? err.message : "";
670210
+ if (msg.includes("abort")) throw err;
670211
+ }
670212
+ const openaiBody = JSON.stringify({
670213
+ model: this.model,
670214
+ messages: this.context,
670215
+ stream: true,
670216
+ temperature: 0.7,
670217
+ max_tokens: 1024
670218
+ });
670219
+ const endpoint = baseUrl.includes("/v1") ? `${baseUrl}/chat/completions` : `${baseUrl}/v1/chat/completions`;
670220
+ const res = await fetch(endpoint, { method: "POST", headers, body: openaiBody, signal });
670221
+ if (!res.ok) {
670222
+ const errText = await res.text().catch(() => "unknown");
670223
+ throw new Error(`Inference ${res.status}: ${errText.slice(0, 200)}`);
670224
+ }
670225
+ return await this.parseOpenAIStream(res);
670226
+ }
670227
+ /** Parse native Ollama /api/chat streaming response (NDJSON, not SSE) */
670228
+ async parseOllamaNativeStream(res, _signal) {
670229
+ const reader = res.body?.getReader();
670230
+ if (!reader) throw new Error("No response body");
670231
+ const decoder = new TextDecoder();
670232
+ let fullText = "";
670233
+ let buffer2 = "";
670234
+ while (true) {
670235
+ const { done, value: value2 } = await reader.read();
670236
+ if (done) break;
670237
+ buffer2 += decoder.decode(value2, { stream: true });
670238
+ const lines = buffer2.split("\n");
670239
+ buffer2 = lines.pop() ?? "";
670240
+ for (const line of lines) {
670241
+ if (!line.trim()) continue;
670242
+ try {
670243
+ const parsed = JSON.parse(line);
670244
+ const content = parsed.message?.content;
670245
+ const thinking = parsed.message?.thinking;
670246
+ if (content && thinking === void 0) {
670247
+ fullText += content;
670248
+ }
670249
+ if (parsed.done) return fullText;
670250
+ } catch {
670251
+ }
670252
+ }
670253
+ }
670254
+ return fullText;
670255
+ }
670256
+ /** Parse OpenAI-compat SSE streaming response */
670257
+ async parseOpenAIStream(res) {
670258
+ const reader = res.body?.getReader();
670259
+ if (!reader) throw new Error("No response body");
670260
+ const decoder = new TextDecoder();
670261
+ let fullText = "";
670262
+ let buffer2 = "";
670263
+ while (true) {
670264
+ const { done, value: value2 } = await reader.read();
670265
+ if (done) break;
670266
+ buffer2 += decoder.decode(value2, { stream: true });
670267
+ const lines = buffer2.split("\n");
670268
+ buffer2 = lines.pop() ?? "";
670269
+ for (const line of lines) {
670270
+ const trimmed = line.trim();
670271
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
670272
+ const data = trimmed.slice(6);
670273
+ if (data === "[DONE]") continue;
670274
+ try {
670275
+ const parsed = JSON.parse(data);
670276
+ const delta = parsed.choices?.[0]?.delta?.content;
670277
+ if (delta) fullText += delta;
670278
+ } catch {
670279
+ }
670280
+ }
670281
+ }
670282
+ return fullText;
670283
+ }
670284
+ // ---------------------------------------------------------------------------
670285
+ // Summary injection to main agent
670286
+ // ---------------------------------------------------------------------------
670287
+ injectSummary() {
670288
+ if (!this.runner) return;
670289
+ const recentTurns = this.context.filter((t2) => t2.role !== "system").slice(-8).map((t2) => `${t2.role === "user" ? "User" : "Assistant"}: ${t2.content}`).join("\n");
670290
+ this.runner.injectUserMessage(
670291
+ `[VOICECHAT SUMMARY] Parallel voice liaison update (for awareness only). Continue your current task; do not respond to this directly.
670292
+
670293
+ ${recentTurns}`
670294
+ );
670295
+ }
670296
+ /** Enqueue narration from main agent events into the voice channel */
670297
+ enqueueAgentNarration(text2, subordinate = true) {
670298
+ if (!text2 || !this.active) return;
670299
+ this.relayTranscript.push({ dir: "fromMain", content: text2, ts: Date.now() });
670300
+ if (subordinate) this.voice.speakSubordinate(text2);
670301
+ else this.voice.speak(text2);
670302
+ }
670303
+ /** Get copies of transcripts for UI/debugging */
670304
+ getTranscripts() {
670305
+ return {
670306
+ voice: this.voiceTranscript.slice(-200),
670307
+ relay: this.relayTranscript.slice(-200)
670308
+ };
670309
+ }
670310
+ };
670311
+ }
670312
+ });
670313
+
670314
+ // packages/cli/src/api/voice-runtime.ts
670315
+ var voice_runtime_exports = {};
670316
+ __export(voice_runtime_exports, {
670317
+ _resetForTests: () => _resetForTests,
670318
+ ensureRuntime: () => ensureRuntime,
670319
+ feedAudioFromClient: () => feedAudioFromClient,
670320
+ getDaemonListenEngine: () => getDaemonListenEngine,
670321
+ getRuntimeStatus: () => getRuntimeStatus,
670322
+ getVoiceBus: () => getVoiceBus,
670323
+ getVoiceEngine: () => getVoiceEngine,
670324
+ isVoiceChatActive: () => isVoiceChatActive,
670325
+ listClients: () => listClients,
670326
+ registerClient: () => registerClient,
670327
+ startVoiceChat: () => startVoiceChat,
670328
+ stopVoiceChat: () => stopVoiceChat,
670329
+ synthesizeAndBroadcast: () => synthesizeAndBroadcast,
670330
+ synthesizeToWav: () => synthesizeToWav,
670331
+ unregisterClient: () => unregisterClient
670332
+ });
670333
+ import { EventEmitter as EventEmitter13 } from "node:events";
670334
+ function getVoiceEngine() {
670335
+ if (!_voiceEngine) {
670336
+ _voiceEngine = new VoiceEngine();
670337
+ }
670338
+ return _voiceEngine;
670339
+ }
670340
+ function getDaemonListenEngine() {
670341
+ if (!_listenEngine) _listenEngine = getListenEngine();
670342
+ return _listenEngine;
670343
+ }
670344
+ function getVoiceBus() {
670345
+ if (!_bus) _bus = new EventEmitter13();
670346
+ return _bus;
670347
+ }
670348
+ function getRuntimeStatus() {
670349
+ return {
670350
+ state: _state3,
670351
+ voiceEnabled: _voiceEngine?.enabled ?? false,
670352
+ voiceReady: _voiceEngine?.ready ?? false,
670353
+ voiceModelId: _voiceEngine?.modelId ?? null,
670354
+ cloneRef: _voiceEngine?.luxttsCloneRef ?? null,
670355
+ listenActive: _listenEngine?.isActive ?? false,
670356
+ listenPaused: _listenEngine?.isPaused ?? false,
670357
+ clientCount: _clients2.size,
670358
+ loadedAt: _loadedAt,
670359
+ lastError: _lastError
670360
+ };
670361
+ }
670362
+ async function ensureRuntime() {
670363
+ if (_state3 === "loading" || _state3 === "listening" || _state3 === "speaking") return;
670364
+ setState("loading");
670365
+ try {
670366
+ const voice = getVoiceEngine();
670367
+ const listen = getDaemonListenEngine();
670368
+ if (!voice.enabled) {
670369
+ await voice.toggle();
670370
+ }
670371
+ if (!listen.isActive) {
670372
+ try {
670373
+ await listen.start();
670374
+ } catch (err) {
670375
+ const m2 = err instanceof Error ? err.message : String(err);
670376
+ _lastError = `listen.start() failed: ${m2}`;
670377
+ getVoiceBus().emit("error", _lastError);
670378
+ }
670379
+ }
670380
+ _loadedAt = Date.now();
670381
+ setState("listening");
670382
+ wireListenToBus();
670383
+ } catch (err) {
670384
+ const m2 = err instanceof Error ? err.message : String(err);
670385
+ _lastError = m2;
670386
+ setState("error");
670387
+ throw err;
670388
+ }
670389
+ }
670390
+ async function registerClient(handle2) {
670391
+ if (_shutdownTimer) {
670392
+ clearTimeout(_shutdownTimer);
670393
+ _shutdownTimer = null;
670394
+ }
670395
+ _clients2.set(handle2.id, handle2);
670396
+ if (_clients2.size === 1 && (_state3 === "idle" || _state3 === "error")) {
670397
+ try {
670398
+ await ensureRuntime();
670399
+ } catch (err) {
670400
+ _clients2.delete(handle2.id);
670401
+ throw err;
670402
+ }
670403
+ }
670404
+ }
670405
+ function unregisterClient(id) {
670406
+ _clients2.delete(id);
670407
+ if (_clients2.size === 0 && _shutdownTimer === null) {
670408
+ _shutdownTimer = setTimeout(() => {
670409
+ _shutdownTimer = null;
670410
+ try {
670411
+ _listenEngine?.pause?.();
670412
+ } catch {
670413
+ }
670414
+ }, IDLE_SHUTDOWN_MS);
670415
+ }
670416
+ }
670417
+ function feedAudioFromClient(clientId, pcmChunk) {
670418
+ if (_ttsSpeaking) return;
670419
+ const listen = _listenEngine;
670420
+ if (!listen || !listen.isActive) return;
670421
+ try {
670422
+ const transcriber = listen.liveTranscriber;
670423
+ if (transcriber?.write) transcriber.write(pcmChunk);
670424
+ } catch {
670425
+ }
670426
+ }
670427
+ async function synthesizeToWav(text2, format3 = "wav") {
670428
+ await ensureRuntime();
670429
+ const voice = _voiceEngine;
670430
+ if (!voice || !voice.ready || !voice.synthesizeToPCM) return null;
670431
+ const result = await voice.synthesizeToPCM(text2);
670432
+ if (!result || !result.pcm || result.pcm.length === 0) return null;
670433
+ const sampleRate = result.sampleRate;
670434
+ const pcm = result.pcm;
670435
+ if (format3 === "pcm") return { bytes: pcm, sampleRate, format: format3 };
670436
+ const header = Buffer.alloc(44);
670437
+ header.write("RIFF", 0);
670438
+ header.writeUInt32LE(36 + pcm.length, 4);
670439
+ header.write("WAVE", 8);
670440
+ header.write("fmt ", 12);
670441
+ header.writeUInt32LE(16, 16);
670442
+ header.writeUInt16LE(1, 20);
670443
+ header.writeUInt16LE(1, 22);
670444
+ header.writeUInt32LE(sampleRate, 24);
670445
+ header.writeUInt32LE(sampleRate * 2, 28);
670446
+ header.writeUInt16LE(2, 32);
670447
+ header.writeUInt16LE(16, 34);
670448
+ header.write("data", 36);
670449
+ header.writeUInt32LE(pcm.length, 40);
670450
+ return { bytes: Buffer.concat([header, pcm]), sampleRate, format: format3 };
670451
+ }
670452
+ async function synthesizeAndBroadcast(text2) {
670453
+ const voice = _voiceEngine;
670454
+ if (!voice || !voice.ready) return;
670455
+ if (!voice.synthesizeToPCM) {
670456
+ getVoiceBus().emit("error", "voice engine has no synthesizeToPCM");
670457
+ return;
670458
+ }
670459
+ setSpeaking(true);
670460
+ try {
670461
+ const result = await voice.synthesizeToPCM(text2);
670462
+ if (!result || !result.pcm || result.pcm.length === 0) return;
670463
+ getVoiceBus().emit("agent_text", { text: text2 });
670464
+ getVoiceBus().emit("tts_pcm", result.pcm, result.sampleRate);
670465
+ } catch (err) {
670466
+ const m2 = err instanceof Error ? err.message : String(err);
670467
+ getVoiceBus().emit("error", `tts: ${m2}`);
670468
+ } finally {
670469
+ setSpeaking(false);
670470
+ }
670471
+ }
670472
+ function listClients() {
670473
+ return Array.from(_clients2.values());
670474
+ }
670475
+ async function startVoiceChat(opts) {
670476
+ if (_voiceChatSession?.isActive) {
670477
+ return { ok: true, message: "VoiceChat already running" };
670478
+ }
670479
+ await ensureRuntime();
670480
+ const voice = getVoiceEngine();
670481
+ const listen = getDaemonListenEngine();
670482
+ if (!voice.ready) return { ok: false, message: "Voice engine not ready" };
670483
+ _voiceChatSession = new VoiceChatSession({
670484
+ voice,
670485
+ listen,
670486
+ backendUrl: opts.backendUrl,
670487
+ model: opts.model,
670488
+ apiKey: opts.apiKey,
670489
+ verbose: opts.verbose === true,
670490
+ onStatus: (msg) => getVoiceBus().emit("status", msg),
670491
+ onUserSpeech: (text2) => getVoiceBus().emit("transcript", { text: text2, final: true }),
670492
+ onPartialTranscript: (text2) => getVoiceBus().emit("transcript", { text: text2, final: false }),
670493
+ onAgentSpeech: (text2) => getVoiceBus().emit("agent_text", { text: text2 }),
670494
+ onStateChange: (s2) => getVoiceBus().emit("session_state", s2)
670495
+ });
670496
+ await _voiceChatSession.start();
670497
+ setState("listening");
670498
+ return { ok: true, message: "VoiceChat started" };
670499
+ }
670500
+ async function stopVoiceChat() {
670501
+ if (!_voiceChatSession) return { ok: true, message: "No active session" };
670502
+ try {
670503
+ if (_voiceChatSession.stop) {
670504
+ await _voiceChatSession.stop();
670505
+ }
670506
+ } catch {
670507
+ }
670508
+ _voiceChatSession = null;
670509
+ setState(_listenEngine?.isActive ? "listening" : "idle");
670510
+ return { ok: true, message: "VoiceChat stopped" };
670511
+ }
670512
+ function isVoiceChatActive() {
670513
+ return _voiceChatSession?.isActive ?? false;
670514
+ }
670515
+ function setState(s2) {
670516
+ if (_state3 === s2) return;
670517
+ _state3 = s2;
670518
+ getVoiceBus().emit("state", s2);
670519
+ }
670520
+ function setSpeaking(speaking) {
670521
+ _ttsSpeaking = speaking;
670522
+ if (speaking) {
670523
+ setState("speaking");
670524
+ getVoiceBus().emit("tts_start");
670525
+ } else {
670526
+ setState(_listenEngine?.isActive ? "listening" : "idle");
670527
+ getVoiceBus().emit("tts_end");
670528
+ }
670529
+ }
670530
+ function wireListenToBus() {
670531
+ if (_wired) return;
670532
+ if (!_listenEngine) return;
670533
+ _wired = true;
670534
+ _listenEngine.on("transcript", (...args) => {
670535
+ const payload = args[0];
670536
+ if (!payload || typeof payload.text !== "string") return;
670537
+ getVoiceBus().emit("transcript", payload);
670538
+ });
670539
+ }
670540
+ function _resetForTests() {
670541
+ _state3 = "idle";
670542
+ _loadedAt = null;
670543
+ _lastError = null;
670544
+ _clients2.clear();
670545
+ _ttsSpeaking = false;
670546
+ if (_shutdownTimer) {
670547
+ clearTimeout(_shutdownTimer);
670548
+ _shutdownTimer = null;
670549
+ }
670550
+ }
670551
+ var _voiceEngine, _listenEngine, _voiceChatSession, _bus, _state3, _loadedAt, _lastError, _clients2, _ttsSpeaking, _shutdownTimer, IDLE_SHUTDOWN_MS, _wired;
670552
+ var init_voice_runtime = __esm({
670553
+ "packages/cli/src/api/voice-runtime.ts"() {
670554
+ "use strict";
670555
+ init_voice();
670556
+ init_listen();
670557
+ init_voicechat();
670558
+ _voiceEngine = null;
670559
+ _listenEngine = null;
670560
+ _voiceChatSession = null;
670561
+ _bus = null;
670562
+ _state3 = "idle";
670563
+ _loadedAt = null;
670564
+ _lastError = null;
670565
+ _clients2 = /* @__PURE__ */ new Map();
670566
+ _ttsSpeaking = false;
670567
+ _shutdownTimer = null;
670568
+ IDLE_SHUTDOWN_MS = 6e4;
670569
+ _wired = false;
670570
+ }
670571
+ });
670572
+
670573
+ // packages/cli/src/api/command-passthrough.ts
670574
+ var command_passthrough_exports = {};
670575
+ __export(command_passthrough_exports, {
670576
+ runCommand: () => runCommand
670577
+ });
670578
+ function stripAnsi5(s2) {
670579
+ return s2.replace(/\x1B(?:\[[\d;?]*[a-zA-Z]|\][^\x07\x1B]*[\x07\x1B]?|[@-Z\\-_])/g, "");
670580
+ }
670581
+ async function runCommand(input, opts) {
670582
+ const start2 = Date.now();
670583
+ const trimmed = input.trim();
670584
+ const slashCmd = trimmed.startsWith("/") ? trimmed : "/" + trimmed;
670585
+ const [rawCmd, ...rest] = slashCmd.slice(1).split(/\s+/);
670586
+ const cmdName = rawCmd ?? "";
670587
+ const argsStr = rest.join(" ");
670588
+ const release = await acquireLock2();
670589
+ try {
670590
+ const quick2 = buildNonInteractiveSummary(cmdName, argsStr, opts?.config);
670591
+ if (quick2) {
670592
+ return {
670593
+ ok: true,
670594
+ command: cmdName,
670595
+ args: argsStr,
670596
+ kind: "handled",
670597
+ output: quick2,
670598
+ ansi: quick2,
670599
+ durationMs: Date.now() - start2
670600
+ };
670601
+ }
670602
+ const buf = [];
670603
+ setContentWriteHook({
670604
+ begin: () => {
670605
+ },
670606
+ end: () => {
670607
+ },
670608
+ redirect: () => (text2) => {
670609
+ buf.push(text2);
670610
+ }
670611
+ });
670612
+ const origWrite = process.stdout.write.bind(process.stdout);
670613
+ process.stdout.write = function(chunk, ...rest2) {
670614
+ if (typeof chunk === "string") buf.push(chunk);
670615
+ else if (chunk instanceof Buffer) buf.push(chunk.toString("utf-8"));
670616
+ const cb = rest2.find((r2) => typeof r2 === "function");
670617
+ if (cb) cb();
670618
+ return true;
670619
+ };
670620
+ let kind = "handled";
670621
+ let errMsg;
670622
+ try {
670623
+ const ctx3 = buildSyntheticContext(opts?.config, opts?.repoRoot);
670624
+ kind = await handleSlashCommand(slashCmd, ctx3);
670625
+ } catch (e2) {
670626
+ kind = "error";
670627
+ errMsg = e2 instanceof Error ? e2.message : String(e2);
670628
+ buf.push(`
670629
+ [error] ${errMsg}
670630
+ `);
670631
+ } finally {
670632
+ process.stdout.write = origWrite;
670633
+ setContentWriteHook({ begin: () => {
670634
+ }, end: () => {
670635
+ } });
670636
+ }
670637
+ const ansi5 = buf.join("");
670638
+ return {
670639
+ ok: kind !== "error" && kind !== "not_a_command",
670640
+ command: cmdName,
670641
+ args: argsStr,
670642
+ kind,
670643
+ output: stripAnsi5(ansi5).trim(),
670644
+ ansi: ansi5,
670645
+ durationMs: Date.now() - start2,
670646
+ error: errMsg
670647
+ };
670648
+ } finally {
670649
+ release();
670650
+ }
670651
+ }
670652
+ function buildNonInteractiveSummary(cmdName, _args, config) {
670653
+ const cfg = config ?? loadConfig();
670654
+ if (cmdName === "setup" || cmdName === "wizard") {
670655
+ return [
670656
+ "omnius setup",
670657
+ "",
670658
+ "The setup wizard is an interactive terminal flow. In the GUI command bridge it is summarized instead of opening prompts, installing software, starting Ollama, or pulling models.",
670659
+ "",
670660
+ `Current backend: ${cfg.backendType ?? "ollama"}`,
670661
+ `Current endpoint: ${cfg.backendUrl ?? "http://127.0.0.1:11434"}`,
670662
+ `Current model: ${cfg.model ?? "qwen3.5:latest"}`,
670663
+ "",
670664
+ "Available non-interactive setup actions:",
670665
+ " /endpoint <url> Set or inspect the inference endpoint.",
670666
+ " /model <name> Set the active model directly.",
670667
+ " /models Show model-selection guidance.",
670668
+ " /config Inspect persisted configuration.",
670669
+ " /doctor Run diagnostics from the terminal if deeper repair is needed.",
670670
+ "",
670671
+ "Open a terminal and run `omnius`, then use /setup for the full guided wizard."
670672
+ ].join("\n");
670673
+ }
670674
+ if (cmdName === "models") {
670675
+ return [
670676
+ "omnius models",
670677
+ "",
670678
+ "The model picker is interactive in the TUI. The GUI bridge does not probe remote model endpoints here, so it cannot hang on a stale backend.",
670679
+ "",
670680
+ `Active model: ${cfg.model ?? "qwen3.5:latest"}`,
670681
+ `Endpoint: ${cfg.backendUrl ?? "http://127.0.0.1:11434"}`,
670682
+ `Backend: ${cfg.backendType ?? "ollama"}`,
670683
+ "",
670684
+ "Use /model <name> to set a model directly, /endpoint to switch providers, or open the TUI for the searchable model picker."
670685
+ ].join("\n");
670686
+ }
670687
+ return null;
670688
+ }
670689
+ function acquireLock2() {
670690
+ let release;
670691
+ const next = new Promise((res) => {
670692
+ release = res;
670693
+ });
670694
+ const prev = _passthroughLock;
670695
+ _passthroughLock = next;
670696
+ return prev.then(() => release);
670697
+ }
670698
+ function buildSyntheticContext(config, repoRoot) {
670699
+ const cfg = config ?? loadConfig();
670700
+ const root = repoRoot ?? process.cwd();
670701
+ let colorsEnabled = loadProjectSettings(root).colors ?? loadGlobalSettings().colors ?? true;
670702
+ let selfModifyEnabled = loadProjectSettings(root).selfModify ?? loadGlobalSettings().selfModify ?? false;
670703
+ return {
670704
+ config: cfg,
670705
+ repoRoot: root,
670706
+ rl: makeRejectingReadline(),
670707
+ setModel: async (model) => {
670708
+ try {
670709
+ const engine = getDaemonListenEngine();
670710
+ await engine.setModel(model);
670711
+ renderInfo(`Model switched to: ${model}`);
670712
+ return `model set to ${model}`;
670713
+ } catch (err) {
670714
+ renderError(`Failed to set model: ${err instanceof Error ? err.message : String(err)}`);
670715
+ return "";
670716
+ }
670717
+ },
670718
+ setVerbose: (_verbose) => {
670719
+ },
670720
+ setEndpoint: async (_url, _t, _k) => {
670721
+ renderError(`/endpoint is TUI-only — use the GUI endpoint picker instead.`);
670722
+ return "";
670723
+ },
670724
+ deactivateStatusBar: () => {
670725
+ },
670726
+ disableMouse: () => {
670727
+ },
670728
+ enableMouse: () => {
670729
+ },
670730
+ isMouseEnabled: () => false,
670731
+ lockFooter: () => {
670732
+ },
670733
+ unlockFooter: () => {
670734
+ },
670735
+ stopBanner: () => {
670736
+ },
670737
+ killEphemeral: () => {
670738
+ },
670739
+ setKeyPool: (_keys) => {
670740
+ },
670741
+ clearScreen: () => {
670742
+ },
670743
+ newSession: () => {
670744
+ },
670745
+ refreshBanner: () => {
670746
+ },
670747
+ exit: () => {
670748
+ renderError("/quit and /exit are TUI-only — close the browser tab to end the GUI session.");
670749
+ },
670750
+ voiceToggle: async () => {
670751
+ renderError(`voice ${TUI_ONLY_HINT} — use the GUI voice button instead.`);
670752
+ return "voice not available in GUI";
670753
+ },
670754
+ voiceSetModel: async (_id2) => {
670755
+ renderError(`voice model ${TUI_ONLY_HINT}`);
670756
+ return "";
670757
+ },
670758
+ getColors: () => colorsEnabled,
670759
+ setColors: (enabled2) => {
670760
+ colorsEnabled = enabled2;
670761
+ },
670762
+ getSelfModify: () => selfModifyEnabled,
670763
+ setSelfModify: (enabled2) => {
670764
+ selfModifyEnabled = enabled2;
670765
+ },
670766
+ saveSettings: (settings) => {
670767
+ saveProjectSettings(root, settings);
670768
+ saveGlobalSettings(settings);
670769
+ },
670770
+ saveLocalSettings: (settings) => {
670771
+ saveProjectSettings(root, settings);
670772
+ }
670773
+ };
670774
+ }
670775
+ function makeRejectingReadline() {
670776
+ const reject = () => {
670777
+ throw new Error(
670778
+ "interactive prompts are not supported via the GUI command bridge — run this command from the TUI (open a terminal and type `omnius`)."
670779
+ );
670780
+ };
670781
+ const noop2 = () => {
670782
+ };
670783
+ return {
670784
+ question: (_q, _cb) => reject(),
670785
+ close: noop2,
670786
+ write: noop2,
670787
+ on: () => noop2,
670788
+ once: () => noop2,
670789
+ off: () => noop2,
670790
+ removeListener: () => noop2,
670791
+ pause: noop2,
670792
+ resume: noop2,
670793
+ setPrompt: noop2,
670794
+ prompt: noop2,
670795
+ line: "",
670796
+ cursor: 0,
670797
+ terminal: false,
670798
+ input: { isTTY: false, on: noop2, once: noop2, removeListener: noop2, pause: noop2, resume: noop2 },
670799
+ output: { write: noop2, columns: 80, rows: 24, isTTY: false }
670800
+ };
670801
+ }
670802
+ var _passthroughLock, TUI_ONLY_HINT;
670803
+ var init_command_passthrough = __esm({
670804
+ "packages/cli/src/api/command-passthrough.ts"() {
670805
+ "use strict";
670806
+ init_render();
670807
+ init_commands();
670808
+ init_config();
670809
+ init_voice_runtime();
670810
+ init_omnius_directory();
670811
+ _passthroughLock = Promise.resolve();
670812
+ TUI_ONLY_HINT = "(this command is TUI-only — no-op in GUI)";
670813
+ }
670814
+ });
670815
+
670816
+ // packages/cli/src/api/projects.ts
670817
+ var projects_exports = {};
670818
+ __export(projects_exports, {
670819
+ _resetCurrentProject: () => _resetCurrentProject,
670820
+ getCurrentProject: () => getCurrentProject,
670821
+ listProjects: () => listProjects,
670822
+ registerProject: () => registerProject,
670823
+ renameProject: () => renameProject,
670824
+ setCurrentProject: () => setCurrentProject,
670825
+ unregisterProject: () => unregisterProject
670826
+ });
670827
+ import { readFileSync as readFileSync112, writeFileSync as writeFileSync74, mkdirSync as mkdirSync84, existsSync as existsSync135, statSync as statSync48, renameSync as renameSync11 } from "node:fs";
670828
+ import { homedir as homedir47 } from "node:os";
670829
+ import { basename as basename36, join as join148, resolve as resolve59 } from "node:path";
670830
+ import { randomUUID as randomUUID18 } from "node:crypto";
670831
+ function readAll2() {
670832
+ try {
670833
+ if (!existsSync135(PROJECTS_FILE)) return { projects: [], schemaVersion: 1 };
670834
+ const raw = readFileSync112(PROJECTS_FILE, "utf8");
670835
+ const parsed = JSON.parse(raw);
670836
+ if (!parsed || !Array.isArray(parsed.projects)) return { projects: [], schemaVersion: 1 };
670837
+ return { projects: parsed.projects, schemaVersion: 1 };
670838
+ } catch {
670839
+ return { projects: [], schemaVersion: 1 };
670840
+ }
670841
+ }
670842
+ function writeAll(file) {
670843
+ mkdirSync84(OMNIUS_DIR3, { recursive: true });
670844
+ const tmp = `${PROJECTS_FILE}.${randomUUID18().slice(0, 8)}.tmp`;
670845
+ writeFileSync74(tmp, JSON.stringify(file, null, 2), "utf8");
670846
+ renameSync11(tmp, PROJECTS_FILE);
670847
+ }
670848
+ function listProjects() {
670849
+ const { projects } = readAll2();
670850
+ const alive = [];
670851
+ for (const p2 of projects) {
670852
+ try {
670853
+ if (statSync48(p2.root).isDirectory()) alive.push(p2);
670854
+ } catch {
670855
+ }
670856
+ }
670857
+ alive.sort((a2, b) => b.lastSeen - a2.lastSeen);
670858
+ return alive;
670859
+ }
670860
+ function registerProject(root, pid) {
670861
+ const canonical = resolve59(root);
670862
+ const now = Date.now();
670863
+ const file = readAll2();
670864
+ const existing = file.projects.find((p2) => p2.root === canonical);
670865
+ let entry;
670866
+ if (existing) {
670867
+ entry = {
670868
+ ...existing,
670869
+ lastSeen: now,
670870
+ pid: pid ?? existing.pid,
670871
+ omniusDir: join148(canonical, ".omnius")
670872
+ };
670873
+ file.projects = file.projects.map((p2) => p2.root === canonical ? entry : p2);
670874
+ } else {
670875
+ entry = {
670876
+ root: canonical,
670877
+ name: basename36(canonical) || canonical,
670878
+ firstSeen: now,
670879
+ lastSeen: now,
670880
+ pid: pid ?? null,
670881
+ omniusDir: join148(canonical, ".omnius")
670882
+ };
670883
+ file.projects.push(entry);
670884
+ }
670885
+ writeAll(file);
670886
+ return entry;
670887
+ }
670888
+ function unregisterProject(root) {
670889
+ const canonical = resolve59(root);
670890
+ const file = readAll2();
670891
+ const before = file.projects.length;
670892
+ file.projects = file.projects.filter((p2) => p2.root !== canonical);
670893
+ if (file.projects.length === before) return false;
670894
+ writeAll(file);
670895
+ return true;
670896
+ }
670897
+ function renameProject(root, name10) {
670898
+ const canonical = resolve59(root);
670899
+ const file = readAll2();
670900
+ const idx = file.projects.findIndex((p2) => p2.root === canonical);
670901
+ if (idx < 0) return null;
670902
+ const next = { ...file.projects[idx], name: name10.trim() || file.projects[idx].name };
670903
+ file.projects[idx] = next;
670904
+ writeAll(file);
670905
+ return next;
670906
+ }
670907
+ function getCurrentProject() {
670908
+ if (!currentRoot) {
670909
+ try {
670910
+ if (existsSync135(CURRENT_FILE)) {
670911
+ const persisted = readFileSync112(CURRENT_FILE, "utf8").trim();
670912
+ if (persisted) currentRoot = persisted;
670913
+ }
670914
+ } catch {
670915
+ }
670916
+ }
670917
+ if (!currentRoot) return null;
670918
+ const all2 = listProjects();
670919
+ return all2.find((p2) => p2.root === currentRoot) ?? null;
670920
+ }
670921
+ function setCurrentProject(root) {
670922
+ const canonical = resolve59(root);
670923
+ const entry = listProjects().find((p2) => p2.root === canonical);
670924
+ if (!entry) return null;
670925
+ currentRoot = canonical;
670926
+ try {
670927
+ mkdirSync84(OMNIUS_DIR3, { recursive: true });
670928
+ writeFileSync74(CURRENT_FILE, `${canonical}
670929
+ `, "utf8");
670930
+ } catch {
670931
+ }
670932
+ return entry;
670933
+ }
670934
+ function _resetCurrentProject() {
670935
+ currentRoot = null;
670936
+ }
670937
+ var OMNIUS_DIR3, PROJECTS_FILE, CURRENT_FILE, currentRoot;
670938
+ var init_projects = __esm({
670939
+ "packages/cli/src/api/projects.ts"() {
670940
+ "use strict";
670941
+ OMNIUS_DIR3 = join148(homedir47(), ".omnius");
670942
+ PROJECTS_FILE = join148(OMNIUS_DIR3, "projects.json");
670943
+ CURRENT_FILE = join148(OMNIUS_DIR3, "current-project");
670944
+ currentRoot = null;
670945
+ }
670946
+ });
670947
+
670948
+ // packages/cli/src/tui/mouse-filter.ts
670949
+ var mouse_filter_exports = {};
670950
+ __export(mouse_filter_exports, {
670951
+ MouseFilterStream: () => MouseFilterStream
670952
+ });
670953
+ import { Transform } from "node:stream";
670954
+ var MouseFilterStream;
670955
+ var init_mouse_filter = __esm({
670956
+ "packages/cli/src/tui/mouse-filter.ts"() {
670957
+ "use strict";
670958
+ MouseFilterStream = class extends Transform {
670959
+ buffer = "";
670960
+ onScroll = null;
670961
+ onActivity = null;
670962
+ onPointer = null;
670963
+ onKeyboard = null;
670964
+ flushTimer = null;
670965
+ expectPrefixlessMouseUntil = 0;
670966
+ constructor(scrollHandler, activityHandler, pointerHandler, keyboardHandler) {
670967
+ super();
670968
+ this.onScroll = scrollHandler;
670969
+ this.onActivity = activityHandler ?? null;
670970
+ this.onPointer = pointerHandler ?? null;
670971
+ this.onKeyboard = keyboardHandler ?? null;
670972
+ }
670973
+ _transform(chunk, _encoding, callback) {
670974
+ this.buffer += chunk.toString();
670975
+ this.processBuffer(callback);
670976
+ }
670977
+ processBuffer(callback) {
670978
+ let output = "";
670979
+ let i2 = 0;
670980
+ while (i2 < this.buffer.length) {
670981
+ const remaining = this.buffer.slice(i2);
670982
+ const prefixlessMouse = this.matchPrefixlessSgrMouse(remaining);
670983
+ if (prefixlessMouse) {
670984
+ this.handleSgrMouse(prefixlessMouse);
670985
+ i2 += prefixlessMouse.raw.length;
670986
+ continue;
670987
+ }
670988
+ if (this.looksLikePartialPrefixlessSgrMouse(remaining)) {
670989
+ break;
670990
+ }
670991
+ if (this.buffer[i2] === "\x1B") {
670096
670992
  const mouseMatch = remaining.match(/^\x1B\[<(\d+);(\d+);(\d+)([Mm])/);
670097
670993
  if (mouseMatch) {
670098
670994
  this.handleSgrMouse({
@@ -670119,1782 +671015,898 @@ var init_mouse_filter = __esm({
670119
671015
  if (remaining.startsWith("\x1B[") && remaining.length === 2) {
670120
671016
  break;
670121
671017
  }
670122
- if (remaining.length === 1) {
670123
- break;
670124
- }
670125
- }
670126
- output += this.buffer[i2];
670127
- i2++;
670128
- }
670129
- this.buffer = this.buffer.slice(i2);
670130
- if (output.length > 0) {
670131
- if (this.onKeyboard) this.onKeyboard();
670132
- this.push(output);
670133
- }
670134
- if (this.buffer.length > 0) {
670135
- if (this.flushTimer) clearTimeout(this.flushTimer);
670136
- this.flushTimer = setTimeout(() => {
670137
- if (this.buffer.length > 0) {
670138
- if (this.buffer.startsWith("\x1B[<") || this.buffer.startsWith("\x1B[M")) {
670139
- this.expectPrefixlessMouseUntil = Date.now() + 1e3;
670140
- this.buffer = "";
670141
- } else if (this.buffer === "\x1B[") {
670142
- this.expectPrefixlessMouseUntil = Date.now() + 1e3;
670143
- this.buffer = "";
670144
- } else if (this.looksLikePartialPrefixlessSgrMouse(this.buffer)) {
670145
- this.buffer = "";
670146
- } else if (this.buffer === "\x1B") {
670147
- this.push(this.buffer);
670148
- this.buffer = "";
670149
- } else {
670150
- this.push(this.buffer);
670151
- this.buffer = "";
670152
- }
670153
- }
670154
- }, 50);
670155
- }
670156
- callback();
670157
- }
670158
- _flush(callback) {
670159
- if (this.flushTimer) {
670160
- clearTimeout(this.flushTimer);
670161
- this.flushTimer = null;
670162
- }
670163
- if (this.buffer.length > 0) {
670164
- if (this.buffer.startsWith("\x1B[<") || this.buffer.startsWith("\x1B[M") || this.buffer === "\x1B[" || this.looksLikePartialPrefixlessSgrMouse(this.buffer)) {
670165
- this.buffer = "";
670166
- callback();
670167
- return;
670168
- }
670169
- this.push(this.buffer);
670170
- this.buffer = "";
670171
- }
670172
- callback();
670173
- }
670174
- matchPrefixlessSgrMouse(input) {
670175
- const match = input.match(/^(<)?(\d{1,3});(\d{1,5});(\d{1,5})([Mm])/);
670176
- if (!match) return null;
670177
- const hasMarker = Boolean(match[1]);
670178
- const btn = parseInt(match[2], 10);
670179
- const col = parseInt(match[3], 10);
670180
- const row = parseInt(match[4], 10);
670181
- const suffix = match[5];
670182
- if (!Number.isFinite(btn) || !Number.isFinite(col) || !Number.isFinite(row))
670183
- return null;
670184
- if (col <= 0 || row <= 0) return null;
670185
- if (!hasMarker && Date.now() > this.expectPrefixlessMouseUntil && !this.isKnownMouseButton(btn))
670186
- return null;
670187
- if (btn < 0 || btn > 255) return null;
670188
- return { raw: match[0], btn, col, row, suffix };
670189
- }
670190
- looksLikePartialPrefixlessSgrMouse(input) {
670191
- if (input.length < 2) return false;
670192
- if (/^<\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) return true;
670193
- if (Date.now() <= this.expectPrefixlessMouseUntil && /^\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) {
670194
- return true;
670195
- }
670196
- return false;
670197
- }
670198
- isKnownMouseButton(btn) {
670199
- if (btn >= 0 && btn <= 6) return true;
670200
- if (btn >= 32 && btn <= 39) return true;
670201
- if (btn >= 64 && btn <= 71) return true;
670202
- if (btn >= 96 && btn <= 103) return true;
670203
- return false;
670204
- }
670205
- handleSgrMouse(mouse) {
670206
- const { btn, col, row, suffix } = mouse;
670207
- if ((btn === 64 || btn === 96) && this.onScroll)
670208
- this.onScroll("up", 3, row);
670209
- else if ((btn === 65 || btn === 97) && this.onScroll)
670210
- this.onScroll("down", 3, row);
670211
- else if (this.onPointer) {
670212
- const hasShift = (btn & 4) !== 0 && btn < 32;
670213
- if (btn === 2) {
670214
- } else if (hasShift) {
670215
- } else if ((btn === 0 || btn === 1) && suffix === "M") {
670216
- this.onPointer("press", col, row);
670217
- } else if (btn >= 32 && btn <= 35 && suffix === "M") {
670218
- this.onPointer("drag", col, row);
670219
- } else if (suffix === "m") {
670220
- this.onPointer("release", col, row);
670221
- }
670222
- }
670223
- if (this.onActivity) this.onActivity();
670224
- }
670225
- };
670226
- }
670227
- });
670228
-
670229
- // packages/cli/src/tui/direct-input.ts
670230
- var direct_input_exports = {};
670231
- __export(direct_input_exports, {
670232
- DirectInput: () => DirectInput
670233
- });
670234
- import { EventEmitter as EventEmitter12 } from "node:events";
670235
- var DirectInput;
670236
- var init_direct_input = __esm({
670237
- "packages/cli/src/tui/direct-input.ts"() {
670238
- "use strict";
670239
- DirectInput = class extends EventEmitter12 {
670240
- /** Current input line text */
670241
- line = "";
670242
- /** Cursor position within .line (0-based) */
670243
- cursor = 0;
670244
- _history;
670245
- _historySize;
670246
- _historyIndex = -1;
670247
- _savedLine = "";
670248
- // saved current input when navigating history
670249
- _completer = null;
670250
- _paused = false;
670251
- _closed = false;
670252
- _input;
670253
- _buffer = "";
670254
- // partial escape sequence buffer
670255
- _flushTimer = null;
670256
- _expectPrefixlessMouseUntil = 0;
670257
- constructor(input, options2) {
670258
- super();
670259
- this._input = input;
670260
- this._history = [...options2?.history ?? []];
670261
- this._historySize = options2?.historySize ?? 500;
670262
- this._completer = options2?.completer ?? null;
670263
- input.on("data", (chunk) => {
670264
- if (this._paused || this._closed) return;
670265
- this.feed(chunk.toString("utf8"));
670266
- });
670267
- input.on("end", () => {
670268
- if (!this._closed) this.close();
670269
- });
670270
- }
670271
- /** Process raw input data — parse escape sequences and printable chars */
670272
- feed(data) {
670273
- const beforeLine = this.line;
670274
- const beforeCursor = this.cursor;
670275
- this._buffer += data;
670276
- this._processBuffer();
670277
- this._emitChangeIfNeeded(beforeLine, beforeCursor);
670278
- }
670279
- /** Pause input processing (for overlay transitions) */
670280
- pause() {
670281
- this._paused = true;
670282
- }
670283
- /** Resume input processing */
670284
- resume() {
670285
- this._paused = false;
670286
- }
670287
- /** Close the input handler */
670288
- close() {
670289
- if (this._closed) return;
670290
- this._closed = true;
670291
- if (this._flushTimer) {
670292
- clearTimeout(this._flushTimer);
670293
- this._flushTimer = null;
670294
- }
670295
- this.emit("close");
670296
- }
670297
- /** No-op — readline compat (StatusBar renders the prompt, not us) */
670298
- setPrompt(_prompt) {
670299
- }
670300
- /** No-op — readline compat */
670301
- prompt(_preserveCursor) {
670302
- }
670303
- /** Set the line content and cursor position (for Esc-to-recall or suggestion apply) */
670304
- setLine(text2, cursorPos) {
670305
- const beforeLine = this.line;
670306
- const beforeCursor = this.cursor;
670307
- this.line = text2;
670308
- this.cursor = cursorPos ?? text2.length;
670309
- this._emitChangeIfNeeded(beforeLine, beforeCursor);
670310
- }
670311
- /** Pre-submit hook — called before Enter submits. Return true to consume Enter. */
670312
- _preSubmit = null;
670313
- setPreSubmit(hook) {
670314
- this._preSubmit = hook;
670315
- }
670316
- /** Navigate history up (older) */
670317
- historyUp() {
670318
- if (this._history.length === 0) return;
670319
- if (this._historyIndex === -1) {
670320
- this._savedLine = this.line;
670321
- }
670322
- if (this._historyIndex < this._history.length - 1) {
670323
- this._historyIndex++;
670324
- this.line = this._history[this._historyIndex];
670325
- this.cursor = this.line.length;
670326
- }
670327
- }
670328
- /** Navigate history down (newer) */
670329
- historyDown() {
670330
- if (this._historyIndex <= -1) return;
670331
- this._historyIndex--;
670332
- if (this._historyIndex === -1) {
670333
- this.line = this._savedLine;
670334
- } else {
670335
- this.line = this._history[this._historyIndex];
670336
- }
670337
- this.cursor = this.line.length;
670338
- }
670339
- /**
670340
- * Move cursor up one wrapped line. Returns true if moved, false if at top line.
670341
- * Used by arrow-up to navigate within wrapped input before falling through to history.
670342
- */
670343
- cursorUpWrapped(availWidth) {
670344
- if (this.line.length <= availWidth) return false;
670345
- const { charPositions, rawLines } = this._computeWrappedLines(availWidth);
670346
- if (rawLines.length <= 1) return false;
670347
- let currentLineIdx = rawLines.length - 1;
670348
- for (let i2 = 0; i2 < charPositions.length; i2++) {
670349
- const lineStart = charPositions[i2];
670350
- const lineEnd = lineStart + rawLines[i2].length;
670351
- if (this.cursor >= lineStart && this.cursor <= lineEnd) {
670352
- currentLineIdx = i2;
670353
- break;
670354
- }
670355
- }
670356
- if (currentLineIdx === 0) return false;
670357
- const prevLineIdx = currentLineIdx - 1;
670358
- const currentColInLine = this.cursor - charPositions[currentLineIdx];
670359
- const prevLineLength = rawLines[prevLineIdx].length;
670360
- const newColInLine = Math.min(currentColInLine, prevLineLength);
670361
- this.cursor = charPositions[prevLineIdx] + newColInLine;
670362
- return true;
670363
- }
670364
- /**
670365
- * Move cursor down one wrapped line. Returns true if moved, false if at bottom line.
670366
- * Used by arrow-down to navigate within wrapped input before falling through to history.
670367
- */
670368
- cursorDownWrapped(availWidth) {
670369
- if (this.line.length <= availWidth) return false;
670370
- const { charPositions, rawLines } = this._computeWrappedLines(availWidth);
670371
- if (rawLines.length <= 1) return false;
670372
- let currentLineIdx = rawLines.length - 1;
670373
- for (let i2 = 0; i2 < charPositions.length; i2++) {
670374
- const lineStart = charPositions[i2];
670375
- const lineEnd = lineStart + rawLines[i2].length;
670376
- if (this.cursor >= lineStart && this.cursor <= lineEnd) {
670377
- currentLineIdx = i2;
670378
- break;
670379
- }
670380
- }
670381
- if (currentLineIdx === rawLines.length - 1) return false;
670382
- const nextLineIdx = currentLineIdx + 1;
670383
- const currentColInLine = this.cursor - charPositions[currentLineIdx];
670384
- const nextLineLength = rawLines[nextLineIdx].length;
670385
- const newColInLine = Math.min(currentColInLine, nextLineLength);
670386
- this.cursor = charPositions[nextLineIdx] + newColInLine;
670387
- return true;
670388
- }
670389
- /**
670390
- * Compute wrapped lines (word-aware). Returns charPositions (start index of each line)
670391
- * and rawLines (text of each line). Matches wrapInput logic in status-bar.ts.
670392
- */
670393
- _computeWrappedLines(availWidth) {
670394
- const width = Math.max(1, availWidth);
670395
- const rawLines = [];
670396
- const charPositions = [];
670397
- const pushWrappedSegment = (segment, segmentStart2) => {
670398
- if (segment.length === 0) {
670399
- charPositions.push(segmentStart2);
670400
- rawLines.push("");
670401
- return;
670402
- }
670403
- let offset = 0;
670404
- while (offset < segment.length) {
670405
- const remaining = segment.slice(offset);
670406
- if (remaining.length <= width) {
670407
- charPositions.push(segmentStart2 + offset);
670408
- rawLines.push(remaining);
670409
- break;
670410
- }
670411
- let breakAt = width;
670412
- const lastSpace = remaining.lastIndexOf(" ", width);
670413
- if (lastSpace > 0 && lastSpace >= width * 0.3) {
670414
- breakAt = lastSpace + 1;
670415
- }
670416
- charPositions.push(segmentStart2 + offset);
670417
- rawLines.push(remaining.slice(0, breakAt));
670418
- offset += breakAt;
670419
- }
670420
- };
670421
- if (this.line.length === 0) {
670422
- pushWrappedSegment("", 0);
670423
- return { charPositions, rawLines };
670424
- }
670425
- let segmentStart = 0;
670426
- while (segmentStart <= this.line.length) {
670427
- const newlineAt = this.line.indexOf("\n", segmentStart);
670428
- const segmentEnd = newlineAt === -1 ? this.line.length : newlineAt;
670429
- pushWrappedSegment(this.line.slice(segmentStart, segmentEnd), segmentStart);
670430
- if (newlineAt === -1) break;
670431
- segmentStart = newlineAt + 1;
670432
- if (segmentStart === this.line.length) {
670433
- pushWrappedSegment("", segmentStart);
670434
- break;
670435
- }
670436
- }
670437
- return { charPositions, rawLines };
670438
- }
670439
- // ---------------------------------------------------------------------------
670440
- // Private: buffer processing and escape sequence parsing
670441
- // ---------------------------------------------------------------------------
670442
- _processBuffer() {
670443
- let i2 = 0;
670444
- while (i2 < this._buffer.length) {
670445
- const remaining = this._buffer.slice(i2);
670446
- const prefixlessMouse = this._matchPrefixlessSgrMouse(remaining);
670447
- if (prefixlessMouse) {
670448
- i2 += prefixlessMouse.length;
670449
- continue;
670450
- }
670451
- if (this._looksLikePartialPrefixlessSgrMouse(remaining)) {
670452
- break;
670453
- }
670454
- const ch = this._buffer[i2];
670455
- const code8 = ch.charCodeAt(0);
670456
- if (code8 === 27) {
670457
- if (remaining.length >= 2 && remaining[1] === "[") {
670458
- const mouseMatch = remaining.match(/^\x1B\[<(\d+);(\d+);(\d+)([Mm])/);
670459
- if (mouseMatch) {
670460
- i2 += mouseMatch[0].length;
670461
- continue;
670462
- }
670463
- if (remaining.startsWith("\x1B[<") && remaining.length < 15) {
670464
- break;
670465
- }
670466
- if (remaining.startsWith("\x1B[M") && remaining.length >= 6) {
670467
- i2 += 6;
670468
- continue;
670469
- }
670470
- if (remaining.startsWith("\x1B[M") && remaining.length < 6) {
670471
- break;
670472
- }
670473
- if (remaining.startsWith("\x1B[<")) {
670474
- this._expectPrefixlessMouseUntil = Date.now() + 1e3;
670475
- let end = 3;
670476
- while (end < remaining.length && remaining[end] !== "M" && remaining[end] !== "m") end++;
670477
- if (end < remaining.length) end++;
670478
- i2 += end;
670479
- continue;
670480
- }
670481
- const csiMatch = remaining.match(/^\x1B\[([0-9;]*)([A-Za-z~])/);
670482
- if (csiMatch) {
670483
- this._handleCSI(csiMatch[1], csiMatch[2]);
670484
- i2 += csiMatch[0].length;
670485
- continue;
670486
- }
670487
- if (remaining.length < 10) {
670488
- break;
670489
- }
670490
- i2 += 2;
670491
- continue;
670492
- }
670493
- if (remaining.length >= 2 && remaining[1] === "O") {
670494
- if (remaining.length >= 3) {
670495
- this._handleSS3(remaining[2]);
670496
- i2 += 3;
670497
- continue;
670498
- }
670499
- break;
670500
- }
670501
- if (remaining.length >= 2 && (remaining[1] === "\r" || remaining[1] === "\n")) {
670502
- this._insertText("\n");
670503
- i2 += 2;
670504
- if (remaining[1] === "\r" && remaining[2] === "\n") i2++;
670505
- continue;
670506
- }
670507
- if (remaining.length === 1) {
670508
- break;
670509
- }
670510
- i2++;
670511
- continue;
670512
- }
670513
- if (code8 < 32 || code8 === 127) {
670514
- if (code8 === 13) {
670515
- if (this._preSubmit?.()) {
670516
- i2++;
670517
- continue;
670518
- }
670519
- this._submit();
670520
- i2++;
670521
- if (this._buffer[i2] === "\n") i2++;
670522
- continue;
670523
- }
670524
- if (code8 === 10) {
670525
- this._insertText("\n");
670526
- i2++;
670527
- continue;
670528
- }
670529
- this._handleControl(code8);
670530
- i2++;
670531
- continue;
671018
+ if (remaining.length === 1) {
671019
+ break;
671020
+ }
670532
671021
  }
670533
- const char = this._buffer[i2];
670534
- this.line = this.line.slice(0, this.cursor) + char + this.line.slice(this.cursor);
670535
- this.cursor++;
671022
+ output += this.buffer[i2];
670536
671023
  i2++;
670537
671024
  }
670538
- this._buffer = this._buffer.slice(i2);
670539
- if (this._buffer.length > 0) {
670540
- if (this._flushTimer) clearTimeout(this._flushTimer);
670541
- this._flushTimer = setTimeout(() => {
670542
- this._flushTimer = null;
670543
- if (this._buffer.length > 0) {
670544
- if (this._buffer.startsWith("\x1B[<") || this._buffer.startsWith("\x1B[M")) {
670545
- this._expectPrefixlessMouseUntil = Date.now() + 1e3;
670546
- this._buffer = "";
670547
- return;
670548
- }
670549
- if (this._buffer === "\x1B[") {
670550
- this._expectPrefixlessMouseUntil = Date.now() + 1e3;
670551
- this._buffer = "";
670552
- return;
670553
- }
670554
- if (this._looksLikePartialPrefixlessSgrMouse(this._buffer)) {
670555
- this._buffer = "";
670556
- return;
670557
- }
670558
- if (this._buffer === "\x1B") {
670559
- this.emit("escape");
671025
+ this.buffer = this.buffer.slice(i2);
671026
+ if (output.length > 0) {
671027
+ if (this.onKeyboard) this.onKeyboard();
671028
+ this.push(output);
671029
+ }
671030
+ if (this.buffer.length > 0) {
671031
+ if (this.flushTimer) clearTimeout(this.flushTimer);
671032
+ this.flushTimer = setTimeout(() => {
671033
+ if (this.buffer.length > 0) {
671034
+ if (this.buffer.startsWith("\x1B[<") || this.buffer.startsWith("\x1B[M")) {
671035
+ this.expectPrefixlessMouseUntil = Date.now() + 1e3;
671036
+ this.buffer = "";
671037
+ } else if (this.buffer === "\x1B[") {
671038
+ this.expectPrefixlessMouseUntil = Date.now() + 1e3;
671039
+ this.buffer = "";
671040
+ } else if (this.looksLikePartialPrefixlessSgrMouse(this.buffer)) {
671041
+ this.buffer = "";
671042
+ } else if (this.buffer === "\x1B") {
671043
+ this.push(this.buffer);
671044
+ this.buffer = "";
671045
+ } else {
671046
+ this.push(this.buffer);
671047
+ this.buffer = "";
670560
671048
  }
670561
- this._buffer = "";
670562
671049
  }
670563
671050
  }, 50);
670564
- } else {
670565
- if (this._flushTimer) {
670566
- clearTimeout(this._flushTimer);
670567
- this._flushTimer = null;
671051
+ }
671052
+ callback();
671053
+ }
671054
+ _flush(callback) {
671055
+ if (this.flushTimer) {
671056
+ clearTimeout(this.flushTimer);
671057
+ this.flushTimer = null;
671058
+ }
671059
+ if (this.buffer.length > 0) {
671060
+ if (this.buffer.startsWith("\x1B[<") || this.buffer.startsWith("\x1B[M") || this.buffer === "\x1B[" || this.looksLikePartialPrefixlessSgrMouse(this.buffer)) {
671061
+ this.buffer = "";
671062
+ callback();
671063
+ return;
670568
671064
  }
671065
+ this.push(this.buffer);
671066
+ this.buffer = "";
670569
671067
  }
671068
+ callback();
670570
671069
  }
670571
- _matchPrefixlessSgrMouse(input) {
671070
+ matchPrefixlessSgrMouse(input) {
670572
671071
  const match = input.match(/^(<)?(\d{1,3});(\d{1,5});(\d{1,5})([Mm])/);
670573
671072
  if (!match) return null;
670574
671073
  const hasMarker = Boolean(match[1]);
670575
671074
  const btn = parseInt(match[2], 10);
670576
671075
  const col = parseInt(match[3], 10);
670577
671076
  const row = parseInt(match[4], 10);
671077
+ const suffix = match[5];
670578
671078
  if (!Number.isFinite(btn) || !Number.isFinite(col) || !Number.isFinite(row))
670579
671079
  return null;
670580
- if (btn < 0 || btn > 255 || col <= 0 || row <= 0) return null;
670581
- if (!hasMarker && Date.now() > this._expectPrefixlessMouseUntil && !this._isKnownMouseButton(btn))
671080
+ if (col <= 0 || row <= 0) return null;
671081
+ if (!hasMarker && Date.now() > this.expectPrefixlessMouseUntil && !this.isKnownMouseButton(btn))
670582
671082
  return null;
670583
- return { length: match[0].length };
671083
+ if (btn < 0 || btn > 255) return null;
671084
+ return { raw: match[0], btn, col, row, suffix };
670584
671085
  }
670585
- _looksLikePartialPrefixlessSgrMouse(input) {
671086
+ looksLikePartialPrefixlessSgrMouse(input) {
670586
671087
  if (input.length < 2) return false;
670587
671088
  if (/^<\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) return true;
670588
- if (Date.now() <= this._expectPrefixlessMouseUntil && /^\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) {
671089
+ if (Date.now() <= this.expectPrefixlessMouseUntil && /^\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) {
670589
671090
  return true;
670590
671091
  }
670591
671092
  return false;
670592
671093
  }
670593
- _isKnownMouseButton(btn) {
671094
+ isKnownMouseButton(btn) {
670594
671095
  if (btn >= 0 && btn <= 6) return true;
670595
671096
  if (btn >= 32 && btn <= 39) return true;
670596
671097
  if (btn >= 64 && btn <= 71) return true;
670597
671098
  if (btn >= 96 && btn <= 103) return true;
670598
671099
  return false;
670599
671100
  }
670600
- _emitChangeIfNeeded(beforeLine, beforeCursor) {
670601
- if (this.line === beforeLine && this.cursor === beforeCursor) return;
670602
- this.emit("change", { line: this.line, cursor: this.cursor });
670603
- }
670604
- /** Handle CSI escape sequence: \x1B[ {params} {final} */
670605
- _handleCSI(params, final2) {
670606
- switch (final2) {
670607
- case "A":
670608
- if (params === "1;2") {
670609
- this.emit("shiftup");
670610
- return;
670611
- }
670612
- this.emit("up");
670613
- return;
670614
- case "B":
670615
- if (params === "1;2") {
670616
- this.emit("shiftdown");
670617
- return;
670618
- }
670619
- this.emit("down");
670620
- return;
670621
- case "C":
670622
- if (params === "1;5") {
670623
- this.emit("ctrl-right");
670624
- return;
670625
- }
670626
- if (this.cursor < this.line.length) this.cursor++;
670627
- return;
670628
- case "D":
670629
- if (params === "1;5") {
670630
- this.emit("ctrl-left");
670631
- return;
670632
- }
670633
- if (this.cursor > 0) this.cursor--;
670634
- return;
670635
- case "H":
670636
- this.cursor = 0;
670637
- return;
670638
- case "F":
670639
- this.cursor = this.line.length;
670640
- return;
670641
- case "~":
670642
- if (this._isShiftEnterCSI(params)) {
670643
- this._insertText("\n");
670644
- return;
670645
- }
670646
- if (params === "3") {
670647
- if (this.cursor < this.line.length) {
670648
- this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1);
670649
- }
670650
- return;
670651
- }
670652
- if (params === "5") {
670653
- this.emit("pageup");
670654
- return;
670655
- }
670656
- if (params === "6") {
670657
- this.emit("pagedown");
670658
- return;
670659
- }
670660
- return;
670661
- case "u": {
670662
- const parts = params.split(";");
670663
- const codepoint = parseInt(parts[0] ?? "0");
670664
- const modifiers = parseInt(parts[1] ?? "1");
670665
- const hasCtrl = modifiers - 1 & 4;
670666
- const hasShift = modifiers - 1 & 1;
670667
- if (hasShift && (codepoint === 10 || codepoint === 13)) {
670668
- this._insertText("\n");
670669
- return;
670670
- }
670671
- if (hasCtrl && hasShift) {
670672
- if (codepoint === 67) {
670673
- this.emit("ctrl-shift-c");
670674
- return;
670675
- }
670676
- if (codepoint === 66) {
670677
- this.emit("ctrl-shift-b");
670678
- return;
670679
- }
670680
- if (codepoint === 86) {
670681
- this.emit("ctrl-shift-v");
670682
- return;
670683
- }
670684
- }
670685
- if (hasCtrl && !hasShift && (codepoint === 73 || codepoint === 105)) {
670686
- this.emit("ctrl-i");
670687
- return;
670688
- }
670689
- return;
670690
- }
670691
- }
670692
- }
670693
- _isShiftEnterCSI(params) {
670694
- const parts = params.split(";").map((part) => parseInt(part, 10));
670695
- if (parts.length === 2) {
670696
- const [codepoint, modifiers] = parts;
670697
- return (codepoint === 10 || codepoint === 13) && modifiers === 2;
670698
- }
670699
- if (parts.length === 3) {
670700
- const [prefix, modifiers, codepoint] = parts;
670701
- return prefix === 27 && modifiers === 2 && (codepoint === 10 || codepoint === 13);
670702
- }
670703
- return false;
670704
- }
670705
- _insertText(text2) {
670706
- this.line = this.line.slice(0, this.cursor) + text2 + this.line.slice(this.cursor);
670707
- this.cursor += text2.length;
670708
- }
670709
- /** Handle SS3 sequence: \x1BO {final} (some terminals use this for arrows/Home/End) */
670710
- _handleSS3(final2) {
670711
- switch (final2) {
670712
- case "A":
670713
- this.emit("up");
670714
- return;
670715
- case "B":
670716
- this.emit("down");
670717
- return;
670718
- case "C":
670719
- if (this.cursor < this.line.length) this.cursor++;
670720
- return;
670721
- case "D":
670722
- if (this.cursor > 0) this.cursor--;
670723
- return;
670724
- case "H":
670725
- this.cursor = 0;
670726
- return;
670727
- case "F":
670728
- this.cursor = this.line.length;
670729
- return;
670730
- }
670731
- }
670732
- /** Handle control characters (ASCII < 32 and DEL) */
670733
- _handleControl(code8) {
670734
- switch (code8) {
670735
- case 127:
670736
- // Backspace (DEL)
670737
- case 8:
670738
- if (this.cursor > 0) {
670739
- this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
670740
- this.cursor--;
670741
- }
670742
- return;
670743
- case 3:
670744
- this.emit("SIGINT");
670745
- return;
670746
- case 4:
670747
- if (this.line.length === 0) this.close();
670748
- return;
670749
- case 9:
670750
- this._handleTab();
670751
- return;
670752
- case 23:
670753
- this._deleteWordLeft();
670754
- return;
670755
- case 21:
670756
- this.line = this.line.slice(this.cursor);
670757
- this.cursor = 0;
670758
- return;
670759
- case 11:
670760
- this.line = this.line.slice(0, this.cursor);
670761
- return;
670762
- case 1:
670763
- this.cursor = 0;
670764
- return;
670765
- case 5:
670766
- this.cursor = this.line.length;
670767
- return;
670768
- case 15:
670769
- this.emit("ctrl-o");
670770
- return;
670771
- case 12:
670772
- this.emit("ctrl-l");
670773
- return;
670774
- case 22:
670775
- this.emit("ctrl-v");
670776
- return;
670777
- case 28:
670778
- this.emit("ctrl-backslash");
670779
- return;
670780
- }
670781
- }
670782
- /** Submit the current line */
670783
- _submit() {
670784
- const line = this.line;
670785
- if (line.trim() && (this._history.length === 0 || this._history[0] !== line)) {
670786
- this._history.unshift(line);
670787
- if (this._history.length > this._historySize) {
670788
- this._history.length = this._historySize;
670789
- }
670790
- }
670791
- this._historyIndex = -1;
670792
- this._savedLine = "";
670793
- this.line = "";
670794
- this.cursor = 0;
670795
- this.emit("line", line);
670796
- }
670797
- /** Handle Tab completion */
670798
- _handleTab() {
670799
- if (this.line.length === 0) {
670800
- this.emit("ctrl-i");
670801
- return;
670802
- }
670803
- if (!this._completer) return;
670804
- this._completer(this.line, (err, result) => {
670805
- if (err || !result) return;
670806
- const [completions, substring] = result;
670807
- if (completions.length === 0) return;
670808
- if (completions.length === 1) {
670809
- const completion = completions[0];
670810
- const before = this.line.slice(0, this.cursor);
670811
- const idx = before.lastIndexOf(substring);
670812
- if (idx >= 0) {
670813
- this.line = before.slice(0, idx) + completion + this.line.slice(this.cursor);
670814
- this.cursor = idx + completion.length;
670815
- }
670816
- } else if (completions.length > 1) {
670817
- let common = completions[0];
670818
- for (let i2 = 1; i2 < completions.length; i2++) {
670819
- const other = completions[i2];
670820
- let j = 0;
670821
- while (j < common.length && j < other.length && common[j] === other[j]) j++;
670822
- common = common.slice(0, j);
670823
- }
670824
- if (common.length > substring.length) {
670825
- const before = this.line.slice(0, this.cursor);
670826
- const idx = before.lastIndexOf(substring);
670827
- if (idx >= 0) {
670828
- this.line = before.slice(0, idx) + common + this.line.slice(this.cursor);
670829
- this.cursor = idx + common.length;
670830
- }
670831
- }
670832
- }
670833
- });
670834
- }
670835
- /** Move cursor left by one word */
670836
- _wordLeft() {
670837
- if (this.cursor === 0) return;
670838
- let i2 = this.cursor - 1;
670839
- while (i2 > 0 && /\s/.test(this.line[i2])) i2--;
670840
- while (i2 > 0 && /\S/.test(this.line[i2 - 1])) i2--;
670841
- this.cursor = i2;
670842
- }
670843
- /** Move cursor right by one word */
670844
- _wordRight() {
670845
- if (this.cursor >= this.line.length) return;
670846
- let i2 = this.cursor;
670847
- while (i2 < this.line.length && /\S/.test(this.line[i2])) i2++;
670848
- while (i2 < this.line.length && /\s/.test(this.line[i2])) i2++;
670849
- this.cursor = i2;
670850
- }
670851
- /** Delete word left of cursor (Ctrl+W) */
670852
- _deleteWordLeft() {
670853
- if (this.cursor === 0) return;
670854
- let i2 = this.cursor - 1;
670855
- while (i2 > 0 && /\s/.test(this.line[i2])) i2--;
670856
- while (i2 > 0 && /\S/.test(this.line[i2 - 1])) i2--;
670857
- this.line = this.line.slice(0, i2) + this.line.slice(this.cursor);
670858
- this.cursor = i2;
670859
- }
670860
- };
670861
- }
670862
- });
670863
-
670864
- // packages/cli/src/api/access-policy.ts
670865
- function defaultAccessMode(bindHost) {
670866
- if (bindHost === "0.0.0.0" || bindHost === "::" || bindHost === "::0") {
670867
- return "lan";
670868
- }
670869
- return "loopback";
670870
- }
670871
- function resolveAccessMode(envValue, bindHost) {
670872
- const v = (envValue || "").toLowerCase().trim();
670873
- if (v === "loopback" || v === "lan" || v === "any") return v;
670874
- return defaultAccessMode(bindHost);
670875
- }
670876
- function stripMappedPrefix(ip) {
670877
- return ip.replace(/^::ffff:/i, "");
670878
- }
670879
- function isLoopbackIP(ip) {
670880
- if (!ip) return false;
670881
- const clean5 = stripMappedPrefix(ip);
670882
- if (clean5 === "::1") return true;
670883
- if (/^127\./.test(clean5)) return true;
670884
- return false;
670885
- }
670886
- function isPrivateIP(ip) {
670887
- if (!ip) return false;
670888
- const clean5 = stripMappedPrefix(ip);
670889
- if (/^10\./.test(clean5)) return true;
670890
- if (/^192\.168\./.test(clean5)) return true;
670891
- const m2 = /^172\.(\d{1,3})\./.exec(clean5);
670892
- if (m2) {
670893
- const second3 = parseInt(m2[1], 10);
670894
- if (second3 >= 16 && second3 <= 31) return true;
670895
- }
670896
- if (/^169\.254\./.test(clean5)) return true;
670897
- if (/^f[cd][0-9a-f]{2}:/i.test(clean5)) return true;
670898
- if (/^fe[89ab][0-9a-f]:/i.test(clean5)) return true;
670899
- return false;
670900
- }
670901
- function isAllowedIP(ip, mode) {
670902
- if (mode === "any") return true;
670903
- if (isLoopbackIP(ip)) return true;
670904
- if (mode === "lan" && isPrivateIP(ip)) return true;
670905
- return false;
670906
- }
670907
- function describeAccessMode(mode) {
670908
- switch (mode) {
670909
- case "loopback":
670910
- return "loopback only";
670911
- case "lan":
670912
- return "loopback + RFC 1918";
670913
- case "any":
670914
- return "any — WIDE OPEN";
670915
- }
670916
- }
670917
- var init_access_policy = __esm({
670918
- "packages/cli/src/api/access-policy.ts"() {
670919
- "use strict";
670920
- }
670921
- });
670922
-
670923
- // packages/cli/src/api/project-preferences.ts
670924
- import { createHash as createHash40 } from "node:crypto";
670925
- import { existsSync as existsSync136, mkdirSync as mkdirSync85, readFileSync as readFileSync113, renameSync as renameSync12, writeFileSync as writeFileSync75, unlinkSync as unlinkSync31 } from "node:fs";
670926
- import { homedir as homedir48 } from "node:os";
670927
- import { join as join149, resolve as resolve60 } from "node:path";
670928
- import { randomUUID as randomUUID19 } from "node:crypto";
670929
- function projectKey(root) {
670930
- const canonical = resolve60(root);
670931
- return createHash40("sha256").update(canonical).digest("hex").slice(0, 16);
670932
- }
670933
- function projectDir(root) {
670934
- return join149(PROJECTS_DIR, projectKey(root));
670935
- }
670936
- function prefsPath(root) {
670937
- return join149(projectDir(root), "preferences.json");
670938
- }
670939
- function rootSentinelPath(root) {
670940
- return join149(projectDir(root), ".root");
670941
- }
670942
- function ensureDir(root) {
670943
- const dir = projectDir(root);
670944
- mkdirSync85(dir, { recursive: true });
670945
- const sentinel = rootSentinelPath(root);
670946
- try {
670947
- if (!existsSync136(sentinel)) {
670948
- writeFileSync75(sentinel, `${resolve60(root)}
670949
- `, "utf8");
670950
- }
670951
- } catch {
670952
- }
670953
- }
670954
- function readProjectPreferences(root) {
670955
- try {
670956
- const file = prefsPath(root);
670957
- if (!existsSync136(file)) return { ...DEFAULT_PREFS };
670958
- const raw = readFileSync113(file, "utf8");
670959
- const parsed = JSON.parse(raw);
670960
- if (!parsed || parsed.v !== SCHEMA_VERSION) return { ...DEFAULT_PREFS };
670961
- return { ...DEFAULT_PREFS, ...parsed, v: SCHEMA_VERSION };
670962
- } catch {
670963
- return { ...DEFAULT_PREFS };
670964
- }
670965
- }
670966
- function writeProjectPreferences(root, partial) {
670967
- ensureDir(root);
670968
- const current = readProjectPreferences(root);
670969
- const merged = {
670970
- ...current,
670971
- ...partial,
670972
- v: SCHEMA_VERSION,
670973
- updatedAt: Date.now()
670974
- };
670975
- const file = prefsPath(root);
670976
- const tmp = `${file}.${randomUUID19().slice(0, 8)}.tmp`;
670977
- writeFileSync75(tmp, JSON.stringify(merged, null, 2), "utf8");
670978
- try {
670979
- renameSync12(tmp, file);
670980
- } catch (err) {
670981
- try {
670982
- writeFileSync75(file, JSON.stringify(merged, null, 2), "utf8");
670983
- } catch {
670984
- }
670985
- try {
670986
- unlinkSync31(tmp);
670987
- } catch {
670988
- }
670989
- throw err;
670990
- }
670991
- return merged;
670992
- }
670993
- function deleteProjectPreferences(root) {
670994
- try {
670995
- const file = prefsPath(root);
670996
- if (!existsSync136(file)) return false;
670997
- unlinkSync31(file);
670998
- return true;
670999
- } catch {
671000
- return false;
671001
- }
671002
- }
671003
- var OMNIUS_DIR4, PROJECTS_DIR, SCHEMA_VERSION, DEFAULT_PREFS;
671004
- var init_project_preferences = __esm({
671005
- "packages/cli/src/api/project-preferences.ts"() {
671006
- "use strict";
671007
- OMNIUS_DIR4 = join149(homedir48(), ".omnius");
671008
- PROJECTS_DIR = join149(OMNIUS_DIR4, "projects");
671009
- SCHEMA_VERSION = 1;
671010
- DEFAULT_PREFS = {
671011
- v: SCHEMA_VERSION,
671012
- updatedAt: 0
671101
+ handleSgrMouse(mouse) {
671102
+ const { btn, col, row, suffix } = mouse;
671103
+ if ((btn === 64 || btn === 96) && this.onScroll)
671104
+ this.onScroll("up", 3, row);
671105
+ else if ((btn === 65 || btn === 97) && this.onScroll)
671106
+ this.onScroll("down", 3, row);
671107
+ else if (this.onPointer) {
671108
+ const hasShift = (btn & 4) !== 0 && btn < 32;
671109
+ if (btn === 2) {
671110
+ } else if (hasShift) {
671111
+ } else if ((btn === 0 || btn === 1) && suffix === "M") {
671112
+ this.onPointer("press", col, row);
671113
+ } else if (btn >= 32 && btn <= 35 && suffix === "M") {
671114
+ this.onPointer("drag", col, row);
671115
+ } else if (suffix === "m") {
671116
+ this.onPointer("release", col, row);
671117
+ }
671118
+ }
671119
+ if (this.onActivity) this.onActivity();
671120
+ }
671013
671121
  };
671014
671122
  }
671015
671123
  });
671016
671124
 
671017
- // packages/cli/src/tui/voicechat.ts
671018
- var voicechat_exports = {};
671019
- __export(voicechat_exports, {
671020
- VoiceChatSession: () => VoiceChatSession
671125
+ // packages/cli/src/tui/direct-input.ts
671126
+ var direct_input_exports = {};
671127
+ __export(direct_input_exports, {
671128
+ DirectInput: () => DirectInput
671021
671129
  });
671022
- import { EventEmitter as EventEmitter13 } from "node:events";
671023
- function clamp0114(x) {
671024
- return x < 0 ? 0 : x > 1 ? 1 : x;
671025
- }
671026
- function alnumRatio(s2) {
671027
- if (!s2) return 0;
671028
- const al = (s2.match(/[\p{L}\p{N}]/gu) || []).length;
671029
- return al / s2.length;
671030
- }
671031
- function wordCount(s2) {
671032
- const words = s2.trim().match(/[\p{L}\p{N}][\p{L}\p{N}'’_-]*/gu);
671033
- return words ? words.length : 0;
671034
- }
671035
- function repeatingCharPenalty(s2) {
671036
- let maxRun = 1, cur = 1;
671037
- for (let i2 = 1; i2 < s2.length; i2++) {
671038
- if (s2[i2] === s2[i2 - 1]) cur++;
671039
- else {
671040
- if (cur > maxRun) maxRun = cur;
671041
- cur = 1;
671042
- }
671043
- }
671044
- if (cur > maxRun) maxRun = cur;
671045
- return Math.min(1, Math.max(0, (maxRun - 3) / 10));
671046
- }
671047
- function computeSignalFromText(text2, confidence2) {
671048
- const t2 = text2.trim();
671049
- if (!t2) return 0;
671050
- if (NOISE_ONLY_RE.test(t2)) return 0.05;
671051
- const len = t2.length;
671052
- const wc = wordCount(t2);
671053
- const alpha = alnumRatio(t2);
671054
- let score = 0;
671055
- if (wc >= 6 && alpha >= 0.6) score = 0.85;
671056
- else if (wc >= 3 && alpha >= 0.5) score = 0.7;
671057
- else if (wc >= 2 && alpha >= 0.4) score = 0.5;
671058
- else if (wc >= 1 && alpha >= 0.3 && len >= 4) score = 0.35;
671059
- else score = 0.15;
671060
- score -= repeatingCharPenalty(t2) * 0.4;
671061
- if (typeof confidence2 === "number" && !Number.isNaN(confidence2)) {
671062
- score = 0.7 * score + 0.3 * clamp0114(confidence2);
671063
- }
671064
- return clamp0114(score);
671065
- }
671066
- function truncateForLog(s2, n2) {
671067
- return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "…";
671068
- }
671069
- function extractToolJson(text2) {
671070
- const lines = text2.split(/\r?\n/);
671071
- for (const line of lines) {
671072
- const t2 = line.trim();
671073
- if (!t2.startsWith("{") || !t2.endsWith("}")) continue;
671074
- try {
671075
- const obj = JSON.parse(t2);
671076
- if (typeof obj.tool === "string") {
671077
- const name10 = obj.tool;
671078
- const args = obj.args && typeof obj.args === "object" ? obj.args : {};
671079
- return { name: name10, args };
671080
- }
671081
- } catch {
671082
- }
671083
- }
671084
- return null;
671085
- }
671086
- function extractToolJsonLoose(text2) {
671087
- const stripped = text2.replace(/```[a-zA-Z]*|```/g, "\n");
671088
- const exact = extractToolJson(stripped);
671089
- if (exact) return exact;
671090
- const match = stripped.match(/[\{][\s\S]*[\}]/);
671091
- if (match) {
671092
- try {
671093
- const obj = JSON.parse(match[0]);
671094
- if (typeof obj.tool === "string") {
671095
- const args = obj.args && typeof obj.args === "object" ? obj.args : {};
671096
- return { name: obj.tool, args };
671097
- }
671098
- } catch {
671099
- }
671100
- }
671101
- return null;
671102
- }
671103
- function stripToolJsonLines(text2) {
671104
- const lines = text2.split(/\r?\n/);
671105
- const kept = lines.filter((l2) => {
671106
- const t2 = l2.trim();
671107
- if (!t2.startsWith("{") || !t2.endsWith("}")) return true;
671108
- try {
671109
- const obj = JSON.parse(t2);
671110
- return !(typeof obj.tool === "string");
671111
- } catch {
671112
- return true;
671113
- }
671114
- });
671115
- return kept.join("\n").trim();
671116
- }
671117
- var VAD_SILENCE_MS, MAX_SEGMENT_MS, MAX_CONTEXT_TURNS, SYSTEM_PROMPT2, MIN_SIGNAL_SCORE, NOISE_ONLY_RE, VoiceChatSession;
671118
- var init_voicechat = __esm({
671119
- "packages/cli/src/tui/voicechat.ts"() {
671130
+ import { EventEmitter as EventEmitter14 } from "node:events";
671131
+ var DirectInput;
671132
+ var init_direct_input = __esm({
671133
+ "packages/cli/src/tui/direct-input.ts"() {
671120
671134
  "use strict";
671121
- VAD_SILENCE_MS = 3e3;
671122
- MAX_SEGMENT_MS = 6500;
671123
- MAX_CONTEXT_TURNS = 20;
671124
- SYSTEM_PROMPT2 = `You are a voice assistant having a live spoken conversation. Keep responses extremely brief — 1-2 sentences max. You're speaking aloud, not writing. Be conversational, direct, and helpful. Don't use markdown or formatting — just natural speech.
671125
-
671126
- Rules:
671127
- - Never invent environment facts (cwd, OS, specs, repo state). If you need a precise fact from the main agent, request a tool by outputting on a single line EXACTLY one JSON object: {"tool": string, "args": object} and nothing else. Then wait for the tool result before answering.
671128
- - You may also request to relay a user task to the main agent by emitting {"tool":"voice_to_main","args":{"message":"...","start":true}}.
671129
- - Prefer tools for factual queries; otherwise, answer directly with a short reply.`;
671130
- MIN_SIGNAL_SCORE = 0.4;
671131
- NOISE_ONLY_RE = /^(?:[.·…\s,;:!?\-–—_()\[\]{}"'`]+|(?:uh|um|erm|hmm|mm+|uhh+|umm+)[\s.!?]*)+$/i;
671132
- VoiceChatSession = class extends EventEmitter13 {
671133
- voice;
671134
- listen;
671135
- backendUrl;
671136
- model;
671137
- apiKey;
671138
- runner;
671139
- toolRelay = null;
671140
- verbose = false;
671141
- debugSnr = false;
671142
- heuristicsEnabled = true;
671143
- toolCatalogNote = null;
671144
- // State machine
671145
- _state = "IDLE";
671146
- active = false;
671147
- // Conversation context — own turns, separate from main agent
671148
- context = [];
671149
- turnCount = 0;
671150
- // Transcripts — separate logs for user<->voice and relay voice<->main
671151
- voiceTranscript = [];
671152
- relayTranscript = [];
671153
- // VAD segment capture
671154
- captureBuffer = "";
671155
- captureStartTime = 0;
671156
- silenceTimer = null;
671157
- maxSegmentTimer = null;
671158
- lastSignalScore = null;
671159
- // Abort control for inference
671160
- abortController = null;
671161
- // Callbacks
671162
- onStatus;
671163
- onUserSpeech;
671164
- onPartialTranscript;
671165
- onAgentSpeech;
671166
- onStateChange;
671167
- // Bound handlers for cleanup
671168
- _onTranscript = null;
671169
- _onError = null;
671170
- _retryMicTimer = null;
671171
- constructor(opts) {
671135
+ DirectInput = class extends EventEmitter14 {
671136
+ /** Current input line text */
671137
+ line = "";
671138
+ /** Cursor position within .line (0-based) */
671139
+ cursor = 0;
671140
+ _history;
671141
+ _historySize;
671142
+ _historyIndex = -1;
671143
+ _savedLine = "";
671144
+ // saved current input when navigating history
671145
+ _completer = null;
671146
+ _paused = false;
671147
+ _closed = false;
671148
+ _input;
671149
+ _buffer = "";
671150
+ // partial escape sequence buffer
671151
+ _flushTimer = null;
671152
+ _expectPrefixlessMouseUntil = 0;
671153
+ constructor(input, options2) {
671172
671154
  super();
671173
- this.voice = opts.voice;
671174
- this.listen = opts.listen;
671175
- this.backendUrl = opts.backendUrl.replace(/\/+$/, "");
671176
- this.model = opts.model;
671177
- this.apiKey = opts.apiKey ?? "";
671178
- this.runner = opts.runner ?? null;
671179
- this.toolRelay = opts.toolRelay ?? null;
671180
- this.verbose = Boolean(opts.verbose);
671181
- this.debugSnr = Boolean(opts.debugSnr);
671182
- this.heuristicsEnabled = opts.heuristicsEnabled !== false;
671183
- if (typeof opts.vadSilenceMs === "number" && opts.vadSilenceMs > 0) {
671184
- this._vadSilenceMs = Math.floor(opts.vadSilenceMs);
671185
- }
671186
- this.onStatus = opts.onStatus ?? (() => {
671187
- });
671188
- this.onUserSpeech = opts.onUserSpeech ?? (() => {
671189
- });
671190
- this.onPartialTranscript = opts.onPartialTranscript ?? (() => {
671191
- });
671192
- this.onAgentSpeech = opts.onAgentSpeech ?? (() => {
671155
+ this._input = input;
671156
+ this._history = [...options2?.history ?? []];
671157
+ this._historySize = options2?.historySize ?? 500;
671158
+ this._completer = options2?.completer ?? null;
671159
+ input.on("data", (chunk) => {
671160
+ if (this._paused || this._closed) return;
671161
+ this.feed(chunk.toString("utf8"));
671193
671162
  });
671194
- this.onStateChange = opts.onStateChange ?? (() => {
671163
+ input.on("end", () => {
671164
+ if (!this._closed) this.close();
671195
671165
  });
671196
671166
  }
671197
- get state() {
671198
- return this._state;
671167
+ /** Process raw input data — parse escape sequences and printable chars */
671168
+ feed(data) {
671169
+ const beforeLine = this.line;
671170
+ const beforeCursor = this.cursor;
671171
+ this._buffer += data;
671172
+ this._processBuffer();
671173
+ this._emitChangeIfNeeded(beforeLine, beforeCursor);
671199
671174
  }
671200
- get isActive() {
671201
- return this.active;
671175
+ /** Pause input processing (for overlay transitions) */
671176
+ pause() {
671177
+ this._paused = true;
671202
671178
  }
671203
- // ---------------------------------------------------------------------------
671204
- // State transitions
671205
- // ---------------------------------------------------------------------------
671206
- setState(next) {
671207
- if (this._state === next) return;
671208
- const prev = this._state;
671209
- this._state = next;
671210
- this.onStateChange(next);
671211
- this.emit("stateChange", { from: prev, to: next });
671179
+ /** Resume input processing */
671180
+ resume() {
671181
+ this._paused = false;
671212
671182
  }
671213
- // ---------------------------------------------------------------------------
671214
- // Start / Stop
671215
- // ---------------------------------------------------------------------------
671216
- async start() {
671217
- if (this.active) return;
671218
- if (!this.voice.enabled || !this.voice.ready) {
671219
- this.onStatus("Enabling voice engine...");
671220
- await this.voice.toggle();
671221
- }
671222
- this.active = true;
671223
- this.context = [{ role: "system", content: SYSTEM_PROMPT2 }];
671224
- if (this.toolRelay) {
671225
- this.toolCatalogNote = `Available tools (emit one-line JSON: {"tool":string,"args":object}):
671226
- - voice_env{} → environment facts (cwd, os, cpu, mem)
671227
- - voice_status{} → main-agent status
671228
- - voice_list_files{dir?: string='.'} → list directory (bounded)
671229
- - voice_read_file{path: string, max?: number=2048} → read file snippet
671230
- - voice_to_main{message: string, start?: boolean=true} → relay/start task`;
671231
- this.context.push({ role: "system", content: this.toolCatalogNote });
671232
- }
671233
- this.turnCount = 0;
671234
- if (this.verbose) this.onStatus("VoiceChat active — LISTENING");
671235
- this._onTranscript = (...args) => {
671236
- let text2;
671237
- let isFinal;
671238
- let snr;
671239
- let confidence2;
671240
- if (typeof args[0] === "object" && args[0] !== null) {
671241
- const evt = args[0];
671242
- text2 = evt.text ?? "";
671243
- isFinal = evt.isFinal ?? false;
671244
- snr = evt.snr;
671245
- confidence2 = evt.confidence;
671246
- } else {
671247
- text2 = String(args[0] ?? "");
671248
- isFinal = Boolean(args[1]);
671249
- }
671250
- if (!text2.trim()) return;
671251
- this.handleTranscript(text2.trim(), isFinal, snr, confidence2);
671252
- };
671253
- this._onError = (err) => {
671254
- const msg = err instanceof Error ? err.message : String(err);
671255
- this.onStatus(`ASR error (voicechat continues without mic): ${msg.slice(0, 80)}`);
671256
- if (this.active && !this._retryMicTimer) {
671257
- this._retryMicTimer = setTimeout(async () => {
671258
- this._retryMicTimer = null;
671259
- if (!this.active) return;
671260
- try {
671261
- await this.listen.stop().catch(() => {
671262
- });
671263
- await this.listen.start();
671264
- if (this.verbose) this.onStatus("Mic auto-recovered — LISTENING");
671265
- } catch {
671266
- }
671267
- }, 1e3);
671268
- }
671269
- };
671270
- this.listen.on("transcript", this._onTranscript);
671271
- this.listen.on("error", this._onError);
671272
- try {
671273
- await this.listen.start();
671274
- this.setState("LISTENING");
671275
- if (this.verbose) this.onStatus("Mic active — LISTENING for speech...");
671276
- } catch (err) {
671277
- this.onStatus(`Mic failed: ${err instanceof Error ? err.message : String(err)}. VoiceChat active without mic.`);
671278
- this.setState("LISTENING");
671183
+ /** Close the input handler */
671184
+ close() {
671185
+ if (this._closed) return;
671186
+ this._closed = true;
671187
+ if (this._flushTimer) {
671188
+ clearTimeout(this._flushTimer);
671189
+ this._flushTimer = null;
671279
671190
  }
671191
+ this.emit("close");
671280
671192
  }
671281
- async stop() {
671282
- if (!this.active) return;
671283
- this.active = false;
671284
- if (this.abortController) {
671285
- this.abortController.abort();
671286
- this.abortController = null;
671287
- }
671288
- if (this.silenceTimer) {
671289
- clearTimeout(this.silenceTimer);
671290
- this.silenceTimer = null;
671291
- }
671292
- if (this.maxSegmentTimer) {
671293
- clearTimeout(this.maxSegmentTimer);
671294
- this.maxSegmentTimer = null;
671295
- }
671296
- if (this.captureBuffer.trim() && (this._state === "CAPTURING" || this._state === "TRANSCRIBING")) {
671297
- this.finalizeSegment();
671298
- }
671299
- if (this._onTranscript) {
671300
- this.listen.removeAllListeners("transcript");
671301
- this._onTranscript = null;
671302
- }
671303
- if (this._onError) {
671304
- this.listen.removeAllListeners("error");
671305
- this._onError = null;
671306
- }
671307
- try {
671308
- await this.listen.stop();
671309
- } catch {
671310
- }
671311
- this.setState("IDLE");
671312
- if (this.verbose) this.onStatus("VoiceChat ended");
671313
- this.emit("stopped");
671193
+ /** No-op — readline compat (StatusBar renders the prompt, not us) */
671194
+ setPrompt(_prompt) {
671314
671195
  }
671315
- // ---------------------------------------------------------------------------
671316
- // Transcript handling — VAD-style segment capture (Voryn pattern)
671317
- // ---------------------------------------------------------------------------
671318
- handleTranscript(text2, isFinal, snr, confidence2) {
671319
- if (!this.active) return;
671320
- if (this._state !== "LISTENING" && this._state !== "CAPTURING") {
671321
- return;
671196
+ /** No-op — readline compat */
671197
+ prompt(_preserveCursor) {
671198
+ }
671199
+ /** Set the line content and cursor position (for Esc-to-recall or suggestion apply) */
671200
+ setLine(text2, cursorPos) {
671201
+ const beforeLine = this.line;
671202
+ const beforeCursor = this.cursor;
671203
+ this.line = text2;
671204
+ this.cursor = cursorPos ?? text2.length;
671205
+ this._emitChangeIfNeeded(beforeLine, beforeCursor);
671206
+ }
671207
+ /** Pre-submit hook — called before Enter submits. Return true to consume Enter. */
671208
+ _preSubmit = null;
671209
+ setPreSubmit(hook) {
671210
+ this._preSubmit = hook;
671211
+ }
671212
+ /** Navigate history up (older) */
671213
+ historyUp() {
671214
+ if (this._history.length === 0) return;
671215
+ if (this._historyIndex === -1) {
671216
+ this._savedLine = this.line;
671322
671217
  }
671323
- if (this._state === "LISTENING") {
671324
- this.setState("CAPTURING");
671325
- this.captureBuffer = "";
671326
- this.captureStartTime = Date.now();
671327
- this.maxSegmentTimer = setTimeout(() => {
671328
- if (this._state === "CAPTURING") {
671329
- this.finalizeSegment();
671330
- }
671331
- }, MAX_SEGMENT_MS);
671218
+ if (this._historyIndex < this._history.length - 1) {
671219
+ this._historyIndex++;
671220
+ this.line = this._history[this._historyIndex];
671221
+ this.cursor = this.line.length;
671332
671222
  }
671333
- this.captureBuffer = text2;
671334
- this.lastSignalScore = typeof snr === "number" && !Number.isNaN(snr) ? clamp0114(snr) : computeSignalFromText(text2, confidence2);
671335
- this.emit("snr", { score: this.lastSignalScore });
671336
- this.onPartialTranscript(text2);
671337
- if (this.silenceTimer) clearTimeout(this.silenceTimer);
671338
- const waitMs = this._vadSilenceMs ?? VAD_SILENCE_MS;
671339
- this.silenceTimer = setTimeout(() => {
671340
- if (this._state === "CAPTURING") {
671341
- this.finalizeSegment();
671342
- }
671343
- }, waitMs);
671344
671223
  }
671345
- // ---------------------------------------------------------------------------
671346
- // Segment finalization → Transcribing → Thinking → Speaking
671347
- // ---------------------------------------------------------------------------
671348
- finalizeSegment() {
671349
- const text2 = this.captureBuffer.trim();
671350
- if (this.silenceTimer) {
671351
- clearTimeout(this.silenceTimer);
671352
- this.silenceTimer = null;
671353
- }
671354
- if (this.maxSegmentTimer) {
671355
- clearTimeout(this.maxSegmentTimer);
671356
- this.maxSegmentTimer = null;
671224
+ /** Navigate history down (newer) */
671225
+ historyDown() {
671226
+ if (this._historyIndex <= -1) return;
671227
+ this._historyIndex--;
671228
+ if (this._historyIndex === -1) {
671229
+ this.line = this._savedLine;
671230
+ } else {
671231
+ this.line = this._history[this._historyIndex];
671357
671232
  }
671358
- this.captureBuffer = "";
671359
- if (!text2) {
671360
- this.setState("LISTENING");
671361
- return;
671233
+ this.cursor = this.line.length;
671234
+ }
671235
+ /**
671236
+ * Move cursor up one wrapped line. Returns true if moved, false if at top line.
671237
+ * Used by arrow-up to navigate within wrapped input before falling through to history.
671238
+ */
671239
+ cursorUpWrapped(availWidth) {
671240
+ if (this.line.length <= availWidth) return false;
671241
+ const { charPositions, rawLines } = this._computeWrappedLines(availWidth);
671242
+ if (rawLines.length <= 1) return false;
671243
+ let currentLineIdx = rawLines.length - 1;
671244
+ for (let i2 = 0; i2 < charPositions.length; i2++) {
671245
+ const lineStart = charPositions[i2];
671246
+ const lineEnd = lineStart + rawLines[i2].length;
671247
+ if (this.cursor >= lineStart && this.cursor <= lineEnd) {
671248
+ currentLineIdx = i2;
671249
+ break;
671250
+ }
671362
671251
  }
671363
- const score = this.lastSignalScore ?? computeSignalFromText(text2);
671364
- if (score < MIN_SIGNAL_SCORE || NOISE_ONLY_RE.test(text2)) {
671365
- if (this.debugSnr) this.onStatus(`Ignoring low-signal utterance (SNR:${score.toFixed(2)}): ${truncateForLog(text2, 48)}`);
671366
- this.emit("snrFiltered", { score, text: text2 });
671367
- this.setState("LISTENING");
671368
- this.captureBuffer = "";
671369
- this.lastSignalScore = null;
671370
- return;
671252
+ if (currentLineIdx === 0) return false;
671253
+ const prevLineIdx = currentLineIdx - 1;
671254
+ const currentColInLine = this.cursor - charPositions[currentLineIdx];
671255
+ const prevLineLength = rawLines[prevLineIdx].length;
671256
+ const newColInLine = Math.min(currentColInLine, prevLineLength);
671257
+ this.cursor = charPositions[prevLineIdx] + newColInLine;
671258
+ return true;
671259
+ }
671260
+ /**
671261
+ * Move cursor down one wrapped line. Returns true if moved, false if at bottom line.
671262
+ * Used by arrow-down to navigate within wrapped input before falling through to history.
671263
+ */
671264
+ cursorDownWrapped(availWidth) {
671265
+ if (this.line.length <= availWidth) return false;
671266
+ const { charPositions, rawLines } = this._computeWrappedLines(availWidth);
671267
+ if (rawLines.length <= 1) return false;
671268
+ let currentLineIdx = rawLines.length - 1;
671269
+ for (let i2 = 0; i2 < charPositions.length; i2++) {
671270
+ const lineStart = charPositions[i2];
671271
+ const lineEnd = lineStart + rawLines[i2].length;
671272
+ if (this.cursor >= lineStart && this.cursor <= lineEnd) {
671273
+ currentLineIdx = i2;
671274
+ break;
671275
+ }
671371
671276
  }
671372
- this.setState("TRANSCRIBING");
671373
- this.onUserSpeech(text2);
671374
- this.voiceTranscript.push({ role: "user", content: text2, ts: Date.now() });
671375
- this.context.push({ role: "user", content: text2 });
671376
- this.turnCount++;
671377
- if (this.runner) {
671378
- try {
671379
- this.runner.injectUserMessage(`[VOICECHAT] ${text2}`);
671380
- } catch {
671277
+ if (currentLineIdx === rawLines.length - 1) return false;
671278
+ const nextLineIdx = currentLineIdx + 1;
671279
+ const currentColInLine = this.cursor - charPositions[currentLineIdx];
671280
+ const nextLineLength = rawLines[nextLineIdx].length;
671281
+ const newColInLine = Math.min(currentColInLine, nextLineLength);
671282
+ this.cursor = charPositions[nextLineIdx] + newColInLine;
671283
+ return true;
671284
+ }
671285
+ /**
671286
+ * Compute wrapped lines (word-aware). Returns charPositions (start index of each line)
671287
+ * and rawLines (text of each line). Matches wrapInput logic in status-bar.ts.
671288
+ */
671289
+ _computeWrappedLines(availWidth) {
671290
+ const width = Math.max(1, availWidth);
671291
+ const rawLines = [];
671292
+ const charPositions = [];
671293
+ const pushWrappedSegment = (segment, segmentStart2) => {
671294
+ if (segment.length === 0) {
671295
+ charPositions.push(segmentStart2);
671296
+ rawLines.push("");
671297
+ return;
671381
671298
  }
671299
+ let offset = 0;
671300
+ while (offset < segment.length) {
671301
+ const remaining = segment.slice(offset);
671302
+ if (remaining.length <= width) {
671303
+ charPositions.push(segmentStart2 + offset);
671304
+ rawLines.push(remaining);
671305
+ break;
671306
+ }
671307
+ let breakAt = width;
671308
+ const lastSpace = remaining.lastIndexOf(" ", width);
671309
+ if (lastSpace > 0 && lastSpace >= width * 0.3) {
671310
+ breakAt = lastSpace + 1;
671311
+ }
671312
+ charPositions.push(segmentStart2 + offset);
671313
+ rawLines.push(remaining.slice(0, breakAt));
671314
+ offset += breakAt;
671315
+ }
671316
+ };
671317
+ if (this.line.length === 0) {
671318
+ pushWrappedSegment("", 0);
671319
+ return { charPositions, rawLines };
671382
671320
  }
671383
- while (this.context.length > MAX_CONTEXT_TURNS + 1) {
671384
- this.context.splice(1, 1);
671321
+ let segmentStart = 0;
671322
+ while (segmentStart <= this.line.length) {
671323
+ const newlineAt = this.line.indexOf("\n", segmentStart);
671324
+ const segmentEnd = newlineAt === -1 ? this.line.length : newlineAt;
671325
+ pushWrappedSegment(this.line.slice(segmentStart, segmentEnd), segmentStart);
671326
+ if (newlineAt === -1) break;
671327
+ segmentStart = newlineAt + 1;
671328
+ if (segmentStart === this.line.length) {
671329
+ pushWrappedSegment("", segmentStart);
671330
+ break;
671331
+ }
671385
671332
  }
671386
- this.think();
671333
+ return { charPositions, rawLines };
671387
671334
  }
671388
671335
  // ---------------------------------------------------------------------------
671389
- // Direct Ollama inference (not through main agent runner)
671336
+ // Private: buffer processing and escape sequence parsing
671390
671337
  // ---------------------------------------------------------------------------
671391
- async think() {
671392
- if (!this.active) return;
671393
- this.setState("THINKING");
671394
- if (this.verbose) this.onStatus("Thinking...");
671395
- this.abortController = new AbortController();
671396
- try {
671397
- if (this.toolRelay?.contextSnapshot) {
671398
- try {
671399
- const snap = await Promise.resolve(this.toolRelay.contextSnapshot());
671400
- if (snap && snap.trim()) {
671401
- this.context.push({ role: "system", content: `Context snapshot (read-only):
671402
- ${snap.trim()}` });
671338
+ _processBuffer() {
671339
+ let i2 = 0;
671340
+ while (i2 < this._buffer.length) {
671341
+ const remaining = this._buffer.slice(i2);
671342
+ const prefixlessMouse = this._matchPrefixlessSgrMouse(remaining);
671343
+ if (prefixlessMouse) {
671344
+ i2 += prefixlessMouse.length;
671345
+ continue;
671346
+ }
671347
+ if (this._looksLikePartialPrefixlessSgrMouse(remaining)) {
671348
+ break;
671349
+ }
671350
+ const ch = this._buffer[i2];
671351
+ const code8 = ch.charCodeAt(0);
671352
+ if (code8 === 27) {
671353
+ if (remaining.length >= 2 && remaining[1] === "[") {
671354
+ const mouseMatch = remaining.match(/^\x1B\[<(\d+);(\d+);(\d+)([Mm])/);
671355
+ if (mouseMatch) {
671356
+ i2 += mouseMatch[0].length;
671357
+ continue;
671403
671358
  }
671404
- } catch {
671359
+ if (remaining.startsWith("\x1B[<") && remaining.length < 15) {
671360
+ break;
671361
+ }
671362
+ if (remaining.startsWith("\x1B[M") && remaining.length >= 6) {
671363
+ i2 += 6;
671364
+ continue;
671365
+ }
671366
+ if (remaining.startsWith("\x1B[M") && remaining.length < 6) {
671367
+ break;
671368
+ }
671369
+ if (remaining.startsWith("\x1B[<")) {
671370
+ this._expectPrefixlessMouseUntil = Date.now() + 1e3;
671371
+ let end = 3;
671372
+ while (end < remaining.length && remaining[end] !== "M" && remaining[end] !== "m") end++;
671373
+ if (end < remaining.length) end++;
671374
+ i2 += end;
671375
+ continue;
671376
+ }
671377
+ const csiMatch = remaining.match(/^\x1B\[([0-9;]*)([A-Za-z~])/);
671378
+ if (csiMatch) {
671379
+ this._handleCSI(csiMatch[1], csiMatch[2]);
671380
+ i2 += csiMatch[0].length;
671381
+ continue;
671382
+ }
671383
+ if (remaining.length < 10) {
671384
+ break;
671385
+ }
671386
+ i2 += 2;
671387
+ continue;
671405
671388
  }
671406
- }
671407
- const lastUser = [...this.context].reverse().find((m2) => m2.role === "user")?.content || "";
671408
- let preAnswered = false;
671409
- if (this.heuristicsEnabled && this.toolRelay && lastUser) {
671410
- const lower = lastUser.toLowerCase();
671411
- const wantList = /(list|show|explore|browse|what's in|whats in|contents).*(dir|directory|folder|files)/.test(lower);
671412
- const wantEnv = /(what\s+dir|cwd|current\s+dir|working\s+directory|where\s+are\s+you)/.test(lower);
671413
- const readMatch = lastUser.match(/(?:read|open|show)\s+file\s+([\w./\\-]+)\b/i);
671414
- const toMainMatch = lastUser.match(/^(?:start|run|do)\s+(.{5,})$/i);
671415
- try {
671416
- if (wantEnv) {
671417
- const out = await this.toolRelay.call("voice_env", {});
671418
- this.context.push({ role: "system", content: `Tool voice_env result (authoritative):
671419
- ${out}` });
671420
- preAnswered = true;
671421
- } else if (wantList) {
671422
- const out = await this.toolRelay.call("voice_list_files", { dir: "." });
671423
- this.context.push({ role: "system", content: `Tool voice_list_files result (authoritative):
671424
- ${out}` });
671425
- preAnswered = true;
671426
- } else if (readMatch) {
671427
- const out = await this.toolRelay.call("voice_read_file", { path: readMatch[1], max: 1024 });
671428
- this.context.push({ role: "system", content: `Tool voice_read_file result (authoritative):
671429
- ${out}` });
671430
- preAnswered = true;
671431
- } else if (toMainMatch) {
671432
- const msg = toMainMatch[1].trim();
671433
- const out = await this.toolRelay.call("voice_to_main", { message: msg, start: true });
671434
- this.relayTranscript.push({ dir: "toMain", content: msg, ts: Date.now() });
671435
- this.context.push({ role: "system", content: `Tool voice_to_main result (authoritative):
671436
- ${out}` });
671437
- preAnswered = true;
671389
+ if (remaining.length >= 2 && remaining[1] === "O") {
671390
+ if (remaining.length >= 3) {
671391
+ this._handleSS3(remaining[2]);
671392
+ i2 += 3;
671393
+ continue;
671438
671394
  }
671439
- } catch {
671395
+ break;
671440
671396
  }
671441
- }
671442
- let response = "";
671443
- for (let i2 = 0; i2 < 3; i2++) {
671444
- response = await this.streamOllamaInference(this.abortController.signal);
671445
- if (!this.toolRelay) break;
671446
- const toolReq = extractToolJsonLoose(response);
671447
- if (!toolReq) break;
671448
- const { name: name10, args } = toolReq;
671449
- let toolOutput = "";
671450
- try {
671451
- toolOutput = await this.toolRelay.call(name10, args);
671452
- } catch (e2) {
671453
- toolOutput = `Tool ${name10} failed: ${e2 instanceof Error ? e2.message : String(e2)}`;
671397
+ if (remaining.length >= 2 && (remaining[1] === "\r" || remaining[1] === "\n")) {
671398
+ this._insertText("\n");
671399
+ i2 += 2;
671400
+ if (remaining[1] === "\r" && remaining[2] === "\n") i2++;
671401
+ continue;
671454
671402
  }
671455
- if (name10 === "voice_to_main") {
671456
- const msg = typeof args?.message === "string" ? String(args.message) : "";
671457
- if (msg) this.relayTranscript.push({ dir: "toMain", content: msg, ts: Date.now() });
671403
+ if (remaining.length === 1) {
671404
+ break;
671458
671405
  }
671459
- this.context.push({ role: "system", content: `Tool ${name10} result (authoritative):
671460
- ${toolOutput}` });
671461
- }
671462
- if (!this.active) return;
671463
- if (this.heuristicsEnabled && this.toolRelay && /\b(can't|cannot)\b/i.test(response) && this.toolCatalogNote) {
671464
- this.context.push({ role: "system", content: `You have tools. Use them. ${this.toolCatalogNote}` });
671465
- response = await this.streamOllamaInference(this.abortController.signal);
671406
+ i2++;
671407
+ continue;
671466
671408
  }
671467
- if (response.trim()) {
671468
- const finalSpoken = stripToolJsonLines(response.trim());
671469
- this.context.push({ role: "assistant", content: finalSpoken });
671470
- this.setState("SPEAKING");
671471
- this.onAgentSpeech(finalSpoken);
671472
- try {
671473
- this.listen.pause();
671474
- } catch {
671409
+ if (code8 < 32 || code8 === 127) {
671410
+ if (code8 === 13) {
671411
+ if (this._preSubmit?.()) {
671412
+ i2++;
671413
+ continue;
671414
+ }
671415
+ this._submit();
671416
+ i2++;
671417
+ if (this._buffer[i2] === "\n") i2++;
671418
+ continue;
671475
671419
  }
671476
- this.voice.speak(finalSpoken);
671477
- this.voiceTranscript.push({ role: "assistant", content: finalSpoken, ts: Date.now() });
671478
- this.voiceTranscript.push({ role: "assistant", content: response.trim(), ts: Date.now() });
671479
- if (this.runner) {
671480
- this.injectSummary();
671420
+ if (code8 === 10) {
671421
+ this._insertText("\n");
671422
+ i2++;
671423
+ continue;
671481
671424
  }
671482
- if (typeof this.voice.waitUntilIdle === "function") {
671483
- try {
671484
- await this.voice.waitUntilIdle();
671485
- } catch {
671425
+ this._handleControl(code8);
671426
+ i2++;
671427
+ continue;
671428
+ }
671429
+ const char = this._buffer[i2];
671430
+ this.line = this.line.slice(0, this.cursor) + char + this.line.slice(this.cursor);
671431
+ this.cursor++;
671432
+ i2++;
671433
+ }
671434
+ this._buffer = this._buffer.slice(i2);
671435
+ if (this._buffer.length > 0) {
671436
+ if (this._flushTimer) clearTimeout(this._flushTimer);
671437
+ this._flushTimer = setTimeout(() => {
671438
+ this._flushTimer = null;
671439
+ if (this._buffer.length > 0) {
671440
+ if (this._buffer.startsWith("\x1B[<") || this._buffer.startsWith("\x1B[M")) {
671441
+ this._expectPrefixlessMouseUntil = Date.now() + 1e3;
671442
+ this._buffer = "";
671443
+ return;
671486
671444
  }
671487
- } else {
671488
- const estimatedMs = Math.max(1500, response.length / 5 * (6e4 / 150));
671489
- await new Promise((r2) => setTimeout(r2, estimatedMs));
671445
+ if (this._buffer === "\x1B[") {
671446
+ this._expectPrefixlessMouseUntil = Date.now() + 1e3;
671447
+ this._buffer = "";
671448
+ return;
671449
+ }
671450
+ if (this._looksLikePartialPrefixlessSgrMouse(this._buffer)) {
671451
+ this._buffer = "";
671452
+ return;
671453
+ }
671454
+ if (this._buffer === "\x1B") {
671455
+ this.emit("escape");
671456
+ }
671457
+ this._buffer = "";
671490
671458
  }
671459
+ }, 50);
671460
+ } else {
671461
+ if (this._flushTimer) {
671462
+ clearTimeout(this._flushTimer);
671463
+ this._flushTimer = null;
671491
671464
  }
671492
- } catch (err) {
671493
- if (!this.active) return;
671494
- const msg = err instanceof Error ? err.message : String(err);
671495
- if (!msg.includes("abort")) {
671496
- this.onStatus(`Inference error: ${msg.slice(0, 100)}`);
671497
- }
671498
- } finally {
671499
- this.abortController = null;
671500
671465
  }
671501
- if (this.active) {
671502
- try {
671503
- await this.listen.resume();
671504
- } catch {
671505
- }
671506
- this.setState("LISTENING");
671507
- if (this.verbose) this.onStatus("LISTENING...");
671466
+ }
671467
+ _matchPrefixlessSgrMouse(input) {
671468
+ const match = input.match(/^(<)?(\d{1,3});(\d{1,5});(\d{1,5})([Mm])/);
671469
+ if (!match) return null;
671470
+ const hasMarker = Boolean(match[1]);
671471
+ const btn = parseInt(match[2], 10);
671472
+ const col = parseInt(match[3], 10);
671473
+ const row = parseInt(match[4], 10);
671474
+ if (!Number.isFinite(btn) || !Number.isFinite(col) || !Number.isFinite(row))
671475
+ return null;
671476
+ if (btn < 0 || btn > 255 || col <= 0 || row <= 0) return null;
671477
+ if (!hasMarker && Date.now() > this._expectPrefixlessMouseUntil && !this._isKnownMouseButton(btn))
671478
+ return null;
671479
+ return { length: match[0].length };
671480
+ }
671481
+ _looksLikePartialPrefixlessSgrMouse(input) {
671482
+ if (input.length < 2) return false;
671483
+ if (/^<\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) return true;
671484
+ if (Date.now() <= this._expectPrefixlessMouseUntil && /^\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) {
671485
+ return true;
671508
671486
  }
671487
+ return false;
671509
671488
  }
671510
- /**
671511
- * Stream inference. Tries native Ollama /api/chat first (supports think:false
671512
- * for reasoning models), falls back to OpenAI-compat /v1/chat/completions.
671513
- */
671514
- async streamOllamaInference(signal) {
671515
- const baseUrl = this.backendUrl.replace(/\/v1\/?$/, "");
671516
- const headers = { "Content-Type": "application/json" };
671517
- if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
671518
- try {
671519
- const nativeBody = JSON.stringify({
671520
- model: this.model,
671521
- messages: this.context,
671522
- stream: true,
671523
- think: false,
671524
- // Disable reasoning — voice chat needs fast, direct responses
671525
- options: { temperature: 0.7, num_predict: 256 }
671526
- });
671527
- const res2 = await fetch(`${baseUrl}/api/chat`, {
671528
- method: "POST",
671529
- headers,
671530
- body: nativeBody,
671531
- signal
671532
- });
671533
- if (res2.ok) {
671534
- return await this.parseOllamaNativeStream(res2, signal);
671489
+ _isKnownMouseButton(btn) {
671490
+ if (btn >= 0 && btn <= 6) return true;
671491
+ if (btn >= 32 && btn <= 39) return true;
671492
+ if (btn >= 64 && btn <= 71) return true;
671493
+ if (btn >= 96 && btn <= 103) return true;
671494
+ return false;
671495
+ }
671496
+ _emitChangeIfNeeded(beforeLine, beforeCursor) {
671497
+ if (this.line === beforeLine && this.cursor === beforeCursor) return;
671498
+ this.emit("change", { line: this.line, cursor: this.cursor });
671499
+ }
671500
+ /** Handle CSI escape sequence: \x1B[ {params} {final} */
671501
+ _handleCSI(params, final2) {
671502
+ switch (final2) {
671503
+ case "A":
671504
+ if (params === "1;2") {
671505
+ this.emit("shiftup");
671506
+ return;
671507
+ }
671508
+ this.emit("up");
671509
+ return;
671510
+ case "B":
671511
+ if (params === "1;2") {
671512
+ this.emit("shiftdown");
671513
+ return;
671514
+ }
671515
+ this.emit("down");
671516
+ return;
671517
+ case "C":
671518
+ if (params === "1;5") {
671519
+ this.emit("ctrl-right");
671520
+ return;
671521
+ }
671522
+ if (this.cursor < this.line.length) this.cursor++;
671523
+ return;
671524
+ case "D":
671525
+ if (params === "1;5") {
671526
+ this.emit("ctrl-left");
671527
+ return;
671528
+ }
671529
+ if (this.cursor > 0) this.cursor--;
671530
+ return;
671531
+ case "H":
671532
+ this.cursor = 0;
671533
+ return;
671534
+ case "F":
671535
+ this.cursor = this.line.length;
671536
+ return;
671537
+ case "~":
671538
+ if (this._isShiftEnterCSI(params)) {
671539
+ this._insertText("\n");
671540
+ return;
671541
+ }
671542
+ if (params === "3") {
671543
+ if (this.cursor < this.line.length) {
671544
+ this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1);
671545
+ }
671546
+ return;
671547
+ }
671548
+ if (params === "5") {
671549
+ this.emit("pageup");
671550
+ return;
671551
+ }
671552
+ if (params === "6") {
671553
+ this.emit("pagedown");
671554
+ return;
671555
+ }
671556
+ return;
671557
+ case "u": {
671558
+ const parts = params.split(";");
671559
+ const codepoint = parseInt(parts[0] ?? "0");
671560
+ const modifiers = parseInt(parts[1] ?? "1");
671561
+ const hasCtrl = modifiers - 1 & 4;
671562
+ const hasShift = modifiers - 1 & 1;
671563
+ if (hasShift && (codepoint === 10 || codepoint === 13)) {
671564
+ this._insertText("\n");
671565
+ return;
671566
+ }
671567
+ if (hasCtrl && hasShift) {
671568
+ if (codepoint === 67) {
671569
+ this.emit("ctrl-shift-c");
671570
+ return;
671571
+ }
671572
+ if (codepoint === 66) {
671573
+ this.emit("ctrl-shift-b");
671574
+ return;
671575
+ }
671576
+ if (codepoint === 86) {
671577
+ this.emit("ctrl-shift-v");
671578
+ return;
671579
+ }
671580
+ }
671581
+ if (hasCtrl && !hasShift && (codepoint === 73 || codepoint === 105)) {
671582
+ this.emit("ctrl-i");
671583
+ return;
671584
+ }
671585
+ return;
671535
671586
  }
671536
- } catch (err) {
671537
- const msg = err instanceof Error ? err.message : "";
671538
- if (msg.includes("abort")) throw err;
671539
671587
  }
671540
- const openaiBody = JSON.stringify({
671541
- model: this.model,
671542
- messages: this.context,
671543
- stream: true,
671544
- temperature: 0.7,
671545
- max_tokens: 1024
671546
- });
671547
- const endpoint = baseUrl.includes("/v1") ? `${baseUrl}/chat/completions` : `${baseUrl}/v1/chat/completions`;
671548
- const res = await fetch(endpoint, { method: "POST", headers, body: openaiBody, signal });
671549
- if (!res.ok) {
671550
- const errText = await res.text().catch(() => "unknown");
671551
- throw new Error(`Inference ${res.status}: ${errText.slice(0, 200)}`);
671588
+ }
671589
+ _isShiftEnterCSI(params) {
671590
+ const parts = params.split(";").map((part) => parseInt(part, 10));
671591
+ if (parts.length === 2) {
671592
+ const [codepoint, modifiers] = parts;
671593
+ return (codepoint === 10 || codepoint === 13) && modifiers === 2;
671552
671594
  }
671553
- return await this.parseOpenAIStream(res);
671595
+ if (parts.length === 3) {
671596
+ const [prefix, modifiers, codepoint] = parts;
671597
+ return prefix === 27 && modifiers === 2 && (codepoint === 10 || codepoint === 13);
671598
+ }
671599
+ return false;
671554
671600
  }
671555
- /** Parse native Ollama /api/chat streaming response (NDJSON, not SSE) */
671556
- async parseOllamaNativeStream(res, _signal) {
671557
- const reader = res.body?.getReader();
671558
- if (!reader) throw new Error("No response body");
671559
- const decoder = new TextDecoder();
671560
- let fullText = "";
671561
- let buffer2 = "";
671562
- while (true) {
671563
- const { done, value: value2 } = await reader.read();
671564
- if (done) break;
671565
- buffer2 += decoder.decode(value2, { stream: true });
671566
- const lines = buffer2.split("\n");
671567
- buffer2 = lines.pop() ?? "";
671568
- for (const line of lines) {
671569
- if (!line.trim()) continue;
671570
- try {
671571
- const parsed = JSON.parse(line);
671572
- const content = parsed.message?.content;
671573
- const thinking = parsed.message?.thinking;
671574
- if (content && thinking === void 0) {
671575
- fullText += content;
671576
- }
671577
- if (parsed.done) return fullText;
671578
- } catch {
671601
+ _insertText(text2) {
671602
+ this.line = this.line.slice(0, this.cursor) + text2 + this.line.slice(this.cursor);
671603
+ this.cursor += text2.length;
671604
+ }
671605
+ /** Handle SS3 sequence: \x1BO {final} (some terminals use this for arrows/Home/End) */
671606
+ _handleSS3(final2) {
671607
+ switch (final2) {
671608
+ case "A":
671609
+ this.emit("up");
671610
+ return;
671611
+ case "B":
671612
+ this.emit("down");
671613
+ return;
671614
+ case "C":
671615
+ if (this.cursor < this.line.length) this.cursor++;
671616
+ return;
671617
+ case "D":
671618
+ if (this.cursor > 0) this.cursor--;
671619
+ return;
671620
+ case "H":
671621
+ this.cursor = 0;
671622
+ return;
671623
+ case "F":
671624
+ this.cursor = this.line.length;
671625
+ return;
671626
+ }
671627
+ }
671628
+ /** Handle control characters (ASCII < 32 and DEL) */
671629
+ _handleControl(code8) {
671630
+ switch (code8) {
671631
+ case 127:
671632
+ // Backspace (DEL)
671633
+ case 8:
671634
+ if (this.cursor > 0) {
671635
+ this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
671636
+ this.cursor--;
671579
671637
  }
671638
+ return;
671639
+ case 3:
671640
+ this.emit("SIGINT");
671641
+ return;
671642
+ case 4:
671643
+ if (this.line.length === 0) this.close();
671644
+ return;
671645
+ case 9:
671646
+ this._handleTab();
671647
+ return;
671648
+ case 23:
671649
+ this._deleteWordLeft();
671650
+ return;
671651
+ case 21:
671652
+ this.line = this.line.slice(this.cursor);
671653
+ this.cursor = 0;
671654
+ return;
671655
+ case 11:
671656
+ this.line = this.line.slice(0, this.cursor);
671657
+ return;
671658
+ case 1:
671659
+ this.cursor = 0;
671660
+ return;
671661
+ case 5:
671662
+ this.cursor = this.line.length;
671663
+ return;
671664
+ case 15:
671665
+ this.emit("ctrl-o");
671666
+ return;
671667
+ case 12:
671668
+ this.emit("ctrl-l");
671669
+ return;
671670
+ case 22:
671671
+ this.emit("ctrl-v");
671672
+ return;
671673
+ case 28:
671674
+ this.emit("ctrl-backslash");
671675
+ return;
671676
+ }
671677
+ }
671678
+ /** Submit the current line */
671679
+ _submit() {
671680
+ const line = this.line;
671681
+ if (line.trim() && (this._history.length === 0 || this._history[0] !== line)) {
671682
+ this._history.unshift(line);
671683
+ if (this._history.length > this._historySize) {
671684
+ this._history.length = this._historySize;
671580
671685
  }
671581
671686
  }
671582
- return fullText;
671687
+ this._historyIndex = -1;
671688
+ this._savedLine = "";
671689
+ this.line = "";
671690
+ this.cursor = 0;
671691
+ this.emit("line", line);
671583
671692
  }
671584
- /** Parse OpenAI-compat SSE streaming response */
671585
- async parseOpenAIStream(res) {
671586
- const reader = res.body?.getReader();
671587
- if (!reader) throw new Error("No response body");
671588
- const decoder = new TextDecoder();
671589
- let fullText = "";
671590
- let buffer2 = "";
671591
- while (true) {
671592
- const { done, value: value2 } = await reader.read();
671593
- if (done) break;
671594
- buffer2 += decoder.decode(value2, { stream: true });
671595
- const lines = buffer2.split("\n");
671596
- buffer2 = lines.pop() ?? "";
671597
- for (const line of lines) {
671598
- const trimmed = line.trim();
671599
- if (!trimmed || !trimmed.startsWith("data: ")) continue;
671600
- const data = trimmed.slice(6);
671601
- if (data === "[DONE]") continue;
671602
- try {
671603
- const parsed = JSON.parse(data);
671604
- const delta = parsed.choices?.[0]?.delta?.content;
671605
- if (delta) fullText += delta;
671606
- } catch {
671693
+ /** Handle Tab completion */
671694
+ _handleTab() {
671695
+ if (this.line.length === 0) {
671696
+ this.emit("ctrl-i");
671697
+ return;
671698
+ }
671699
+ if (!this._completer) return;
671700
+ this._completer(this.line, (err, result) => {
671701
+ if (err || !result) return;
671702
+ const [completions, substring] = result;
671703
+ if (completions.length === 0) return;
671704
+ if (completions.length === 1) {
671705
+ const completion = completions[0];
671706
+ const before = this.line.slice(0, this.cursor);
671707
+ const idx = before.lastIndexOf(substring);
671708
+ if (idx >= 0) {
671709
+ this.line = before.slice(0, idx) + completion + this.line.slice(this.cursor);
671710
+ this.cursor = idx + completion.length;
671711
+ }
671712
+ } else if (completions.length > 1) {
671713
+ let common = completions[0];
671714
+ for (let i2 = 1; i2 < completions.length; i2++) {
671715
+ const other = completions[i2];
671716
+ let j = 0;
671717
+ while (j < common.length && j < other.length && common[j] === other[j]) j++;
671718
+ common = common.slice(0, j);
671719
+ }
671720
+ if (common.length > substring.length) {
671721
+ const before = this.line.slice(0, this.cursor);
671722
+ const idx = before.lastIndexOf(substring);
671723
+ if (idx >= 0) {
671724
+ this.line = before.slice(0, idx) + common + this.line.slice(this.cursor);
671725
+ this.cursor = idx + common.length;
671726
+ }
671607
671727
  }
671608
671728
  }
671609
- }
671610
- return fullText;
671729
+ });
671611
671730
  }
671612
- // ---------------------------------------------------------------------------
671613
- // Summary injection to main agent
671614
- // ---------------------------------------------------------------------------
671615
- injectSummary() {
671616
- if (!this.runner) return;
671617
- const recentTurns = this.context.filter((t2) => t2.role !== "system").slice(-8).map((t2) => `${t2.role === "user" ? "User" : "Assistant"}: ${t2.content}`).join("\n");
671618
- this.runner.injectUserMessage(
671619
- `[VOICECHAT SUMMARY] Parallel voice liaison update (for awareness only). Continue your current task; do not respond to this directly.
671620
-
671621
- ${recentTurns}`
671622
- );
671731
+ /** Move cursor left by one word */
671732
+ _wordLeft() {
671733
+ if (this.cursor === 0) return;
671734
+ let i2 = this.cursor - 1;
671735
+ while (i2 > 0 && /\s/.test(this.line[i2])) i2--;
671736
+ while (i2 > 0 && /\S/.test(this.line[i2 - 1])) i2--;
671737
+ this.cursor = i2;
671623
671738
  }
671624
- /** Enqueue narration from main agent events into the voice channel */
671625
- enqueueAgentNarration(text2, subordinate = true) {
671626
- if (!text2 || !this.active) return;
671627
- this.relayTranscript.push({ dir: "fromMain", content: text2, ts: Date.now() });
671628
- if (subordinate) this.voice.speakSubordinate(text2);
671629
- else this.voice.speak(text2);
671739
+ /** Move cursor right by one word */
671740
+ _wordRight() {
671741
+ if (this.cursor >= this.line.length) return;
671742
+ let i2 = this.cursor;
671743
+ while (i2 < this.line.length && /\S/.test(this.line[i2])) i2++;
671744
+ while (i2 < this.line.length && /\s/.test(this.line[i2])) i2++;
671745
+ this.cursor = i2;
671630
671746
  }
671631
- /** Get copies of transcripts for UI/debugging */
671632
- getTranscripts() {
671633
- return {
671634
- voice: this.voiceTranscript.slice(-200),
671635
- relay: this.relayTranscript.slice(-200)
671636
- };
671747
+ /** Delete word left of cursor (Ctrl+W) */
671748
+ _deleteWordLeft() {
671749
+ if (this.cursor === 0) return;
671750
+ let i2 = this.cursor - 1;
671751
+ while (i2 > 0 && /\s/.test(this.line[i2])) i2--;
671752
+ while (i2 > 0 && /\S/.test(this.line[i2 - 1])) i2--;
671753
+ this.line = this.line.slice(0, i2) + this.line.slice(this.cursor);
671754
+ this.cursor = i2;
671637
671755
  }
671638
671756
  };
671639
671757
  }
671640
671758
  });
671641
671759
 
671642
- // packages/cli/src/api/voice-runtime.ts
671643
- var voice_runtime_exports = {};
671644
- __export(voice_runtime_exports, {
671645
- _resetForTests: () => _resetForTests,
671646
- ensureRuntime: () => ensureRuntime,
671647
- feedAudioFromClient: () => feedAudioFromClient,
671648
- getDaemonListenEngine: () => getDaemonListenEngine,
671649
- getRuntimeStatus: () => getRuntimeStatus,
671650
- getVoiceBus: () => getVoiceBus,
671651
- getVoiceEngine: () => getVoiceEngine,
671652
- isVoiceChatActive: () => isVoiceChatActive,
671653
- listClients: () => listClients,
671654
- registerClient: () => registerClient,
671655
- startVoiceChat: () => startVoiceChat,
671656
- stopVoiceChat: () => stopVoiceChat,
671657
- synthesizeAndBroadcast: () => synthesizeAndBroadcast,
671658
- synthesizeToWav: () => synthesizeToWav,
671659
- unregisterClient: () => unregisterClient
671660
- });
671661
- import { EventEmitter as EventEmitter14 } from "node:events";
671662
- function getVoiceEngine() {
671663
- if (!_voiceEngine) {
671664
- _voiceEngine = new VoiceEngine();
671760
+ // packages/cli/src/api/access-policy.ts
671761
+ function defaultAccessMode(bindHost) {
671762
+ if (bindHost === "0.0.0.0" || bindHost === "::" || bindHost === "::0") {
671763
+ return "lan";
671665
671764
  }
671666
- return _voiceEngine;
671765
+ return "loopback";
671667
671766
  }
671668
- function getDaemonListenEngine() {
671669
- if (!_listenEngine) _listenEngine = getListenEngine();
671670
- return _listenEngine;
671767
+ function resolveAccessMode(envValue, bindHost) {
671768
+ const v = (envValue || "").toLowerCase().trim();
671769
+ if (v === "loopback" || v === "lan" || v === "any") return v;
671770
+ return defaultAccessMode(bindHost);
671671
671771
  }
671672
- function getVoiceBus() {
671673
- if (!_bus) _bus = new EventEmitter14();
671674
- return _bus;
671772
+ function stripMappedPrefix(ip) {
671773
+ return ip.replace(/^::ffff:/i, "");
671675
671774
  }
671676
- function getRuntimeStatus() {
671677
- return {
671678
- state: _state3,
671679
- voiceEnabled: _voiceEngine?.enabled ?? false,
671680
- voiceReady: _voiceEngine?.ready ?? false,
671681
- voiceModelId: _voiceEngine?.modelId ?? null,
671682
- cloneRef: _voiceEngine?.luxttsCloneRef ?? null,
671683
- listenActive: _listenEngine?.isActive ?? false,
671684
- listenPaused: _listenEngine?.isPaused ?? false,
671685
- clientCount: _clients2.size,
671686
- loadedAt: _loadedAt,
671687
- lastError: _lastError
671688
- };
671775
+ function isLoopbackIP(ip) {
671776
+ if (!ip) return false;
671777
+ const clean5 = stripMappedPrefix(ip);
671778
+ if (clean5 === "::1") return true;
671779
+ if (/^127\./.test(clean5)) return true;
671780
+ return false;
671689
671781
  }
671690
- async function ensureRuntime() {
671691
- if (_state3 === "loading" || _state3 === "listening" || _state3 === "speaking") return;
671692
- setState("loading");
671693
- try {
671694
- const voice = getVoiceEngine();
671695
- const listen = getDaemonListenEngine();
671696
- if (!voice.enabled) {
671697
- await voice.toggle();
671698
- }
671699
- if (!listen.isActive) {
671700
- try {
671701
- await listen.start();
671702
- } catch (err) {
671703
- const m2 = err instanceof Error ? err.message : String(err);
671704
- _lastError = `listen.start() failed: ${m2}`;
671705
- getVoiceBus().emit("error", _lastError);
671706
- }
671707
- }
671708
- _loadedAt = Date.now();
671709
- setState("listening");
671710
- wireListenToBus();
671711
- } catch (err) {
671712
- const m2 = err instanceof Error ? err.message : String(err);
671713
- _lastError = m2;
671714
- setState("error");
671715
- throw err;
671782
+ function isPrivateIP(ip) {
671783
+ if (!ip) return false;
671784
+ const clean5 = stripMappedPrefix(ip);
671785
+ if (/^10\./.test(clean5)) return true;
671786
+ if (/^192\.168\./.test(clean5)) return true;
671787
+ const m2 = /^172\.(\d{1,3})\./.exec(clean5);
671788
+ if (m2) {
671789
+ const second3 = parseInt(m2[1], 10);
671790
+ if (second3 >= 16 && second3 <= 31) return true;
671716
671791
  }
671792
+ if (/^169\.254\./.test(clean5)) return true;
671793
+ if (/^f[cd][0-9a-f]{2}:/i.test(clean5)) return true;
671794
+ if (/^fe[89ab][0-9a-f]:/i.test(clean5)) return true;
671795
+ return false;
671717
671796
  }
671718
- async function registerClient(handle2) {
671719
- if (_shutdownTimer) {
671720
- clearTimeout(_shutdownTimer);
671721
- _shutdownTimer = null;
671722
- }
671723
- _clients2.set(handle2.id, handle2);
671724
- if (_clients2.size === 1 && (_state3 === "idle" || _state3 === "error")) {
671725
- try {
671726
- await ensureRuntime();
671727
- } catch (err) {
671728
- _clients2.delete(handle2.id);
671729
- throw err;
671730
- }
671731
- }
671797
+ function isAllowedIP(ip, mode) {
671798
+ if (mode === "any") return true;
671799
+ if (isLoopbackIP(ip)) return true;
671800
+ if (mode === "lan" && isPrivateIP(ip)) return true;
671801
+ return false;
671732
671802
  }
671733
- function unregisterClient(id) {
671734
- _clients2.delete(id);
671735
- if (_clients2.size === 0 && _shutdownTimer === null) {
671736
- _shutdownTimer = setTimeout(() => {
671737
- _shutdownTimer = null;
671738
- try {
671739
- _listenEngine?.pause?.();
671740
- } catch {
671741
- }
671742
- }, IDLE_SHUTDOWN_MS);
671803
+ function describeAccessMode(mode) {
671804
+ switch (mode) {
671805
+ case "loopback":
671806
+ return "loopback only";
671807
+ case "lan":
671808
+ return "loopback + RFC 1918";
671809
+ case "any":
671810
+ return "any — WIDE OPEN";
671743
671811
  }
671744
671812
  }
671745
- function feedAudioFromClient(clientId, pcmChunk) {
671746
- if (_ttsSpeaking) return;
671747
- const listen = _listenEngine;
671748
- if (!listen || !listen.isActive) return;
671749
- try {
671750
- const transcriber = listen.liveTranscriber;
671751
- if (transcriber?.write) transcriber.write(pcmChunk);
671752
- } catch {
671813
+ var init_access_policy = __esm({
671814
+ "packages/cli/src/api/access-policy.ts"() {
671815
+ "use strict";
671753
671816
  }
671817
+ });
671818
+
671819
+ // packages/cli/src/api/project-preferences.ts
671820
+ import { createHash as createHash40 } from "node:crypto";
671821
+ import { existsSync as existsSync136, mkdirSync as mkdirSync85, readFileSync as readFileSync113, renameSync as renameSync12, writeFileSync as writeFileSync75, unlinkSync as unlinkSync31 } from "node:fs";
671822
+ import { homedir as homedir48 } from "node:os";
671823
+ import { join as join149, resolve as resolve60 } from "node:path";
671824
+ import { randomUUID as randomUUID19 } from "node:crypto";
671825
+ function projectKey(root) {
671826
+ const canonical = resolve60(root);
671827
+ return createHash40("sha256").update(canonical).digest("hex").slice(0, 16);
671754
671828
  }
671755
- async function synthesizeToWav(text2, format3 = "wav") {
671756
- await ensureRuntime();
671757
- const voice = _voiceEngine;
671758
- if (!voice || !voice.ready || !voice.synthesizeToPCM) return null;
671759
- const result = await voice.synthesizeToPCM(text2);
671760
- if (!result || !result.pcm || result.pcm.length === 0) return null;
671761
- const sampleRate = result.sampleRate;
671762
- const pcm = result.pcm;
671763
- if (format3 === "pcm") return { bytes: pcm, sampleRate, format: format3 };
671764
- const header = Buffer.alloc(44);
671765
- header.write("RIFF", 0);
671766
- header.writeUInt32LE(36 + pcm.length, 4);
671767
- header.write("WAVE", 8);
671768
- header.write("fmt ", 12);
671769
- header.writeUInt32LE(16, 16);
671770
- header.writeUInt16LE(1, 20);
671771
- header.writeUInt16LE(1, 22);
671772
- header.writeUInt32LE(sampleRate, 24);
671773
- header.writeUInt32LE(sampleRate * 2, 28);
671774
- header.writeUInt16LE(2, 32);
671775
- header.writeUInt16LE(16, 34);
671776
- header.write("data", 36);
671777
- header.writeUInt32LE(pcm.length, 40);
671778
- return { bytes: Buffer.concat([header, pcm]), sampleRate, format: format3 };
671779
- }
671780
- async function synthesizeAndBroadcast(text2) {
671781
- const voice = _voiceEngine;
671782
- if (!voice || !voice.ready) return;
671783
- if (!voice.synthesizeToPCM) {
671784
- getVoiceBus().emit("error", "voice engine has no synthesizeToPCM");
671785
- return;
671786
- }
671787
- setSpeaking(true);
671788
- try {
671789
- const result = await voice.synthesizeToPCM(text2);
671790
- if (!result || !result.pcm || result.pcm.length === 0) return;
671791
- getVoiceBus().emit("agent_text", { text: text2 });
671792
- getVoiceBus().emit("tts_pcm", result.pcm, result.sampleRate);
671793
- } catch (err) {
671794
- const m2 = err instanceof Error ? err.message : String(err);
671795
- getVoiceBus().emit("error", `tts: ${m2}`);
671796
- } finally {
671797
- setSpeaking(false);
671798
- }
671829
+ function projectDir(root) {
671830
+ return join149(PROJECTS_DIR, projectKey(root));
671799
671831
  }
671800
- function listClients() {
671801
- return Array.from(_clients2.values());
671832
+ function prefsPath(root) {
671833
+ return join149(projectDir(root), "preferences.json");
671802
671834
  }
671803
- async function startVoiceChat(opts) {
671804
- if (_voiceChatSession?.isActive) {
671805
- return { ok: true, message: "VoiceChat already running" };
671806
- }
671807
- await ensureRuntime();
671808
- const voice = getVoiceEngine();
671809
- const listen = getDaemonListenEngine();
671810
- if (!voice.ready) return { ok: false, message: "Voice engine not ready" };
671811
- _voiceChatSession = new VoiceChatSession({
671812
- voice,
671813
- listen,
671814
- backendUrl: opts.backendUrl,
671815
- model: opts.model,
671816
- apiKey: opts.apiKey,
671817
- verbose: opts.verbose === true,
671818
- onStatus: (msg) => getVoiceBus().emit("status", msg),
671819
- onUserSpeech: (text2) => getVoiceBus().emit("transcript", { text: text2, final: true }),
671820
- onPartialTranscript: (text2) => getVoiceBus().emit("transcript", { text: text2, final: false }),
671821
- onAgentSpeech: (text2) => getVoiceBus().emit("agent_text", { text: text2 }),
671822
- onStateChange: (s2) => getVoiceBus().emit("session_state", s2)
671823
- });
671824
- await _voiceChatSession.start();
671825
- setState("listening");
671826
- return { ok: true, message: "VoiceChat started" };
671835
+ function rootSentinelPath(root) {
671836
+ return join149(projectDir(root), ".root");
671827
671837
  }
671828
- async function stopVoiceChat() {
671829
- if (!_voiceChatSession) return { ok: true, message: "No active session" };
671838
+ function ensureDir(root) {
671839
+ const dir = projectDir(root);
671840
+ mkdirSync85(dir, { recursive: true });
671841
+ const sentinel = rootSentinelPath(root);
671830
671842
  try {
671831
- if (_voiceChatSession.stop) {
671832
- await _voiceChatSession.stop();
671843
+ if (!existsSync136(sentinel)) {
671844
+ writeFileSync75(sentinel, `${resolve60(root)}
671845
+ `, "utf8");
671833
671846
  }
671834
671847
  } catch {
671835
671848
  }
671836
- _voiceChatSession = null;
671837
- setState(_listenEngine?.isActive ? "listening" : "idle");
671838
- return { ok: true, message: "VoiceChat stopped" };
671839
- }
671840
- function isVoiceChatActive() {
671841
- return _voiceChatSession?.isActive ?? false;
671842
- }
671843
- function setState(s2) {
671844
- if (_state3 === s2) return;
671845
- _state3 = s2;
671846
- getVoiceBus().emit("state", s2);
671847
671849
  }
671848
- function setSpeaking(speaking) {
671849
- _ttsSpeaking = speaking;
671850
- if (speaking) {
671851
- setState("speaking");
671852
- getVoiceBus().emit("tts_start");
671853
- } else {
671854
- setState(_listenEngine?.isActive ? "listening" : "idle");
671855
- getVoiceBus().emit("tts_end");
671850
+ function readProjectPreferences(root) {
671851
+ try {
671852
+ const file = prefsPath(root);
671853
+ if (!existsSync136(file)) return { ...DEFAULT_PREFS };
671854
+ const raw = readFileSync113(file, "utf8");
671855
+ const parsed = JSON.parse(raw);
671856
+ if (!parsed || parsed.v !== SCHEMA_VERSION) return { ...DEFAULT_PREFS };
671857
+ return { ...DEFAULT_PREFS, ...parsed, v: SCHEMA_VERSION };
671858
+ } catch {
671859
+ return { ...DEFAULT_PREFS };
671856
671860
  }
671857
671861
  }
671858
- function wireListenToBus() {
671859
- if (_wired) return;
671860
- if (!_listenEngine) return;
671861
- _wired = true;
671862
- _listenEngine.on("transcript", (...args) => {
671863
- const payload = args[0];
671864
- if (!payload || typeof payload.text !== "string") return;
671865
- getVoiceBus().emit("transcript", payload);
671866
- });
671862
+ function writeProjectPreferences(root, partial) {
671863
+ ensureDir(root);
671864
+ const current = readProjectPreferences(root);
671865
+ const merged = {
671866
+ ...current,
671867
+ ...partial,
671868
+ v: SCHEMA_VERSION,
671869
+ updatedAt: Date.now()
671870
+ };
671871
+ const file = prefsPath(root);
671872
+ const tmp = `${file}.${randomUUID19().slice(0, 8)}.tmp`;
671873
+ writeFileSync75(tmp, JSON.stringify(merged, null, 2), "utf8");
671874
+ try {
671875
+ renameSync12(tmp, file);
671876
+ } catch (err) {
671877
+ try {
671878
+ writeFileSync75(file, JSON.stringify(merged, null, 2), "utf8");
671879
+ } catch {
671880
+ }
671881
+ try {
671882
+ unlinkSync31(tmp);
671883
+ } catch {
671884
+ }
671885
+ throw err;
671886
+ }
671887
+ return merged;
671867
671888
  }
671868
- function _resetForTests() {
671869
- _state3 = "idle";
671870
- _loadedAt = null;
671871
- _lastError = null;
671872
- _clients2.clear();
671873
- _ttsSpeaking = false;
671874
- if (_shutdownTimer) {
671875
- clearTimeout(_shutdownTimer);
671876
- _shutdownTimer = null;
671889
+ function deleteProjectPreferences(root) {
671890
+ try {
671891
+ const file = prefsPath(root);
671892
+ if (!existsSync136(file)) return false;
671893
+ unlinkSync31(file);
671894
+ return true;
671895
+ } catch {
671896
+ return false;
671877
671897
  }
671878
671898
  }
671879
- var _voiceEngine, _listenEngine, _voiceChatSession, _bus, _state3, _loadedAt, _lastError, _clients2, _ttsSpeaking, _shutdownTimer, IDLE_SHUTDOWN_MS, _wired;
671880
- var init_voice_runtime = __esm({
671881
- "packages/cli/src/api/voice-runtime.ts"() {
671899
+ var OMNIUS_DIR4, PROJECTS_DIR, SCHEMA_VERSION, DEFAULT_PREFS;
671900
+ var init_project_preferences = __esm({
671901
+ "packages/cli/src/api/project-preferences.ts"() {
671882
671902
  "use strict";
671883
- init_voice();
671884
- init_listen();
671885
- init_voicechat();
671886
- _voiceEngine = null;
671887
- _listenEngine = null;
671888
- _voiceChatSession = null;
671889
- _bus = null;
671890
- _state3 = "idle";
671891
- _loadedAt = null;
671892
- _lastError = null;
671893
- _clients2 = /* @__PURE__ */ new Map();
671894
- _ttsSpeaking = false;
671895
- _shutdownTimer = null;
671896
- IDLE_SHUTDOWN_MS = 6e4;
671897
- _wired = false;
671903
+ OMNIUS_DIR4 = join149(homedir48(), ".omnius");
671904
+ PROJECTS_DIR = join149(OMNIUS_DIR4, "projects");
671905
+ SCHEMA_VERSION = 1;
671906
+ DEFAULT_PREFS = {
671907
+ v: SCHEMA_VERSION,
671908
+ updatedAt: 0
671909
+ };
671898
671910
  }
671899
671911
  });
671900
671912
 
@@ -692095,6 +692107,51 @@ data: ${JSON.stringify(data)}
692095
692107
  }
692096
692108
  return;
692097
692109
  }
692110
+ if (pathname === "/v1/model" && method === "GET") {
692111
+ const listen = getDaemonListenEngine();
692112
+ const current = listen.currentModel ?? "base";
692113
+ jsonResponse(res, 200, { model: current });
692114
+ return;
692115
+ }
692116
+ if (pathname === "/v1/model" && method === "POST") {
692117
+ const body = await parseJsonBody(req3);
692118
+ const modelId = (body?.model ?? "").trim();
692119
+ if (!modelId) {
692120
+ jsonResponse(res, 400, { error: "missing_model" });
692121
+ return;
692122
+ }
692123
+ try {
692124
+ await getDaemonListenEngine().setModel(modelId);
692125
+ jsonResponse(res, 200, { ok: true, model: modelId });
692126
+ } catch (err) {
692127
+ jsonResponse(res, 500, { error: "model_switch_failed", message: err instanceof Error ? err.message : String(err) });
692128
+ }
692129
+ return;
692130
+ }
692131
+ if (pathname === "/v1/endpoint" && method === "GET") {
692132
+ const listen = getDaemonListenEngine();
692133
+ const current = listen.currentEndpoint ?? null;
692134
+ jsonResponse(res, 200, { endpoint: current });
692135
+ return;
692136
+ }
692137
+ if (pathname === "/v1/endpoint" && method === "POST") {
692138
+ const body = await parseJsonBody(req3);
692139
+ const url = (body?.url ?? "").trim();
692140
+ const type = body?.type ?? "ollama";
692141
+ const key = body?.key;
692142
+ if (!url) {
692143
+ jsonResponse(res, 400, { error: "missing_url" });
692144
+ return;
692145
+ }
692146
+ try {
692147
+ const listen = getDaemonListenEngine();
692148
+ await listen.setEndpoint?.(url, type, key);
692149
+ jsonResponse(res, 200, { ok: true, endpoint: url });
692150
+ } catch (err) {
692151
+ jsonResponse(res, 500, { error: "endpoint_switch_failed", message: err instanceof Error ? err.message : String(err) });
692152
+ }
692153
+ return;
692154
+ }
692098
692155
  if (pathname === "/v1/codegraph/snapshot" && method === "GET") {
692099
692156
  const workingDir = process.cwd();
692100
692157
  const db = getCodeGraphDBIfReady(workingDir);