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(); } PushToTalk(pressed) { daemon.setPushToTalk?.(Boolean(pressed)); } Say(text) { return daemon.say?.(text); } Ask(text) { return daemon.ask(text); } 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()); } 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(); } ConfirmationRequired(tool, args, pattern) { this.emit('ConfirmationRequired', String(tool), String(args), String(pattern)); } StateChanged(state) { this.emit('StateChanged', state); } Reply(text) { this.emit('Reply', String(text)); } Token(text) { this.emit('Token', String(text)); } Error(code, message) { this.emit('Error', String(code), String(message)); } WakeHeard(phrase) { this.emit('WakeHeard', String(phrase)); } PartialTranscript(text) { this.emit('PartialTranscript', String(text)); } FinalTranscript(text) { this.emit('FinalTranscript', String(text)); } SpeakingLevel(rms) { this.emit('SpeakingLevel', Number(rms)); } ListeningLevel(rms) { this.emit('ListeningLevel', Number(rms)); } ChipOffered(id, label, payload) { this.emit('ChipOffered', String(id), String(label), String(payload)); } JobProgress(id, pct, label) { this.emit('JobProgress', String(id), Number(pct), String(label)); } ComputerStep(json) { this.emit('ComputerStep', String(json)); } ComputerHighlight(json) { this.emit('ComputerHighlight', String(json)); } } Session.configureMembers({ methods: { Arm: { inSignature: '', outSignature: '' }, Sleep: { inSignature: '', outSignature: '', method: 'Sleep' }, Shutdown: { inSignature: '', outSignature: '', method: 'Shutdown' }, PushToTalk: { inSignature: 'b', outSignature: '', method: 'PushToTalk' }, Say: { inSignature: 's', outSignature: '', method: 'Say' }, Ask: { inSignature: 's', outSignature: '', method: 'Ask' }, 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' }, 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', signal: true }, Reply: { signature: 's', signal: true }, Token: { signature: 's', signal: true }, Error: { signature: 'ss', signal: true }, WakeHeard: { signature: 's', signal: true }, PartialTranscript: { signature: 's', signal: true }, FinalTranscript: { signature: 's', signal: true }, SpeakingLevel: { signature: 'd', signal: true }, ListeningLevel: { signature: 'd', signal: true }, ChipOffered: { signature: 'sss', signal: true }, JobProgress: { signature: 'sds', signal: true }, ComputerStep: { signature: 's', signal: true }, ComputerHighlight: { signature: 's', signal: true }, ConfirmationRequired: { signature: 'sss', signal: true }, }, }); 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(event?.args || {}), event?.pattern || 'explicit confirmation required')); for (const signal of ['WakeHeard', 'PartialTranscript', 'FinalTranscript', 'SpeakingLevel', 'ListeningLevel', 'ChipOffered', 'JobProgress', 'ComputerStep', 'ComputerHighlight']) { daemon.on(signal, (...args) => iface[signal](...args)); } return bus; } export { BUS_NAME, OBJECT, IFACE };