diff --git a/src/lib/integrations.ts b/src/lib/integrations.ts index 06fe493..e4fa537 100644 --- a/src/lib/integrations.ts +++ b/src/lib/integrations.ts @@ -6,6 +6,13 @@ import type { Env } from '../index'; * All calls are fire-and-forget safe — failures are logged but don't break the caller. */ +/** Distinguishes "no data" from "service error" for array-returning methods */ +export interface ServiceArrayResult { + data: T[]; + error?: string; + status?: number; +} + // ── ChittyLedger ───────────────────────────────────────────── // Audit trail: entries, chain verification, custody queries // NOTE: Evidence/case operations live on ChittyEvidence, not ChittyLedger. @@ -34,7 +41,7 @@ export function ledgerClient(env: Env) { async function get(path: string): Promise { try { const res = await fetch(`${baseUrl}${path}`, { headers }); - if (!res.ok) { console.error(`[ledger] GET ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[ledger] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[ledger] GET ${path} error:`, err); return null; } } @@ -44,7 +51,7 @@ export function ledgerClient(env: Env) { const res = await fetch(`${baseUrl}${path}`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); - if (!res.ok) { console.error(`[ledger] POST ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const errBody = await res.text().catch(() => ''); console.error(`[ledger] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[ledger] POST ${path} error:`, err); return null; } } @@ -54,17 +61,22 @@ export function ledgerClient(env: Env) { addEntry: (entry: LedgerEntryPayload) => post<{ id: string; sequenceNumber: number; hash: string }>('/entries', entry), - /** Search ledger entries */ - searchEntries: async (params: { entityType?: string; entityId?: string; actor?: string; status?: string; limit?: number }): Promise[]> => { + /** Search ledger entries — returns { data, error? } to distinguish empty from failure */ + searchEntries: async (params: { entityType?: string; entityId?: string; actor?: string; status?: string; limit?: number }): Promise>> => { const qs = new URLSearchParams( Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)] as [string, string]) ).toString(); - return await get[]>(`/entries${qs ? `?${qs}` : ''}`) ?? []; + const result = await get[]>(`/entries${qs ? `?${qs}` : ''}`); + if (result === null) return { data: [], error: 'ChittyLedger unreachable or returned error' }; + return { data: result }; }, - /** Get chain of custody for an entity */ - getChainOfCustody: async (entityId: string) => - await get[]>(`/custody/${encodeURIComponent(entityId)}`) ?? [], + /** Get chain of custody for an entity — returns { data, error? } to distinguish empty from failure */ + getChainOfCustody: async (entityId: string): Promise>> => { + const result = await get[]>(`/custody/${encodeURIComponent(entityId)}`); + if (result === null) return { data: [], error: 'ChittyLedger unreachable or returned error' }; + return { data: result }; + }, /** Verify ledger chain integrity */ verifyChain: () => get<{ valid: boolean; errors: string[] }>('/verify'), @@ -115,7 +127,7 @@ export function evidenceClient(env: Env) { async function get(path: string): Promise { try { const res = await fetch(`${baseUrl}${path}`, { headers }); - if (!res.ok) { console.error(`[evidence] GET ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[evidence] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[evidence] GET ${path} error:`, err); @@ -130,7 +142,7 @@ export function evidenceClient(env: Env) { headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); - if (!res.ok) { console.error(`[evidence] POST ${path} failed: ${res.status}`); return null; } + if (!res.ok) { const errBody = await res.text().catch(() => ''); console.error(`[evidence] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[evidence] POST ${path} error:`, err); @@ -224,7 +236,7 @@ export function financeClient(env: Env) { const res = await fetch(`${baseUrl}${path}`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) return null; + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[finance] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[finance] ${path} error:`, err); @@ -276,7 +288,8 @@ export function chargeClient(env: Env) { body: JSON.stringify(body), }); if (!res.ok) { - console.error(`[charge] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[charge] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -423,7 +436,8 @@ export function connectClient(env: Env) { signal: AbortSignal.timeout(30000), }); if (!res.ok) { - console.error(`[connect] POST ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[connect] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -452,7 +466,7 @@ export function connectClient(env: Env) { const res = await fetch(`${baseUrl}/api/discover/${encodeURIComponent(serviceName)}`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) { console.error(`[connect/discover] ${serviceName} failed: ${res.status}`); return null; } + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[connect/discover] ${serviceName} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } const data = await res.json() as { url: string }; // Store in KV with TTL try { await env.COMMAND_KV.put(key, JSON.stringify({ url: data.url }), { expirationTtl: 300 }); } catch (err) { console.warn('[connect/discover] KV write failed:', err); } @@ -543,7 +557,8 @@ export function mercuryClient(token: string) { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, }); if (!res.ok) { - console.error(`[mercury] ${path} failed: ${res.status}`); + const body = await res.text().catch(() => ''); + console.error(`[mercury] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; @@ -578,7 +593,7 @@ export function booksClient(env: Env) { const res = await fetch(`${baseUrl}/api/summary`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) return null; + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[books] GET /api/summary failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as Record; } catch (err) { console.error('[books] summary error:', err); @@ -594,7 +609,8 @@ export function booksClient(env: Env) { body: JSON.stringify(payload), }); if (!res.ok) { - console.error(`[books] record-transaction failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[books] POST /api/transaction failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as Record; @@ -618,7 +634,7 @@ export function assetsClient(env: Env) { const res = await fetch(`${baseUrl}${path}`, { headers: { 'X-Source-Service': 'chittycommand' }, }); - if (!res.ok) return null; + if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`[assets] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; } catch (err) { console.error(`[assets] ${path} error:`, err); @@ -634,7 +650,8 @@ export function assetsClient(env: Env) { body: JSON.stringify(body), }); if (!res.ok) { - console.error(`[assets] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[assets] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -672,7 +689,8 @@ export function scrapeClient(env: Env) { signal: AbortSignal.timeout(30000), }); if (!res.ok) { - console.error(`[scrape] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[scrape] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -734,7 +752,8 @@ export function routerClient(env: Env) { signal: AbortSignal.timeout(60000), }); if (!res.ok) { - console.error(`[router] ${path} failed: ${res.status}`); + const errBody = await res.text().catch(() => ''); + console.error(`[router] POST ${path} failed: ${res.status} — ${errBody.slice(0, 500)}`); return null; } return await res.json() as T; @@ -752,7 +771,8 @@ export function routerClient(env: Env) { signal: AbortSignal.timeout(10000), }); if (!res.ok) { - console.error(`[router] GET ${path} failed: ${res.status}`); + const body = await res.text().catch(() => ''); + console.error(`[router] GET ${path} failed: ${res.status} — ${body.slice(0, 500)}`); return null; } return await res.json() as T; diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 0258e30..31b3bee 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -1225,16 +1225,21 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN // Facts from ChittyEvidence const evidence = evidenceClient(env); if (evidence) { - const facts = startDate && endDate - ? await evidence.getFactsByDateRange(caseId, startDate, endDate) - : await evidence.getEnrichedFacts(caseId); - if (facts) { - for (const f of facts) { - if (!f.fact_date) continue; - events.push({ id: `fact:${f.id}`, date: f.fact_date, type: 'fact', title: f.fact_text.slice(0, 120), factType: f.fact_type, confidence: f.confidence, status: f.verification_status }); + try { + const facts = startDate && endDate + ? await evidence.getFactsByDateRange(caseId, startDate, endDate) + : await evidence.getEnrichedFacts(caseId); + if (facts) { + for (const f of facts) { + if (!f.fact_date) continue; + events.push({ id: `fact:${f.id}`, date: f.fact_date, type: 'fact', title: f.fact_text.slice(0, 120), factType: f.fact_type, confidence: f.confidence, status: f.verification_status }); + } + } else { + warnings.push('Failed to fetch facts from ChittyEvidence'); } - } else { - warnings.push('Failed to fetch facts from ChittyEvidence'); + } catch (err) { + console.error('[mcp/get_case_timeline] evidence facts error:', err); + warnings.push('Evidence facts unavailable: service error'); } } else { warnings.push('ChittyEvidence not configured'); diff --git a/src/routes/timeline.ts b/src/routes/timeline.ts index b4d59c1..c418df2 100644 --- a/src/routes/timeline.ts +++ b/src/routes/timeline.ts @@ -65,14 +65,16 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => { // 2. Fetch legal deadlines from ChittyCommand DB const sql = getDb(c.env); try { - const deadlines = await sql` - SELECT id, title, description, deadline_date, deadline_type, status, urgency_score - FROM cc_legal_deadlines - WHERE case_ref = ${caseId} - ${startDate ? sql`AND deadline_date >= ${startDate}` : sql``} - ${endDate ? sql`AND deadline_date <= ${endDate}` : sql``} - ORDER BY deadline_date ASC - `; + let deadlines; + if (startDate && endDate) { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date >= ${startDate} AND deadline_date <= ${endDate} ORDER BY deadline_date ASC`; + } else if (startDate) { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date >= ${startDate} ORDER BY deadline_date ASC`; + } else if (endDate) { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date <= ${endDate} ORDER BY deadline_date ASC`; + } else { + deadlines = await sql`SELECT id, title, description, deadline_date, deadline_type, status, urgency_score FROM cc_legal_deadlines WHERE case_ref = ${caseId} ORDER BY deadline_date ASC`; + } for (const d of deadlines) { events.push({ id: `deadline:${d.id}`,