Initial commit: Paperclip plugin for Legal AI integration

16 agent tools, event handler for auto-linking, sync job every 15m.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:03:43 +00:00
commit 587a2a76ca
1183 changed files with 629235 additions and 0 deletions

103
dist/legal-api.d.ts vendored Normal file
View File

@@ -0,0 +1,103 @@
/**
* HTTP client for Ezer Mishpati legal-ai REST API.
*/
export declare class LegalApi {
private baseUrl;
constructor(baseUrl: string);
private request;
listCases(): Promise<CaseSummary[]>;
getCase(caseNumber: string): Promise<CaseDetails>;
createCase(data: CaseCreateInput): Promise<CaseDetails>;
updateCase(caseNumber: string, data: CaseUpdateInput): Promise<CaseDetails>;
getCaseStatus(caseNumber: string): Promise<WorkflowStatus>;
search(query: string, limit?: number, sectionType?: string): Promise<SearchResult[]>;
searchCase(caseNumber: string, query: string, limit?: number): Promise<SearchResult[]>;
getTemplate(caseNumber: string): Promise<{
template: string;
}>;
getProcessingStatus(): Promise<ProcessingStatus>;
health(): Promise<{
status: string;
}>;
}
export interface CaseSummary {
case_number: string;
title: string;
status: string;
}
export interface CaseDetails {
id: string;
case_number: string;
title: string;
status: string;
appellants: string[];
respondents: string[];
subject: string;
property_address: string;
expected_outcome: string;
documents?: DocumentInfo[];
[key: string]: unknown;
}
export interface CaseCreateInput {
case_number: string;
title: string;
appellants?: string[];
respondents?: string[];
subject?: string;
property_address?: string;
permit_number?: string;
committee_type?: string;
hearing_date?: string;
notes?: string;
expected_outcome?: string;
}
export interface CaseUpdateInput {
status?: string;
title?: string;
subject?: string;
notes?: string;
hearing_date?: string;
decision_date?: string;
tags?: string[];
expected_outcome?: string;
}
export interface DocumentInfo {
id: string;
title: string;
doc_type: string;
extraction_status: string;
page_count?: number;
}
export interface WorkflowStatus {
case_number: string;
title: string;
status: string;
documents: Array<{
title: string;
type: string;
extraction: string;
chunks: number;
pages?: number;
}>;
total_documents: number;
total_chunks: number;
has_draft: boolean;
draft_size_bytes: number;
next_steps: string[];
}
export interface SearchResult {
score: number;
case_number?: string;
document: string;
section: string;
page: number;
content: string;
}
export interface ProcessingStatus {
cases: number;
documents: number;
pending_processing: number;
chunks: number;
style_corpus_entries: number;
style_patterns: number;
}

63
dist/legal-api.js vendored Normal file
View File

@@ -0,0 +1,63 @@
/**
* HTTP client for Ezer Mishpati legal-ai REST API.
*/
export class LegalApi {
baseUrl;
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(path, init) {
const res = await fetch(`${this.baseUrl}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...init?.headers,
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Legal API ${res.status}: ${text}`);
}
return res.json();
}
async listCases() {
return this.request("/api/cases");
}
async getCase(caseNumber) {
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/details`);
}
async createCase(data) {
return this.request("/api/cases/create", {
method: "POST",
body: JSON.stringify(data),
});
}
async updateCase(caseNumber, data) {
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
async getCaseStatus(caseNumber) {
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/status`);
}
async search(query, limit = 10, sectionType = "") {
const params = new URLSearchParams({ query, limit: String(limit) });
if (sectionType)
params.set("section_type", sectionType);
return this.request(`/api/search?${params}`);
}
async searchCase(caseNumber, query, limit = 10) {
const params = new URLSearchParams({ query, limit: String(limit) });
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/search?${params}`);
}
async getTemplate(caseNumber) {
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/template`);
}
async getProcessingStatus() {
return this.request("/api/processing-status");
}
async health() {
return this.request("/health");
}
}

206
dist/manifest.d.ts vendored Normal file
View File

