omnius 1.0.254 → 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
@@ -669686,2215 +669686,2227 @@ 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, "");
669694
+ import { EventEmitter as EventEmitter12 } from "node:events";
669695
+ function clamp0114(x) {
669696
+ return x < 0 ? 0 : x > 1 ? 1 : x;
669696
669697
  }
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
- };
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;
669702
+ }
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;
669706
+ }
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;
669717
669714
  }
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;
669715
+ }
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);
669735
+ }
669736
+ return clamp0114(score);
669737
+ }
669738
+ function truncateForLog(s2, n2) {
669739
+ return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "…";
669740
+ }
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;
669738
669746
  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
- } });
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
+ }
669753
+ } catch {
669752
669754
  }
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
669755
  }
669756
+ return null;
669767
669757
  }
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");
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) {
669764
+ try {
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 };
669769
+ }
669770
+ } catch {
669771
+ }
669802
669772
  }
669803
669773
  return null;
669804
669774
  }
669805
- function acquireLock2() {
669806
- let release;
669807
- const next = new Promise((res) => {
669808
- release = res;
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
+ }
669809
669786
  });
669810
- const prev = _passthroughLock;
669811
- _passthroughLock = next;
669812
- return prev.then(() => release);
669787
+ return kept.join("\n").trim();
669813
669788
  }
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);
669877
- }
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
- }
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 };
669944
- }
669945
- }
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);
669951
- }
669952
- function listProjects() {
669953
- const { projects } = readAll2();
669954
- const alive = [];
669955
- for (const p2 of projects) {
669956
- try {
669957
- if (statSync48(p2.root).isDirectory()) alive.push(p2);
669958
- } catch {
669959
- }
669960
- }
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;
670010
- }
670011
- function getCurrentProject() {
670012
- if (!currentRoot) {
670013
- try {
670014
- if (existsSync135(CURRENT_FILE)) {
670015
- const persisted = readFileSync112(CURRENT_FILE, "utf8").trim();
670016
- if (persisted) currentRoot = persisted;
670017
- }
670018
- } catch {
670019
- }
670020
- }
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;
670037
- }
670038
- function _resetCurrentProject() {
670039
- currentRoot = null;
670040
- }
670041
- var OMNIUS_DIR3, PROJECTS_FILE, CURRENT_FILE, currentRoot;
670042
- var init_projects = __esm({
670043
- "packages/cli/src/api/projects.ts"() {
670044
- "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
- });
670051
-
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"() {
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"() {
670061
669792
  "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) {
670071
- 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") {
670096
- const mouseMatch = remaining.match(/^\x1B\[<(\d+);(\d+);(\d+)([Mm])/);
670097
- if (mouseMatch) {
670098
- this.handleSgrMouse({
670099
- raw: mouseMatch[0],
670100
- btn: parseInt(mouseMatch[1]),
670101
- col: parseInt(mouseMatch[2]),
670102
- row: parseInt(mouseMatch[3]),
670103
- suffix: mouseMatch[4]
670104
- });
670105
- i2 += mouseMatch[0].length;
670106
- continue;
670107
- }
670108
- if (remaining.startsWith("\x1B[M")) {
670109
- if (remaining.length >= 6) {
670110
- if (this.onActivity) this.onActivity();
670111
- i2 += 6;
670112
- continue;
670113
- }
670114
- break;
670115
- }
670116
- if (remaining.startsWith("\x1B[<") && remaining.length < 15) {
670117
- break;
670118
- }
670119
- if (remaining.startsWith("\x1B[") && remaining.length === 2) {
670120
- break;
670121
- }
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
- });
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.
670228
669797
 
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];
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) {
669844
+ super();
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);
670336
669857
  }
670337
- this.cursor = this.line.length;
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
+ });
670338
669868
  }
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;
669869
+ get state() {
669870
+ return this._state;
670363
669871
  }
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;
669872
+ get isActive() {
669873
+ return this.active;
670388
669874
  }
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 };
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 });
670438
669884
  }
