import { Buffer } from "node:buffer"; import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { promises as fs } from "node:fs"; import path from "node:path"; import { PLUGIN_STATE_SCOPE_KINDS, definePlugin, runWorker, type PaperclipPlugin, type PluginContext, type PluginEntityQuery, type PluginEvent, type PluginHealthDiagnostics, type PluginJobContext, type PluginLauncherRegistration, type PluginWebhookInput, type PluginWorkspace, type PluginStateScopeKind, type ScopeKey, type ToolResult, type ToolRunContext, } from "@paperclipai/plugin-sdk"; import type { Goal, Issue } from "@paperclipai/shared"; import { DEFAULT_CONFIG, JOB_KEYS, PLUGIN_ID, RUNTIME_LAUNCHER, SAFE_COMMANDS, STREAM_CHANNELS, TOOL_NAMES, WEBHOOK_KEYS, } from "./constants.js"; type KitchenSinkConfig = { showSidebarEntry?: boolean; showSidebarPanel?: boolean; showProjectSidebarItem?: boolean; showCommentAnnotation?: boolean; showCommentContextMenuItem?: boolean; enableWorkspaceDemos?: boolean; enableProcessDemos?: boolean; secretRefExample?: string; httpDemoUrl?: string; allowedCommands?: string[]; workspaceScratchFile?: string; }; type DemoRecord = { id: string; level: "info" | "warning" | "error"; source: string; message: string; createdAt: string; data?: unknown; }; type ProcessResult = { commandKey: string; cwd: string; code: number | null; stdout: string; stderr: string; startedAt: string; finishedAt: string; }; const recentRecords: DemoRecord[] = []; const runtimeLaunchers = new Map(); let currentContext: PluginContext | null = null; let lastProcessResult: ProcessResult | null = null; function isScopeKind(value: unknown): value is PluginStateScopeKind { return typeof value === "string" && PLUGIN_STATE_SCOPE_KINDS.includes(value as PluginStateScopeKind); } function summarizeError(error: unknown): string { if (error instanceof Error) return error.message; return String(error); } function pushRecord(record: Omit): DemoRecord { const next: DemoRecord = { id: randomUUID(), createdAt: new Date().toISOString(), ...record, }; recentRecords.unshift(next); if (recentRecords.length > 50) recentRecords.length = 50; return next; } async function getConfig(ctx: PluginContext): Promise { const config = await ctx.config.get(); return { ...DEFAULT_CONFIG, ...(config as KitchenSinkConfig), }; } async function writeInstanceState(ctx: PluginContext, stateKey: string, value: unknown): Promise { await ctx.state.set({ scopeKind: "instance", stateKey }, value); } async function readInstanceState(ctx: PluginContext, stateKey: string): Promise { return await ctx.state.get({ scopeKind: "instance", stateKey }) as T | null; } async function resolveWorkspace( ctx: PluginContext, companyId: string, projectId: string, workspaceId?: string, ): Promise { const workspaces = await ctx.projects.listWorkspaces(projectId, companyId); if (workspaces.length === 0) { throw new Error("No workspaces configured for this project"); } if (!workspaceId) return workspaces[0]!; const workspace = workspaces.find((entry) => entry.id === workspaceId); if (!workspace) { throw new Error("Workspace not found"); } return workspace; } function ensureInsideWorkspace(workspacePath: string, relativePath: string): string { const root = path.resolve(workspacePath); const resolved = path.resolve(root, relativePath); const relative = path.relative(root, resolved); if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("Requested path escapes the selected workspace"); } return resolved; } function parseJsonish(value: string): unknown { const trimmed = value.trim(); if (trimmed.length === 0) return ""; try { return JSON.parse(trimmed) as unknown; } catch { return value; } } function parseScopeKey(params: Record): ScopeKey { const scopeKind = isScopeKind(params.scopeKind) ? params.scopeKind : "instance"; const scopeId = typeof params.scopeId === "string" && params.scopeId.length > 0 ? params.scopeId : undefined; const namespace = typeof params.namespace === "string" && params.namespace.length > 0 ? params.namespace : undefined; const stateKey = typeof params.stateKey === "string" && params.stateKey.length > 0 ? params.stateKey : "demo"; return { scopeKind, scopeId, namespace, stateKey }; } async function runCuratedCommand( ctx: PluginContext, config: KitchenSinkConfig, companyId: string, projectId: string, workspaceId: string | undefined, commandKey: string, ): Promise { if (!config.enableProcessDemos) { throw new Error("Process demos are disabled in plugin settings"); } const allowedCommands = new Set(config.allowedCommands ?? DEFAULT_CONFIG.allowedCommands); if (!allowedCommands.has(commandKey)) { throw new Error(`Command "${commandKey}" is not allowed by plugin settings`); } const definition = SAFE_COMMANDS.find((entry) => entry.key === commandKey); if (!definition) { throw new Error(`Unknown curated command "${commandKey}"`); } const workspace = await resolveWorkspace(ctx, companyId, projectId, workspaceId); const cwd = workspace.path; const startedAt = new Date().toISOString(); const child = spawn(definition.command, definition.args, { cwd, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += String(chunk); }); child.stderr.on("data", (chunk) => { stderr += String(chunk); }); const code = await new Promise((resolve, reject) => { child.on("error", reject); child.on("close", resolve); }); const result: ProcessResult = { commandKey, cwd, code, stdout: stdout.trim(), stderr: stderr.trim(), startedAt, finishedAt: new Date().toISOString(), }; lastProcessResult = result; pushRecord({ level: code === 0 ? "info" : "warning", source: "process", message: `Ran curated command "${commandKey}"`, data: { code, cwd }, }); await ctx.metrics.write("process.run", 1, { command: commandKey, exit_code: String(code ?? -1) }); return result; } function getCurrentCompanyId(params: Record): string { const companyId = typeof params.companyId === "string" ? params.companyId : ""; if (!companyId) { throw new Error("companyId is required"); } return companyId; } function getListLimit(params: Record, fallback = 50): number { const value = typeof params.limit === "number" ? params.limit : Number(params.limit ?? fallback); if (!Number.isFinite(value)) return fallback; return Math.max(1, Math.min(200, Math.floor(value))); } async function listIssuesForCompany(ctx: PluginContext, companyId: string, limit = 50): Promise { return await ctx.issues.list({ companyId, limit, offset: 0 }); } async function listGoalsForCompany(ctx: PluginContext, companyId: string, limit = 50): Promise { return await ctx.goals.list({ companyId, limit, offset: 0 }); } function recentRecordsSnapshot(): DemoRecord[] { return recentRecords.slice(0, 20); } function runtimeLaunchersSnapshot(): PluginLauncherRegistration[] { return [...runtimeLaunchers.values()]; } async function registerDataHandlers(ctx: PluginContext): Promise { ctx.data.register("plugin-config", async () => { return await getConfig(ctx); }); ctx.data.register("overview", async (params) => { const companyId = typeof params.companyId === "string" ? params.companyId : ""; const config = await getConfig(ctx); const companies = await ctx.companies.list({ limit: 200, offset: 0 }); const projects = companyId ? await ctx.projects.list({ companyId, limit: 200, offset: 0 }) : []; const issues = companyId ? await listIssuesForCompany(ctx, companyId, 200) : []; const goals = companyId ? await listGoalsForCompany(ctx, companyId, 200) : []; const agents = companyId ? await ctx.agents.list({ companyId, limit: 200, offset: 0 }) : []; const lastJob = await readInstanceState(ctx, "last-job-run"); const lastWebhook = await readInstanceState(ctx, "last-webhook"); const entityRecords = await ctx.entities.list({ limit: 10 } satisfies PluginEntityQuery); return { pluginId: PLUGIN_ID, version: ctx.manifest.version, capabilities: ctx.manifest.capabilities, config, runtimeLaunchers: runtimeLaunchersSnapshot(), recentRecords: recentRecordsSnapshot(), counts: { companies: companies.length, projects: projects.length, issues: issues.length, goals: goals.length, agents: agents.length, entities: entityRecords.length, }, lastJob, lastWebhook, lastProcessResult, streamChannels: STREAM_CHANNELS, safeCommands: SAFE_COMMANDS, manifest: { jobs: ctx.manifest.jobs ?? [], webhooks: ctx.manifest.webhooks ?? [], tools: ctx.manifest.tools ?? [], }, }; }); ctx.data.register("companies", async (params) => { return await ctx.companies.list({ limit: getListLimit(params), offset: 0 }); }); ctx.data.register("projects", async (params) => { const companyId = getCurrentCompanyId(params); return await ctx.projects.list({ companyId, limit: getListLimit(params), offset: 0 }); }); ctx.data.register("issues", async (params) => { const companyId = getCurrentCompanyId(params); return await listIssuesForCompany(ctx, companyId, getListLimit(params)); }); ctx.data.register("goals", async (params) => { const companyId = getCurrentCompanyId(params); return await listGoalsForCompany(ctx, companyId, getListLimit(params)); }); ctx.data.register("agents", async (params) => { const companyId = getCurrentCompanyId(params); return await ctx.agents.list({ companyId, limit: getListLimit(params), offset: 0 }); }); ctx.data.register("workspaces", async (params) => { const companyId = getCurrentCompanyId(params); const projectId = typeof params.projectId === "string" ? params.projectId : ""; if (!projectId) return []; return await ctx.projects.listWorkspaces(projectId, companyId); }); ctx.data.register("state-value", async (params) => { const input = parseScopeKey(params); const value = await ctx.state.get(input); return { scope: input, value, }; }); ctx.data.register("entities", async (params) => { const query: PluginEntityQuery = { entityType: typeof params.entityType === "string" && params.entityType.length > 0 ? params.entityType : undefined, scopeKind: isScopeKind(params.scopeKind) ? params.scopeKind : undefined, scopeId: typeof params.scopeId === "string" && params.scopeId.length > 0 ? params.scopeId : undefined, limit: typeof params.limit === "number" ? params.limit : 25, offset: 0, }; return await ctx.entities.list(query); }); ctx.data.register("comment-context", async (params) => { const companyId = getCurrentCompanyId(params); const issueId = typeof params.issueId === "string" ? params.issueId : ""; const commentId = typeof params.commentId === "string" ? params.commentId : ""; if (!issueId || !commentId) return null; const comments = await ctx.issues.listComments(issueId, companyId); const comment = comments.find((entry) => entry.id === commentId) ?? null; if (!comment) return null; return { commentId: comment.id, issueId, preview: comment.body.slice(0, 160), length: comment.body.length, copiedCount: (await ctx.entities.list({ entityType: "copied-comment", scopeKind: "issue", scopeId: issueId, limit: 100, offset: 0, })).filter((entry) => entry.externalId === commentId).length, }; }); ctx.data.register("entity-context", async (params) => { const companyId = typeof params.companyId === "string" ? params.companyId : ""; const entityId = typeof params.entityId === "string" ? params.entityId : ""; const entityType = typeof params.entityType === "string" ? params.entityType : ""; if (!companyId || !entityId || !entityType) return null; if (entityType === "project") { return await ctx.projects.get(entityId, companyId); } if (entityType === "issue") { return await ctx.issues.get(entityId, companyId); } if (entityType === "goal") { return await ctx.goals.get(entityId, companyId); } if (entityType === "agent") { return await ctx.agents.get(entityId, companyId); } return { entityId, entityType, companyId }; }); } async function registerActionHandlers(ctx: PluginContext): Promise { ctx.actions.register("emit-demo-event", async (params) => { const companyId = getCurrentCompanyId(params); const message = typeof params.message === "string" && params.message.trim().length > 0 ? params.message.trim() : "Kitchen Sink demo event"; await ctx.events.emit("demo-event", companyId, { message, source: "kitchen-sink", emittedAt: new Date().toISOString(), }); pushRecord({ level: "info", source: "events.emit", message, data: { companyId }, }); await ctx.metrics.write("demo.events.emitted", 1, { source: "manual" }); return { ok: true, message }; }); ctx.actions.register("write-scoped-state", async (params) => { const input = parseScopeKey(params); const valueInput = typeof params.value === "string" ? params.value : JSON.stringify(params.value ?? ""); const value = parseJsonish(valueInput); await ctx.state.set(input, value); pushRecord({ level: "info", source: "state", message: `Wrote state key ${input.stateKey}`, data: input, }); await ctx.metrics.write("demo.state.write", 1, { scope: input.scopeKind }); return { ok: true, scope: input, value }; }); ctx.actions.register("delete-scoped-state", async (params) => { const input = parseScopeKey(params); await ctx.state.delete(input); pushRecord({ level: "warning", source: "state", message: `Deleted state key ${input.stateKey}`, data: input, }); return { ok: true, scope: input }; }); ctx.actions.register("upsert-entity", async (params) => { const title = typeof params.title === "string" && params.title.length > 0 ? params.title : "Kitchen Sink Entity"; const entityType = typeof params.entityType === "string" && params.entityType.length > 0 ? params.entityType : "demo-record"; const scopeKind = isScopeKind(params.scopeKind) ? params.scopeKind : "instance"; const scopeId = typeof params.scopeId === "string" && params.scopeId.length > 0 ? params.scopeId : undefined; const status = typeof params.status === "string" && params.status.length > 0 ? params.status : "active"; const data = typeof params.data === "string" ? parseJsonish(params.data) : params.data; const record = await ctx.entities.upsert({ entityType, scopeKind, scopeId, externalId: typeof params.externalId === "string" && params.externalId.length > 0 ? params.externalId : randomUUID(), title, status, data: typeof data === "object" && data !== null ? data as Record : { value: data }, }); pushRecord({ level: "info", source: "entities", message: `Upserted entity ${record.entityType}`, data: { id: record.id, scopeKind: record.scopeKind }, }); return record; }); ctx.actions.register("create-issue", async (params) => { const companyId = getCurrentCompanyId(params); const title = typeof params.title === "string" && params.title.trim().length > 0 ? params.title.trim() : "Kitchen Sink demo issue"; const description = typeof params.description === "string" ? params.description : undefined; const projectId = typeof params.projectId === "string" && params.projectId.length > 0 ? params.projectId : undefined; const issue = await ctx.issues.create({ companyId, projectId, title, description }); pushRecord({ level: "info", source: "issues.create", message: `Created issue ${issue.title}`, data: { issueId: issue.id }, }); await ctx.activity.log({ companyId, entityType: "issue", entityId: issue.id, message: `Kitchen Sink created issue "${issue.title}"`, metadata: { plugin: PLUGIN_ID }, }); return issue; }); ctx.actions.register("advance-issue-status", async (params) => { const companyId = getCurrentCompanyId(params); const issueId = typeof params.issueId === "string" ? params.issueId : ""; const status = typeof params.status === "string" ? params.status : ""; if (!issueId || !status) { throw new Error("issueId and status are required"); } const issue = await ctx.issues.update(issueId, { status: status as Issue["status"] }, companyId); pushRecord({ level: "info", source: "issues.update", message: `Updated issue ${issue.id} to ${issue.status}`, }); return issue; }); ctx.actions.register("create-goal", async (params) => { const companyId = getCurrentCompanyId(params); const title = typeof params.title === "string" && params.title.trim().length > 0 ? params.title.trim() : "Kitchen Sink demo goal"; const description = typeof params.description === "string" ? params.description : undefined; const goal = await ctx.goals.create({ companyId, title, description, level: "team", status: "planned" }); pushRecord({ level: "info", source: "goals.create", message: `Created goal ${goal.title}`, data: { goalId: goal.id }, }); return goal; }); ctx.actions.register("advance-goal-status", async (params) => { const companyId = getCurrentCompanyId(params); const goalId = typeof params.goalId === "string" ? params.goalId : ""; const status = typeof params.status === "string" ? params.status : ""; if (!goalId || !status) { throw new Error("goalId and status are required"); } const goal = await ctx.goals.update(goalId, { status: status as Goal["status"] }, companyId); pushRecord({ level: "info", source: "goals.update", message: `Updated goal ${goal.id} to ${goal.status}`, }); return goal; }); ctx.actions.register("write-activity", async (params) => { const companyId = getCurrentCompanyId(params); const entityType = typeof params.entityType === "string" ? params.entityType : undefined; const entityId = typeof params.entityId === "string" ? params.entityId : undefined; const message = typeof params.message === "string" && params.message.length > 0 ? params.message : "Kitchen Sink wrote an activity entry"; await ctx.activity.log({ companyId, entityType, entityId, message, metadata: { plugin: PLUGIN_ID }, }); pushRecord({ level: "info", source: "activity", message, data: { entityType, entityId }, }); return { ok: true }; }); ctx.actions.register("write-metric", async (params) => { const value = typeof params.value === "number" ? params.value : Number(params.value ?? 1); const name = typeof params.name === "string" && params.name.length > 0 ? params.name : "manual"; await ctx.metrics.write(`demo.${name}`, Number.isFinite(value) ? value : 1, { source: "manual" }); pushRecord({ level: "info", source: "metrics", message: `Wrote metric demo.${name}`, data: { value }, }); return { ok: true, value }; }); ctx.actions.register("http-fetch", async (params) => { const config = await getConfig(ctx); const url = typeof params.url === "string" && params.url.length > 0 ? params.url : config.httpDemoUrl || DEFAULT_CONFIG.httpDemoUrl; const started = Date.now(); const response = await ctx.http.fetch(url, { method: "GET" }); const body = await response.text(); const result = { ok: response.ok, status: response.status, url, durationMs: Date.now() - started, body: body.slice(0, 2000), }; pushRecord({ level: response.ok ? "info" : "warning", source: "http", message: `Fetched ${url}`, data: { status: response.status }, }); return result; }); ctx.actions.register("resolve-secret", async (params) => { const config = await getConfig(ctx); const secretRef = typeof params.secretRef === "string" && params.secretRef.length > 0 ? params.secretRef : config.secretRefExample || ""; if (!secretRef) { throw new Error("No secret reference configured"); } const resolved = await ctx.secrets.resolve(secretRef); pushRecord({ level: "info", source: "secrets", message: `Resolved secret reference ${secretRef}`, }); return { secretRef, resolvedLength: resolved.length, preview: resolved.length > 0 ? `${resolved.slice(0, 2)}***` : "", }; }); ctx.actions.register("run-process", async (params) => { const config = await getConfig(ctx); const companyId = getCurrentCompanyId(params); const projectId = typeof params.projectId === "string" ? params.projectId : ""; const workspaceId = typeof params.workspaceId === "string" && params.workspaceId.length > 0 ? params.workspaceId : undefined; const commandKey = typeof params.commandKey === "string" ? params.commandKey : "pwd"; if (!projectId) throw new Error("projectId is required"); return await runCuratedCommand(ctx, config, companyId, projectId, workspaceId, commandKey); }); ctx.actions.register("read-workspace-file", async (params) => { const config = await getConfig(ctx); if (!config.enableWorkspaceDemos) { throw new Error("Workspace demos are disabled in plugin settings"); } const companyId = getCurrentCompanyId(params); const projectId = typeof params.projectId === "string" ? params.projectId : ""; const workspaceId = typeof params.workspaceId === "string" && params.workspaceId.length > 0 ? params.workspaceId : undefined; const relativePath = typeof params.relativePath === "string" && params.relativePath.length > 0 ? params.relativePath : config.workspaceScratchFile || DEFAULT_CONFIG.workspaceScratchFile; if (!projectId) throw new Error("projectId is required"); const workspace = await resolveWorkspace(ctx, companyId, projectId, workspaceId); const fullPath = ensureInsideWorkspace(workspace.path, relativePath); const content = await fs.readFile(fullPath, "utf8"); return { workspaceId: workspace.id, relativePath, content, }; }); ctx.actions.register("write-workspace-scratch", async (params) => { const config = await getConfig(ctx); if (!config.enableWorkspaceDemos) { throw new Error("Workspace demos are disabled in plugin settings"); } const companyId = getCurrentCompanyId(params); const projectId = typeof params.projectId === "string" ? params.projectId : ""; const workspaceId = typeof params.workspaceId === "string" && params.workspaceId.length > 0 ? params.workspaceId : undefined; const relativePath = typeof params.relativePath === "string" && params.relativePath.length > 0 ? params.relativePath : config.workspaceScratchFile || DEFAULT_CONFIG.workspaceScratchFile; const content = typeof params.content === "string" ? params.content : "Kitchen Sink workspace demo"; if (!projectId) throw new Error("projectId is required"); const workspace = await resolveWorkspace(ctx, companyId, projectId, workspaceId); const fullPath = ensureInsideWorkspace(workspace.path, relativePath); await fs.writeFile(fullPath, content, "utf8"); pushRecord({ level: "info", source: "workspace", message: `Wrote scratch file ${relativePath}`, data: { workspaceId: workspace.id }, }); return { workspaceId: workspace.id, relativePath, bytes: Buffer.byteLength(content, "utf8"), }; }); ctx.actions.register("start-progress-stream", async (params) => { const companyId = getCurrentCompanyId(params); const steps = typeof params.steps === "number" ? params.steps : 5; void (async () => { ctx.streams.open(STREAM_CHANNELS.progress, companyId); try { for (let index = 1; index <= steps; index += 1) { ctx.streams.emit(STREAM_CHANNELS.progress, { step: index, total: steps, message: `Progress step ${index}/${steps}`, }); await new Promise((resolve) => setTimeout(resolve, 350)); } } finally { ctx.streams.close(STREAM_CHANNELS.progress); } })(); return { ok: true, channel: STREAM_CHANNELS.progress }; }); ctx.actions.register("invoke-agent", async (params) => { const companyId = getCurrentCompanyId(params); const agentId = typeof params.agentId === "string" ? params.agentId : ""; const prompt = typeof params.prompt === "string" && params.prompt.length > 0 ? params.prompt : "Kitchen Sink test invocation"; if (!agentId) throw new Error("agentId is required"); const result = await ctx.agents.invoke(agentId, companyId, { prompt, reason: "Kitchen Sink plugin demo" }); pushRecord({ level: "info", source: "agents.invoke", message: `Invoked agent ${agentId}`, data: result, }); return result; }); ctx.actions.register("pause-agent", async (params) => { const companyId = getCurrentCompanyId(params); const agentId = typeof params.agentId === "string" ? params.agentId : ""; if (!agentId) throw new Error("agentId is required"); return await ctx.agents.pause(agentId, companyId); }); ctx.actions.register("resume-agent", async (params) => { const companyId = getCurrentCompanyId(params); const agentId = typeof params.agentId === "string" ? params.agentId : ""; if (!agentId) throw new Error("agentId is required"); return await ctx.agents.resume(agentId, companyId); }); ctx.actions.register("ask-agent", async (params) => { const companyId = getCurrentCompanyId(params); const agentId = typeof params.agentId === "string" ? params.agentId : ""; const prompt = typeof params.prompt === "string" && params.prompt.length > 0 ? params.prompt : "Say hello from the Kitchen Sink plugin."; if (!agentId) throw new Error("agentId is required"); ctx.streams.open(STREAM_CHANNELS.agentChat, companyId); const session = await ctx.agents.sessions.create(agentId, companyId, { reason: "Kitchen Sink plugin chat demo", }); await ctx.agents.sessions.sendMessage(session.sessionId, companyId, { prompt, reason: "Kitchen Sink demo", onEvent: (event) => { ctx.streams.emit(STREAM_CHANNELS.agentChat, { eventType: event.eventType, stream: event.stream, message: event.message, payload: event.payload, }); if (event.eventType === "done" || event.eventType === "error") { ctx.streams.close(STREAM_CHANNELS.agentChat); } }, }); pushRecord({ level: "info", source: "agent.sessions", message: `Started agent session ${session.sessionId}`, data: { agentId, sessionId: session.sessionId }, }); return { channel: STREAM_CHANNELS.agentChat, sessionId: session.sessionId }; }); ctx.actions.register("copy-comment-context", async (params) => { const companyId = getCurrentCompanyId(params); const issueId = typeof params.issueId === "string" ? params.issueId : ""; const commentId = typeof params.commentId === "string" ? params.commentId : ""; if (!issueId || !commentId) { throw new Error("issueId and commentId are required"); } const comments = await ctx.issues.listComments(issueId, companyId); const comment = comments.find((entry) => entry.id === commentId); if (!comment) { throw new Error("Comment not found"); } const record = await ctx.entities.upsert({ entityType: "copied-comment", scopeKind: "issue", scopeId: issueId, externalId: comment.id, title: `Copied comment ${comment.id.slice(0, 8)}`, status: "captured", data: { commentId: comment.id, issueId, body: comment.body, }, }); pushRecord({ level: "info", source: "comments", message: `Copied comment ${comment.id} into plugin entities`, data: { recordId: record.id }, }); return record; }); } async function registerToolHandlers(ctx: PluginContext): Promise { ctx.tools.register( TOOL_NAMES.echo, { displayName: "Kitchen Sink Echo", description: "Echoes the provided message back to the caller.", parametersSchema: { type: "object", properties: { message: { type: "string" }, }, required: ["message"], }, }, async (params, runCtx): Promise => { const payload = params as { message?: string }; return { content: payload.message ?? "No message provided", data: { runCtx, message: payload.message ?? "", }, }; }, ); ctx.tools.register( TOOL_NAMES.companySummary, { displayName: "Kitchen Sink Company Summary", description: "Summarizes current company counts from the Paperclip APIs.", parametersSchema: { type: "object", properties: {} }, }, async (_params, runCtx): Promise => { const projects = await ctx.projects.list({ companyId: runCtx.companyId, limit: 50, offset: 0 }); const issues = await ctx.issues.list({ companyId: runCtx.companyId, limit: 50, offset: 0 }); const goals = await ctx.goals.list({ companyId: runCtx.companyId, limit: 50, offset: 0 }); const agents = await ctx.agents.list({ companyId: runCtx.companyId, limit: 50, offset: 0 }); return { content: `Company has ${projects.length} projects, ${issues.length} issues, ${goals.length} goals, and ${agents.length} agents.`, data: { companyId: runCtx.companyId, projects: projects.length, issues: issues.length, goals: goals.length, agents: agents.length, }, }; }, ); ctx.tools.register( TOOL_NAMES.createIssue, { displayName: "Kitchen Sink Create Issue", description: "Creates an issue in the current run context.", parametersSchema: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, }, required: ["title"], }, }, async (params, runCtx): Promise => { const payload = params as { title?: string; description?: string }; if (!payload.title) { return { error: "title is required" }; } const issue = await ctx.issues.create({ companyId: runCtx.companyId, projectId: runCtx.projectId, title: payload.title, description: payload.description, }); return { content: `Created issue ${issue.title}`, data: issue, }; }, ); } async function registerEventHandlers(ctx: PluginContext): Promise { ctx.events.on("issue.created", async (event: PluginEvent) => { pushRecord({ level: "info", source: "events.subscribe", message: "Observed issue.created", data: event, }); }); ctx.events.on("issue.updated", async (event: PluginEvent) => { pushRecord({ level: "info", source: "events.subscribe", message: "Observed issue.updated", data: event, }); }); ctx.events.on(`plugin.${PLUGIN_ID}.demo-event`, async (event: PluginEvent) => { pushRecord({ level: "info", source: "plugin-event", message: "Observed plugin demo event", data: event, }); }); } async function registerJobs(ctx: PluginContext): Promise { ctx.jobs.register(JOB_KEYS.heartbeat, async (job: PluginJobContext) => { const payload = { jobKey: job.jobKey, runId: job.runId, trigger: job.trigger, scheduledAt: job.scheduledAt, completedAt: new Date().toISOString(), }; await writeInstanceState(ctx, "last-job-run", payload); pushRecord({ level: "info", source: "jobs", message: "Kitchen Sink demo job ran", data: payload, }); await ctx.metrics.write("jobs.demo_heartbeat", 1, { trigger: job.trigger }); }); } const plugin: PaperclipPlugin = definePlugin({ async setup(ctx) { currentContext = ctx; runtimeLaunchers.set(RUNTIME_LAUNCHER.id, RUNTIME_LAUNCHER); ctx.launchers.register(RUNTIME_LAUNCHER); pushRecord({ level: "info", source: "setup", message: "Kitchen Sink plugin setup complete", data: { pluginId: PLUGIN_ID }, }); await registerEventHandlers(ctx); await registerJobs(ctx); await registerDataHandlers(ctx); await registerActionHandlers(ctx); await registerToolHandlers(ctx); }, async onHealth(): Promise { const ctx = currentContext; const config = ctx ? await getConfig(ctx) : DEFAULT_CONFIG; return { status: "ok", message: "Kitchen Sink plugin ready", details: { recordsTracked: recentRecords.length, runtimeLaunchers: runtimeLaunchers.size, processDemosEnabled: config.enableProcessDemos === true, workspaceDemosEnabled: config.enableWorkspaceDemos !== false, }, }; }, async onConfigChanged(newConfig) { pushRecord({ level: "info", source: "config", message: "Kitchen Sink config changed", data: newConfig, }); }, async onValidateConfig(config) { const errors: string[] = []; const warnings: string[] = []; const typed = config as KitchenSinkConfig; if (typed.httpDemoUrl && typeof typed.httpDemoUrl !== "string") { errors.push("httpDemoUrl must be a string"); } if (typed.allowedCommands && !Array.isArray(typed.allowedCommands)) { errors.push("allowedCommands must be an array"); } if (Array.isArray(typed.allowedCommands)) { const allowed = new Set(SAFE_COMMANDS.map((command) => command.key)); const invalid = typed.allowedCommands.filter((value) => typeof value !== "string" || !allowed.has(value)); if (invalid.length > 0) { errors.push(`allowedCommands contains unsupported values: ${invalid.join(", ")}`); } } if (typed.enableProcessDemos) { warnings.push("Process demos run local child processes and are intended only for trusted development environments."); } return { ok: errors.length === 0, warnings, errors, }; }, async onWebhook(input: PluginWebhookInput) { const payload = { endpointKey: input.endpointKey, requestId: input.requestId, rawBody: input.rawBody, parsedBody: input.parsedBody, receivedAt: new Date().toISOString(), }; const ctx = currentContext; if (ctx) { await writeInstanceState(ctx, "last-webhook", payload); } pushRecord({ level: "info", source: "webhook", message: `Received webhook ${input.endpointKey}`, data: payload, }); if (input.endpointKey !== WEBHOOK_KEYS.demo) { throw new Error(`Unsupported webhook endpoint "${input.endpointKey}"`); } }, async onShutdown() { pushRecord({ level: "warning", source: "shutdown", message: "Kitchen Sink plugin shutting down", }); }, }); export default plugin; runWorker(plugin, import.meta.url);