const IFACE = 'io.qvac.Jarvis.Session'; const OBJECT = '/io/qvac/Jarvis'; const BUS_NAME = 'io.qvac.Jarvis'; export async function serveOnSessionBus(daemon) { // dbus-next is CommonJS. Load it through Bare's Node compatibility map so // its builtin imports use bare-* implementations where supported. const { createRequire } = await import('node:module'); const require = createRequire(import.meta.url); // dbus-next enables TCP_NODELAY on every transport. Bare's Unix-pipe // transport exposes the Node-compatible method on the wrapper, but its // underlying pipe does not implement the native socket operation. D-Bus is // request/response traffic, so disabling this optional optimization keeps // the transport functional without changing message semantics. const net = require('node:net', { with: { imports: 'bare-node-runtime/imports' } }); if (net.Socket?.prototype) net.Socket.prototype.setNoDelay = function setNoDelay() { return this; }; const dbus = require('dbus-next', { with: { imports: 'bare-node-runtime/imports' } }); const { Interface } = dbus.interface; class Session extends Interface { constructor() { super(IFACE); } Arm() { daemon.arm(); } Sleep() { daemon.sleep(); } Shutdown() { daemon.close(); } SetMuted(muted) { daemon.setMuted?.(Boolean(muted)); } SetListening(on) { daemon.setListening?.(Boolean(on)); } PushToTalk(pressed) { daemon.setPushToTalk?.(Boolean(pressed)); } async Say(text) { await daemon.say?.(text); } async Ask(text) { await daemon.ask(text); } ReloadSettings() { return daemon.reloadSettings(); } PreviewVoice(text) { return daemon.previewVoice(text); } StopSpeech() { daemon.stopSpeech(); } async ResetContext() { await daemon.resetContext(); } Confirm(jobId, toolCallId, decision) { daemon.confirmPermission?.(jobId, toolCallId, decision); } Cancel() { daemon.cancel(); } SetMode(mode) { daemon.mode = mode; daemon.emit('ModeChanged', mode); } GetState() { return daemon.state; } EnrollWake(phrase) { daemon.emit('EnrollWake', String(phrase)); } IngestPath(filePath) { daemon.emit('IngestPath', String(filePath)); } ComputerGrant(persist) { daemon.computerGrant(persist); } ComputerRevoke() { daemon.computerRevoke(); } ComputerStatus() { return JSON.stringify(daemon.computer.status()); } WebcamGrant() { daemon.webcamGrant(); } WebcamRevoke() { daemon.webcamRevoke(); } WebcamStatus() { return JSON.stringify(daemon.camera.status()); } GetRuntimeStatus() { return daemon.runtimeStatus(); } AssessModelFit(model) { return daemon.assessModelFit(model); } DownloadModel(model) { return daemon.downloadModel(model); } CancelModel(model) { return daemon.cancelModel(model); } WipeComputerTraces() { return daemon.wipeComputerTraces(); } // configureMembers wraps signal methods and emits through dbus-next's // private $emitter. Returning the payload is sufficient; calling // `this.emit` here crashes because Interface does not expose that method. ConfirmationRequired(tool, args, pattern) { return [String(tool), String(args), String(pattern)]; } ContextReset() { return; } StateChanged(state) { return String(state); } Reply(text) { return String(text); } Token(text) { return String(text); } Error(code, message) { return [String(code), String(message)]; } WakeHeard(phrase) { return String(phrase); } PartialTranscript(text) { return String(text); } FinalTranscript(text) { return String(text); } SpeakingLevel(rms) { return Number(rms); } ListeningLevel(rms) { return Number(rms); } ChipOffered(id, label, payload) { return [String(id), String(label), String(payload)]; } JobProgress(id, pct, label) { return [String(id), Number(pct), String(label)]; } ComputerStep(json) { return String(json); } ComputerHighlight(json) { return String(json); } Thinking(text) { return String(text); } ToolCall(json) { return String(json); } ToolResult(json) { return String(json); } } Session.configureMembers({ methods: { Arm: { inSignature: '', outSignature: '' }, ReloadSettings: { inSignature: '', outSignature: 's' }, PreviewVoice: { inSignature: 's', outSignature: '' }, StopSpeech: { inSignature: '', outSignature: '' }, Sleep: { inSignature: '', outSignature: '', method: 'Sleep' }, Shutdown: { inSignature: '', outSignature: '', method: 'Shutdown' }, PushToTalk: { inSignature: 'b', outSignature: '', method: 'PushToTalk' }, SetMuted: { inSignature: 'b', outSignature: '', method: 'SetMuted' }, SetListening: { inSignature: 'b', outSignature: '', method: 'SetListening' }, Say: { inSignature: 's', outSignature: '', method: 'Say' }, Ask: { inSignature: 's', outSignature: '', method: 'Ask' }, ResetContext: { inSignature: '', outSignature: '', method: 'ResetContext' }, Confirm: { inSignature: 'sss', outSignature: '', method: 'Confirm' }, Cancel: { inSignature: '', outSignature: '', method: 'Cancel' }, SetMode: { inSignature: 's', outSignature: '', method: 'SetMode' }, GetState: { inSignature: '', outSignature: 's', method: 'GetState' }, EnrollWake: { inSignature: 's', outSignature: '', method: 'EnrollWake' }, IngestPath: { inSignature: 's', outSignature: '', method: 'IngestPath' }, ComputerGrant: { inSignature: 'b', outSignature: '', method: 'ComputerGrant' }, ComputerRevoke: { inSignature: '', outSignature: '', method: 'ComputerRevoke' }, ComputerStatus: { inSignature: '', outSignature: 's', method: 'ComputerStatus' }, WebcamGrant: { inSignature: '', outSignature: '', method: 'WebcamGrant' }, WebcamRevoke: { inSignature: '', outSignature: '', method: 'WebcamRevoke' }, WebcamStatus: { inSignature: '', outSignature: 's', method: 'WebcamStatus' }, GetRuntimeStatus: { inSignature: '', outSignature: 's', method: 'GetRuntimeStatus' }, AssessModelFit: { inSignature: 's', outSignature: 's', method: 'AssessModelFit' }, DownloadModel: { inSignature: 's', outSignature: 's', method: 'DownloadModel' }, CancelModel: { inSignature: 's', outSignature: 's', method: 'CancelModel' }, WipeComputerTraces: { inSignature: '', outSignature: 'b' }, }, signals: { StateChanged: { signature: 's' }, ContextReset: { signature: '' }, Reply: { signature: 's' }, Token: { signature: 's' }, Error: { signature: 'ss' }, WakeHeard: { signature: 's' }, PartialTranscript: { signature: 's' }, FinalTranscript: { signature: 's' }, SpeakingLevel: { signature: 'd' }, ListeningLevel: { signature: 'd' }, ChipOffered: { signature: 'sss' }, JobProgress: { signature: 'sds' }, ComputerStep: { signature: 's' }, ComputerHighlight: { signature: 's' }, ConfirmationRequired: { signature: 'sss' }, Thinking: { signature: 's' }, ToolCall: { signature: 's' }, ToolResult: { signature: 's' }, }, }); const bus = dbus.sessionBus(); // Bare streams emit `data` but do not keep a Node-style readable buffer. // dbus-next's authentication and message parser use read()/readable, so // provide that small compatibility layer for this one D-Bus connection. const stream = bus._connection?.stream; if (globalThis.Bare && stream && !stream.__jarvisReadableBuffer) { let pending = Buffer.alloc(0); let readableScheduled = false; stream.__jarvisReadableBuffer = true; stream.on('data', (chunk) => { pending = Buffer.concat([pending, Buffer.from(chunk)]); if (!readableScheduled) { readableScheduled = true; setTimeout(() => { readableScheduled = false; stream.emit('readable'); }, 0); } }); stream.read = (size) => { if (pending.length === 0) return null; if (size == null || size >= pending.length) { const result = pending; pending = Buffer.alloc(0); return result; } const result = pending.subarray(0, size); pending = pending.subarray(size); return result; }; } await bus.requestName(BUS_NAME); const iface = new Session(); bus.export(OBJECT, iface); daemon.on('StateChanged', (state) => iface.StateChanged(state)); daemon.on('Reply', (reply) => iface.Reply(reply)); daemon.on('Token', (token) => iface.Token(token)); daemon.on('Error', (code, message) => iface.Error(code, message)); daemon.on('ConfirmationRequired', (event) => iface.ConfirmationRequired(event?.tool || 'action', JSON.stringify({ jobId: event?.jobId || '', toolCallId: event?.toolCallId || '', args: event?.args || {} }), event?.pattern || 'explicit confirmation required')); for (const signal of ['ContextReset', 'WakeHeard', 'PartialTranscript', 'FinalTranscript', 'SpeakingLevel', 'ListeningLevel', 'ChipOffered', 'JobProgress', 'ComputerStep', 'ComputerHighlight', 'Thinking', 'ToolCall', 'ToolResult']) { daemon.on(signal, (...args) => iface[signal](...args)); } return bus; } export { BUS_NAME, OBJECT, IFACE };