670439
669885
  // ---------------------------------------------------------------------------
670440
- // Private: buffer processing and escape sequence parsing
669886
+ // Start / Stop
670441
669887
  // ---------------------------------------------------------------------------
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;
670532
- }
670533
- const char = this._buffer[i2];
670534
- this.line = this.line.slice(0, this.cursor) + char + this.line.slice(this.cursor);
670535
- this.cursor++;
670536
- i2++;
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 });
670537
669904
  }
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");
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 {
670560
669938
  }
670561
- this._buffer = "";
670562
- }
670563
- }, 50);
670564
- } else {
670565
- if (this._flushTimer) {
670566
- clearTimeout(this._flushTimer);
670567
- this._flushTimer = null;
669939
+ }, 1e3);
670568
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");
670569
669951
  }
670570
669952
  }
670571
- _matchPrefixlessSgrMouse(input) {
670572
- const match = input.match(/^(<)?(\d{1,3});(\d{1,5});(\d{1,5})([Mm])/);
670573
- if (!match) return null;
670574
- const hasMarker = Boolean(match[1]);
670575
- const btn = parseInt(match[2], 10);
670576
- const col = parseInt(match[3], 10);
670577
- const row = parseInt(match[4], 10);
670578
- if (!Number.isFinite(btn) || !Number.isFinite(col) || !Number.isFinite(row))
670579
- return null;
670580
- if (btn < 0 || btn > 255 || col <= 0 || row <= 0) return null;
670581
- if (!hasMarker && Date.now() > this._expectPrefixlessMouseUntil && !this._isKnownMouseButton(btn))
670582
- return null;
670583
- return { length: match[0].length };
670584
- }
670585
- _looksLikePartialPrefixlessSgrMouse(input) {
670586
- if (input.length < 2) return false;
670587
- 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)) {
670589
- return true;
669953
+ async stop() {
669954
+ if (!this.active) return;
669955
+ this.active = false;
669956
+ if (this.abortController) {
669957
+ this.abortController.abort();
669958
+ this.abortController = null;
670590
669959
  }
670591
- return false;
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");
670592
669986
  }
670593
- _isKnownMouseButton(btn) {
670594
- if (btn >= 0 && btn <= 6) return true;
670595
- if (btn >= 32 && btn <= 39) return true;
670596
- if (btn >= 64 && btn <= 71) return true;
670597
- if (btn >= 96 && btn <= 103) return true;
670598
- return false;
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);
670599
670016
  }
670600
- _emitChangeIfNeeded(beforeLine, beforeCursor) {
670601
- if (this.line === beforeLine && this.cursor === beforeCursor) return;
670602
- this.emit("change", { line: this.line, cursor: this.cursor });
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();
670603
670059
  }
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;
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 {
670645
670077
  }
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);
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;
670649
670110
  }
670650
- return;
670111
+ } catch {
670651
670112
  }
670652
- if (params === "5") {
670653
- this.emit("pageup");
670654
- return;
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)}`;
670655
670126
  }
670656
- if (params === "6") {
670657
- this.emit("pagedown");
670658
- return;
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() });
670659
670130
  }
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;
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 {
670670
670147
  }
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
- }
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();
670684
670153
  }
670685
- if (hasCtrl && !hasShift && (codepoint === 73 || codepoint === 105)) {
670686
- this.emit("ctrl-i");
670687
- return;
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));
670688
670162
  }
670689
- return;
670690
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;
670691
670172
  }
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);
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...");
670702
670180
  }
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
670181
  }
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;
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;
670730
670211
  }
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;
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)}`);
670780
670224
  }
670225
+ return await this.parseOpenAIStream(res);
670781
670226
  }
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;
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
+ }
670789
670252
  }
670790
670253
  }
