Files
paperclip/packages/plugins/plugin-llm-wiki/src/worker.ts
T
Dotta 508355b8fc [codex] Add LLM Wiki plugin package to master (#5716)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The plugin system is the extension surface for optional product
capabilities without baking every workflow into core.
> - The LLM Wiki plugin package was reviewed in stacked PR #5592, which
targeted `pap-9173-llm-wiki-rest`.
> - The stack base PR #5597 merged to `master` before #5592 was merged
into that branch, so the plugin package never reached `master`.
> - A direct PR from `pap-9173-llm-wiki-rest` back to `master` would be
noisy because that branch has diverged from current `master`.
> - This pull request reapplies the reviewed
`packages/plugins/plugin-llm-wiki/` package onto current `master` and
updates Docker deps-stage manifest coverage.
> - The branch intentionally no longer changes `pnpm-workspace.yaml`
after maintainer feedback; because the new package is now a root
workspace importer, the remaining integration question is how
maintainers want the root lockfile handled under the current PR policy.

## What Changed

- Added the LLM Wiki plugin package under
`packages/plugins/plugin-llm-wiki/` from the merged PR #5592 head.
- Preserved the post-review cleanup from #5592: generated
design/screenshot artifacts are not committed, and `src/ui/index.tsx` /
`src/wiki.ts` are small public entrypoints.
- Added the new plugin package manifest to the Docker deps stage so
policy can validate package manifest coverage.
- Removed the earlier `pnpm-workspace.yaml` exclusion per maintainer
request, so the plugin is included by the existing `packages/plugins/*`
workspace glob.

## Verification

Current head:
- PGlite migration harness: ran migrations 001-003, verified old
non-space distillation unique constraints were removed, inserted
duplicate cursor and work-item keys in a second space, then reran
migration 003 successfully
- `node ./scripts/check-docker-deps-stage.mjs`
- `git diff --check`

Known current-head install result after removing the workspace
exclusion:
- `pnpm install --frozen-lockfile` fails because `pnpm-lock.yaml` has no
importer for `packages/plugins/plugin-llm-wiki/package.json`.

Previously verified on the same plugin source before the
workspace-exclusion removal:
- `pnpm --filter @paperclipai/plugin-sdk build`
- `cd packages/plugins/plugin-llm-wiki && pnpm install --lockfile=false
&& pnpm test`

## Risks

- The branch now includes `packages/plugins/plugin-llm-wiki` in the root
workspace but does not update `pnpm-lock.yaml`. Root frozen install will
fail until maintainers choose a lockfile path that fits repo policy.
- Committing `pnpm-lock.yaml` directly on this PR conflicts with the
current PR policy check, while excluding the package from
`pnpm-workspace.yaml` was rejected in maintainer feedback.
- The package includes UI code already reviewed in #5592; generated
screenshot/design artifacts were intentionally removed per maintainer
request, so visual review should regenerate screenshots locally if
needed.
- The package depends on plugin host support from #5597, which is
already merged to `master`.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI GPT-5 Codex via Codex CLI, tool use and local code execution
enabled; context window not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run the targeted checks listed above
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Stack context: #5592 was merged into `pap-9173-llm-wiki-rest` after
#5597 had already merged that branch to `master`, so this follow-up PR
is needed to carry the plugin package itself into `master`.

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-11 20:45:41 -05:00

1082 lines
44 KiB
TypeScript

import {
definePlugin,
runWorker,
type PluginApiRequestInput,
type PluginContext,
type PluginManagedRoutineDeclaration,
type PluginManagedRoutineResolution,
} from "@paperclipai/plugin-sdk";
import {
PAPERCLIP_DISTILL_SKILL_KEY,
WIKI_MAINTENANCE_ROUTINE_KEYS,
WIKI_ROOT_FOLDER_KEY,
} from "./manifest.js";
import {
bootstrapWikiRoot,
bootstrapSpace,
assemblePaperclipSourceBundle,
archiveSpace,
captureWikiSource,
createSpace,
createPaperclipDistillationRun,
createPaperclipDistillationWorkItem,
createOperationIssue,
distillPaperclipProjectPage,
enableActiveProjectDistillation,
fileQueryAnswerAsPage,
getDistillationOverview,
getDistillationPageProvenance,
getDistillationAutoApplyRestriction,
getEventIngestionSettings,
listPaperclipIngestionCandidates,
getPaperclipIngestionProfile,
getOverview,
listSpaces,
handlePaperclipEventIngestion,
listWikiAgentOptions,
listWikiProjectOptions,
listOperations,
listPages,
listSources,
readCompanyIdFromParams,
readTemplate,
readWikiPage,
recordPaperclipDistillationOutcome,
reconcileWikiAgentResource,
reconcileWikiProjectResource,
reconcileWikiRoutineResources,
reconcileWikiSkillResources,
registerWikiTools,
resetWikiSkillResources,
resetWikiAgentResource,
resetWikiProjectResource,
selectWikiAgentResource,
selectWikiProjectResource,
startWikiQuerySession,
spaceFolderStatus,
updateEventIngestionSettings,
updatePaperclipIngestionProfile,
updateSpace,
writeTemplate,
writeWikiPage,
} from "./wiki.js";
function stringField(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function routineKeyField(value: unknown): (typeof WIKI_MAINTENANCE_ROUTINE_KEYS)[number] {
const routineKey = stringField(value);
if (!routineKey) {
throw new Error(`routineKey is required; valid values: ${WIKI_MAINTENANCE_ROUTINE_KEYS.join(", ")}`);
}
if (!WIKI_MAINTENANCE_ROUTINE_KEYS.includes(routineKey as (typeof WIKI_MAINTENANCE_ROUTINE_KEYS)[number])) {
throw new Error(`Unknown managed routine: ${routineKey}`);
}
return routineKey as (typeof WIKI_MAINTENANCE_ROUTINE_KEYS)[number];
}
function routineOverridesFromParams(params: Record<string, unknown>) {
const overrides: { assigneeAgentId?: string; projectId?: string } = {};
const assigneeAgentId = stringField(params.assigneeAgentId);
const projectId = stringField(params.projectId);
if (assigneeAgentId) overrides.assigneeAgentId = assigneeAgentId;
if (projectId) overrides.projectId = projectId;
return overrides;
}
let activeContext: PluginContext | null = null;
const PAPERCLIP_EVENT_INGESTION_EVENTS = [
"issue.created",
"issue.updated",
"issue.comment.created",
"issue.document.created",
"issue.document.updated",
] as const;
type ManagedRoutineDefaultDrift = {
changedFields: string[];
defaultTitle: string;
defaultDescription: string | null;
};
type ManagedRoutineSettingsResolution = PluginManagedRoutineResolution & {
defaultDrift: ManagedRoutineDefaultDrift | null;
};
function requireContext(): PluginContext {
if (!activeContext) throw new Error("LLM Wiki plugin has not been set up");
return activeContext;
}
function normalizeRoutineTemplateText(value: unknown): string | null {
if (typeof value !== "string") return null;
const normalized = value.replace(/\r\n/g, "\n").trim();
return normalized.length > 0 ? normalized : null;
}
function manualDistillScopeLabel(input: { projectId?: string | null; rootIssueId?: string | null }) {
if (input.rootIssueId) return "selected root issue";
if (input.projectId) return "selected project";
return "company-wide stale cursor scan";
}
function buildManualDistillPrompt(input: { companyId: string; projectId?: string | null; rootIssueId?: string | null }) {
const scopeLabel = manualDistillScopeLabel(input);
return [
"Manual LLM Wiki distillation requested outside recurring cadence.",
"",
"Prompt source: LLM Wiki plugin action `distill-paperclip-now` (`packages/plugins/plugin-llm-wiki/src/worker.ts`).",
`Required skill: use the installed \`${PAPERCLIP_DISTILL_SKILL_KEY}\` skill before changing wiki files.`,
"",
"Scope:",
`- Company ID: ${input.companyId}`,
`- Requested scope: ${scopeLabel}`,
input.projectId ? `- Source project ID: ${input.projectId}` : null,
input.rootIssueId ? `- Source root issue ID: ${input.rootIssueId}` : null,
!input.projectId && !input.rootIssueId
? "- Do not hardcode a single project. Find non-plugin Paperclip issues/comments/documents that changed in any project after the last processed cursor and are old enough for the stale/debounce threshold."
: null,
"",
"Process:",
"1. Read the wiki root AGENTS.md, wiki/index.md, and recent wiki/log.md entries.",
"2. Assemble bounded Paperclip source bundles for every eligible project or root issue, excluding LLM Wiki plugin-operation issues.",
"3. Turn durable signal into project standups, wiki-insightful project pages, decisions, history, index, and log updates per the paperclip-distill skill.",
"4. Surface clipped, low-signal, stale-hash, or source-window warnings instead of hiding them.",
].filter((line): line is string => line !== null).join("\n");
}
function withManagedRoutineDefaultDrift(
routine: PluginManagedRoutineResolution,
declaration: PluginManagedRoutineDeclaration | undefined,
): ManagedRoutineSettingsResolution {
if (!routine.routine || !declaration) {
return { ...routine, defaultDrift: null };
}
const changedFields: string[] = [];
if (normalizeRoutineTemplateText(routine.routine.title) !== normalizeRoutineTemplateText(declaration.title)) {
changedFields.push("title");
}
if (normalizeRoutineTemplateText(routine.routine.description) !== normalizeRoutineTemplateText(declaration.description ?? null)) {
changedFields.push("description");
}
if (routine.routine.priority !== (declaration.priority ?? "medium")) {
changedFields.push("priority");
}
if (routine.routine.concurrencyPolicy !== (declaration.concurrencyPolicy ?? "coalesce_if_active")) {
changedFields.push("concurrency policy");
}
if (routine.routine.catchUpPolicy !== (declaration.catchUpPolicy ?? "skip_missed")) {
changedFields.push("catch-up policy");
}
return {
...routine,
defaultDrift: changedFields.length > 0
? {
changedFields,
defaultTitle: declaration.title,
defaultDescription: declaration.description ?? null,
}
: null,
};
}
const plugin = definePlugin({
async setup(ctx) {
activeContext = ctx;
await registerWikiTools(ctx);
for (const eventName of PAPERCLIP_EVENT_INGESTION_EVENTS) {
ctx.events.on(eventName, async (event) => {
const result = await handlePaperclipEventIngestion(ctx, event);
if (result.status === "recorded") {
ctx.logger.info("LLM Wiki recorded Paperclip event for cursor discovery", {
eventType: event.eventType,
companyId: event.companyId,
sourceKind: result.sourceKind,
sourceId: result.sourceId,
cursorId: result.cursorId,
});
}
});
}
ctx.data.register("overview", async (params) => {
const companyId = readCompanyIdFromParams(params);
return getOverview(ctx, companyId);
});
ctx.data.register("health", async (params) => {
const companyId = stringField(params.companyId);
return companyId
? getOverview(ctx, companyId)
: { status: "ok", checkedAt: new Date().toISOString(), message: "LLM Wiki worker is running" };
});
ctx.actions.register("bootstrap-root", async (params) => {
return bootstrapWikiRoot(ctx, {
companyId: readCompanyIdFromParams(params),
path: stringField(params.path),
});
});
ctx.data.register("spaces", async (params) => {
return listSpaces(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
});
});
ctx.data.register("space", async (params) => {
return spaceFolderStatus(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
});
});
ctx.actions.register("create-space", async (params) => {
return createSpace(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
slug: stringField(params.slug),
displayName: stringField(params.displayName),
folderMode: stringField(params.folderMode) as "managed_subfolder" | "existing_local_folder" | null,
accessScope: stringField(params.accessScope) as "shared" | "personal" | "team" | null,
settings: typeof params.settings === "object" && params.settings != null ? params.settings as Record<string, unknown> : null,
});
});
ctx.actions.register("update-space", async (params) => {
return updateSpace(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
displayName: stringField(params.displayName),
status: stringField(params.status) as "active" | "archived" | null,
settings: typeof params.settings === "object" && params.settings != null ? params.settings as Record<string, unknown> : null,
});
});
ctx.actions.register("bootstrap-space", async (params) => {
return bootstrapSpace(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
});
});
ctx.actions.register("archive-space", async (params) => {
return archiveSpace(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
});
});
ctx.actions.register("create-operation", async (params) => {
const operationType = stringField(params.operationType);
if (
operationType !== "ingest" &&
operationType !== "query" &&
operationType !== "lint" &&
operationType !== "file-as-page" &&
operationType !== "index" &&
operationType !== "distill" &&
operationType !== "backfill"
) {
throw new Error("operationType must be ingest, query, lint, file-as-page, index, distill, or backfill");
}
return createOperationIssue(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
operationType,
title: stringField(params.title),
prompt: stringField(params.prompt),
useCheapModelProfile: params.useCheapModelProfile === true,
});
});
ctx.actions.register("capture-source", async (params) => {
return captureWikiSource(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
sourceType: stringField(params.sourceType),
title: stringField(params.title),
url: stringField(params.url),
contents: typeof params.contents === "string" ? params.contents : "",
rawPath: stringField(params.rawPath),
metadata: typeof params.metadata === "object" && params.metadata != null ? params.metadata as Record<string, unknown> : null,
});
});
ctx.actions.register("write-page", async (params) => {
return writeWikiPage(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
path: stringField(params.path) ?? "",
contents: typeof params.contents === "string" ? params.contents : "",
expectedHash: stringField(params.expectedHash),
summary: stringField(params.summary),
sourceRefs: params.sourceRefs,
writer: "board_ui",
});
});
ctx.actions.register("write-template", async (params) => {
return writeTemplate(ctx, {
companyId: readCompanyIdFromParams(params),
path: stringField(params.path) ?? "",
contents: typeof params.contents === "string" ? params.contents : "",
});
});
ctx.actions.register("update-event-ingestion-settings", async (params) => {
const requestedSources = typeof params.sources === "object" && params.sources != null && !Array.isArray(params.sources)
? params.sources as Record<string, unknown>
: null;
const sources: { issues?: boolean; comments?: boolean; documents?: boolean } = {};
if (requestedSources && Object.prototype.hasOwnProperty.call(requestedSources, "issues")) {
sources.issues = requestedSources.issues === true;
}
if (requestedSources && Object.prototype.hasOwnProperty.call(requestedSources, "comments")) {
sources.comments = requestedSources.comments === true;
}
if (requestedSources && Object.prototype.hasOwnProperty.call(requestedSources, "documents")) {
sources.documents = requestedSources.documents === true;
}
const settings: {
enabled?: boolean;
wikiId?: string;
maxCharacters?: number;
sources?: typeof sources;
} = {
wikiId: stringField(params.wikiId) ?? undefined,
maxCharacters: typeof params.maxCharacters === "number" ? params.maxCharacters : undefined,
};
if (typeof params.enabled === "boolean") {
settings.enabled = params.enabled;
}
if (Object.keys(sources).length > 0) {
settings.sources = sources;
}
return updateEventIngestionSettings(ctx, {
companyId: readCompanyIdFromParams(params),
settings,
});
});
ctx.data.register("paperclip-ingestion-profile", async (params) => {
return getPaperclipIngestionProfile(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
});
});
ctx.data.register("paperclip-ingestion-candidates", async (params) => {
return listPaperclipIngestionCandidates(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
query: stringField(params.query),
});
});
ctx.actions.register("update-paperclip-ingestion-profile", async (params) => {
return updatePaperclipIngestionProfile(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
profile: params.profile,
});
});
ctx.actions.register("queue-paperclip-ingestion-backfill", async (params) => {
const companyId = readCompanyIdFromParams(params);
const sourceScope = typeof params.sourceScope === "object" && params.sourceScope != null && !Array.isArray(params.sourceScope)
? params.sourceScope as Record<string, unknown>
: {};
const sourceScopeKind = stringField(sourceScope.kind);
const projectIds = Array.isArray(sourceScope.projectIds) ? sourceScope.projectIds.map(stringField).filter((id): id is string => Boolean(id)) : [];
const issueIds = Array.isArray(sourceScope.issueIds) ? sourceScope.issueIds.map(stringField).filter((id): id is string => Boolean(id)) : [];
const scopes = sourceScopeKind === "selected_projects"
? projectIds.map((projectId) => ({ projectId, rootIssueId: null as string | null }))
: sourceScopeKind === "root_issues"
? issueIds.map((rootIssueId) => ({ projectId: null as string | null, rootIssueId }))
: [];
if (scopes.length === 0) {
return {
status: "refused_policy",
wikiId: stringField(params.wikiId) ?? "default",
spaceSlug: stringField(params.spaceSlug) ?? "default",
warnings: ["Backfill requires a selected project or root issue scope in Phase 4."],
};
}
const backfillStartAt = stringField(params.backfillStartAt);
const backfillEndAt = stringField(params.backfillEndAt);
const wikiId = stringField(params.wikiId);
const spaceSlug = stringField(params.spaceSlug);
const requestedByIssueId = stringField(params.requestedByIssueId);
const idempotencyKey = stringField(params.idempotencyKey);
const queued: Array<{ workItemId: string; issueId: string; projectId: string | null; rootIssueId: string | null }> = [];
for (const scope of scopes) {
const idempotencyScope = scope.rootIssueId ? `root:${scope.rootIssueId}` : `project:${scope.projectId}`;
const workItem = await createPaperclipDistillationWorkItem(ctx, {
companyId,
wikiId,
spaceSlug,
kind: "backfill",
projectId: scope.projectId,
rootIssueId: scope.rootIssueId,
requestedByIssueId,
priority: "low",
idempotencyKey: idempotencyKey && scopes.length === 1
? idempotencyKey
: `${idempotencyKey ?? "profile-backfill"}:${idempotencyScope}:${backfillStartAt ?? "begin"}:${backfillEndAt ?? "now"}`,
metadata: { backfillStartAt, backfillEndAt, requestedFrom: "queue-paperclip-ingestion-backfill" },
});
const operation = await createOperationIssue(ctx, {
companyId,
wikiId,
spaceSlug,
operationType: "backfill",
title: scope.rootIssueId ? "Backfill Paperclip root issue wiki history" : "Backfill Paperclip project wiki history",
useCheapModelProfile: params.useCheapModelProfile === true,
prompt: [
"Backfill LLM Wiki distillation was queued from a per-space Paperclip ingestion profile.",
scope.projectId ? `Project ID: ${scope.projectId}` : null,
scope.rootIssueId ? `Root issue ID: ${scope.rootIssueId}` : null,
backfillStartAt ? `Start: ${backfillStartAt}` : null,
backfillEndAt ? `End: ${backfillEndAt}` : null,
"Process this bounded window through the profile destination space only.",
].filter(Boolean).join("\n"),
});
queued.push({
workItemId: workItem.workItemId,
issueId: operation.issue.id,
projectId: scope.projectId,
rootIssueId: scope.rootIssueId,
});
}
const primary = queued[0];
return {
status: "queued",
wikiId: stringField(params.wikiId) ?? "default",
spaceSlug: stringField(params.spaceSlug) ?? "default",
workItemId: primary?.workItemId ?? null,
issueId: primary?.issueId ?? null,
workItems: queued,
warnings: [],
};
});
ctx.actions.register("ingest-source", async (params) => {
const companyId = readCompanyIdFromParams(params);
const wikiId = stringField(params.wikiId);
const spaceSlug = stringField(params.spaceSlug);
const sourceType = stringField(params.sourceType) ?? "text";
const title = stringField(params.title) ?? sourceType.toUpperCase();
const contents = typeof params.contents === "string" ? params.contents : "";
const url = stringField(params.url);
const captured = await captureWikiSource(ctx, {
companyId,
wikiId,
spaceSlug,
sourceType,
title,
url,
contents,
rawPath: stringField(params.rawPath),
metadata: typeof params.metadata === "object" && params.metadata != null ? params.metadata as Record<string, unknown> : null,
});
const op = await createOperationIssue(ctx, {
companyId,
wikiId,
spaceSlug,
operationType: "ingest",
title: `Ingest ${sourceType}: ${title}`,
prompt: [
`Ingest a captured source from raw/${captured.rawPath.replace(/^raw\//, "")}.`,
url ? `Source URL: ${url}` : null,
"Follow the installed wiki-ingest skill: read the raw file end to end, summarise into wiki/sources/<slug>.md, update related entity/concept/synthesis pages, refresh wiki/index.md, and append wiki/log.md.",
].filter(Boolean).join("\n"),
});
return { status: "ok", source: captured, operation: op };
});
ctx.actions.register("assemble-paperclip-source-bundle", async (params) => {
return assemblePaperclipSourceBundle(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
projectId: stringField(params.projectId),
rootIssueId: stringField(params.rootIssueId),
maxCharacters: typeof params.maxCharacters === "number" ? params.maxCharacters : null,
maxCharactersPerSource: typeof params.maxCharactersPerSource === "number" ? params.maxCharactersPerSource : null,
backfillStartAt: stringField(params.backfillStartAt),
backfillEndAt: stringField(params.backfillEndAt),
routineRun: params.routineRun === true,
includeComments: params.includeComments !== false,
includeDocuments: params.includeDocuments !== false,
});
});
ctx.actions.register("create-paperclip-distillation-run", async (params) => {
return createPaperclipDistillationRun(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
projectId: stringField(params.projectId),
rootIssueId: stringField(params.rootIssueId),
maxCharacters: typeof params.maxCharacters === "number" ? params.maxCharacters : null,
maxCharactersPerSource: typeof params.maxCharactersPerSource === "number" ? params.maxCharactersPerSource : null,
backfillStartAt: stringField(params.backfillStartAt),
backfillEndAt: stringField(params.backfillEndAt),
routineRun: params.routineRun === true,
includeComments: params.includeComments !== false,
includeDocuments: params.includeDocuments !== false,
workItemId: stringField(params.workItemId),
operationIssueId: stringField(params.operationIssueId),
});
});
ctx.actions.register("record-paperclip-distillation-outcome", async (params) => {
const status = stringField(params.status);
if (status !== "succeeded" && status !== "failed" && status !== "review_required") {
throw new Error("status must be succeeded, failed, or review_required");
}
const runId = stringField(params.runId);
if (!runId) throw new Error("runId is required");
return recordPaperclipDistillationOutcome(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
runId,
cursorId: stringField(params.cursorId),
status,
sourceHash: stringField(params.sourceHash),
sourceWindowEnd: stringField(params.sourceWindowEnd),
warning: stringField(params.warning),
costCents: typeof params.costCents === "number" ? params.costCents : null,
retryCount: typeof params.retryCount === "number" ? params.retryCount : null,
});
});
ctx.actions.register("distill-paperclip-project-page", async (params) => {
return distillPaperclipProjectPage(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
projectId: stringField(params.projectId),
rootIssueId: stringField(params.rootIssueId),
maxCharacters: typeof params.maxCharacters === "number" ? params.maxCharacters : null,
maxCharactersPerSource: typeof params.maxCharactersPerSource === "number" ? params.maxCharactersPerSource : null,
backfillStartAt: stringField(params.backfillStartAt),
backfillEndAt: stringField(params.backfillEndAt),
routineRun: params.routineRun === true,
includeComments: params.includeComments !== false,
includeDocuments: params.includeDocuments !== false,
workItemId: stringField(params.workItemId),
operationIssueId: stringField(params.operationIssueId),
autoApply: params.autoApply === true ? true : params.autoApply === false ? false : undefined,
expectedProjectPageHash: stringField(params.expectedProjectPageHash),
includeSupportingPages: params.includeSupportingPages !== false,
});
});
ctx.actions.register("distill-paperclip-now", async (params) => {
const companyId = readCompanyIdFromParams(params);
const spaceSlug = stringField(params.spaceSlug);
const projectId = stringField(params.projectId);
const rootIssueId = stringField(params.rootIssueId);
const idempotencyScope = rootIssueId ? `root:${rootIssueId}` : projectId ? `project:${projectId}` : "company";
const workItem = await createPaperclipDistillationWorkItem(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug,
kind: "manual",
projectId,
rootIssueId,
requestedByIssueId: stringField(params.requestedByIssueId),
priority: "medium",
idempotencyKey: stringField(params.idempotencyKey) ?? `manual:${idempotencyScope}`,
metadata: { requestedFrom: "distill-paperclip-now" },
});
const operation = await createOperationIssue(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug,
operationType: "distill",
title: rootIssueId
? "Distill Paperclip root issue into wiki"
: projectId
? "Distill Paperclip project into wiki"
: "Distill Paperclip changes into wiki",
useCheapModelProfile: params.useCheapModelProfile === true,
prompt: buildManualDistillPrompt({ companyId, projectId, rootIssueId }),
});
return { status: "queued", workItem, operation };
});
ctx.actions.register("enable-paperclip-distillation-active-projects", async (params) => {
return enableActiveProjectDistillation(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
limit: typeof params.limit === "number" ? params.limit : null,
});
});
ctx.actions.register("backfill-paperclip-distillation", async (params) => {
const companyId = readCompanyIdFromParams(params);
const spaceSlug = stringField(params.spaceSlug);
const projectId = stringField(params.projectId);
const rootIssueId = stringField(params.rootIssueId);
if (!projectId && !rootIssueId) throw new Error("projectId or rootIssueId is required");
const backfillStartAt = stringField(params.backfillStartAt);
const backfillEndAt = stringField(params.backfillEndAt);
const idempotencyScope = rootIssueId ? `root:${rootIssueId}` : `project:${projectId}`;
const workItem = await createPaperclipDistillationWorkItem(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug,
kind: "backfill",
projectId,
rootIssueId,
requestedByIssueId: stringField(params.requestedByIssueId),
priority: "low",
idempotencyKey: stringField(params.idempotencyKey) ?? `backfill:${idempotencyScope}:${backfillStartAt ?? "begin"}:${backfillEndAt ?? "now"}`,
metadata: { backfillStartAt, backfillEndAt, requestedFrom: "backfill-paperclip-distillation" },
});
const operation = await createOperationIssue(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug,
operationType: "backfill",
title: rootIssueId ? "Backfill Paperclip root issue wiki history" : "Backfill Paperclip project wiki history",
useCheapModelProfile: params.useCheapModelProfile === true,
prompt: [
"Backfill LLM Wiki distillation requested for a bounded Paperclip source window.",
projectId ? `Project ID: ${projectId}` : null,
rootIssueId ? `Root issue ID: ${rootIssueId}` : null,
backfillStartAt ? `Start: ${backfillStartAt}` : null,
backfillEndAt ? `End: ${backfillEndAt}` : null,
"Do not process whole-company history; stay within the selected project/root issue and date window.",
].filter(Boolean).join("\n"),
});
const result = await distillPaperclipProjectPage(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug,
projectId,
rootIssueId,
maxCharacters: typeof params.maxCharacters === "number" ? params.maxCharacters : null,
maxCharactersPerSource: typeof params.maxCharactersPerSource === "number" ? params.maxCharactersPerSource : null,
backfillStartAt,
backfillEndAt,
routineRun: params.routineRun === true,
includeComments: params.includeComments !== false,
includeDocuments: params.includeDocuments !== false,
autoApply: params.autoApply === true ? true : params.autoApply === false ? false : undefined,
expectedProjectPageHash: stringField(params.expectedProjectPageHash),
includeSupportingPages: params.includeSupportingPages !== false,
workItemId: workItem.workItemId,
operationIssueId: operation.issue.id,
});
return { ...result, workItem, operation };
});
ctx.actions.register("create-paperclip-distillation-work-item", async (params) => {
const kind = stringField(params.kind);
if (
kind !== "manual" &&
kind !== "retry" &&
kind !== "backfill" &&
kind !== "priority_override" &&
kind !== "review_patch"
) {
throw new Error("kind must be manual, retry, backfill, priority_override, or review_patch");
}
const priority = stringField(params.priority);
if (priority && priority !== "critical" && priority !== "high" && priority !== "medium" && priority !== "low") {
throw new Error("priority must be critical, high, medium, or low");
}
return createPaperclipDistillationWorkItem(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
kind,
projectId: stringField(params.projectId),
rootIssueId: stringField(params.rootIssueId),
requestedByIssueId: stringField(params.requestedByIssueId),
priority: priority as "critical" | "high" | "medium" | "low" | null,
idempotencyKey: stringField(params.idempotencyKey),
metadata: typeof params.metadata === "object" && params.metadata != null ? params.metadata as Record<string, unknown> : null,
});
});
ctx.actions.register("file-as-page", async (params) => {
return fileQueryAnswerAsPage(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
querySessionId: stringField(params.querySessionId),
question: stringField(params.question),
answer: stringField(params.answer),
path: stringField(params.path) ?? "",
title: stringField(params.title),
contents: stringField(params.contents),
expectedHash: stringField(params.expectedHash),
});
});
ctx.actions.register("start-query", async (params) => {
return startWikiQuerySession(ctx, {
companyId: readCompanyIdFromParams(params),
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
question: stringField(params.question) ?? "",
title: stringField(params.title),
});
});
ctx.actions.register("reset-managed-agent", async (params) => {
return resetWikiAgentResource(ctx, readCompanyIdFromParams(params));
});
ctx.actions.register("reset-managed-project", async (params) => {
return resetWikiProjectResource(ctx, readCompanyIdFromParams(params));
});
ctx.actions.register("reconcile-managed-agent", async (params) => {
return reconcileWikiAgentResource(ctx, readCompanyIdFromParams(params));
});
ctx.actions.register("reconcile-managed-project", async (params) => {
return reconcileWikiProjectResource(ctx, readCompanyIdFromParams(params));
});
ctx.actions.register("reconcile-managed-skills", async (params) => {
return { managedSkills: await reconcileWikiSkillResources(ctx, readCompanyIdFromParams(params)) };
});
ctx.actions.register("reset-managed-skills", async (params) => {
return { managedSkills: await resetWikiSkillResources(ctx, readCompanyIdFromParams(params)) };
});
ctx.actions.register("select-managed-agent", async (params) => {
const agentId = stringField(params.agentId);
if (!agentId) throw new Error("agentId is required");
return selectWikiAgentResource(ctx, {
companyId: readCompanyIdFromParams(params),
agentId,
});
});
ctx.actions.register("select-managed-project", async (params) => {
const projectId = stringField(params.projectId);
if (!projectId) throw new Error("projectId is required");
return selectWikiProjectResource(ctx, {
companyId: readCompanyIdFromParams(params),
projectId,
});
});
ctx.actions.register("reset-managed-routine", async (params) => {
return ctx.routines.managed.reset(
routineKeyField(params.routineKey),
readCompanyIdFromParams(params),
routineOverridesFromParams(params),
);
});
ctx.actions.register("reconcile-managed-routine", async (params) => {
return ctx.routines.managed.reconcile(
routineKeyField(params.routineKey),
readCompanyIdFromParams(params),
routineOverridesFromParams(params),
);
});
ctx.actions.register("reconcile-managed-routines", async (params) => {
return reconcileWikiRoutineResources(ctx, readCompanyIdFromParams(params));
});
ctx.actions.register("update-managed-routine-status", async (params) => {
const status = stringField(params.status);
if (!status) throw new Error("status is required");
return ctx.routines.managed.update(routineKeyField(params.routineKey), readCompanyIdFromParams(params), {
status,
});
});
ctx.actions.register("run-managed-routine", async (params) => {
return ctx.routines.managed.run(
routineKeyField(params.routineKey),
readCompanyIdFromParams(params),
routineOverridesFromParams(params),
);
});
ctx.data.register("pages", async (params) => {
const companyId = readCompanyIdFromParams(params);
return listPages(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
pageType: stringField(params.pageType),
includeRaw: params.includeRaw === true || params.includeRaw === "true",
limit: typeof params.limit === "number" ? params.limit : null,
});
});
ctx.data.register("sources", async (params) => {
const companyId = readCompanyIdFromParams(params);
return listSources(ctx, { companyId, wikiId: stringField(params.wikiId), spaceSlug: stringField(params.spaceSlug), limit: typeof params.limit === "number" ? params.limit : null });
});
ctx.data.register("page-content", async (params) => {
const companyId = readCompanyIdFromParams(params);
const path = stringField(params.path);
if (!path) throw new Error("path is required");
return readWikiPage(ctx, { companyId, wikiId: stringField(params.wikiId), spaceSlug: stringField(params.spaceSlug), path });
});
ctx.data.register("template", async (params) => {
const companyId = readCompanyIdFromParams(params);
const path = stringField(params.path) ?? "AGENTS.md";
return readTemplate(ctx, { companyId, path });
});
ctx.data.register("operations", async (params) => {
const companyId = readCompanyIdFromParams(params);
return listOperations(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
operationType: stringField(params.operationType),
status: stringField(params.status),
limit: typeof params.limit === "number" ? params.limit : null,
});
});
ctx.data.register("distillation-overview", async (params) => {
const companyId = readCompanyIdFromParams(params);
return getDistillationOverview(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
limit: typeof params.limit === "number" ? params.limit : null,
});
});
ctx.data.register("distillation-page-provenance", async (params) => {
const companyId = readCompanyIdFromParams(params);
const pagePath = stringField(params.pagePath);
if (!pagePath) {
return { binding: null, runs: [], snapshot: null, cursor: null };
}
return getDistillationPageProvenance(ctx, {
companyId,
wikiId: stringField(params.wikiId),
spaceSlug: stringField(params.spaceSlug),
pagePath,
});
});
ctx.data.register("settings", async (params) => {
const companyId = readCompanyIdFromParams(params);
const folder = await ctx.localFolders.status(companyId, WIKI_ROOT_FOLDER_KEY);
const overview = await getOverview(ctx, companyId);
const managedRoutines = await Promise.all(
WIKI_MAINTENANCE_ROUTINE_KEYS.map((routineKey) => ctx.routines.managed.get(routineKey, companyId)),
);
const managedRoutinesWithDefaultDrift = managedRoutines.map((routine) =>
withManagedRoutineDefaultDrift(
routine,
ctx.manifest.routines?.find((declaration) => declaration.routineKey === routine.resourceKey),
),
);
return {
folder,
spaces: await listSpaces(ctx, { companyId }),
managedAgent: overview.managedAgent,
managedProject: overview.managedProject,
managedSkills: overview.managedSkills,
managedRoutine: managedRoutinesWithDefaultDrift[0],
managedRoutines: managedRoutinesWithDefaultDrift,
distillationPolicy: getDistillationAutoApplyRestriction(),
eventIngestion: await getEventIngestionSettings(ctx, companyId),
agentOptions: await listWikiAgentOptions(ctx, companyId),
projectOptions: await listWikiProjectOptions(ctx, companyId),
capabilities: ctx.manifest.capabilities,
};
});
},
async onApiRequest(input: PluginApiRequestInput) {
const ctx = requireContext();
if (input.routeKey === "overview") {
return { body: await getOverview(ctx, input.companyId) };
}
if (input.routeKey === "bootstrap") {
const body = input.body as Record<string, unknown> | null;
return {
status: 201,
body: await bootstrapWikiRoot(ctx, {
companyId: input.companyId,
path: stringField(body?.path),
}),
};
}
if (input.routeKey === "spaces") {
return {
body: await listSpaces(ctx, {
companyId: input.companyId,
wikiId: stringField(input.query.wikiId),
}),
};
}
if (input.routeKey === "create-space") {
const body = input.body as Record<string, unknown> | null;
return {
status: 201,
body: await createSpace(ctx, {
companyId: input.companyId,
wikiId: stringField(body?.wikiId),
slug: stringField(body?.slug),
displayName: stringField(body?.displayName),
folderMode: stringField(body?.folderMode) as "managed_subfolder" | "existing_local_folder" | null,
accessScope: stringField(body?.accessScope) as "shared" | "personal" | "team" | null,
settings: typeof body?.settings === "object" && body.settings != null ? body.settings as Record<string, unknown> : null,
}),
};
}
if (input.routeKey === "update-space") {
const body = input.body as Record<string, unknown> | null;
return {
body: await updateSpace(ctx, {
companyId: input.companyId,
wikiId: stringField(body?.wikiId),
spaceSlug: input.params.spaceSlug,
displayName: stringField(body?.displayName),
status: stringField(body?.status) as "active" | "archived" | null,
settings: typeof body?.settings === "object" && body.settings != null ? body.settings as Record<string, unknown> : null,
}),
};
}
if (input.routeKey === "bootstrap-space") {
const body = input.body as Record<string, unknown> | null;
return {
status: 201,
body: await bootstrapSpace(ctx, {
companyId: input.companyId,
wikiId: stringField(body?.wikiId),
spaceSlug: input.params.spaceSlug,
}),
};
}
if (input.routeKey === "archive-space") {
const body = input.body as Record<string, unknown> | null;
return {
body: await archiveSpace(ctx, {
companyId: input.companyId,
wikiId: stringField(body?.wikiId),
spaceSlug: input.params.spaceSlug,
}),
};
}
if (input.routeKey === "capture-source") {
const body = input.body as Record<string, unknown> | null;
return {
status: 201,
body: await captureWikiSource(ctx, {
companyId: input.companyId,
wikiId: stringField(body?.wikiId),
spaceSlug: stringField(body?.spaceSlug),
sourceType: stringField(body?.sourceType),
title: stringField(body?.title),
url: stringField(body?.url),
contents: typeof body?.contents === "string" ? body.contents : "",
rawPath: stringField(body?.rawPath),
metadata: typeof body?.metadata === "object" && body.metadata != null ? body.metadata as Record<string, unknown> : null,
}),
};
}
if (input.routeKey === "operations") {
return {
body: await listOperations(ctx, {
companyId: input.companyId,
wikiId: stringField(input.query.wikiId),
spaceSlug: stringField(input.query.spaceSlug),
operationType: stringField(input.query.operationType),
status: stringField(input.query.status),
limit: typeof input.query.limit === "string" ? Number(input.query.limit) : null,
}),
};
}
if (input.routeKey === "start-query") {
const body = input.body as Record<string, unknown> | null;
return {
status: 201,
body: await startWikiQuerySession(ctx, {
companyId: input.companyId,
wikiId: stringField(body?.wikiId),
spaceSlug: stringField(body?.spaceSlug),
question: stringField(body?.question) ?? "",
title: stringField(body?.title),
}),
};
}
if (input.routeKey === "file-as-page") {
const body = input.body as Record<string, unknown> | null;
return {
status: 201,
body: await fileQueryAnswerAsPage(ctx, {
companyId: input.companyId,
wikiId: stringField(body?.wikiId),
spaceSlug: stringField(body?.spaceSlug),
querySessionId: stringField(body?.querySessionId),
question: stringField(body?.question),
answer: stringField(body?.answer),
path: stringField(body?.path) ?? "",
title: stringField(body?.title),
contents: stringField(body?.contents),
expectedHash: stringField(body?.expectedHash),
}),
};
}
return { status: 404, body: { error: `Unknown LLM Wiki route: ${input.routeKey}` } };
},
async onHealth() {
return {
status: "ok",
message: "LLM Wiki plugin worker is running",
details: {
surfaces: ["page", "sidebar", "settings", "tools", "database", "local-folder"],
},
};
}
});
export default plugin;
runWorker(plugin, import.meta.url);