SOURCE / PINNED RELEASE
Made of little things.
Body Journal
- Release
- e8049f8da4e2…
- Author-recorded commit
- 61c04bf0e8aa…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7
Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { readDiagnostic, visibleDiagnostic, type Diagnostic } from './ai-diagnostics.js';
import { cvm, type CvmServerRef, type JsonObject } from '@napplet/sdk';
import { IMAGE_CHUNK_CHARS, type BodyImage, type VisionCheck } from './vision-contract.js';
import { visionChallenge } from './vision-challenge.js';
import { runtimeHasDomain } from './domain-availability.js';
import { boundedText, isProvider, object, parseReport, PROTOCOL_VERSION, type ProviderId, type Report } from './ai-contract.js';
export interface Connection { id: string; label: string; provider: ProviderId; model: string }
export interface ConnectionSettings { version: 1; connections: Connection[]; activeId: string; bridge: CvmServerRef }
export const CONNECTIONS_KEY = 'body-journal-connections-v1';
export function parseSettings(raw: string): ConnectionSettings {
if (raw.length > 12_000) throw new Error('Connection settings are too large.');
const data = object(JSON.parse(raw));
if (data.version !== 1 || !Array.isArray(data.connections) || data.connections.length > 8) throw new Error('Unsupported connections.');
const ids = new Set<string>();
const connections = data.connections.map(value => {
const c = object(value), id = boundedText(c.id, 80);
if (!/^[a-z0-9-]+$/.test(id) || ids.has(id) || !isProvider(c.provider)) throw new Error('Invalid connection.');
ids.add(id);
return { id, label: boundedText(c.label, 60), provider: c.provider, model: modelId(c.model) };
});
const bridge = object(data.bridge);
const pubkey = boundedText(bridge.pubkey, 64, true);
if (pubkey && !/^[0-9a-f]{64}$/.test(pubkey)) throw new Error('Invalid bridge key.');
if (!Array.isArray(bridge.relays) || bridge.relays.length > 3) throw new Error('Invalid relay list.');
const relays = bridge.relays.map(parseRelay);
return { version: 1, connections, activeId: ids.has(String(data.activeId)) ? String(data.activeId) : connections[0]?.id || '', bridge: { pubkey, relays } };
}
export function modelId(value: unknown): string {
const text = boundedText(value, 120);
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/.test(text)) throw new Error('Use the provider’s model ID, without spaces.');
return text;
}
export function parseRelay(value: unknown): string {
const url = new URL(boundedText(value, 250));
if (url.username || url.password || url.hash || url.search || !(url.protocol === 'wss:' || (url.protocol === 'ws:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))) throw new Error('Use a WSS relay URL (loopback WS is allowed for development).');
return url.href;
}
export function validateBridge(bridge: CvmServerRef): void {
if (!/^[0-9a-f]{64}$/.test(bridge.pubkey)) throw new Error('Enter the bridge’s 64-character public key.');
if (!bridge.relays?.length || bridge.relays.length > 3) throw new Error('Enter one to three relay URLs from your bridge.');
bridge.relays.forEach(parseRelay);
}
export class AiClient {
private servers = new Map<string, CvmServerRef>();
private lifetime = new AbortController();
async call(bridge: CvmServerRef, tool: string, args: JsonObject): Promise<Record<string, unknown>> {
if (this.lifetime.signal.aborted) throw new Error('Connection session ended.');
if (!runtimeHasDomain('cvm')) throw new Error('This host has no AI service connection. Your case can still be edited and saved.');
validateBridge(bridge);
this.servers.set(bridge.pubkey, bridge);
let result;
try { result = await cvm.callTool(bridge, tool, args, { timeoutMs: 12_000, initialize: true, payment: 'deny' }); }
catch { throw new Error('The bridge could not be reached or the host denied access. Check the public key, relay and host approval. No automatic retry was made.'); }
if (result.isError) throw new Error('The bridge rejected the request. Check its version and configuration.');
let data;
try {
const content = result.structuredContent ?? JSON.parse(result.content.filter(c => c.type === 'text').map(c => c.text).join(''));
data = object(content);
} catch { throw new Error('The bridge returned an unreadable response.'); }
this.checkError(data);
return data;
}
private checkError(data: Record<string, unknown>): void {
if (data.ok === false) {
// Bridge errors are untrusted and can echo secrets. Show only local messages.
const messages: Record<string, string> = {
'image-invalid': 'The bridge could not accept an image. No assessment was started.',
'images-expired': 'Prepared images expired or are incomplete. Review the request and send again.',
authentication: 'The provider rejected the API key or account access.',
'rate-limit': 'Provider quota or rate limit reached. Check your account and try later.',
'model-unavailable': 'The model ID or request options were rejected by the provider.',
timeout: 'The request timed out. The provider may still charge for work already started.',
busy: 'The bridge is busy. Wait before trying again.',
'invalid-report': 'The model returned an unsupported report format. No assessment was accepted.',
incomplete: 'The model did not finish. No partial assessment was accepted.',
'invalid-response': 'The provider response could not be read. Open request diagnostics for details.',
'models-unavailable': 'The provider model list is unavailable. You can still enter a model ID manually.',
refused: 'The model declined this request.',
expired: 'This result expired or belongs to a different host session. No new inference was started.',
};
throw new Error(messages[String(data.code)] || 'The bridge could not complete the request. Check the connection; no automatic retry was made.');
}
}
async checkBridge(bridge: CvmServerRef): Promise<void> {
const info = await this.call(bridge, 'body_journal_info', {});
if (info.protocolVersion !== PROTOCOL_VERSION || info.encryptionRequired !== true || info.input !== 'text-and-images' || info.jobs !== true || info.diagnostics !== true || info.modelDiscovery !== true) throw new Error('Update and restart your Body Journal AI bridge (protocol 4). No key or case was sent.');
}
private async job(bridge: CvmServerRef, tool: string, args: JsonObject, signal?: AbortSignal, onProgress?: (d: Diagnostic) => void): Promise<Record<string, unknown>> {
signal = signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
const requestId = `request-${Date.now().toString(36)}-${Array.from(crypto.getRandomValues(new Uint8Array(12)), b => b.toString(16).padStart(2, '0')).join('')}`;
if (signal?.aborted) throw new Error('Stopped waiting.');
onProgress?.({stage:'queued',elapsedMs:0,output:'',reasoning:'',events:[],requestId});
await this.call(bridge, tool, { ...args, requestId });
let diagnostics: Diagnostic | undefined;
const deadline = Date.now() + 110_000;
while (Date.now() < deadline) {
if (signal?.aborted) throw new Error('Stopped waiting.');
const state = await this.call(bridge, 'body_journal_result', { requestId, outputOffset: diagnostics?.output.length || 0, reasoningOffset: diagnostics?.reasoning.length || 0 });
if (signal?.aborted) throw new Error('Stopped waiting.');
let more = false;
if (state.diagnostics) {
const parsed = readDiagnostic(state.diagnostics, diagnostics, String(args.apiKey || '')); diagnostics = parsed.diagnostic; more = parsed.more;
onProgress?.({...visibleDiagnostic(diagnostics,String(args.apiKey || '')),requestId});
}
if (state.status === 'complete' && more) continue;
if (state.status === 'complete') { const result = object(state.result); this.checkError(result); return result; }
if (state.status !== 'pending') throw new Error('Invalid job status from bridge.');
await new Promise<void>((resolve, reject) => {
const stop = () => { clearTimeout(timer); reject(new Error('Stopped waiting.')); };
const timer = setTimeout(() => { signal?.removeEventListener('abort', stop); resolve(); }, 2500);
signal?.addEventListener('abort', stop, { once: true });
});
}
throw new Error('The result was not available before the deadline. The provider may still charge for the request.');
}
async test(bridge: CvmServerRef, c: Connection, apiKey: string): Promise<VisionCheck> {
await this.checkBridge(bridge);
const result = await this.job(bridge, 'body_journal_test', { provider: c.provider, model: c.model, apiKey, challenge:visionChallenge() });
if (result.ok !== true) throw new Error('The connection test did not succeed.');
const v=object(result.vision);
if(!['verified','unsupported','unconfirmed'].includes(String(v.state)))throw new Error('Invalid vision test result.');
return {state:v.state as VisionCheck['state'],model:boundedText(v.model,120)};
}
async analyze(bridge: CvmServerRef, c: Connection, apiKey: string, caseText: string, signal?: AbortSignal, images:BodyImage[]=[], onProgress?: (d: Diagnostic) => void): Promise<{ report: Report; model: string }> {
onProgress?.({stage:'checking',elapsedMs:0,output:'',reasoning:'',events:[]});
await this.checkBridge(bridge);
const refs:{id:string;sha256:string;concernId:number|null;caption:string;kind:'body-map'|'patient-photo';attachmentId?:string}[]=[];
try {
if (images.length) onProgress?.({stage:'uploading',elapsedMs:0,output:'',reasoning:'',events:[]});
for(const image of images){
if(signal?.aborted || this.lifetime.signal.aborted)throw new Error('Stopped waiting.');
const sha256=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(image.dataUrl))),b=>b.toString(16).padStart(2,'0')).join('');
const id=`image-${Array.from(crypto.getRandomValues(new Uint8Array(12)),b=>b.toString(16).padStart(2,'0')).join('')}`;
refs.push({id,sha256,concernId:image.concernId,caption:image.caption.slice(0,1200),kind:image.kind || 'body-map',...(image.attachmentId ? { attachmentId: image.attachmentId } : {})});
const total=Math.ceil(image.dataUrl.length/IMAGE_CHUNK_CHARS);
for(let index=0;index<total;index++){
if(signal?.aborted || this.lifetime.signal.aborted)throw new Error('Stopped waiting.');
await this.call(bridge,'body_journal_image',{id,sha256,index,total,chunk:image.dataUrl.slice(index*IMAGE_CHUNK_CHARS,(index+1)*IMAGE_CHUNK_CHARS)});
}
}
const result = await this.job(bridge, 'body_journal_analyze', { provider: c.provider, model: c.model, apiKey, caseText, images:refs }, signal, onProgress);
if (result.ok !== true) throw new Error('The analysis did not complete.');
return { report: parseReport(result.report, true), model: boundedText(result.model, 120) };
} finally {
if(refs.length) { try { await this.call(bridge,'body_journal_images_clear',{ids:refs.map(r=>r.id)}); } catch { /* The bridge also expires abandoned image chunks. */ } }
}
}
async models(bridge: CvmServerRef, provider: ProviderId, apiKey: string): Promise<{models: string[]; truncated: boolean}> {
await this.checkBridge(bridge);
const result = await this.job(bridge,'body_journal_models',{provider,apiKey});
if (!Array.isArray(result.models) || result.models.length > 300) throw new Error('Invalid model list from bridge.');
return {models:[...new Set(result.models.map(modelId))],truncated:result.truncated === true};
}
dispose(): void {
this.lifetime.abort();
for (const ref of this.servers.values()) { try { void cvm.close(ref).catch(() => {}); } catch { /* Host may already be gone. */ } }
this.servers.clear();
}
}