670791
- this._historyIndex = -1;
670792
- this._savedLine = "";
670793
- this.line = "";
670794
- this.cursor = 0;
670795
- this.emit("line", line);
670254
+ return fullText;
670796
670255
  }
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
- }
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 {
670831
670279
  }
670832
670280
  }
670833
- });
670281
+ }
670282
+ return fullText;
670834
670283
  }
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;
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
+ );
670842
670295
  }
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;
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);
670850
670302
  }
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;
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
+ };
670859
670309
  }
670860
670310
  };
670861
670311
  }
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";
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 {
670868
670425
  }
670869
- return "loopback";
670870
670426
  }
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);
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 };
670875
670451
  }
670876
- function stripMappedPrefix(ip) {
670877
- return ip.replace(/^::ffff:/i, "");
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
+ }
670878
670471
  }
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;
670472
+ function listClients() {
670473
+ return Array.from(_clients2.values());
670885
670474
  }
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;
670475
+ async function startVoiceChat(opts) {
670476
+ if (_voiceChatSession?.isActive) {
670477
+ return { ok: true, message: "VoiceChat already running" };
670895
670478
  }
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;
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" };
670900
670499
  }
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;
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" };
670906
670511
  }
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";
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();
670915
670650
  }
670916
670651
  }
670917
- var init_access_policy = __esm({
670918
- "packages/cli/src/api/access-policy.ts"() {
670919
- "use strict";
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");
670920
670673
  }
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");
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;
670938
670688
  }
670939
- function rootSentinelPath(root) {
670940
- return join149(projectDir(root), ".root");
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);
670941
670697
  }
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");
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);
670950
670772
  }
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
- }
670773
+ };
670965
670774
  }
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()
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 }
670974
670800
  };
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
670801
  }
670993
- function deleteProjectPreferences(root) {
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() {
670994
670832
  try {
670995
- const file = prefsPath(root);
670996
- if (!existsSync136(file)) return false;
670997
- unlinkSync31(file);
670998
- return true;
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 };
670999
670838
  } 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
671013
- };
670839
+ return { projects: [], schemaVersion: 1 };
671014
670840
  }
671015
- });
671016
-
671017
- // packages/cli/src/tui/voicechat.ts
671018
- var voicechat_exports = {};
671019
- __export(voicechat_exports, {
671020
- VoiceChatSession: () => VoiceChatSession
671021
- });
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
670841
  }
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;
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);
671034
670847
  }
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;
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 {
671042
670855
  }
671043
670856
  }
671044
- if (cur > maxRun) maxRun = cur;
671045
- return Math.min(1, Math.max(0, (maxRun - 3) / 10));
670857
+ alive.sort((a2, b) => b.lastSeen - a2.lastSeen);
670858
+ return alive;
671046
670859
  }
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);
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);
671063
670884
  }
671064
- return clamp0114(score);
670885
+ writeAll(file);
670886
+ return entry;
671065
670887
  }
671066
- function truncateForLog(s2, n2) {
671067
- return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "…";
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;
671068
670896
  }
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;
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) {
671074
670909
  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 };
670910
+ if (existsSync135(CURRENT_FILE)) {
670911
+ const persisted = readFileSync112(CURRENT_FILE, "utf8").trim();
670912
+ if (persisted) currentRoot = persisted;
671080
670913
  }
671081
670914
  } catch {
671082
670915
  }
671083
670916
  }
671084
- return null;
670917
+ if (!currentRoot) return null;
670918
+ const all2 = listProjects();
670919
+ return all2.find((p2) => p2.root === currentRoot) ?? null;
671085
670920
  }
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
- }
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 {
671100
670931
  }
