Updates
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { SKILLS } from './catalog.js';
|
||||
import { qvacStatus } from '../daemon/qvac-master.js';
|
||||
|
||||
const PERMISSIONS = Object.freeze({ read: 'read', write: 'write', dangerous: 'dangerous', computerUse: 'computer-use' });
|
||||
|
||||
async function desktopApps() {
|
||||
const dirs = ['/usr/share/applications', path.join(os.homedir(), '.local/share/applications')];
|
||||
const apps = [];
|
||||
for (const dir of dirs) {
|
||||
let entries = [];
|
||||
try { entries = await readdir(dir, { withFileTypes: true }); } catch { continue; }
|
||||
for (const entry of entries) {
|
||||
if (!entry.name.endsWith('.desktop')) continue;
|
||||
try {
|
||||
const text = await readFile(path.join(dir, entry.name), 'utf8');
|
||||
const name = text.match(/^Name=(.*)$/m)?.[1];
|
||||
const exec = text.match(/^Exec=([^\n ]+)/m)?.[1];
|
||||
if (name && exec) apps.push({ id: entry.name, name, exec });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return apps.slice(0, 500);
|
||||
}
|
||||
|
||||
async function searchFiles(root, query, limit = 50) {
|
||||
const hits = [];
|
||||
async function walk(dir, depth) {
|
||||
if (depth > 8 || hits.length >= limit) return;
|
||||
let entries = [];
|
||||
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
||||
for (const entry of entries) {
|
||||
if (hits.length >= limit) break;
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.name.toLowerCase().includes(query.toLowerCase())) hits.push(full);
|
||||
if (entry.isDirectory()) await walk(full, depth + 1);
|
||||
}
|
||||
}
|
||||
await walk(path.resolve(root), 0);
|
||||
return hits;
|
||||
}
|
||||
|
||||
export function createPhase2Tools({ cwd = process.cwd(), computer } = {}) {
|
||||
return [
|
||||
{
|
||||
name: 'app_list', permission: PERMISSIONS.read,
|
||||
description: 'List locally installed desktop applications without launching anything.',
|
||||
parameters: { type: 'object', properties: {} }, execute: () => desktopApps(),
|
||||
},
|
||||
{
|
||||
name: 'fs_search', permission: PERMISSIONS.read,
|
||||
description: 'Search local file and directory names below the Jarvis workspace.',
|
||||
parameters: { type: 'object', properties: { query: { type: 'string' }, root: { type: 'string' }, limit: { type: 'number' } }, required: ['query'] },
|
||||
execute: ({ query, root = cwd, limit = 50 }) => searchFiles(root, query, Math.min(100, Number(limit) || 50)),
|
||||
},
|
||||
{
|
||||
name: 'fs_read', permission: PERMISSIONS.read,
|
||||
description: 'Read a UTF-8 local text file below the Jarvis workspace, capped at 400 KiB.',
|
||||
parameters: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
|
||||
execute: async ({ file }) => { const target = path.resolve(cwd, file); if (!target.startsWith(`${path.resolve(cwd)}${path.sep}`)) throw new Error('path is outside the Jarvis workspace'); return (await readFile(target, 'utf8')).slice(0, 400 * 1024); },
|
||||
},
|
||||
{
|
||||
name: 'fs_write', permission: PERMISSIONS.write,
|
||||
description: 'Write a local text file only after an explicit confirmation flag is supplied.',
|
||||
parameters: { type: 'object', properties: { file: { type: 'string' }, contents: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['file', 'contents', 'confirmed'] },
|
||||
execute: async ({ file, contents, confirmed }) => { if (confirmed !== true) return { confirmation_required: true, action: 'write', file }; const target = path.resolve(cwd, file); if (!target.startsWith(`${path.resolve(cwd)}${path.sep}`)) throw new Error('path is outside the Jarvis workspace'); await writeFile(target, String(contents), 'utf8'); return { ok: true, file: target, bytes: String(contents).length }; },
|
||||
},
|
||||
{
|
||||
name: 'memory_recall', permission: PERMISSIONS.read,
|
||||
description: 'List local Jarvis memory notes; contents stay on this machine.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
execute: async () => { const dir = path.join(os.homedir(), '.local/share/jarvis/memory'); try { return (await readdir(dir)).slice(0, 200); } catch { return []; } },
|
||||
},
|
||||
{
|
||||
name: 'memory_write', permission: PERMISSIONS.write,
|
||||
description: 'Write a local Jarvis memory note only after explicit confirmation.',
|
||||
parameters: { type: 'object', properties: { name: { type: 'string' }, text: { type: 'string' }, confirmed: { type: 'boolean' } }, required: ['name', 'text', 'confirmed'] },
|
||||
execute: async ({ name, text, confirmed }) => { if (confirmed !== true) return { confirmation_required: true, action: 'memory_write', name }; const dir = path.join(os.homedir(), '.local/share/jarvis/memory'); const safe = String(name).replace(/[^a-zA-Z0-9._-]/g, '_'); await (await import('node:fs/promises')).mkdir(dir, { recursive: true }); await writeFile(path.join(dir, safe), String(text), 'utf8'); return { ok: true, name: safe }; },
|
||||
},
|
||||
{
|
||||
name: 'rag_workspaces', permission: PERMISSIONS.read,
|
||||
description: 'List configured local Jarvis RAG workspace directories.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
execute: async () => { const dir = path.join(os.homedir(), '.local/share/jarvis/memory'); try { return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return []; } },
|
||||
},
|
||||
{
|
||||
name: 'capability_status', permission: PERMISSIONS.read,
|
||||
description: 'Report the truthful local status of every Jarvis capability and the single QVAC master.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
execute: () => ({ qvac: qvacStatus(), capabilities: SKILLS.map(([id, label, permission]) => ({ id, label, permission, status: permission === 'computer-use' && !computer?.status?.().active ? 'grant-required' : 'registered' })) }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export { PERMISSIONS };
|
||||
@@ -0,0 +1,9 @@
|
||||
import { callQvac } from '../daemon/qvac-master.js';
|
||||
|
||||
export function createQvacTools() {
|
||||
return [
|
||||
{ name: 'qvac_runtime_state', description: 'Read the single QVAC master lifecycle state.', parameters: { type: 'object', properties: {} }, execute: () => callQvac('state') },
|
||||
{ name: 'qvac_system_resources', description: 'Read QVAC-observed local CPU/GPU resources.', parameters: { type: 'object', properties: {} }, execute: () => callQvac('getSystemResources', { sample: true }) },
|
||||
{ name: 'qvac_assess_model_fit', description: 'Ask QVAC whether a model is likely to fit before downloading it.', parameters: { type: 'object', properties: { input: { type: 'object' } }, required: ['input'] }, execute: ({ input }) => callQvac('assessModelFit', input) },
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const VOICE_SYSTEM_PROMPT = `You are Jarvis, a local Ubuntu GNOME voice assistant running through QVAC.
|
||||
Use short spoken replies of one to three sentences unless the user asks for detail.
|
||||
Ground every desktop, file, memory, and model claim in a tool result. Never claim cloud access.
|
||||
Computer use requires an explicit user grant; never click or type while it is inactive, locked, or revoked.
|
||||
Destructive actions require confirmation in both the HUD and spoken conversation.
|
||||
Prefer structured tools and accessibility references over coordinates.
|
||||
When a useful follow-up action exists, append a HUD sidecar exactly as <jarvis_hud>{"title":"...","chips":[{"id":"...","label":"..."}],"confirmation":null}</jarvis_hud>.
|
||||
The sidecar is for the HUD and must not be spoken.`;
|
||||
|
||||
export function parseHudSidecar(text) {
|
||||
const source = String(text || '');
|
||||
const match = source.match(/<jarvis_hud>([\s\S]*?)<\/jarvis_hud>/i);
|
||||
if (!match) return { spoken: source.trim(), hud: null };
|
||||
let hud = null;
|
||||
try { hud = JSON.parse(match[1]); } catch { hud = { error: 'invalid hud sidecar' }; }
|
||||
return { spoken: source.replace(match[0], '').trim(), hud };
|
||||
}
|
||||
Reference in New Issue
Block a user