@@ -0,0 +1,206 @@
declare const _default: {
id: string;
apiVersion: number;
version: string;
displayName: string;
description: string;
author: string;
categories: readonly ["connector"];
capabilities: readonly ["events.subscribe", "issues.read", "issues.create", "issues.update", "issue.comments.create", "agent.tools.register", "http.outbound", "plugin.state.read", "plugin.state.write", "jobs.schedule", "activity.log.write", "companies.read", "projects.read"];
entrypoints: {
worker: string;
};
instanceConfigSchema: {
type: "object";
properties: {
legalApiBaseUrl: {
type: "string";
default: string;
description: string;
};
};
};
tools: ({
toolKey: string;
name: string;
displayName: string;
description: string;
parametersSchema: {
type: "object";
properties: {
case_number?: undefined;
title?: undefined;
appellants?: undefined;
respondents?: undefined;
subject?: undefined;
property_address?: undefined;
expected_outcome?: undefined;
status?: undefined;
query?: undefined;
limit?: undefined;
section_type?: undefined;
};
required?: undefined;
};
} | {
toolKey: string;
name: string;
displayName: string;
description: string;
parametersSchema: {
type: "object";
properties: {
case_number: {
type: "string";
description: string;
};
title?: undefined;
appellants?: undefined;
respondents?: undefined;
subject?: undefined;
property_address?: undefined;
expected_outcome?: undefined;
status?: undefined;
query?: undefined;
limit?: undefined;
section_type?: undefined;
};
required: string[];
};
} | {
toolKey: string;
name: string;
displayName: string;
description: string;
parametersSchema: {
type: "object";
properties: {
case_number: {
type: "string";
description?: undefined;
};
title: {
type: "string";
};
appellants: {
type: "array";
items: {
type: "string";
};
};
respondents: {
type: "array";
items: {
type: "string";
};
};
subject: {
type: "string";
};
property_address: {
type: "string";
};
expected_outcome: {
type: "string";
};
status?: undefined;
query?: undefined;
limit?: undefined;
section_type?: undefined;
};
required: string[];
};
} | {
toolKey: string;
name: string;
displayName: string;
description: string;
parametersSchema: {
type: "object";
properties: {
case_number: {
type: "string";
description?: undefined;
};
status: {
type: "string";
};
title: {
type: "string";
};
expected_outcome: {
type: "string";
};
appellants?: undefined;
respondents?: undefined;
subject?: undefined;
property_address?: undefined;
query?: undefined;
limit?: undefined;
section_type?: undefined;
};
required: string[];
};
} | {
toolKey: string;
name: string;
displayName: string;
description: string;
parametersSchema: {
type: "object";
properties: {
case_number: {
type: "string";
description?: undefined;
};
title?: undefined;
appellants?: undefined;
respondents?: undefined;
subject?: undefined;
property_address?: undefined;
expected_outcome?: undefined;
status?: undefined;
query?: undefined;
limit?: undefined;
section_type?: undefined;
};
required: string[];
};
} | {
toolKey: string;
name: string;
displayName: string;
description: string;
parametersSchema: {
type: "object";
properties: {
query: {
type: "string";
description: string;
};
limit: {
type: "number";
};
section_type: {
type: "string";
};
case_number?: undefined;
title?: undefined;
appellants?: undefined;
respondents?: undefined;
subject?: undefined;
property_address?: undefined;
expected_outcome?: undefined;
status?: undefined;
};
required: string[];
};
})[];
jobs: {
jobKey: string;
displayName: string;
description: string;
schedule: string;
}[];
};
export default _default;

150
dist/manifest.js vendored Normal file
View File