671101
- return null;
670932
+ return entry;
671102
670933
  }
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();
670934
+ function _resetCurrentProject() {
670935
+ currentRoot = null;
671116
670936
  }
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"() {
670937
+ var OMNIUS_DIR3, PROJECTS_FILE, CURRENT_FILE, currentRoot;
670938
+ var init_projects = __esm({
670939
+ "packages/cli/src/api/projects.ts"() {
671120
670940
  "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.
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
+ });
671125
670947
 
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) {
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) {
671172
670967
  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 ?? (() => {
671193
- });
671194
- this.onStateChange = opts.onStateChange ?? (() => {
671195
- });
671196
- }
671197
- get state() {
671198
- return this._state;
671199
- }
671200
- get isActive() {
671201
- return this.active;
670968
+ this.onScroll = scrollHandler;
670969
+ this.onActivity = activityHandler ?? null;
670970
+ this.onPointer = pointerHandler ?? null;
670971
+ this.onKeyboard = keyboardHandler ?? null;
671202
670972
  }
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 });
670973
+ _transform(chunk, _encoding, callback) {
670974
+ this.buffer += chunk.toString();
670975
+ this.processBuffer(callback);
671212
670976
  }
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]);
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;
671249
670987
  }
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 {
670988
+ if (this.looksLikePartialPrefixlessSgrMouse(remaining)) {
670989
+ break;
670990
+ }
670991
+ if (this.buffer[i2] === "\x1B") {
670992
+ const mouseMatch = remaining.match(/^\x1B\[<(\d+);(\d+);(\d+)([Mm])/);
670993
+ if (mouseMatch) {
670994
+ this.handleSgrMouse({
670995
+ raw: mouseMatch[0],
670996
+ btn: parseInt(mouseMatch[1]),
670997
+ col: parseInt(mouseMatch[2]),
670998
+ row: parseInt(mouseMatch[3]),
670999
+ suffix: mouseMatch[4]
671000
+ });
671001
+ i2 += mouseMatch[0].length;
671002
+ continue;
671003
+ }
671004
+ if (remaining.startsWith("\x1B[M")) {
671005
+ if (remaining.length >= 6) {
671006
+ if (this.onActivity) this.onActivity();
671007
+ i2 += 6;
671008
+ continue;
671266
671009
  }
671267
- }, 1e3);
671010
+ break;
671011
+ }
671012
+ if (remaining.startsWith("\x1B[<") && remaining.length < 15) {
671013
+ break;
671014
+ }
671015
+ if (remaining.startsWith("\x1B[") && remaining.length === 2) {
671016
+ break;
671017
+ }
671018
+ if (remaining.length === 1) {
671019
+ break;
671020
+ }
671268
671021
  }
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");
671022
+ output += this.buffer[i2];
671023
+ i2++;
671279
671024
  }
671280
- }
671281
- async stop() {
671282
- if (!this.active) return;
671283
- this.active = false;
671284
- if (this.abortController) {
671285
- this.abortController.abort();
671286
- this.abortController = null;
671025
+ this.buffer = this.buffer.slice(i2);
671026
+ if (output.length > 0) {
671027
+ if (this.onKeyboard) this.onKeyboard();
671028
+ this.push(output);
671287
671029
  }
671288
- if (this.silenceTimer) {
671289
- clearTimeout(this.silenceTimer);
671290
- this.silenceTimer = null;
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 = "";
671048
+ }
671049
+ }
671050
+ }, 50);
671291
671051
  }
671292
- if (this.maxSegmentTimer) {
671293
- clearTimeout(this.maxSegmentTimer);
671294
- this.maxSegmentTimer = null;
671052
+ callback();
671053
+ }
671054
+ _flush(callback) {
671055
+ if (this.flushTimer) {
671056
+ clearTimeout(this.flushTimer);
671057
+ this.flushTimer = null;
671295
671058
  }
671296
- if (this.captureBuffer.trim() && (this._state === "CAPTURING" || this._state === "TRANSCRIBING")) {
671297
- this.finalizeSegment();
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;
671064
+ }
671065
+ this.push(this.buffer);
671066
+ this.buffer = "";
671298
671067
  }
