diff --git a/package-lock.json b/package-lock.json index b79b1c5..af55040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "@marcusgroup/plugin-legal-ai", "version": "0.1.0", "dependencies": { - "@paperclipai/plugin-sdk": "^2026.525.0", + "@paperclipai/plugin-sdk": "^2026.722.0", "react": "^19.0.0" }, "devDependencies": { @@ -625,12 +625,12 @@ } }, "node_modules/@paperclipai/plugin-sdk": { - "version": "2026.525.0", - "resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.525.0.tgz", - "integrity": "sha512-7ivXbFSwH7cS+/2WNbPwQve5vVtLnqgxX5lmqTKn+i3fOY03d5KHaYF2bbI5zeTkAAwwEJ0Nz3rfBWNPguRRUw==", + "version": "2026.722.0", + "resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.722.0.tgz", + "integrity": "sha512-2rMJCBo8ZAouuMsapdaY4/l+oHmrXKHW/k/HfPOLCxFng7+g04c5JhoFq+/ptGfNcW3kXzs34y7brviDCml06Q==", "license": "MIT", "dependencies": { - "@paperclipai/shared": "2026.525.0", + "@paperclipai/shared": "2026.722.0", "zod": "^3.24.2" }, "bin": { @@ -646,9 +646,9 @@ } }, "node_modules/@paperclipai/shared": { - "version": "2026.525.0", - "resolved": "https://registry.npmjs.org/@paperclipai/shared/-/shared-2026.525.0.tgz", - "integrity": "sha512-fbVrEx96oxkxXbRBFDLAgW5Z0lRC7Ii+BeCNhKqKRCg9m2IwtP97CGoOeAROdRrvRpP8Neb3vgaaDbDjXT74lQ==", + "version": "2026.722.0", + "resolved": "https://registry.npmjs.org/@paperclipai/shared/-/shared-2026.722.0.tgz", + "integrity": "sha512-JciI6EbtNwHSeoyN5ZkshwqpyfIluyO46JHp8I7pPCvpJ7Uv7MSZEBs3lKXdXLadFEN6qzfwNmf39xjIFC8isA==", "license": "MIT", "dependencies": { "zod": "^3.24.2" diff --git a/package.json b/package.json index 9677ab5..9be39cb 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "biome:fix": "biome check --write src/" }, "dependencies": { - "@paperclipai/plugin-sdk": "^2026.525.0", + "@paperclipai/plugin-sdk": "^2026.722.0", "react": "^19.0.0" }, "devDependencies": { diff --git a/src/legal-api.ts b/src/legal-api.ts index fee4dd8..161df03 100644 --- a/src/legal-api.ts +++ b/src/legal-api.ts @@ -2,11 +2,23 @@ * HTTP client for Ezer Mishpati legal-ai REST API. */ +/** + * Where the client gets its base URL. A plain string still works; a resolver is + * used when the URL comes from company-scoped plugin config, which cannot be + * read at worker startup (see worker.ts — `ctx.config.get` needs a company). + */ +export type BaseUrlSource = string | (() => string | Promise); + export class LegalApi { - constructor(private baseUrl: string) {} + constructor(private baseUrl: BaseUrlSource) {} + + private async resolveBaseUrl(): Promise { + return typeof this.baseUrl === "string" ? this.baseUrl : this.baseUrl(); + } private async request(path: string, init?: RequestInit): Promise { - const res = await fetch(`${this.baseUrl}${path}`, { + const base = await this.resolveBaseUrl(); + const res = await fetch(`${base}${path}`, { ...init, headers: { "Content-Type": "application/json", diff --git a/src/worker.ts b/src/worker.ts index 9ff5690..2a82af6 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -16,16 +16,79 @@ const CEO_AGENT_IDS: Record = { "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562", // CMPA (היטלי השבחה) }; +const DEFAULT_LEGAL_API_BASE = "http://localhost:8085"; + +/** + * Resolve `legalApiBaseUrl` from company-scoped plugin config. + * + * ⚠️ Plugin config became company-scoped in @paperclipai/server 2026.722.0 + * (migration `0164_plugin_config_company_scope`). A bare `ctx.config.get()` now + * throws `company context is required` unless the host can derive the company + * from the current invocation — which it CANNOT during `setup()`, because + * worker initialisation belongs to no company. Reading config at startup is what + * made the whole plugin fail to activate on the 722.0 upgrade. + * + * So: never call this from setup(); call it from a handler, passing the + * companyId that handler was given. Tool handlers get `runCtx.companyId`; data + * handlers get `params.companyId`. Scheduled jobs have no company at all, so + * they fall back to probing the companies we know about. + */ +/** Pull the companyId the host passes to `ctx.data` handlers, if present. */ +function companyIdOf(params: unknown): string | undefined { + const id = (params as { companyId?: unknown } | null)?.companyId; + return typeof id === "string" && id.trim() ? id : undefined; +} + +async function resolveApiBase( + ctx: PluginContext, + companyId?: string, +): Promise { + const read = async (id?: string): Promise => { + try { + const cfg = (await ctx.config.get(id)) as { + legalApiBaseUrl?: unknown; + } | null; + const url = cfg?.legalApiBaseUrl; + return typeof url === "string" && url.trim() ? url.trim() : null; + } catch { + return null; + } + }; + + if (companyId) { + const scoped = await read(companyId); + if (scoped) return scoped; + } + + // No companyId passed in — but the host derives one itself when the call + // happens inside a host-scoped invocation (e.g. a tool handler). Ask before + // falling back to guesswork, so tools honour their own company's config. + const derived = await read(undefined); + if (derived) return derived; + + // Genuinely no company (scheduled jobs): try each company we know of. All of + // them point at the same legal-ai instance in practice, so the first hit wins. + for (const knownCompanyId of Object.keys(CEO_AGENT_IDS)) { + const url = await read(knownCompanyId); + if (url) return url; + } + + ctx.logger.warn("legalApiBaseUrl unresolved — using default", { + companyId: companyId ?? null, + fallback: DEFAULT_LEGAL_API_BASE, + }); + return DEFAULT_LEGAL_API_BASE; +} + const plugin = definePlugin({ async setup(ctx) { pluginCtx = ctx; // save for onWebhook - 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 }); + // Lazy: the URL is fetched on the first request, from inside a handler + // that has company context — not here. See resolveApiBase() above. + const api = new LegalApi(() => resolveApiBase(ctx)); + + ctx.logger.info("Legal AI plugin starting"); // ── Tools ────────────────────────────────────────────────────── @@ -859,9 +922,8 @@ const plugin = definePlugin({ ctx.jobs.register("stale-case-reminder", async (_job) => { ctx.logger.info("stale-case-reminder: starting"); - const config = await ctx.config.get(); - const apiBase = - (config.legalApiBaseUrl as string) ?? "http://localhost:8085"; + // Scheduled job — no company context; resolveApiBase probes known companies. + const apiBase = await resolveApiBase(ctx); let resp: Awaited>; try { @@ -933,9 +995,8 @@ const plugin = definePlugin({ ctx.jobs.register("weekly-feedback-analysis", async (_job) => { ctx.logger.info("weekly-feedback-analysis: starting"); - const config = await ctx.config.get(); - const apiBase = - (config.legalApiBaseUrl as string) ?? "http://localhost:8085"; + // Scheduled job — no company context; resolveApiBase probes known companies. + const apiBase = await resolveApiBase(ctx); const resp = await ctx.http.fetch( `${apiBase}/api/chair-feedback/weekly-summary`, @@ -1129,9 +1190,7 @@ const plugin = definePlugin({ if (!issueId) return null; const caseNumber = await resolveCaseNumber(issueId); if (!caseNumber) return null; - const config2 = await ctx.config.get(); - const apiBase = - (config2.legalApiBaseUrl as string) ?? "http://localhost:8085"; + const apiBase = await resolveApiBase(ctx, companyIdOf(params)); try { const resp = await ctx.http.fetch( `${apiBase}/api/cases/${encodeURIComponent(caseNumber)}/legal-arguments`, @@ -1168,9 +1227,7 @@ const plugin = definePlugin({ if (!issueId) return []; const caseNumber = await resolveCaseNumber(issueId); if (!caseNumber) return []; - const config2 = await ctx.config.get(); - const apiBase = - (config2.legalApiBaseUrl as string) ?? "http://localhost:8085"; + const apiBase = await resolveApiBase(ctx, companyIdOf(params)); try { const resp = await ctx.http.fetch( `${apiBase}/api/cases/${encodeURIComponent(caseNumber)}/precedents`, @@ -1206,9 +1263,7 @@ const plugin = definePlugin({ if (!issueId) return []; const caseNumber = await resolveCaseNumber(issueId); if (!caseNumber) return []; - const config2 = await ctx.config.get(); - const apiBase = - (config2.legalApiBaseUrl as string) ?? "http://localhost:8085"; + const apiBase = await resolveApiBase(ctx, companyIdOf(params)); try { const url = new URL(`${apiBase}/api/missing-precedents`); url.searchParams.set("case_number", caseNumber);