@@ -0,0 +1,150 @@
export default {
id: "marcusgroup.legal-ai",
apiVersion: 1,
version: "0.1.0",
displayName: "Ezer Mishpati - Legal AI",
description: "Integration with legal decision drafting system — case management, semantic search, and workflow tracking",
author: "Marcus Group",
categories: ["connector"],
capabilities: [
"events.subscribe",
"issues.read",
"issues.create",
"issues.update",
"issue.comments.create",
"agent.tools.register",
"http.outbound",
"plugin.state.read",
"plugin.state.write",
"jobs.schedule",
"activity.log.write",
"companies.read",
"projects.read",
],
entrypoints: {
worker: "dist/worker.js",
},
instanceConfigSchema: {
type: "object",
properties: {
legalApiBaseUrl: {
type: "string",
default: "http://localhost:8085",
description: "Base URL for the Ezer Mishpati API",
},
},
},
tools: [
{
toolKey: "legal_case_list",
name: "legal_case_list",
displayName: "List Legal Cases",
description: "List all appeal cases in the legal system",
parametersSchema: { type: "object", properties: {} },
},
{
toolKey: "legal_case_get",
name: "legal_case_get",
displayName: "Get Legal Case Details",
description: "Get full details of a legal case including documents",
parametersSchema: {
type: "object",
properties: {
case_number: { type: "string", description: "Case number" },
},
required: ["case_number"],
},
},
{
toolKey: "legal_case_create",
name: "legal_case_create",
displayName: "Create Legal Case",
description: "Create a new appeal case with linked Paperclip issue",
parametersSchema: {
type: "object",
properties: {
case_number: { type: "string" },
title: { type: "string" },
appellants: { type: "array", items: { type: "string" } },
respondents: { type: "array", items: { type: "string" } },
subject: { type: "string" },
property_address: { type: "string" },
expected_outcome: { type: "string" },
},
required: ["case_number", "title"],
},
},
{
toolKey: "legal_case_update",
name: "legal_case_update",
displayName: "Update Legal Case",
description: "Update a legal case status, title, or outcome",
parametersSchema: {
type: "object",
properties: {
case_number: { type: "string" },
status: { type: "string" },
title: { type: "string" },
expected_outcome: { type: "string" },
},
required: ["case_number"],
},
},
{
toolKey: "legal_case_status",
name: "legal_case_status",
displayName: "Get Case Workflow Status",
description: "Get full workflow status: documents, drafts, next steps",
parametersSchema: {
type: "object",
properties: {
case_number: { type: "string" },
},
required: ["case_number"],
},
},
{
toolKey: "legal_search",
name: "legal_search",
displayName: "Search Legal Precedents",
description: "Semantic RAG search across previous decisions",
parametersSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query in Hebrew" },
limit: { type: "number" },
section_type: { type: "string" },
},
required: ["query"],
},
},
{
toolKey: "legal_case_template",
name: "legal_case_template",
displayName: "Get Decision Template",
description: "Get outcome-aware decision template for 12-block structure",
parametersSchema: {
type: "object",
properties: {
case_number: { type: "string" },
},
required: ["case_number"],
},
},
{
toolKey: "legal_processing_status",
name: "legal_processing_status",
displayName: "Get Processing Status",
description: "Get overall system processing status",
parametersSchema: { type: "object", properties: {} },
},
],
jobs: [
{
jobKey: "sync-case-status",
displayName: "Sync Legal Case Status",
description: "Polls legal-ai for case status changes and updates Paperclip issues",
schedule: "*/15 * * * *",
},
],
};

2
dist/worker.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare const plugin: import("@paperclipai/plugin-sdk").PaperclipPlugin;
export default plugin;

311
dist/worker.js vendored Normal file
View File