671299
- if (this._onTranscript) {
671300
- this.listen.removeAllListeners("transcript");
671301
- this._onTranscript = null;
671068
+ callback();
671069
+ }
671070
+ matchPrefixlessSgrMouse(input) {
671071
+ const match = input.match(/^(<)?(\d{1,3});(\d{1,5});(\d{1,5})([Mm])/);
671072
+ if (!match) return null;
671073
+ const hasMarker = Boolean(match[1]);
671074
+ const btn = parseInt(match[2], 10);
671075
+ const col = parseInt(match[3], 10);
671076
+ const row = parseInt(match[4], 10);
671077
+ const suffix = match[5];
671078
+ if (!Number.isFinite(btn) || !Number.isFinite(col) || !Number.isFinite(row))
671079
+ return null;
671080
+ if (col <= 0 || row <= 0) return null;
671081
+ if (!hasMarker && Date.now() > this.expectPrefixlessMouseUntil && !this.isKnownMouseButton(btn))
671082
+ return null;
671083
+ if (btn < 0 || btn > 255) return null;
671084
+ return { raw: match[0], btn, col, row, suffix };
671085
+ }
671086
+ looksLikePartialPrefixlessSgrMouse(input) {
671087
+ if (input.length < 2) return false;
671088
+ if (/^<\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) return true;
671089
+ if (Date.now() <= this.expectPrefixlessMouseUntil && /^\d{1,3};\d{0,5}(?:;\d{0,5})?$/.test(input)) {
671090
+ return true;
671302
671091
  }
671303
- if (this._onError) {
671304
- this.listen.removeAllListeners("error");
671305
- this._onError = null;
671092
+ return false;
671093
+ }
671094
+ isKnownMouseButton(btn) {
671095
+ if (btn >= 0 && btn <= 6) return true;
671096
+ if (btn >= 32 && btn <= 39) return true;
671097
+ if (btn >= 64 && btn <= 71) return true;
671098
+ if (btn >= 96 && btn <= 103) return true;
671099
+ return false;
671100
+ }
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
+ }
671306
671118
  }
671307
- try {
671308
- await this.listen.stop();
671309
- } catch {
671119
+ if (this.onActivity) this.onActivity();
671120
+ }
671121
+ };
671122
+ }
671123
+ });
671124
+
671125
+ // packages/cli/src/tui/direct-input.ts
671126
+ var direct_input_exports = {};
671127
+ __export(direct_input_exports, {
671128
+ DirectInput: () => DirectInput
671129
+ });
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"() {
671134
+ "use strict";
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) {
671154
+ super();
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"));
671162
+ });
671163
+ input.on("end", () => {
671164
+ if (!this._closed) this.close();
671165
+ });
671166
+ }
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);
671174
+ }
671175
+ /** Pause input processing (for overlay transitions) */
671176
+ pause() {
671177
+ this._paused = true;
671178
+ }
671179
+ /** Resume input processing */
671180
+ resume() {
671181
+ this._paused = false;
671182
+ }
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;
671310
671190
  }
671311
- this.setState("IDLE");
671312
- if (this.verbose) this.onStatus("VoiceChat ended");
671313
- this.emit("stopped");
671191
+ this.emit("close");
671314
671192
  }
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;
671193
+ /** No-op — readline compat (StatusBar renders the prompt, not us) */
671194
+ setPrompt(_prompt) {
671195
+ }
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;
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;
671381
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;
671388
+ }
671389
+ if (remaining.length >= 2 && remaining[1] === "O") {
671390
+ if (remaining.length >= 3) {
671391
+ this._handleSS3(remaining[2]);
671392
+ i2 += 3;
671393
+ continue;
671394
+ }
671395
+ break;
671396
+ }
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;
671402
+ }
671403
+ if (remaining.length === 1) {
671404
+ break;
671405
671405
  }
