Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type Env = {
CHITTYSCHEMA_URL?: string;
CHITTYCERT_URL?: string;
CHITTYEVIDENCE_URL?: string;
CHITTYEVIDENCE_TOKEN?: string;
CHITTY_CONNECT_TOKEN?: string;
CHITTYLEDGER_TOKEN?: string;
PLAID_CLIENT_ID?: string;
Expand Down
31 changes: 17 additions & 14 deletions src/lib/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,36 +51,36 @@ export function ledgerClient(env: Env) {
Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)] as [string, string])
).toString();
const res = await fetch(`${baseUrl}/entries${qs ? `?${qs}` : ''}`, { headers });
if (!res.ok) return [];
if (!res.ok) { console.error(`[ledger] GET /entries failed: ${res.status}`); return []; }
return await res.json() as Record<string, unknown>[];
} catch { return []; }
} catch (err) { console.error('[ledger] GET /entries error:', err); return []; }
},

/** Get chain of custody for an entity */
getChainOfCustody: async (entityId: string): Promise<Record<string, unknown>[]> => {
try {
const res = await fetch(`${baseUrl}/custody/${encodeURIComponent(entityId)}`, { headers });
if (!res.ok) return [];
if (!res.ok) { console.error(`[ledger] GET /custody failed: ${res.status}`); return []; }
return await res.json() as Record<string, unknown>[];
} catch { return []; }
} catch (err) { console.error('[ledger] GET /custody error:', err); return []; }
},

/** Verify ledger chain integrity */
verifyChain: async (): Promise<{ valid: boolean; errors: string[] } | null> => {
try {
const res = await fetch(`${baseUrl}/verify`, { headers });
if (!res.ok) return null;
if (!res.ok) { console.error(`[ledger] GET /verify failed: ${res.status}`); return null; }
return await res.json() as { valid: boolean; errors: string[] };
} catch { return null; }
} catch (err) { console.error('[ledger] GET /verify error:', err); return null; }
},

/** Get ledger statistics */
getStatistics: async (): Promise<Record<string, unknown> | null> => {
try {
const res = await fetch(`${baseUrl}/statistics`, { headers });
if (!res.ok) return null;
if (!res.ok) { console.error(`[ledger] GET /statistics failed: ${res.status}`); return null; }
return await res.json() as Record<string, unknown>;
} catch { return null; }
} catch (err) { console.error('[ledger] GET /statistics error:', err); return null; }
},
};
}
Expand Down Expand Up @@ -119,11 +119,14 @@ export function evidenceClient(env: Env) {
if (!baseUrl) return null;

const headers: Record<string, string> = { 'X-Source-Service': 'chittycommand' };
if (env.CHITTYEVIDENCE_TOKEN) {
headers['Authorization'] = `Bearer ${env.CHITTYEVIDENCE_TOKEN}`;
}

async function get<T>(path: string): Promise<T | null> {
try {
const res = await fetch(`${baseUrl}${path}`, { headers });
if (!res.ok) return null;
if (!res.ok) { console.error(`[evidence] GET ${path} failed: ${res.status}`); return null; }
return await res.json() as T;
} catch (err) {
console.error(`[evidence] ${path} error:`, err);
Expand Down Expand Up @@ -458,20 +461,20 @@ export function connectClient(env: Env) {
try {
const obj = JSON.parse(cached) as { url?: string };
if (obj?.url) return obj.url;
} catch { /* ignore */ }
} catch (err) { console.warn('[connect/discover] KV cache parse failed:', err); }
}
} catch { /* ignore */ }
} catch (err) { console.warn('[connect/discover] KV read failed:', err); }

try {
const res = await fetch(`${baseUrl}/api/discover/${encodeURIComponent(serviceName)}`, {
headers: { 'X-Source-Service': 'chittycommand' },
});
if (!res.ok) return null;
if (!res.ok) { console.error(`[connect/discover] ${serviceName} failed: ${res.status}`); 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 { /* ignore */ }
try { await env.COMMAND_KV.put(key, JSON.stringify({ url: data.url }), { expirationTtl: 300 }); } catch (err) { console.warn('[connect/discover] KV write failed:', err); }
return data.url;
} catch { return null; }
} catch (err) { console.error('[connect/discover] fetch error:', err); return null; }
},