@@ -0,0 +1,311 @@
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
import { LegalApi } from "./legal-api.js";
const plugin = definePlugin({
async setup(ctx) {
const config = (await ctx.config.get());
const baseUrl = config?.legalApiBaseUrl || "http://localhost:8085";
const api = new LegalApi(baseUrl);
ctx.logger.info("Legal AI plugin starting", { url: baseUrl });
// ── Tools ──────────────────────────────────────────────────────
ctx.tools.register("legal_case_list", {
displayName: "רשימת תיקי ערר",
description: "List all appeal cases in the legal system. Returns case number, title, and status (new/in_progress/drafted/reviewed/final).",
parametersSchema: {
type: "object",
properties: {},
},
}, async () => {
const cases = await api.listCases();
return {
content: JSON.stringify(cases, null, 2),
data: cases,
};
});
ctx.tools.register("legal_case_get", {
displayName: "פרטי תיק ערר",
description: "Get full details of a legal case including documents list. Provide the case number (e.g. 123/24).",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number } = params;
const result = await api.getCase(case_number);
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
ctx.tools.register("legal_case_create", {
displayName: "יצירת תיק ערר",
description: "Create a new appeal case. Case numbers: 1xxx=licensing, 8xxx=betterment levy, 9xxx=compensation. Also creates a linked Paperclip issue.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 1234/24)",
},
title: { type: "string", description: "Case title" },
appellants: {
type: "array",
items: { type: "string" },
description: "Appellant names",
},
respondents: {
type: "array",
items: { type: "string" },
description: "Respondent names",
},
subject: { type: "string", description: "Case subject" },
property_address: {
type: "string",
description: "Property address",
},
expected_outcome: {
type: "string",
enum: [
"rejection",
"partial_acceptance",
"full_acceptance",
"betterment_levy",
],
description: "Expected outcome type",
},
},
required: ["case_number", "title"],
},
}, async (params, runCtx) => {
const input = params;
// Create case in legal-ai
const legalCase = await api.createCase(input);
// Create linked Paperclip issue
const issue = await ctx.issues.create({
companyId: runCtx.companyId,
title: `[ערר ${input.case_number}] ${input.title}`,
description: `תיק ערר חדש\nנושא: ${input.subject || ""}\nתוצאה צפויה: ${input.expected_outcome || "לא הוגדרה"}`,
});
// Store mapping in plugin state
await ctx.state.set({
scopeKind: "issue",
scopeId: issue.id,
stateKey: "legal-case-number",
}, input.case_number);
await ctx.activity.log({
companyId: runCtx.companyId,
message: `נוצר תיק ערר ${input.case_number} וקושר ל-issue ${issue.id}`,
});
return {
content: `Case ${input.case_number} created and linked to Paperclip issue.\n\n${JSON.stringify(legalCase, null, 2)}`,
data: { legalCase, issueId: issue.id },
};
});
ctx.tools.register("legal_case_update", {
displayName: "עדכון תיק ערר",
description: "Update a legal case's status, title, subject, or expected outcome.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
status: {
type: "string",
enum: [
"new",
"in_progress",
"drafted",
"reviewed",
"final",
],
description: "New case status",
},
title: { type: "string" },
subject: { type: "string" },
notes: { type: "string" },
expected_outcome: {
type: "string",
enum: [
"rejection",
"partial_acceptance",
"full_acceptance",
"betterment_levy",
],
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number, ...updates } = params;
const result = await api.updateCase(case_number, updates);
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
ctx.tools.register("legal_case_status", {
displayName: "סטטוס תהליך עבודה",
description: "Get full workflow status for a case: documents processed, chunks created, draft progress, and suggested next steps.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number } = params;
const result = await api.getCaseStatus(case_number);
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
ctx.tools.register("legal_search", {
displayName: "חיפוש תקדימים משפטיים",
description: "Semantic search (RAG) across previous decisions and documents. Query in Hebrew for best results.",
parametersSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query in Hebrew",
},
limit: { type: "number", description: "Max results (default 10)" },
section_type: {
type: "string",
description: "Filter by section type: facts, legal_analysis, conclusion, ruling",
},
},
required: ["query"],
},
}, async (params) => {
const { query, limit, section_type } = params;
const results = await api.search(query, limit || 10, section_type || "");
return {
content: JSON.stringify(results, null, 2),
data: results,
};
});
ctx.tools.register("legal_case_template", {
displayName: "תבנית החלטה",
description: "Get an outcome-aware decision template for a case, with guidance for the 12-block structure.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number } = params;
const result = await api.getTemplate(case_number);
return {
content: result.template,
data: result,
};
});
ctx.tools.register("legal_processing_status", {
displayName: "סטטוס עיבוד כללי",
description: "Get overall processing status: total cases, documents, pending processing, chunks, and style corpus entries.",
parametersSchema: {
type: "object",
properties: {},
},
}, async () => {
const result = await api.getProcessingStatus();
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
// ── Events ─────────────────────────────────────────────────────
ctx.events.on("issue.created", async (event) => {
// Auto-link issues with case number in title
if (!event.companyId || !event.entityId)
return;
const issue = await ctx.issues.get(event.entityId, event.companyId);
if (!issue)
return;
const match = issue.title.match(/ערר\s+(\d+\/\d+)/);
if (match) {
const caseNumber = match[1];
await ctx.state.set({
scopeKind: "issue",
scopeId: issue.id,
stateKey: "legal-case-number",
}, caseNumber);
ctx.logger.info("Auto-linked issue to legal case", {
issueId: issue.id,
caseNumber,
});
}
});
// ── Jobs ───────────────────────────────────────────────────────
ctx.jobs.register("sync-case-status", async (job) => {
ctx.logger.info("Starting case status sync", { runId: job.runId });
try {
const cases = await api.listCases();
const companies = await ctx.companies.list();
if (!companies.length)
return;
const companyId = companies[0].id;
const issues = await ctx.issues.list({ companyId });
for (const legalCase of cases) {
for (const issue of issues) {
const linkedCase = await ctx.state.get({
scopeKind: "issue",
scopeId: issue.id,
stateKey: "legal-case-number",
});
if (linkedCase === legalCase.case_number) {
// Map legal-ai status to Paperclip issue status
const statusMap = {
new: "todo",
in_progress: "in_progress",
drafted: "in_progress",
reviewed: "in_progress",
final: "done",
};
const targetStatus = statusMap[legalCase.status];
if (targetStatus && issue.status !== targetStatus) {
await ctx.issues.update(issue.id, { status: targetStatus }, companyId);
await ctx.issues.createComment(issue.id, `סטטוס תיק עודכן ל: ${legalCase.status}`, companyId);
ctx.logger.info("Synced issue status", {
issueId: issue.id,
caseNumber: legalCase.case_number,
newStatus: targetStatus,
});
}
}
}
}
ctx.logger.info("Case status sync completed", {
casesChecked: cases.length,
});
}
catch (err) {
ctx.logger.error("Case status sync failed", { error: String(err) });
}
});
ctx.logger.info("Legal AI plugin ready");
},
async onHealth() {
return { status: "ok" };
},
});
export default plugin;
runWorker(plugin, import.meta.url);