fix(config): קריאת config מותאמת ל-scope של חברה (Paperclip 2026.722.0) #7
16
package-lock.json
generated
16
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<string>);
|
||||
|
||||
export class LegalApi {
|
||||
constructor(private baseUrl: string) {}
|
||||
constructor(private baseUrl: BaseUrlSource) {}
|
||||
|
||||
private async resolveBaseUrl(): Promise<string> {
|
||||
return typeof this.baseUrl === "string" ? this.baseUrl : this.baseUrl();
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||
const base = await this.resolveBaseUrl();
|
||||
const res = await fetch(`${base}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -16,16 +16,79 @@ const CEO_AGENT_IDS: Record<string, string> = {
|
||||
"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<string> {
|
||||
const read = async (id?: string): Promise<string | null> => {
|
||||
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<ReturnType<typeof ctx.http.fetch>>;
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user