npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oxstudio.io/paperclip-connector

v0.0.2

Published

Alpha: PaperClip plugin that pushes agent work products into Ox Studio datasets as content briefs and reports pipeline results back to the originating ticket.

Readme

@oxstudio.io/paperclip-connector

PaperClip plugin ("OxStudio Connector") that pushes agent work products into Ox Studio datasets as CONTENT_BRIEF records and reports pipeline results back to the originating ticket.

Alpha. The 0.0.x line tracks the PaperClip plugin API, which is itself alpha ("expect breaking changes while this feature settles"). Interfaces may change between releases without a deprecation window.

Upgrading from 0.0.1. The new version asks for three additional capabilities - agent.tools.register (the submit-finding tool), issues.create and issues.wakeup (the optional issue feedback channel) - so PaperClip will ask an operator to approve them. Behavior does not change until you choose it: feedback.mode defaults to comment, exactly what 0.0.1 did. Note that the state layout moves forward one way only: after the first poll on 0.0.2, deliveries awaiting confirmation are recorded in a shape 0.0.1 cannot read, so a downgrade would orphan them (the briefs themselves are already in Ox Studio; only the feedback for them would be lost).

Two ways in

PASSIVE  agent attaches brief.json, then closes the ticket (status -> done)
         -> issue.updated / agent.run.finished
         -> brief from the attachment, or a fallback from title and description
         -> POST {baseUrl}/ingest/datasets/:id/records
         -> on failure: retry queue (5 min to 6 h backoff, up to 48 h)

ACTIVE   agent calls the submit-finding tool mid-run
         -> brief validated, dataset from the parameter or the mapping
         -> POST {baseUrl}/ingest/datasets/:id/records
         -> record ids returned synchronously, in the same run

                                                    Ox Studio
                                                    dataset record
                                                          | dataset trigger
                                                          v
                                                    sequence runs the
                                                    content pipeline
                                                          |
BACK     feedback-poll job, every 5 minutes               v
         GET /ingest/content-outputs?externalRef=   ContentOutput
         -> comment on the originating ticket, and/or
         -> a todo issue assigned to an agent, which is then woken

The passive path suits "one ticket, one piece of content". The active path suits a research agent that finds several topics in a single run and wants each confirmed before the run ends. Both land in the same dataset, are deduplicated by the same externalRef, and report back through the same feedback loop.

