Files
gnome-jarvis/daemon/dbus-service.js
T
snxraven 7f92e3311b
Rolling release / release (push) Successful in 9m29s
Complete local Bare smoke path
2026-09-11 18:27:56 -04:00

140 lines
6.9 KiB
JavaScript

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)); }
async Say(text) { await daemon.say?.(text); }
async Ask(text) { await 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) { 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); }
}
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' },
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' },
},
});
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 };