// ── Prompt Registry (ContextConsciousness) ─────────────────
Expand Down
48 changes: 26 additions & 22 deletions src/routes/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ async function executeTool(env: Env, sql: NeonQueryFunction<false, false>, toolN
const caseId = String(args.case_id || '').trim();
if (!caseId) throw new Error('Missing argument: case_id');
const ev = evidenceClient(env);
if (!ev) return { facts: [] };
if (!ev) return { error: 'ChittyEvidence not configured', facts: [] };
const facts = await ev.getStatementOfFacts(caseId);
return { case_id: caseId, facts: facts || [] };
}
Expand All @@ -630,7 +630,7 @@ async function executeTool(env: Env, sql: NeonQueryFunction<false, false>, toolN
const caseId = String(args.case_id || '').trim();
if (!caseId) throw new Error('Missing argument: case_id');
const ev = evidenceClient(env);
if (!ev) return { contradictions: [] };
if (!ev) return { error: 'ChittyEvidence not configured', contradictions: [] };
const contradictions = await ev.getContradictions(caseId);
return { case_id: caseId, contradictions: contradictions || [] };
}
Expand Down Expand Up @@ -798,17 +798,17 @@ async function executeTool(env: Env, sql: NeonQueryFunction<false, false>, toolN
try {
const cached = await env.COMMAND_KV.get(key);
if (cached) {
try { const obj = JSON.parse(cached) as { url?: string }; if (obj?.url) return { service, url: obj.url, cached: true }; } catch {}
try { const obj = JSON.parse(cached) as { url?: string }; if (obj?.url) return { service, url: obj.url, cached: true }; } catch (err) { console.warn('[mcp/connect_discover] KV cache parse failed:', err); }
}
} catch {}
} catch (err) { console.warn('[mcp/connect_discover] KV read failed:', err); }
try {
const res = await fetch(`${baseUrl}/api/discover/${encodeURIComponent(service)}`, {
headers: { 'X-Source-Service': 'chittycommand' },
signal: AbortSignal.timeout(3000),
});
if (!res.ok) return { error: `Service not found`, code: res.status };
const data = await res.json() as { url: string };
try { await env.COMMAND_KV.put(key, JSON.stringify({ url: data.url }), { expirationTtl: 300 }); } catch {}
try { await env.COMMAND_KV.put(key, JSON.stringify({ url: data.url }), { expirationTtl: 300 }); } catch (err) { console.warn('[mcp/connect_discover] KV write failed:', err); }
return { service, url: data.url };
} catch (err) {
return { error: String(err) };
Expand Down Expand Up @@ -1228,25 +1228,29 @@ async function executeTool(env: Env, sql: NeonQueryFunction<false, false>, toolN
}

// Deadlines from DB (with date filtering)
let deadlines;
if (startDate && endDate) {
deadlines = await sql`SELECT id, title, deadline_date, deadline_type, status 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, deadline_date, deadline_type, status 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, deadline_date, deadline_type, status FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date <= ${endDate} ORDER BY deadline_date ASC`;
} else {
deadlines = await sql`SELECT id, title, deadline_date, deadline_type, status FROM cc_legal_deadlines WHERE case_ref = ${caseId} ORDER BY deadline_date ASC`;
}
for (const d of deadlines) {
events.push({ id: `deadline:${d.id}`, date: d.deadline_date, type: 'deadline', title: d.title, deadlineType: d.deadline_type, status: d.status });
}
try {
let deadlines;
if (startDate && endDate) {
deadlines = await sql`SELECT id, title, deadline_date, deadline_type, status 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, deadline_date, deadline_type, status 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, deadline_date, deadline_type, status FROM cc_legal_deadlines WHERE case_ref = ${caseId} AND deadline_date <= ${endDate} ORDER BY deadline_date ASC`;
} else {
deadlines = await sql`SELECT id, title, deadline_date, deadline_type, status FROM cc_legal_deadlines WHERE case_ref = ${caseId} ORDER BY deadline_date ASC`;
}
for (const d of deadlines) {
events.push({ id: `deadline:${d.id}`, date: d.deadline_date, type: 'deadline', title: d.title, deadlineType: d.deadline_type, status: d.status });
}
} catch (err) { console.error('[mcp/get_case_timeline] deadlines query error:', err); }

// Disputes from DB
const disputes = await sql`SELECT id, title, status, dispute_type, created_at FROM cc_disputes WHERE metadata->>'case_ref' = ${caseId} OR metadata->>'ledger_case_id' = ${caseId}`;
for (const d of disputes) {
events.push({ id: `dispute:${d.id}`, date: d.created_at, type: 'dispute', title: d.title, status: d.status, disputeType: d.dispute_type });
}
try {
const disputes = await sql`SELECT id, title, status, dispute_type, created_at FROM cc_disputes WHERE metadata->>'case_ref' = ${caseId} OR metadata->>'ledger_case_id' = ${caseId}`;
for (const d of disputes) {
events.push({ id: `dispute:${d.id}`, date: d.created_at, type: 'dispute', title: d.title, status: d.status, disputeType: d.dispute_type });
}
} catch (err) { console.error('[mcp/get_case_timeline] disputes query error:', err); }

// Documents already covered by ChittyEvidence facts above

Expand Down
5 changes: 5 additions & 0 deletions src/routes/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => {
const endDate = c.req.query('end');

const events: TimelineEvent[] = [];
const warnings: string[] = [];

// 1. Fetch enriched facts from ChittyEvidence
const evidence = evidenceClient(c.env);
Expand Down Expand Up @@ -55,6 +56,7 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => {
}
} catch (err) {
console.error('[timeline] evidence facts error:', err);
warnings.push('Evidence facts unavailable: service error');
}
}

Expand Down Expand Up @@ -86,6 +88,7 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => {
}
} catch (err) {
console.error('[timeline] deadlines error:', err);
warnings.push('Legal deadlines unavailable: database error');
}

// 3. Fetch dispute milestones
Expand Down Expand Up @@ -114,6 +117,7 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => {
}
} catch (err) {
console.error('[timeline] disputes error:', err);
warnings.push('Dispute milestones unavailable: database error');
}

// 4. Evidence documents already fetched in section 1 via evidenceClient
Expand All @@ -135,6 +139,7 @@ timelineRoutes.get('/cases/:caseId/timeline', async (c) => {
documents: events.filter(e => e.type === 'document').length,
},
events,
...(warnings.length > 0 ? { warnings, partial: true } : {}),
});
});

Expand Down
2 changes: 1 addition & 1 deletion tests/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe('MCP — tools/list', () => {
expect(tools.length).toBeGreaterThanOrEqual(1);
});

it('exposes exactly 38 tools', async () => {
it('exposes exactly 48 tools', async () => {
const { post } = buildApp();
const res = await post({ jsonrpc: '2.0', id: 1, method: 'tools/list' });
const json = await res.json() as Record<string, unknown>;
Expand Down
Loading