Requirements

  • A self-hosted PaperClip instance (Node.js >= 24.11, pnpm >= 9.15; dashboard on http://localhost:3100 by default). New instance: npx paperclipai onboard and back up secrets/master.key as the PaperClip docs instruct.
  • An Ox Studio account with at least one project and a dataset of type Content brief (CONTENT_BRIEF).

Installation

Path A - npm package (recommended)

In the PaperClip dashboard open Settings -> Plugins, click Install Plugin and enter the package name:

@oxstudio.io/paperclip-connector

or from the CLI:

npx paperclipai plugin install @oxstudio.io/paperclip-connector

Path B - git clone (development)

git clone <this repo> oxstudio-paperclip-connector
cd oxstudio-paperclip-connector
pnpm install
pnpm build       # the host resolves ./dist/* from the manifest, so build first
npx paperclipai plugin install .

After changing plugin code run pnpm build, then reload the worker explicitly:

npx paperclipai plugin upgrade oxstudio.connector

That re-reads the manifest from disk, restarts the worker on the new bundle and leaves the saved configuration alone. Do not rely on the dev watcher: despite the message the installer prints, this host does not restart the worker when dist/ changes (see Field notes).

Either way, "OxStudio Connector" appears under Settings -> Plugins with status ready.

Configuration, step by step

  1. Generate a service key in Ox Studio: open Settings -> Integrations -> Data Sources -> Add data input, pick the PaperClip AI preset, choose the project and the target dataset (or create one - schema type Content brief), optionally wire a sequence trigger, and click Generate. Copy the one-time service key (oxs_…) - it is shown exactly once.

    Ox Studio data inputs

  2. Find the dataset ID: open the dataset page in Ox Studio and read the number from the URL: /projects/<projectId>/datasets/<datasetId>.

  3. Configure the plugin: in PaperClip open Settings -> Plugins -> OxStudio Connector and fill the form:

    • Ox Studio base URL - the ingest API origin, e.g. https://app.oxstudio.example/api/data (the wizard's instructions show the exact URL for your instance),
    • Service key - paste the oxs_… key; on save it becomes an encrypted PaperClip company secret and the form only ever shows configured / not configured,
    • Dataset mappings - at least one rule; leave the matchers empty to route every finished ticket to that dataset ID,
    • Result feedback - leave enabled to get pipeline results back as ticket comments.

    Click Save configuration, then Test connection - it sends a small self-test brief (externalRef paperclip:selftest:…) to the first mapped dataset; on the Ox Studio Data Sources page the input goes Idle -> Active.

    Plugin settings page

Development

pnpm install
pnpm build       # bundles dist/worker.js + dist/manifest.js + dist/ui (esbuild presets from the SDK)
pnpm test        # vitest (config schema, mapping, ingest client, pipeline, feedback loop)
pnpm typecheck
pnpm lint
pnpm dev         # esbuild watch mode

Troubleshooting

| Symptom | Cause and fix | | --- | --- | | Test connection: "Service key rejected" / health degraded with lastAuthFailureAt | The key was revoked or the input disabled in Ox Studio (deleting a data input permanently revokes its key). Generate a new key in the Data Sources wizard and paste it into the settings form. | | Test connection: "Dataset not found for this key" (404) | The dataset ID belongs to a different project than the service key. Keys are project-scoped - generate the key in the same project as the dataset. | | Briefs deliver but no comment ever comes back | The dataset has no sequence trigger (Ox Studio dataset page -> Automation), or the feedback section is disabled. Self-hosted instances always receive feedback via polling - push callbacks cannot reach a PaperClip behind NAT/localhost (Ox Studio's anti-SSRF callback policy). | | Ticket closed but nothing was sent | No mapping matched (check agent/project/label matchers), the ticket's status is not done, or the ticket title is shorter than 3 characters with no brief.json (the plugin comments the reason on the ticket in that case). | | Plugin status error after a local-path install | The built worker is missing - run pnpm build in the plugin directory, then uninstall and install again so the server persists the package path. | | Diagnostics shows "Ox Studio unreachable" | Check oxstudioBaseUrl, that the Ox Studio data manager is running, and that the address is reachable from the machine running the PaperClip worker (a private address is fine - the connector uses direct fetch, not ctx.http). | | Diagnostics is green but no content is produced | The connector confirms delivery, not production. Check the Ox Studio side: the dataset trigger, the project processor and free sequence concurrency slots. | | Feedback issue appears but the agent never starts | The wakeup was rejected. Check heartbeat-runs for the agent and its maxConcurrentRuns; the connector records the failure on the delivery (wakeup.ok = false) and does not retry it. | | A finished run sent nothing | The run had no ticket behind it (payload.issueId was null). That is correct behavior - use the submit-finding tool for work that is not tied to a ticket. | | Diagnostics shows deliveries waiting for retry | Ox Studio was unreachable or rejected the key when the ticket closed. Fix the cause, then press Retry deliveries now instead of waiting for the backoff. |

For agents: the submit-finding tool

An agent does not have to close a ticket to send work to Ox Studio. The plugin registers a tool, oxstudio.connector:submit-finding, that an agent can call mid-run:

| Parameter | | | --- | --- | | topic (required) | What the content should be about, one sentence | | notes | Findings: facts, angles, quotes, numbers | | angle, audience, keyMessages, language | Optional brief fields | | sources | URLs backing the findings | | datasetId | Explicit target; omit to use the configured mapping for the calling agent or project | | externalRef | Stable id; resubmitting the same ref is a no-op. Omit and one is derived from the run and the topic |

The call is synchronous: on success the agent gets back { outcome: "created", datasetId, recordIds, externalRef } and can confirm the submission before the run ends. Other outcomes are equally explicit - skipped (already submitted), queued (Ox Studio unreachable, the brief is in the retry queue, do not resubmit), or an error sentence saying whether the agent should fix the brief or fetch an operator.

Operator step: allow the tool. PaperClip gates plugin tools behind its tool access policy, which denies by default (tool-access-policy.js). Until oxstudio.connector:submit-finding is allowed in the calling agent's tool profile, the agent will not see the tool and the handler is never reached. That is the first thing to check when an agent reports the tool missing.

To exercise it by hand:

curl -sS -X POST http://127.0.0.1:3100/api/plugins/tools/execute \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $PAPERCLIP_TOKEN" \
  -d '{"tool":"oxstudio.connector:submit-finding",
       "parameters":{"topic":"...","notes":"..."},
       "runContext":{"agentId":"...","runId":"...","companyId":"...","projectId":"..."}}'

or through the CLI:

npx paperclipai plugin tool:execute --payload-json '{"tool":"oxstudio.connector:submit-finding", ...}'

runContext is validated against the database, so all four ids must be real and belong together: the agent and the project must belong to the company, and the run must belong to that agent. Fabricated ids are rejected with HTTP 403 before the plugin is reached.

Diagnostics

The settings page opens with a Diagnostics card: a single status badge (healthy / degraded / not working), one line per check (configuration, service key, dataset mappings, Ox Studio reachability, deliveries waiting for retry), the counters and timestamps that matter (last accepted delivery, last feedback poll, retry queue depth and next attempt, pending confirmations), and three buttons - Run diagnostics, Retry deliveries now and Poll now.

Two things worth knowing:

  • Run diagnostics only reads. It calls GET {baseUrl}/ingest/content-outputs?externalRef=paperclip:diagnostics:probe (scope output:read) with the configured service key, so it exercises the same host, network path and credential as a delivery without creating anything. Test connection, by contrast, writes a real self-test brief into the first mapped dataset.
  • This card is the only place the connector reports on itself. PaperClip's own plugin health (the ready badge, the Status tab, GET /api/plugins/:id/health) reports host checks only - registry, manifest and status - so it stays green even when every delivery is failing.

The card never carries a secret value: the service key appears exclusively as a boolean ("Service key resolvable"), and a test asserts that no resolved secret can reach the UI bridge.

Settings page and feedback loop

The plugin contributes a settingsPage UI slot, so Settings -> Plugins -> OxStudio Connector shows a dedicated form: Ox Studio base URL, the service key (pasted once - it becomes an encrypted PaperClip company secret and the form afterwards only shows configured / not configured), the mapping editor (agents / projects / labels -> dataset ID) and the feedback section. The form validates with the same Zod schema the worker uses. Test connection sends a small self-test brief (externalRef paperclip:selftest:<ts>) to the first mapped dataset and reports success / bad key (401) / dataset outside the key's project (404) / network.

The feedback loop is a scheduled job (feedback-poll, cron every 5 minutes, paced per company by feedback.pollMinutes): for every delivery awaiting confirmation it calls GET {baseUrl}/ingest/content-outputs?externalRef=… (scope output:read). A terminal output (SUCCESS/FAILURE) is reported exactly once through the configured channels (see Feedback modes), carrying status, execution, title and the permalink when the output payload has one. The confirmedAt mark lives in plugin state, so worker restarts never duplicate a comment or an issue. Deliveries with no output for 7 days are marked expired and logged, silently. The same job first drains the retry queue, and it does that even when the feedback loop is disabled: undelivered briefs are not feedback, and turning feedback off must not discard them.

Polling rather than a push callback, because a self-hosted PaperClip behind NAT or localhost is unreachable for Ox Studio's anti-SSRF callback policy.

Feedback modes

feedback.mode decides where a finished content result goes:

| Mode | What happens | | --- | --- | | comment (default) | A comment on the originating ticket. Unchanged from 0.0.1. | | issue | A todo issue carrying the result, assigned to a chosen agent, which is then woken. | | both | Comment and issue. |

The issue modes exist because a comment records a result but does not act on it. The agent that ordered the content finished its run long ago; nothing in PaperClip picks the outcome up. Pointing the result at an agent - a librarian that files it, links it to the topic, or decides what comes next - closes that loop. The connector deliberately stops there: it delivers the facts and wakes the right agent, and every decision about what the result means belongs to that agent, which has the context the plugin does not.

Two details worth knowing:

  • Submissions made through submit-finding have no ticket, so in comment mode they have nowhere to report back to. The connector confirms them and logs why once. If agents use the tool, pick issue or both.
  • Creating the issue does not start the agent. ctx.issues.create only assigns it; the connector follows with an explicit ctx.issues.requestWakeup (idempotency key derived from the brief reference). If the wakeup fails - an agent at its concurrency limit, say - the issue still exists and the result is still confirmed, so nothing is delivered twice.

Configuration

| Field | Description | | --- | --- | | oxstudioBaseUrl | Ox Studio ingest API origin, e.g. https://app.oxstudio.example/api/data | | serviceKeySecretRef | Service key from the Ox Studio Data Sources wizard. Declared with format: "secret-ref", so the host stores it as an encrypted secret and the worker only ever sees a reference; the value is resolved at call time via ctx.secrets.resolve() and is never logged or persisted by the plugin. | | mappings[] | Routing rules (agent / project / label -> datasetId); first match wins, empty matcher lists match everything | | feedback | { enabled, pollMinutes, mode, issueAssigneeAgentId?, issueProjectId? } for the result feedback loop. See Feedback modes above. The job runs on a 5-minute cron, so 5 minutes is the finest cadence that exists: smaller values are raised to 5 on save (with a warning) rather than rejected, and Poll now on the settings page covers the "check right now" case. |

The manifest carries the JSON Schema (host renders the settings form from it); src/config.ts holds the equivalent Zod schema the worker validates against at runtime (onValidateConfig, onConfigChanged, health checks).

How work products become briefs

The worker listens to issue.updated (primary trigger: ticket status becomes done) and agent.run.finished (for runs that carry an issueId). For each candidate ticket it resolves the target dataset from mappings, reads the work product and delivers one CONTENT_BRIEF record to POST {oxstudioBaseUrl}/ingest/datasets/:datasetId/records.

externalRef is always paperclip:<companyId>:<issueId> for tickets and paperclip:<companyId>:run:<runId>:<hash of the topic> for tool submissions - stable per unit of work, so repeating an action cannot create duplicates (the send registry in plugin state skips locally, and the ingest API dedupes server-side by returning skipped).

Delivery failures are handled by class:

| Failure | What happens | | --- | --- | | Network error or 5xx | The brief goes into the retry queue with the record marked pending. Backoff runs 5 min, 15 min, 45 min, 2 h 15, then 6 h, giving up after 10 attempts or 48 hours. | | 401/403 (rejected key) | Also queued, but the retry pass stops without burning an attempt: nothing succeeds until an operator fixes the key. Diagnostics shows the rejection. | | 400/422 (bad brief) | Not retried - the payload will not improve. The reason is commented on the ticket and the delivery is marked failed. |

Binary attachments (images, video) are NOT transferred - the dataset accepts JSON only; links can travel in sources[].

Agent conventions

Attach brief.json first, set the ticket to done second. The connector reads the attachments at the moment the ticket closes and never re-reads them. A ticket closed without the attachment gets a best-effort brief assembled from the title, the description and the last agent comment, and attaching the file afterwards changes nothing: the externalRef is already spent, so Ox Studio answers skipped. There is no second chance, by design - that is also what makes re-closing a ticket safe.

If the agent can call tools, submit-finding is the better path: it validates immediately, returns the record ids inside the same run, and has no ordering requirement at all.

The brief.json payload follows the Ox Studio content-brief contract (mirrored in src/mapping/content-brief.ts): topic required, 3-300 characters; optional angle, audience, keyMessages[], mustInclude[], mustAvoid[], sources[] with {url|excerpt, title, authority 0-1}, targets, language, notes. The attachment is authoritative - an invalid brief.json is rejected with a comment on the ticket, not silently replaced.

Snippet to paste into an agent's instructions:

When you finish research that should become content:
1. Preferred: call the `submit-finding` tool with `topic` and `notes`. It returns the Ox Studio
   record ids, so you can confirm the submission before ending the run.
2. Fallback (no tool access): attach a `brief.json` file to the ticket, then set the ticket to `done`.
   Attach the file BEFORE changing the status - the connector reads attachments at the moment the
   ticket closes and never re-reads them.

Ox Studio side: what must be running

The connector confirms delivery, not production. A green Diagnostics card means the brief reached Ox Studio; whether content comes out the other end depends on the Ox Studio instance:

| Requirement | Symptom when it is missing | | --- | --- | | DatasetTriggerSequence enabled in SystemSettings.SERVICES_ACTIVITY | The record sits in the dataset and no execution is ever created. | | A running project-processor | The execution is created and stays PENDING. | | Free maxConcurrency slots on the sequence | Executions queue indefinitely. The usual cause is hung executions holding slots; close them in the Ox Studio UI to release them. | | A service key with both dataset:write and output:read | Deliveries work, but Diagnostics and the feedback loop return 401. Keys from the Data Sources wizard carry both. |

Field notes (SDK findings worth keeping)

Knowledge gathered from the PaperClip sources that the SDK docs do not spell out - kept here for future connectors:

  • A plugin is an npm package with a paperclipPlugin block in package.json pointing at built bundles: { "manifest": "./dist/manifest.js", "worker": "./dist/worker.js", "ui": "./dist/ui/" }. dist/manifest.js must exist at install time.
  • instanceConfigSchema in the manifest is JSON Schema, not Zod (the manifest is serialized into the DB). Zod is for runtime validation inside the worker; the SDK re-exports it (import { z } from '@paperclipai/plugin-sdk', zod v4).
  • Declaring a config field with format: "secret-ref" makes the dashboard render a secret picker; the stored value is an EnvSecretRefBinding ({ type: "secret_ref", secretId, ... }) or a raw string that the server converts to a stored secret on save. ctx.secrets.resolve() accepts both.
  • onHealth() takes no context argument - keep the PluginContext from setup() in module scope. ctx.config.get(companyId?) is company-scoped; remember the companyId from onConfigChanged(newConfig, { companyId }).
  • runWorker(plugin, import.meta.url) at the bottom of the worker is safe to import from tests - it verifies the module is the process entrypoint and returns otherwise.
  • @paperclipai/plugin-sdk/testing exports createTestHarness({ manifest, config? }) with an in-memory host; @paperclipai/plugin-sdk/bundlers exports createPluginBundlerPresets() with ready esbuild configs for worker, manifest and UI bundles.
  • The host validates the manifest on install and rejects an empty ui.slots array (ui.slots: Too small: expected array to have >=1 items) - omit ui entirely until the plugin ships a real slot.
  • The server validates saved config strictly against instanceConfigSchema (ajv). A format: "secret-ref" field must NOT declare type: "string" - the picker stores a binding object { type: "secret_ref", secretId } and a string type rejects it ("Configuration does not match the plugin's instanceConfigSchema"). Declare the field with format only.
  • The "paste a raw value" fallback in the secret field stores the raw string in configJson (no automatic conversion to a stored secret), and ctx.secrets.resolve fails closed on non-UUID strings. Always create a company secret (Settings -> Secrets or the field's "Create secret" button) and bind it.
  • ctx.http.fetch resolves DNS and only allows public addresses (it filters private/reserved ranges). For a connector whose target service lives on the operator's own network, use standard Node fetch - the SDK documents that direct fetch is allowed and ctx.http exists for host-managed tracing.
  • A settingsPage UI slot REPLACES the auto-generated config form on the plugin page ("If the plugin has a custom settingsPage slot, prefer that over auto-generated form" - ui/src/pages/PluginSettings.tsx), so a custom settings page must implement config editing itself. Plugin UI runs same-origin with the dashboard session and may call host APIs directly (/api/plugins/<pluginKey>/config, /api/companies/:id/secrets).
  • In UI bundles never import the SDK root (@paperclipai/plugin-sdk) - it drags in Node-only modules (dev-server) and the browser build fails on node:fs. Import @paperclipai/plugin-sdk/ui and depend on zod directly.
  • GET /api/plugins/:id/health and the dashboard's "Health Status" card return the host's own checks only (registry, manifest, status); the host in 2026.831.1 never calls the worker's onHealth(). A plugin that wants to tell an operator something about itself has to render it, which is why the connector's diagnostics live on its settings page. onHealth stays implemented and delegates to the same collector, ready for a host version that starts calling it.
  • ctx.issues.create assigns an agent but does not start it. A created issue sits in the assignee's queue until something requests a wakeup, so a plugin that wants an agent to act has to call ctx.issues.requestWakeup(issueId, companyId, { reason, idempotencyKey }) itself. originKind for plugins is the template literal type plugin:${string}, which makes plugin-created issues filterable via ctx.issues.list({ originKindPrefix }).
  • Run-lifecycle events ARE emitted, but not through the activity log: the server calls publishRunLifecyclePluginEvent(run) in @paperclipai/server/dist/services/heartbeat.js and maps run.status to agent.run.started / agent.run.finished (succeeded) / agent.run.failed (failed or timed_out) / agent.run.cancelled. There is no agent.run.completed. The envelope carries entityType: "heartbeat_run" and entityId = run.id, so the ticket must be read from payload.issueId (sourced from run.contextSnapshot, null for runs without a ticket). Ticket status changes are a separate path: issue.updated from the activity log (server/src/services/activity-log.ts), emitted post-commit, with entityId = issue id.
  • When auditing host behavior, read the installed package (~/.npm/_npx/*/node_modules/@paperclipai/server/dist). GitHub code search does not return every server file, and an earlier audit of this connector concluded from it that agent.run.finished was never emitted - it is.
  • Local-path installs: npx paperclipai plugin install <path>; the dashboard can also install by npm package name (Install Plugin button). Reinstalling over an existing install returns Plugin already installed.
  • For a manifest change on a local-path install, npx paperclipai plugin upgrade <key> is the cheap path: it re-reads the manifest from disk, keeps configJson untouched and restarts the worker. uninstall + install . also works and (without --force) likewise preserves the config and even the same plugin row; --force is a hard purge of config and state, so use it only when you mean to wipe. Caveat from plugin-lifecycle.js: if the new manifest ADDS capabilities, upgrade parks the plugin in upgrade_pending with the worker stopped until an operator approves.
  • The dev watcher does not reload the worker here. onboard.log shows plugin-dev-watcher: initialized but never watching local plugin for changes, and a pnpm build produces no new worker started line even though the plugin was installed from a local path. After rebuilding, reload explicitly with plugin disable <key> && plugin enable <key> (or plugin upgrade <key>), otherwise every live check runs against the old bundle.
  • Do not put a real floor in instanceConfigSchema. The host validates configJson against that schema and returns HTTP 400 before calling the worker's validateConfig, so a tightened minimum both makes onValidateConfig warnings unreachable and locks operators out of re-saving a config that already stores a smaller value. Keep the manifest schema permissive, enforce the limit in the Zod schema (clamp on read) and in the settings page input, and surface the adjustment as a warnings[] entry - the host renders it as Warnings: … from config/test.

License

MIT