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

700
node_modules/@paperclipai/plugin-sdk/dist/testing.js generated vendored Normal file
View File

@@ -0,0 +1,700 @@
import { randomUUID } from "node:crypto";
function normalizeScope(input) {
return {
scopeKind: input.scopeKind,
scopeId: input.scopeId,
namespace: input.namespace ?? "default",
stateKey: input.stateKey,
};
}
function stateMapKey(input) {
const normalized = normalizeScope(input);
return `${normalized.scopeKind}|${normalized.scopeId ?? ""}|${normalized.namespace}|${normalized.stateKey}`;
}
function allowsEvent(filter, event) {
if (!filter)
return true;
if (filter.companyId && filter.companyId !== String(event.payload?.companyId ?? ""))
return false;
if (filter.projectId && filter.projectId !== String(event.payload?.projectId ?? ""))
return false;
if (filter.agentId && filter.agentId !== String(event.payload?.agentId ?? ""))
return false;
return true;
}
function requireCapability(manifest, allowed, capability) {
if (allowed.has(capability))
return;
throw new Error(`Plugin '${manifest.id}' is missing required capability '${capability}' in test harness`);
}
function requireCompanyId(companyId) {
if (!companyId)
throw new Error("companyId is required for this operation");
return companyId;
}
function isInCompany(record, companyId) {
return Boolean(record && record.companyId === companyId);
}
/**
* Create an in-memory host harness for plugin worker tests.
*
* The harness enforces declared capabilities and simulates host APIs, so tests
* can validate plugin behavior without spinning up the Paperclip server runtime.
*/
export function createTestHarness(options) {
const manifest = options.manifest;
const capabilitySet = new Set(options.capabilities ?? manifest.capabilities);
let currentConfig = { ...(options.config ?? {}) };
const logs = [];
const activity = [];
const metrics = [];
const state = new Map();
const entities = new Map();
const entityExternalIndex = new Map();
const companies = new Map();
const projects = new Map();
const issues = new Map();
const issueComments = new Map();
const agents = new Map();
const goals = new Map();
const projectWorkspaces = new Map();
const sessions = new Map();
const sessionEventCallbacks = new Map();
const events = [];
const jobs = new Map();
const launchers = new Map();
const dataHandlers = new Map();
const actionHandlers = new Map();
const toolHandlers = new Map();
const ctx = {
manifest,
config: {
async get() {
return { ...currentConfig };
},
},
events: {
on(name, filterOrFn, maybeFn) {
requireCapability(manifest, capabilitySet, "events.subscribe");
let registration;
if (typeof filterOrFn === "function") {
registration = { name, fn: filterOrFn };
}
else {
if (!maybeFn)
throw new Error("event handler is required");
registration = { name, filter: filterOrFn, fn: maybeFn };
}
events.push(registration);
return () => {
const idx = events.indexOf(registration);
if (idx !== -1)
events.splice(idx, 1);
};
},
async emit(name, companyId, payload) {
requireCapability(manifest, capabilitySet, "events.emit");
await harness.emit(`plugin.${manifest.id}.${name}`, payload, { companyId });
},
},
jobs: {
register(key, fn) {
requireCapability(manifest, capabilitySet, "jobs.schedule");
jobs.set(key, fn);
},
},
launchers: {
register(launcher) {
launchers.set(launcher.id, launcher);
},
},
http: {
async fetch(url, init) {
requireCapability(manifest, capabilitySet, "http.outbound");
return fetch(url, init);
},
},
secrets: {
async resolve(secretRef) {
requireCapability(manifest, capabilitySet, "secrets.read-ref");
return `resolved:${secretRef}`;
},
},
activity: {
async log(entry) {
requireCapability(manifest, capabilitySet, "activity.log.write");
activity.push(entry);
},
},
state: {
async get(input) {
requireCapability(manifest, capabilitySet, "plugin.state.read");
return state.has(stateMapKey(input)) ? state.get(stateMapKey(input)) : null;
},
async set(input, value) {
requireCapability(manifest, capabilitySet, "plugin.state.write");
state.set(stateMapKey(input), value);
},
async delete(input) {
requireCapability(manifest, capabilitySet, "plugin.state.write");
state.delete(stateMapKey(input));
},
},
entities: {
async upsert(input) {
const externalKey = input.externalId
? `${input.entityType}|${input.scopeKind}|${input.scopeId ?? ""}|${input.externalId}`
: null;
const existingId = externalKey ? entityExternalIndex.get(externalKey) : undefined;
const existing = existingId ? entities.get(existingId) : undefined;
const now = new Date().toISOString();
const previousExternalKey = existing?.externalId
? `${existing.entityType}|${existing.scopeKind}|${existing.scopeId ?? ""}|${existing.externalId}`
: null;
const record = existing
? {
...existing,
entityType: input.entityType,
scopeKind: input.scopeKind,
scopeId: input.scopeId ?? null,
externalId: input.externalId ?? null,
title: input.title ?? null,
status: input.status ?? null,
data: input.data,
updatedAt: now,
}
: {
id: randomUUID(),
entityType: input.entityType,
scopeKind: input.scopeKind,
scopeId: input.scopeId ?? null,
externalId: input.externalId ?? null,
title: input.title ?? null,
status: input.status ?? null,
data: input.data,
createdAt: now,
updatedAt: now,
};
entities.set(record.id, record);
if (previousExternalKey && previousExternalKey !== externalKey) {
entityExternalIndex.delete(previousExternalKey);
}
if (externalKey)
entityExternalIndex.set(externalKey, record.id);
return record;
},
async list(query) {
let out = [...entities.values()];
if (query.entityType)
out = out.filter((r) => r.entityType === query.entityType);
if (query.scopeKind)
out = out.filter((r) => r.scopeKind === query.scopeKind);
if (query.scopeId)
out = out.filter((r) => r.scopeId === query.scopeId);
if (query.externalId)
out = out.filter((r) => r.externalId === query.externalId);
if (query.offset)
out = out.slice(query.offset);
if (query.limit)
out = out.slice(0, query.limit);
return out;
},
},
projects: {
async list(input) {
requireCapability(manifest, capabilitySet, "projects.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...projects.values()];
out = out.filter((project) => project.companyId === companyId);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(projectId, companyId) {
requireCapability(manifest, capabilitySet, "projects.read");
const project = projects.get(projectId);
return isInCompany(project, companyId) ? project : null;
},
async listWorkspaces(projectId, companyId) {
requireCapability(manifest, capabilitySet, "project.workspaces.read");
if (!isInCompany(projects.get(projectId), companyId))
return [];
return projectWorkspaces.get(projectId) ?? [];
},
async getPrimaryWorkspace(projectId, companyId) {
requireCapability(manifest, capabilitySet, "project.workspaces.read");
if (!isInCompany(projects.get(projectId), companyId))
return null;
const workspaces = projectWorkspaces.get(projectId) ?? [];
return workspaces.find((workspace) => workspace.isPrimary) ?? null;
},
async getWorkspaceForIssue(issueId, companyId) {
requireCapability(manifest, capabilitySet, "project.workspaces.read");
const issue = issues.get(issueId);
if (!isInCompany(issue, companyId))
return null;
const projectId = issue?.projectId;
if (!projectId)
return null;
if (!isInCompany(projects.get(projectId), companyId))
return null;
const workspaces = projectWorkspaces.get(projectId) ?? [];
return workspaces.find((workspace) => workspace.isPrimary) ?? null;
},
},
companies: {
async list(input) {
requireCapability(manifest, capabilitySet, "companies.read");
let out = [...companies.values()];
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(companyId) {
requireCapability(manifest, capabilitySet, "companies.read");
return companies.get(companyId) ?? null;
},
},
issues: {
async list(input) {
requireCapability(manifest, capabilitySet, "issues.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...issues.values()];
out = out.filter((issue) => issue.companyId === companyId);
if (input?.projectId)
out = out.filter((issue) => issue.projectId === input.projectId);
if (input?.assigneeAgentId)
out = out.filter((issue) => issue.assigneeAgentId === input.assigneeAgentId);
if (input?.status)
out = out.filter((issue) => issue.status === input.status);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(issueId, companyId) {
requireCapability(manifest, capabilitySet, "issues.read");
const issue = issues.get(issueId);
return isInCompany(issue, companyId) ? issue : null;
},
async create(input) {
requireCapability(manifest, capabilitySet, "issues.create");
const now = new Date();
const record = {
id: randomUUID(),
companyId: input.companyId,
projectId: input.projectId ?? null,
projectWorkspaceId: null,
goalId: input.goalId ?? null,
parentId: input.parentId ?? null,
title: input.title,
description: input.description ?? null,
status: "todo",
priority: input.priority ?? "medium",
assigneeAgentId: input.assigneeAgentId ?? null,
assigneeUserId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: null,
issueNumber: null,
identifier: null,
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
executionWorkspaceSettings: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
createdAt: now,
updatedAt: now,
};
issues.set(record.id, record);
return record;
},
async update(issueId, patch, companyId) {
requireCapability(manifest, capabilitySet, "issues.update");
const record = issues.get(issueId);
if (!isInCompany(record, companyId))
throw new Error(`Issue not found: ${issueId}`);
const updated = {
...record,
...patch,
updatedAt: new Date(),
};
issues.set(issueId, updated);
return updated;
},
async listComments(issueId, companyId) {
requireCapability(manifest, capabilitySet, "issue.comments.read");
if (!isInCompany(issues.get(issueId), companyId))
return [];
return issueComments.get(issueId) ?? [];
},
async createComment(issueId, body, companyId) {
requireCapability(manifest, capabilitySet, "issue.comments.create");
const parentIssue = issues.get(issueId);
if (!isInCompany(parentIssue, companyId)) {
throw new Error(`Issue not found: ${issueId}`);
}
const now = new Date();
const comment = {
id: randomUUID(),
companyId: parentIssue.companyId,
issueId,
authorAgentId: null,
authorUserId: null,
body,
createdAt: now,
updatedAt: now,
};
const current = issueComments.get(issueId) ?? [];
current.push(comment);
issueComments.set(issueId, current);
return comment;
},
documents: {
async list(issueId, companyId) {
requireCapability(manifest, capabilitySet, "issue.documents.read");
if (!isInCompany(issues.get(issueId), companyId))
return [];
return [];
},
async get(issueId, _key, companyId) {
requireCapability(manifest, capabilitySet, "issue.documents.read");
if (!isInCompany(issues.get(issueId), companyId))
return null;
return null;
},
async upsert(input) {
requireCapability(manifest, capabilitySet, "issue.documents.write");
const parentIssue = issues.get(input.issueId);
if (!isInCompany(parentIssue, input.companyId)) {
throw new Error(`Issue not found: ${input.issueId}`);
}
throw new Error("documents.upsert is not implemented in test context");
},
async delete(issueId, _key, companyId) {
requireCapability(manifest, capabilitySet, "issue.documents.write");
const parentIssue = issues.get(issueId);
if (!isInCompany(parentIssue, companyId)) {
throw new Error(`Issue not found: ${issueId}`);
}
},
},
},
agents: {
async list(input) {
requireCapability(manifest, capabilitySet, "agents.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...agents.values()];
out = out.filter((agent) => agent.companyId === companyId);
if (input?.status)
out = out.filter((agent) => agent.status === input.status);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agents.read");
const agent = agents.get(agentId);
return isInCompany(agent, companyId) ? agent : null;
},
async pause(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agents.pause");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
if (agent.status === "terminated")
throw new Error("Cannot pause terminated agent");
const updated = { ...agent, status: "paused", updatedAt: new Date() };
agents.set(agentId, updated);
return updated;
},
async resume(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agents.resume");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
if (agent.status === "terminated")
throw new Error("Cannot resume terminated agent");
if (agent.status === "pending_approval")
throw new Error("Pending approval agents cannot be resumed");
const updated = { ...agent, status: "idle", updatedAt: new Date() };
agents.set(agentId, updated);
return updated;
},
async invoke(agentId, companyId, opts) {
requireCapability(manifest, capabilitySet, "agents.invoke");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
if (agent.status === "paused" ||
agent.status === "terminated" ||
agent.status === "pending_approval") {
throw new Error(`Agent is not invokable in its current state: ${agent.status}`);
}
return { runId: randomUUID() };
},
sessions: {
async create(agentId, companyId, opts) {
requireCapability(manifest, capabilitySet, "agent.sessions.create");
const cid = requireCompanyId(companyId);
const agent = agents.get(agentId);
if (!isInCompany(agent, cid))
throw new Error(`Agent not found: ${agentId}`);
const session = {
sessionId: randomUUID(),
agentId,
companyId: cid,
status: "active",
createdAt: new Date().toISOString(),
};
sessions.set(session.sessionId, session);
return session;
},
async list(agentId, companyId) {
requireCapability(manifest, capabilitySet, "agent.sessions.list");
const cid = requireCompanyId(companyId);
return [...sessions.values()].filter((s) => s.agentId === agentId && s.companyId === cid && s.status === "active");
},
async sendMessage(sessionId, companyId, opts) {
requireCapability(manifest, capabilitySet, "agent.sessions.send");
const session = sessions.get(sessionId);
if (!session || session.status !== "active")
throw new Error(`Session not found or closed: ${sessionId}`);
if (session.companyId !== companyId)
throw new Error(`Session not found: ${sessionId}`);
if (opts.onEvent) {
sessionEventCallbacks.set(sessionId, opts.onEvent);
}
return { runId: randomUUID() };
},
async close(sessionId, companyId) {
requireCapability(manifest, capabilitySet, "agent.sessions.close");
const session = sessions.get(sessionId);
if (!session)
throw new Error(`Session not found: ${sessionId}`);
if (session.companyId !== companyId)
throw new Error(`Session not found: ${sessionId}`);
session.status = "closed";
sessionEventCallbacks.delete(sessionId);
},
},
},
goals: {
async list(input) {
requireCapability(manifest, capabilitySet, "goals.read");
const companyId = requireCompanyId(input?.companyId);
let out = [...goals.values()];
out = out.filter((goal) => goal.companyId === companyId);
if (input?.level)
out = out.filter((goal) => goal.level === input.level);
if (input?.status)
out = out.filter((goal) => goal.status === input.status);
if (input?.offset)
out = out.slice(input.offset);
if (input?.limit)
out = out.slice(0, input.limit);
return out;
},
async get(goalId, companyId) {
requireCapability(manifest, capabilitySet, "goals.read");
const goal = goals.get(goalId);
return isInCompany(goal, companyId) ? goal : null;
},
async create(input) {
requireCapability(manifest, capabilitySet, "goals.create");
const now = new Date();
const record = {
id: randomUUID(),
companyId: input.companyId,
title: input.title,
description: input.description ?? null,
level: input.level ?? "task",
status: input.status ?? "planned",
parentId: input.parentId ?? null,
ownerAgentId: input.ownerAgentId ?? null,
createdAt: now,
updatedAt: now,
};
goals.set(record.id, record);
return record;
},
async update(goalId, patch, companyId) {
requireCapability(manifest, capabilitySet, "goals.update");
const record = goals.get(goalId);
if (!isInCompany(record, companyId))
throw new Error(`Goal not found: ${goalId}`);
const updated = {
...record,
...patch,
updatedAt: new Date(),
};
goals.set(goalId, updated);
return updated;
},
},
data: {
register(key, handler) {
dataHandlers.set(key, handler);
},
},
actions: {
register(key, handler) {
actionHandlers.set(key, handler);
},
},
streams: (() => {
const channelCompanyMap = new Map();
return {
open(channel, companyId) {
channelCompanyMap.set(channel, companyId);
},
emit(_channel, _event) {
// No-op in test harness — events are not forwarded
},
close(channel) {
channelCompanyMap.delete(channel);
},
};
})(),
tools: {
register(name, _decl, fn) {
requireCapability(manifest, capabilitySet, "agent.tools.register");
toolHandlers.set(name, fn);
},
},
metrics: {
async write(name, value, tags) {
requireCapability(manifest, capabilitySet, "metrics.write");
metrics.push({ name, value, tags });
},
},
logger: {
info(message, meta) {
logs.push({ level: "info", message, meta });
},
warn(message, meta) {
logs.push({ level: "warn", message, meta });
},
error(message, meta) {
logs.push({ level: "error", message, meta });
},
debug(message, meta) {
logs.push({ level: "debug", message, meta });
},
},
};
const harness = {
ctx,
seed(input) {
for (const row of input.companies ?? [])
companies.set(row.id, row);
for (const row of input.projects ?? [])
projects.set(row.id, row);
for (const row of input.issues ?? [])
issues.set(row.id, row);
for (const row of input.issueComments ?? []) {
const list = issueComments.get(row.issueId) ?? [];
list.push(row);
issueComments.set(row.issueId, list);
}
for (const row of input.agents ?? [])
agents.set(row.id, row);
for (const row of input.goals ?? [])
goals.set(row.id, row);
},
setConfig(config) {
currentConfig = { ...config };
},
async emit(eventType, payload, base) {
const event = {
eventId: base?.eventId ?? randomUUID(),
eventType,
companyId: base?.companyId ?? "test-company",
occurredAt: base?.occurredAt ?? new Date().toISOString(),
actorId: base?.actorId,
actorType: base?.actorType,
entityId: base?.entityId,
entityType: base?.entityType,
payload,
};
for (const handler of events) {
const exactMatch = handler.name === event.eventType;
const wildcardPluginAll = handler.name === "plugin.*" && String(event.eventType).startsWith("plugin.");
const wildcardPluginOne = String(handler.name).endsWith(".*")
&& String(event.eventType).startsWith(String(handler.name).slice(0, -1));
if (!exactMatch && !wildcardPluginAll && !wildcardPluginOne)
continue;
if (!allowsEvent(handler.filter, event))
continue;
await handler.fn(event);
}
},
async runJob(jobKey, partial = {}) {
const handler = jobs.get(jobKey);
if (!handler)
throw new Error(`No job handler registered for '${jobKey}'`);
await handler({
jobKey,
runId: partial.runId ?? randomUUID(),
trigger: partial.trigger ?? "manual",
scheduledAt: partial.scheduledAt ?? new Date().toISOString(),
});
},
async getData(key, params = {}) {
const handler = dataHandlers.get(key);
if (!handler)
throw new Error(`No data handler registered for '${key}'`);
return await handler(params);
},
async performAction(key, params = {}) {
const handler = actionHandlers.get(key);
if (!handler)
throw new Error(`No action handler registered for '${key}'`);
return await handler(params);
},
async executeTool(name, params, runCtx = {}) {
const handler = toolHandlers.get(name);
if (!handler)
throw new Error(`No tool handler registered for '${name}'`);
const ctxToPass = {
agentId: runCtx.agentId ?? "agent-test",
runId: runCtx.runId ?? randomUUID(),
companyId: runCtx.companyId ?? "company-test",
projectId: runCtx.projectId ?? "project-test",
};
return await handler(params, ctxToPass);
},
getState(input) {
return state.get(stateMapKey(input));
},
simulateSessionEvent(sessionId, event) {
const cb = sessionEventCallbacks.get(sessionId);
if (!cb)
throw new Error(`No active session event callback for session: ${sessionId}`);
cb({ ...event, sessionId });
},
logs,
activity,
metrics,
};
return harness;
}
//# sourceMappingURL=testing.js.map