Back to Body Journal
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.

bridge/server.mjs
import { createHash, randomBytes } from 'node:crypto';
import { pathToFileURL } from 'node:url';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { NostrServerTransport, PrivateKeySigner, ApplesauceRelayPool, EncryptionMode } from '@contextvm/sdk';
import { z } from 'zod';
import { callProvider, testConnection, listModels, ProviderError } from './provider.mjs';
import { diagnosticPacket } from '../src/ai-diagnostics.ts';
import { ImageStore } from './image-store.mjs';
import { IMAGE_CHUNK_CHARS, MAX_IMAGE_CHARS, MAX_IMAGES } from '../src/vision-contract.ts';
import { MAX_CASE_BYTES, PROTOCOL_VERSION } from '../src/ai-contract.ts';

export function createBridge({ fetchImpl, timeoutMs } = {}) {
  const server = new McpServer({ name: 'body-journal-ai', version: '0.1.0' });
  const shutdown = new AbortController();
  const images = new ImageStore();
  const actors = new Map(), jobs = new Map(); let active = 0;
  const result = value => ({ content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value });
  const credentials = { requestId: z.string().regex(/^[a-zA-Z0-9-]{8,80}$/), provider: z.enum(['openai', 'kimi', 'deepseek']), model: z.string().min(1).max(120), apiKey: z.string().min(8).max(512) };
  function run(args, extra, operation) {
    const analyze = operation === 'analyze';
    const actor = extra._meta?.clientPubkey;
    if (typeof actor !== 'string' || !/^[0-9a-f]{64}$/.test(actor)) return result({ ok: false, code: 'identity', message: 'A host transport identity is required.' });
    const now = Date.now(), jobKey = `${actor}:${args.requestId}`;
    for (const [key, job] of jobs) if (job.completedAt && now - job.completedAt > 120_000) jobs.delete(key);
    const digest = createHash('sha256').update(JSON.stringify({ ...args, operation })).digest('hex');
    const existing = jobs.get(jobKey);
    if (existing) return result(existing.digest === digest ? { ok: true, jobId: args.requestId, status: existing.status } : { ok: false, code: 'invalid-input', message: 'Request ID has already been used.' });
    for (const [key, usage] of actors) if (!usage.active && now - usage.start > 60_000) actors.delete(key);
    let usage = actors.get(actor);
    if (!usage) { if (actors.size >= 1000) return result({ ok: false, code: 'busy', message: 'Bridge is busy. Try later.' }); usage = { start: now, count: 0, active: false }; actors.set(actor, usage); }
    if (usage.active || usage.count >= 10 || active >= 4 || jobs.size >= 100) return result({ ok: false, code: 'busy', message: 'Bridge is busy or its request limit was reached. Try later.' });
    let input;
    try { input = analyze ? { ...args, images: images.take(actor,args.images || []) } : args; }
    catch { return result({ok:false,code:'images-expired'}); }
    usage.active = true; usage.count++; active++;
    const job = { status: 'pending', digest, completedAt: 0 }; jobs.set(jobKey, job);
    void (analyze ? callProvider : operation === 'models' ? listModels : testConnection)(input, { analyze, fetchImpl, timeoutMs: timeoutMs ?? 90_000, signal: shutdown.signal, onProgress: analyze ? d => { job.diagnostics = d; } : undefined })
      .then(value => { job.value = value; job.status = 'complete'; })
      .catch(error => { job.value = { ok: false, code: error instanceof ProviderError ? error.code : 'invalid-input', message: error instanceof ProviderError ? error.message : 'Request could not be processed.' }; job.status = 'complete'; })
      .finally(() => { job.completedAt = Date.now(); usage.active = false; active--; });
    return result({ ok: true, jobId: args.requestId, status: 'pending' });
  }
  server.registerTool('body_journal_info', {
    description: 'Describe this bridge without sending credentials or health information.', inputSchema: {},
  }, async () => result({ protocolVersion: PROTOCOL_VERSION, encryptionRequired: true, providers: ['openai', 'kimi', 'deepseek'], input: 'text-and-images', jobs: true, diagnostics: true, modelDiscovery: true, resultRetentionSeconds: 120, persistsRequests: false }));
  server.registerTool('body_journal_test', {
    description: 'Test text access and image reading with up to two small billed synthetic requests. No patient data.', inputSchema: { ...credentials, challenge: z.object({image:z.string().max(MAX_IMAGE_CHARS),answer:z.string().regex(/^[A-Z2-9]{6}$/)}).optional() },
  }, (args, extra) => run(args, extra, 'test'));
  server.registerTool('body_journal_analyze', {
    description: 'Send the reviewed self-reported case to the selected provider for a preliminary structured report.',
    inputSchema: { ...credentials, caseText: z.string().min(2).max(MAX_CASE_BYTES), images: z.array(z.object({id:z.string().max(80),sha256:z.string().regex(/^[a-f0-9]{64}$/),concernId:z.number().int().positive().nullable(),kind:z.enum(['body-map','patient-photo']).default('body-map'),attachmentId:z.string().regex(/^photo-[a-f0-9]{24}$/).optional(),caption:z.string().max(1200)})).max(MAX_IMAGES).optional() },
  }, (args, extra) => run(args, extra, 'analyze'));
  server.registerTool('body_journal_models', {
    description: 'List models available to this provider key. No case or inference request.',
    inputSchema: { requestId: credentials.requestId, provider: credentials.provider, apiKey: credentials.apiKey },
  }, (args, extra) => run(args, extra, 'models'));
  server.registerTool('body_journal_image', {
    description:'Stage a chunk of a reviewed image in temporary actor-scoped memory. No provider call.',
    inputSchema:{id:z.string().max(80),index:z.number().int(),total:z.number().int(),chunk:z.string().max(IMAGE_CHUNK_CHARS),sha256:z.string().regex(/^[a-f0-9]{64}$/)},
  },async(args,extra)=>{try{return result(images.put(extra._meta?.clientPubkey,args));}catch{return result({ok:false,code:'image-invalid'});}});
  server.registerTool('body_journal_images_clear', {
    description:'Delete this actor’s staged images.',inputSchema:{ids:z.array(z.string().max(80)).max(MAX_IMAGES)},
  },async({ids},extra)=>{images.clear(extra._meta?.clientPubkey,ids);return result({ok:true});});
  server.registerTool('body_journal_result', {
    description: 'Read the status or final result of this transport actor’s existing request. This never starts inference.',
    inputSchema: { requestId: z.string().regex(/^[a-zA-Z0-9-]{8,80}$/), outputOffset:z.number().int().min(0).max(48000).default(0), reasoningOffset:z.number().int().min(0).max(24000).default(0) },
  }, async ({ requestId, outputOffset, reasoningOffset }, extra) => {
    const job = jobs.get(`${extra._meta?.clientPubkey}:${requestId}`);
    if (!job || (job.completedAt && Date.now() - job.completedAt > 120_000)) return result({ ok: false, code: 'expired', message: 'No result is available for this request.' });
    const diagnostic = job.diagnostics ? {diagnostics:diagnosticPacket(job.diagnostics,outputOffset,reasoningOffset)} : {};
    return result(job.status === 'pending' ? { ok: true, status: 'pending', ...diagnostic } : { ok: true, status: 'complete', result: job.value, ...diagnostic });
  });
  const sweep = setInterval(() => { images.sweep(); const now = Date.now(); for (const [key, job] of jobs) if (job.completedAt && now - job.completedAt > 120_000) jobs.delete(key); }, 30_000);
  sweep.unref();
  return { server, abort: () => { shutdown.abort(); clearInterval(sweep); jobs.clear(); actors.clear(); images.items.clear(); } };
}
export async function startBridge({ secretKey, relays, fetchImpl, timeoutMs, allowedPublicKeys } = {}) {
  if (!/^[0-9a-f]{64}$/i.test(secretKey || '')) throw new Error('Use a separate 64-character hex key for BODY_JOURNAL_SERVER_KEY.');
  if (!Array.isArray(relays) || !relays.length || relays.length > 3) throw new Error('Configure one to three relay URLs.');
  for (const relay of relays) {
    const url = new URL(relay);
    if (url.username || url.password || url.hash || !(url.protocol === 'wss:' || (url.protocol === 'ws:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))) throw new Error('Use WSS relays, or a loopback WS relay for tests.');
  }
  const signer = new PrivateKeySigner(secretKey), pool = new ApplesauceRelayPool(relays);
  const transport = new NostrServerTransport({
    signer, relayHandler: pool, encryptionMode: EncryptionMode.REQUIRED,
    injectClientPubkey: true, isAnnouncedServer: false, publishRelayList: false, allowedPublicKeys, logLevel: 'silent', maxSessions: 100,
    oversizedTransfer: { enabled: false }, openStream: { enabled: false },
  });
  const { server, abort } = createBridge({ fetchImpl, timeoutMs });
  try { await server.connect(transport); } catch (error) { await transport.close(); throw error; }
  return { pubkey: await signer.getPublicKey(), close: async () => { abort(); await server.close(); await pool.disconnect(); } };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  try {
    const secretKey = process.env.BODY_JOURNAL_SERVER_KEY || randomBytes(32).toString('hex');
    const relays = (process.env.BODY_JOURNAL_RELAYS || 'wss://relay.contextvm.org').split(',').map(s => s.trim());
    const allowedPublicKeys = process.env.BODY_JOURNAL_ALLOWED_CLIENTS?.split(',').map(s => s.trim());
    if (allowedPublicKeys?.some(key => !/^[0-9a-f]{64}$/.test(key))) throw new Error('Invalid BODY_JOURNAL_ALLOWED_CLIENTS.');
    const bridge = await startBridge({ secretKey, relays, allowedPublicKeys });
    console.log(JSON.stringify({ name: 'Body Journal AI bridge', pubkey: bridge.pubkey, relays, ephemeralIdentity: !process.env.BODY_JOURNAL_SERVER_KEY, encryptionRequired: true }));
    const stop = async () => { await bridge.close(); process.exit(0); };
    process.once('SIGINT', stop); process.once('SIGTERM', stop);
  } catch { console.error('Bridge could not start. Check the Node version, server key and relay configuration.'); process.exitCode = 1; }
}