From bec5d1bf3acdc5f485fdd5473e515b9e93f01126 Mon Sep 17 00:00:00 2001 From: Chaim Marcus Date: Mon, 13 Apr 2026 16:54:55 +0000 Subject: [PATCH] Add biome config and update plugin source + dependencies Co-Authored-By: Claude Opus 4.6 (1M context) --- biome.json | 34 ++ package-lock.json | 164 +++++++ package.json | 11 +- src/legal-api.ts | 373 ++++++++------- src/manifest.ts | 309 +++++++------ src/worker.ts | 1125 +++++++++++++++++++++++---------------------- 6 files changed, 1138 insertions(+), 878 deletions(-) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..d4c1106 --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.11/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**", "!!**/dist"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 1ce4b3a..6a9e144 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,174 @@ "@paperclipai/plugin-sdk": "^2026.325.0" }, "devDependencies": { + "@biomejs/biome": "2.4.11", "@types/node": "^25.5.0", "typescript": "^6.0.2" } }, + "node_modules/@biomejs/biome": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.11.tgz", + "integrity": "sha512-nWxHX8tf3Opb/qRgZpBbsTOqOodkbrkJ7S+JxJAruxOReaDPPmPuLBAGQ8vigyUgo0QBB+oQltNEAvalLcjggA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.11", + "@biomejs/cli-darwin-x64": "2.4.11", + "@biomejs/cli-linux-arm64": "2.4.11", + "@biomejs/cli-linux-arm64-musl": "2.4.11", + "@biomejs/cli-linux-x64": "2.4.11", + "@biomejs/cli-linux-x64-musl": "2.4.11", + "@biomejs/cli-win32-arm64": "2.4.11", + "@biomejs/cli-win32-x64": "2.4.11" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.11.tgz", + "integrity": "sha512-wOt+ed+L2dgZanWyL6i29qlXMc088N11optzpo10peayObBaAshbTcxKUchzEMp9QSY8rh5h6VfAFE3WTS1rqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.11.tgz", + "integrity": "sha512-gZ6zR8XmZlExfi/Pz/PffmdpWOQ8Qhy7oBztgkR8/ylSRyLwfRPSadmiVCV8WQ8PoJ2MWUy2fgID9zmtgUUJmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.11.tgz", + "integrity": "sha512-avdJaEElXrKceK0va9FkJ4P5ci3N01TGkc6ni3P8l3BElqbOz42Wg2IyX3gbh0ZLEd4HVKEIrmuVu/AMuSeFFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.11.tgz", + "integrity": "sha512-+Sbo1OAmlegtdwqFE8iOxFIWLh1B3OEgsuZfBpyyN/kWuqZ8dx9ZEes6zVnDMo+zRHF2wLynRVhoQmV7ohxl2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.11.tgz", + "integrity": "sha512-TagWV0iomp5LnEnxWFg4nQO+e52Fow349vaX0Q/PIcX6Zhk4GGBgp3qqZ8PVkpC+cuehRctMf3+6+FgQ8jCEFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.11.tgz", + "integrity": "sha512-bexd2IklK7ZgPhrz6jXzpIL6dEAH9MlJU1xGTrypx+FICxrXUp4CqtwfiuoDKse+UlgAlWtzML3jrMqeEAHEhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.11.tgz", + "integrity": "sha512-RJhaTnY8byzxDt4bDVb7AFPHkPcjOPK3xBip4ZRTrN3TEfyhjLRm3r3mqknqydgVTB74XG8l4jMLwEACEeihVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.11.tgz", + "integrity": "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@paperclipai/plugin-sdk": { "version": "2026.325.0", "resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.325.0.tgz", diff --git a/package.json b/package.json index f6ae91f..c167cbd 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,18 @@ }, "scripts": { "build": "tsc", - "dev": "paperclip-plugin-dev-server" + "dev": "paperclip-plugin-dev-server", + "format": "biome format --write src/", + "format:check": "biome format src/", + "biome": "biome check src/", + "biome:fix": "biome check --write src/" }, "dependencies": { "@paperclipai/plugin-sdk": "^2026.325.0" }, "devDependencies": { - "typescript": "^6.0.2", - "@types/node": "^25.5.0" + "@biomejs/biome": "2.4.11", + "@types/node": "^25.5.0", + "typescript": "^6.0.2" } } diff --git a/src/legal-api.ts b/src/legal-api.ts index e0a6432..11a7fa8 100644 --- a/src/legal-api.ts +++ b/src/legal-api.ts @@ -3,227 +3,260 @@ */ export class LegalApi { - constructor(private baseUrl: string) {} + constructor(private baseUrl: string) {} - private async request(path: string, init?: RequestInit): Promise { - 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() as Promise; - } + private async request(path: string, init?: RequestInit): Promise { + 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() as Promise; + } - async listCases(): Promise { - return this.request("/api/cases"); - } + async listCases(): Promise { + return this.request("/api/cases"); + } - async getCase(caseNumber: string): Promise { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/details`); - } + async getCase(caseNumber: string): Promise { + return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/details`); + } - async createCase(data: CaseCreateInput): Promise { - return this.request("/api/cases/create", { - method: "POST", - body: JSON.stringify(data), - }); - } + async createCase(data: CaseCreateInput): Promise { + return this.request("/api/cases/create", { + method: "POST", + body: JSON.stringify(data), + }); + } - async updateCase(caseNumber: string, data: CaseUpdateInput): Promise { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}`, { - method: "PUT", - body: JSON.stringify(data), - }); - } + async updateCase( + caseNumber: string, + data: CaseUpdateInput, + ): Promise { + return this.request(`/api/cases/${encodeURIComponent(caseNumber)}`, { + method: "PUT", + body: JSON.stringify(data), + }); + } - async getCaseStatus(caseNumber: string): Promise { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/status`); - } + async getCaseStatus(caseNumber: string): Promise { + return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/status`); + } - async search(query: string, limit = 10, sectionType = ""): Promise { - const params = new URLSearchParams({ query, limit: String(limit) }); - if (sectionType) params.set("section_type", sectionType); - return this.request(`/api/search?${params}`); - } + async search( + query: string, + limit = 10, + sectionType = "", + ): Promise { + const params = new URLSearchParams({ query, limit: String(limit) }); + if (sectionType) params.set("section_type", sectionType); + return this.request(`/api/search?${params}`); + } - async searchCase(caseNumber: string, query: string, limit = 10): Promise { - const params = new URLSearchParams({ query, limit: String(limit) }); - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/search?${params}`); - } + async searchCase( + caseNumber: string, + query: string, + limit = 10, + ): Promise { + const params = new URLSearchParams({ query, limit: String(limit) }); + return this.request( + `/api/cases/${encodeURIComponent(caseNumber)}/search?${params}`, + ); + } - async getTemplate(caseNumber: string): Promise<{ template: string }> { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/template`); - } + async getTemplate(caseNumber: string): Promise<{ template: string }> { + return this.request( + `/api/cases/${encodeURIComponent(caseNumber)}/template`, + ); + } - async getProcessingStatus(): Promise { - return this.request("/api/processing-status"); - } + async getProcessingStatus(): Promise { + return this.request("/api/processing-status"); + } - async health(): Promise<{ status: string }> { - return this.request("/health"); - } + async health(): Promise<{ status: string }> { + return this.request("/health"); + } - // ── New methods for expanded workflow ── + // ── New methods for expanded workflow ── - async listDocuments(caseNumber: string): Promise { - const details = await this.getCase(caseNumber); - return details.documents || []; - } + async listDocuments(caseNumber: string): Promise { + const details = await this.getCase(caseNumber); + return details.documents || []; + } - async getDocumentText(docId: string): Promise { - return this.request(`/api/documents/${docId}/text`); - } + async getDocumentText(docId: string): Promise { + return this.request(`/api/documents/${docId}/text`); + } - async findSimilarCases(caseNumber: string): Promise { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/search?query=similar&limit=5`); - } + async findSimilarCases(caseNumber: string): Promise { + return this.request( + `/api/cases/${encodeURIComponent(caseNumber)}/search?query=similar&limit=5`, + ); + } - async setOutcome(caseNumber: string, outcome: string, reasoning?: string): Promise<{ status: string }> { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/outcome`, { - method: "POST", - body: JSON.stringify({ outcome, reasoning: reasoning || "" }), - }); - } + async setOutcome( + caseNumber: string, + outcome: string, + reasoning?: string, + ): Promise<{ status: string }> { + return this.request( + `/api/cases/${encodeURIComponent(caseNumber)}/outcome`, + { + method: "POST", + body: JSON.stringify({ outcome, reasoning: reasoning || "" }), + }, + ); + } - async getClaims(caseNumber: string): Promise { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/claims`); - } + async getClaims(caseNumber: string): Promise { + return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/claims`); + } - async setDirection(caseNumber: string, directionDoc: Record): Promise<{ status: string }> { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/direction`, { - method: "POST", - body: JSON.stringify({ direction_doc: directionDoc }), - }); - } + async setDirection( + caseNumber: string, + directionDoc: Record, + ): Promise<{ status: string }> { + return this.request( + `/api/cases/${encodeURIComponent(caseNumber)}/direction`, + { + method: "POST", + body: JSON.stringify({ direction_doc: directionDoc }), + }, + ); + } - async runQA(caseNumber: string): Promise { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/qa`, { - method: "POST", - }); - } + async runQA(caseNumber: string): Promise { + return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/qa`, { + method: "POST", + }); + } - async triggerLearning(caseNumber: string): Promise<{ status: string }> { - return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/learn`, { - method: "POST", - }); - } + async triggerLearning(caseNumber: string): Promise<{ status: string }> { + return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/learn`, { + method: "POST", + }); + } - async getStyleGuide(): Promise { - // Return static style guide reference - return "ראה skill-legal-decision/SKILL.md — מדריך סגנון מלא של דפנה תמיר"; - } + async getStyleGuide(): Promise { + // Return static style guide reference + return "ראה skill-legal-decision/SKILL.md — מדריך סגנון מלא של דפנה תמיר"; + } } // Types export interface CaseSummary { - case_number: string; - title: string; - status: string; + 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; + 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; + 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; + 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; + 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[]; + 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; + 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; + cases: number; + documents: number; + pending_processing: number; + chunks: number; + style_corpus_entries: number; + style_patterns: number; } export interface ClaimsResponse { - case_number: string; - claims: Record>; - total: number; + case_number: string; + claims: Record< + string, + Array<{ party_role: string; claim_text: string; claim_index: number }> + >; + total: number; } export interface QAResponse { - passed: boolean; - checks: Array<{ - check_name: string; - passed: boolean; - severity: string; - details: string; - }>; - status: string; + passed: boolean; + checks: Array<{ + check_name: string; + passed: boolean; + severity: string; + details: string; + }>; + status: string; } diff --git a/src/manifest.ts b/src/manifest.ts index 0e2e8b8..e7ff18e 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -1,152 +1,161 @@ 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"] as const, - 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", - ] as const, - entrypoints: { - worker: "dist/worker.js", - }, - instanceConfigSchema: { - type: "object" as const, - properties: { - legalApiBaseUrl: { - type: "string" as const, - 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" as const, 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" as const, - properties: { - case_number: { type: "string" as const, 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" as const, - properties: { - case_number: { type: "string" as const }, - title: { type: "string" as const }, - appellants: { type: "array" as const, items: { type: "string" as const } }, - respondents: { type: "array" as const, items: { type: "string" as const } }, - subject: { type: "string" as const }, - property_address: { type: "string" as const }, - expected_outcome: { type: "string" as const }, - }, - 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" as const, - properties: { - case_number: { type: "string" as const }, - status: { type: "string" as const }, - title: { type: "string" as const }, - expected_outcome: { type: "string" as const }, - }, - 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" as const, - properties: { - case_number: { type: "string" as const }, - }, - required: ["case_number"], - }, - }, - { - toolKey: "legal_search", - name: "legal_search", - displayName: "Search Legal Precedents", - description: "Semantic RAG search across previous decisions", - parametersSchema: { - type: "object" as const, - properties: { - query: { type: "string" as const, description: "Search query in Hebrew" }, - limit: { type: "number" as const }, - section_type: { type: "string" as const }, - }, - 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" as const, - properties: { - case_number: { type: "string" as const }, - }, - required: ["case_number"], - }, - }, - { - toolKey: "legal_processing_status", - name: "legal_processing_status", - displayName: "Get Processing Status", - description: "Get overall system processing status", - parametersSchema: { type: "object" as const, 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 * * * *", - }, - ], + 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"] as const, + 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", + ] as const, + entrypoints: { + worker: "dist/worker.js", + }, + instanceConfigSchema: { + type: "object" as const, + properties: { + legalApiBaseUrl: { + type: "string" as const, + 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" as const, 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" as const, + properties: { + case_number: { type: "string" as const, 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" as const, + properties: { + case_number: { type: "string" as const }, + title: { type: "string" as const }, + appellants: { + type: "array" as const, + items: { type: "string" as const }, + }, + respondents: { + type: "array" as const, + items: { type: "string" as const }, + }, + subject: { type: "string" as const }, + property_address: { type: "string" as const }, + expected_outcome: { type: "string" as const }, + }, + 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" as const, + properties: { + case_number: { type: "string" as const }, + status: { type: "string" as const }, + title: { type: "string" as const }, + expected_outcome: { type: "string" as const }, + }, + 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" as const, + properties: { + case_number: { type: "string" as const }, + }, + required: ["case_number"], + }, + }, + { + toolKey: "legal_search", + name: "legal_search", + displayName: "Search Legal Precedents", + description: "Semantic RAG search across previous decisions", + parametersSchema: { + type: "object" as const, + properties: { + query: { + type: "string" as const, + description: "Search query in Hebrew", + }, + limit: { type: "number" as const }, + section_type: { type: "string" as const }, + }, + 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" as const, + properties: { + case_number: { type: "string" as const }, + }, + required: ["case_number"], + }, + }, + { + toolKey: "legal_processing_status", + name: "legal_processing_status", + displayName: "Get Processing Status", + description: "Get overall system processing status", + parametersSchema: { type: "object" as const, 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 * * * *", + }, + ], }; diff --git a/src/worker.ts b/src/worker.ts index a18d1a4..d2fa2eb 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -2,600 +2,615 @@ 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()) as { legalApiBaseUrl?: string } | null; - const baseUrl = config?.legalApiBaseUrl || "http://localhost:8085"; - const api = new LegalApi(baseUrl); + async setup(ctx) { + const config = (await ctx.config.get()) as { + legalApiBaseUrl?: string; + } | null; + const baseUrl = config?.legalApiBaseUrl || "http://localhost:8085"; + const api = new LegalApi(baseUrl); - ctx.logger.info("Legal AI plugin starting", { url: baseUrl }); + ctx.logger.info("Legal AI plugin starting", { url: baseUrl }); - // ── Tools ────────────────────────────────────────────────────── + // ── 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_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 as { case_number: string }; - const result = await api.getCase(case_number); - return { - content: JSON.stringify(result, null, 2), - data: result, - }; - }, - ); + 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 as { case_number: string }; + 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 as { - case_number: string; - title: string; - appellants?: string[]; - respondents?: string[]; - subject?: string; - property_address?: string; - expected_outcome?: string; - }; + 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 as { + case_number: string; + title: string; + appellants?: string[]; + respondents?: string[]; + subject?: string; + property_address?: string; + expected_outcome?: string; + }; - // Create case in legal-ai - const legalCase = await api.createCase(input); + // 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 || "לא הוגדרה"}`, - }); + // 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, - ); + // 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}`, - }); + 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 }, - }; - }, - ); + 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 as { - case_number: string; - status?: string; - title?: string; - subject?: string; - notes?: string; - expected_outcome?: string; - }; - const result = await api.updateCase(case_number, updates); - return { - content: JSON.stringify(result, null, 2), - data: result, - }; - }, - ); + 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 as { + case_number: string; + status?: string; + title?: string; + subject?: string; + notes?: string; + expected_outcome?: string; + }; + 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 as { case_number: string }; - const result = await api.getCaseStatus(case_number); - 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 as { case_number: string }; + 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 as { - query: string; - limit?: number; - section_type?: string; - }; - const results = await api.search(query, limit || 10, section_type || ""); - return { - content: JSON.stringify(results, null, 2), - data: results, - }; - }, - ); + 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 as { + query: string; + limit?: number; + section_type?: string; + }; + 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 as { case_number: string }; - const result = await api.getTemplate(case_number); - return { - content: result.template, - data: result, - }; - }, - ); + 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 as { case_number: string }; + 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, - }; - }, - ); + 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, + }; + }, + ); - // ── New Tools (Phase 3) ───────────────────────────────────────── + // ── New Tools (Phase 3) ───────────────────────────────────────── - ctx.tools.register( - "legal_document_list", - { - displayName: "רשימת מסמכים בתיק", - description: "List all documents in a case with their extraction status.", - parametersSchema: { - type: "object", - properties: { - case_number: { type: "string", description: "Case number" }, - }, - required: ["case_number"], - }, - }, - async (params) => { - const { case_number } = params as { case_number: string }; - const docs = await api.listDocuments(case_number); - return { content: JSON.stringify(docs, null, 2), data: docs }; - }, - ); + ctx.tools.register( + "legal_document_list", + { + displayName: "רשימת מסמכים בתיק", + description: + "List all documents in a case with their extraction status.", + parametersSchema: { + type: "object", + properties: { + case_number: { type: "string", description: "Case number" }, + }, + required: ["case_number"], + }, + }, + async (params) => { + const { case_number } = params as { case_number: string }; + const docs = await api.listDocuments(case_number); + return { content: JSON.stringify(docs, null, 2), data: docs }; + }, + ); - ctx.tools.register( - "legal_set_outcome", - { - displayName: "הזנת תוצאת ערר", - description: "Set the decision outcome (rejection/full_acceptance/partial_acceptance) and optional reasoning from Dafna.", - parametersSchema: { - type: "object", - properties: { - case_number: { type: "string", description: "Case number" }, - outcome: { - type: "string", - enum: ["rejection", "full_acceptance", "partial_acceptance"], - description: "Decision outcome", - }, - reasoning: { type: "string", description: "Optional reasoning from Dafna" }, - }, - required: ["case_number", "outcome"], - }, - }, - async (params) => { - const { case_number, outcome, reasoning } = params as { - case_number: string; - outcome: string; - reasoning?: string; - }; - const result = await api.setOutcome(case_number, outcome, reasoning); - return { content: JSON.stringify(result, null, 2), data: result }; - }, - ); + ctx.tools.register( + "legal_set_outcome", + { + displayName: "הזנת תוצאת ערר", + description: + "Set the decision outcome (rejection/full_acceptance/partial_acceptance) and optional reasoning from Dafna.", + parametersSchema: { + type: "object", + properties: { + case_number: { type: "string", description: "Case number" }, + outcome: { + type: "string", + enum: ["rejection", "full_acceptance", "partial_acceptance"], + description: "Decision outcome", + }, + reasoning: { + type: "string", + description: "Optional reasoning from Dafna", + }, + }, + required: ["case_number", "outcome"], + }, + }, + async (params) => { + const { case_number, outcome, reasoning } = params as { + case_number: string; + outcome: string; + reasoning?: string; + }; + const result = await api.setOutcome(case_number, outcome, reasoning); + return { content: JSON.stringify(result, null, 2), data: result }; + }, + ); - ctx.tools.register( - "legal_get_claims", - { - displayName: "טענות מחולצות", - description: "Get extracted claims for a case, grouped by party role.", - parametersSchema: { - type: "object", - properties: { - case_number: { type: "string", description: "Case number" }, - }, - required: ["case_number"], - }, - }, - async (params) => { - const { case_number } = params as { case_number: string }; - const result = await api.getClaims(case_number); - return { content: JSON.stringify(result, null, 2), data: result }; - }, - ); + ctx.tools.register( + "legal_get_claims", + { + displayName: "טענות מחולצות", + description: "Get extracted claims for a case, grouped by party role.", + parametersSchema: { + type: "object", + properties: { + case_number: { type: "string", description: "Case number" }, + }, + required: ["case_number"], + }, + }, + async (params) => { + const { case_number } = params as { case_number: string }; + const result = await api.getClaims(case_number); + return { content: JSON.stringify(result, null, 2), data: result }; + }, + ); - ctx.tools.register( - "legal_search_case", - { - displayName: "חיפוש בתוך תיק", - description: "Semantic search within a specific case's documents.", - parametersSchema: { - type: "object", - properties: { - case_number: { type: "string", description: "Case number" }, - query: { type: "string", description: "Search query in Hebrew" }, - }, - required: ["case_number", "query"], - }, - }, - async (params) => { - const { case_number, query } = params as { case_number: string; query: string }; - const results = await api.searchCase(case_number, query); - return { content: JSON.stringify(results, null, 2), data: results }; - }, - ); + ctx.tools.register( + "legal_search_case", + { + displayName: "חיפוש בתוך תיק", + description: "Semantic search within a specific case's documents.", + parametersSchema: { + type: "object", + properties: { + case_number: { type: "string", description: "Case number" }, + query: { type: "string", description: "Search query in Hebrew" }, + }, + required: ["case_number", "query"], + }, + }, + async (params) => { + const { case_number, query } = params as { + case_number: string; + query: string; + }; + const results = await api.searchCase(case_number, query); + return { content: JSON.stringify(results, null, 2), data: results }; + }, + ); - ctx.tools.register( - "legal_find_similar", - { - displayName: "תקדימים דומים", - description: "Find similar cases/precedents for a given case.", - parametersSchema: { - type: "object", - properties: { - case_number: { type: "string", description: "Case number" }, - }, - required: ["case_number"], - }, - }, - async (params) => { - const { case_number } = params as { case_number: string }; - const results = await api.findSimilarCases(case_number); - return { content: JSON.stringify(results, null, 2), data: results }; - }, - ); + ctx.tools.register( + "legal_find_similar", + { + displayName: "תקדימים דומים", + description: "Find similar cases/precedents for a given case.", + parametersSchema: { + type: "object", + properties: { + case_number: { type: "string", description: "Case number" }, + }, + required: ["case_number"], + }, + }, + async (params) => { + const { case_number } = params as { case_number: string }; + const results = await api.findSimilarCases(case_number); + return { content: JSON.stringify(results, null, 2), data: results }; + }, + ); - ctx.tools.register( - "legal_run_qa", - { - displayName: "בדיקת איכות", - description: "Run QA validation on a drafted decision. Checks: grounding, claims coverage, neutral background, weights.", - parametersSchema: { - type: "object", - properties: { - case_number: { type: "string", description: "Case number" }, - }, - required: ["case_number"], - }, - }, - async (params) => { - const { case_number } = params as { case_number: string }; - const result = await api.runQA(case_number); - return { content: JSON.stringify(result, null, 2), data: result }; - }, - ); + ctx.tools.register( + "legal_run_qa", + { + displayName: "בדיקת איכות", + description: + "Run QA validation on a drafted decision. Checks: grounding, claims coverage, neutral background, weights.", + parametersSchema: { + type: "object", + properties: { + case_number: { type: "string", description: "Case number" }, + }, + required: ["case_number"], + }, + }, + async (params) => { + const { case_number } = params as { case_number: string }; + const result = await api.runQA(case_number); + return { content: JSON.stringify(result, null, 2), data: result }; + }, + ); - ctx.tools.register( - "legal_trigger_learning", - { - displayName: "הפעלת לולאת למידה", - description: "Trigger the learning loop — compare draft to final signed version.", - parametersSchema: { - type: "object", - properties: { - case_number: { type: "string", description: "Case number" }, - }, - required: ["case_number"], - }, - }, - async (params) => { - const { case_number } = params as { case_number: string }; - const result = await api.triggerLearning(case_number); - return { content: JSON.stringify(result, null, 2), data: result }; - }, - ); + ctx.tools.register( + "legal_trigger_learning", + { + displayName: "הפעלת לולאת למידה", + description: + "Trigger the learning loop — compare draft to final signed version.", + parametersSchema: { + type: "object", + properties: { + case_number: { type: "string", description: "Case number" }, + }, + required: ["case_number"], + }, + }, + async (params) => { + const { case_number } = params as { case_number: string }; + const result = await api.triggerLearning(case_number); + return { content: JSON.stringify(result, null, 2), data: result }; + }, + ); - ctx.tools.register( - "legal_style_guide", - { - displayName: "מדריך סגנון", - description: "Get reference to Dafna's writing style guide.", - parametersSchema: { - type: "object", - properties: {}, - }, - }, - async () => { - const guide = await api.getStyleGuide(); - return { content: guide, data: { reference: guide } }; - }, - ); + ctx.tools.register( + "legal_style_guide", + { + displayName: "מדריך סגנון", + description: "Get reference to Dafna's writing style guide.", + parametersSchema: { + type: "object", + properties: {}, + }, + }, + async () => { + const guide = await api.getStyleGuide(); + return { content: guide, data: { reference: guide } }; + }, + ); - // ── Events ───────────────────────────────────────────────────── + // ── 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; + 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, - }); - } - }); + 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 ─────────────────────────────────────────────────────── + // ── Jobs ─────────────────────────────────────────────────────── - ctx.jobs.register("sync-case-status", async (job) => { - ctx.logger.info("Starting case status sync", { runId: job.runId }); + 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; + 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 }); + 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", - }); + 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 13 legal-ai statuses to Paperclip issue status - const statusMap: Record = { - new: "todo", - uploading: "todo", - processing: "in_progress", - documents_ready: "in_progress", - outcome_set: "in_progress", - brainstorming: "in_progress", - direction_approved: "in_progress", - drafting: "in_progress", - qa_review: "in_progress", - drafted: "in_progress", - exported: "in_progress", - reviewed: "in_progress", - final: "done", - }; + if (linkedCase === legalCase.case_number) { + // Map 13 legal-ai statuses to Paperclip issue status + const statusMap: Record = + { + new: "todo", + uploading: "todo", + processing: "in_progress", + documents_ready: "in_progress", + outcome_set: "in_progress", + brainstorming: "in_progress", + direction_approved: "in_progress", + drafting: "in_progress", + qa_review: "in_progress", + drafted: "in_progress", + exported: "in_progress", + reviewed: "in_progress", + final: "done", + }; - const statusLabels: Record = { - new: "תיק חדש", - uploading: "העלאת מסמכים", - processing: "עיבוד מסמכים", - documents_ready: "מסמכים מוכנים — הזן תוצאה", - outcome_set: "תוצאה הוזנה — נדרש סיעור מוחות", - brainstorming: "גיבוש כיוון בתהליך", - direction_approved: "כיוון אושר — מוכן לכתיבה", - drafting: "כתיבת החלטה בתהליך", - qa_review: "בדיקת איכות", - drafted: "טיוטה מוכנה — בדוק ושלח לדפנה", - exported: "DOCX נוצר — ממתין לדפנה", - reviewed: "דפנה הגיהה — העלה גרסה סופית", - final: "גרסה סופית — לולאת למידה", - }; + const statusLabels: Record = { + new: "תיק חדש", + uploading: "העלאת מסמכים", + processing: "עיבוד מסמכים", + documents_ready: "מסמכים מוכנים — הזן תוצאה", + outcome_set: "תוצאה הוזנה — נדרש סיעור מוחות", + brainstorming: "גיבוש כיוון בתהליך", + direction_approved: "כיוון אושר — מוכן לכתיבה", + drafting: "כתיבת החלטה בתהליך", + qa_review: "בדיקת איכות", + drafted: "טיוטה מוכנה — בדוק ושלח לדפנה", + exported: "DOCX נוצר — ממתין לדפנה", + reviewed: "דפנה הגיהה — העלה גרסה סופית", + final: "גרסה סופית — לולאת למידה", + }; - const targetStatus = statusMap[legalCase.status]; - const label = statusLabels[legalCase.status] || legalCase.status; + const targetStatus = statusMap[legalCase.status]; + const label = statusLabels[legalCase.status] || legalCase.status; - if (targetStatus && issue.status !== targetStatus) { - await ctx.issues.update(issue.id, { status: targetStatus }, companyId); - await ctx.issues.createComment( - issue.id, - `📋 ${label}`, - companyId, - ); - ctx.logger.info("Synced issue status", { - issueId: issue.id, - caseNumber: legalCase.case_number, - newStatus: targetStatus, - }); - } - } - } - } + if (targetStatus && issue.status !== targetStatus) { + await ctx.issues.update( + issue.id, + { status: targetStatus }, + companyId, + ); + await ctx.issues.createComment( + issue.id, + `📋 ${label}`, + 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("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"); - }, + ctx.logger.info("Legal AI plugin ready"); + }, - async onHealth() { - return { status: "ok" as const }; - }, + async onHealth() { + return { status: "ok" as const }; + }, }); export default plugin;