671406
+ i2++;
671407
+ continue;
671406
671408
  }
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;
671409
+ if (code8 < 32 || code8 === 127) {
671410
+ if (code8 === 13) {
671411
+ if (this._preSubmit?.()) {
671412
+ i2++;
671413
+ continue;
671438
671414
  }
671439
- } catch {
671415
+ this._submit();
671416
+ i2++;
671417
+ if (this._buffer[i2] === "\n") i2++;
671418
+ continue;
671419
+ }
671420
+ if (code8 === 10) {
671421
+ this._insertText("\n");
671422
+ i2++;
671423
+ continue;
671424
+ }
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;
671444
+ }
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 = "";
671458
+ }
671459
+ }, 50);
671460
+ } else {
671461
+ if (this._flushTimer) {
671462
+ clearTimeout(this._flushTimer);
671463
+ this._flushTimer = null;
671464
+ }
671465
+ }
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;
671486
+ }
671487
+ return false;
671488
+ }
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;
671440
671528
  }
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)}`;
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;
671454
671541
  }
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() });
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;
671458
671547
  }
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);
671466
- }
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 {
671548
+ if (params === "5") {
671549
+ this.emit("pageup");
671550
+ return;
671475
671551
  }
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();
671552
+ if (params === "6") {
671553
+ this.emit("pagedown");
671554
+ return;
671481
671555
  }
671482
- if (typeof this.voice.waitUntilIdle === "function") {
671483
- try {
671484
- await this.voice.waitUntilIdle();
671485
- } catch {
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;
671486
671579
  }
671487
- } else {
671488
- const estimatedMs = Math.max(1500, response.length / 5 * (6e4 / 150));
671489
- await new Promise((r2) => setTimeout(r2, estimatedMs));
671490
671580
  }
671581
+ if (hasCtrl && !hasShift && (codepoint === 73 || codepoint === 105)) {
671582
+ this.emit("ctrl-i");
671583
+ return;
671584
+ }
671585
+ return;
671491
671586
  }
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
- }
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...");
671508
671587
  }
671509
671588
  }
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);
671535
- }
671536
- } catch (err) {
671537
- const msg = err instanceof Error ? err.message : "";
671538
- if (msg.includes("abort")) throw err;
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;
671539
671594
  }
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)}`);
671595
+ if (parts.length === 3) {
671596
+ const [prefix, modifiers, codepoint] = parts;
671597
+ return prefix === 27 && modifiers === 2 && (codepoint === 10 || codepoint === 13);
671552
671598
  }
671553
- return await this.parseOpenAIStream(res);
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;
671667
- }
671668
- function getDaemonListenEngine() {
671669
- if (!_listenEngine) _listenEngine = getListenEngine();
671670
- return _listenEngine;
671671
- }
671672
- function getVoiceBus() {
671673
- if (!_bus) _bus = new EventEmitter14();
671674
- return _bus;
671675
- }
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
- };
671765
+ return "loopback";
671689
671766
  }
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;
671716
- }
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);
671717
671771
  }
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
- }
671772
+ function stripMappedPrefix(ip) {
671773
+ return ip.replace(/^::ffff:/i, "");
671732
671774
  }
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);
671743
- }
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;
671744
671781
  }
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 {
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;
671753
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;
671754
671796
  }
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 };
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;
671779
671802
  }
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;
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";
671786
671811
  }
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);
671812
+ }
671813
+ var init_access_policy = __esm({
671814
+ "packages/cli/src/api/access-policy.ts"() {
671815
+ "use strict";
671798
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);
671799
671828
  }
671800
- function listClients() {
671801
- return Array.from(_clients2.values());
671829
+ function projectDir(root) {
671830
+ return join149(PROJECTS_DIR, projectKey(root));
671802
671831
  }
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" };
671832
+ function prefsPath(root) {
671833
+ return join149(projectDir(root), "preferences.json");
671827
671834
  }
671828
- async function stopVoiceChat() {
671829
- if (!_voiceChatSession) return { ok: true, message: "No active session" };
671835
+ function rootSentinelPath(root) {
671836
+ return join149(projectDir(root), ".root");
671837
+ }
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);