Compare commits

..

25 Commits

Author SHA1 Message Date
Dotta
59507f18ec Allow onboarding to continue after failed env test 2026-03-04 14:46:29 -06:00
Dotta
b198b4a02c fix(server): require embedded-postgres for embedded DB mode 2026-03-04 14:46:03 -06:00
Dotta
5606f76ab4 Add onboarding retry action to unset ANTHROPIC_API_KEY 2026-03-04 14:40:12 -06:00
Dotta
0542f555ba Fix markdown list markers in editor and comment rendering 2026-03-04 12:20:29 -06:00
Dotta
18c9eb7b1e Filter out archived companies from new issue company selector
Companies with status "archived" are now hidden from the company
dropdown in the NewIssueDialog, matching the expected behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:20:14 -06:00
Dotta
675e0dcff1 docs: document issue search (q= param) in Paperclip skill
The API already supports full-text search via ?q= on the issues list
endpoint. Added documentation to both SKILL.md and the API reference
so agents know they can search issues by title, identifier,
description, and comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:35:13 -06:00
Dotta
b66c6d017a Adjust docker onboard smoke defaults and console guidance 2026-03-04 10:48:36 -06:00
Dotta
bbf7490f32 Fix onboard smoke Docker flow for clean npx runs 2026-03-04 10:42:07 -06:00
Dotta
5dffdbb382 chore: release v0.2.6 2026-03-04 10:24:03 -06:00
Dotta
ea637110ac Add Ubuntu onboard smoke flow and lazy-load auth startup 2026-03-04 10:15:11 -06:00
Dotta
3ae9d95354 fix: stabilize paperclipai run server import errors 2026-03-04 10:02:23 -06:00
Dotta
a95e38485d fix: doctor command auto-creates directories and treats LLM as optional
Instead of showing alarming warnings on first run when storage, log,
and database directories don't exist, the doctor checks now silently
create them and report pass. LLM provider is treated as optional
rather than a warning when not configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:58:30 -06:00
Dotta
c7c96feef7 docs: simplify quickstart to npx onboard, mention create-adapter skill
- Remove Docker option from quickstart, make `npx paperclipai onboard --yes` the recommended path
- Add tip about `create-agent-adapter` skill in the creating-an-adapter guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:11:06 -06:00
Dotta
7e387a1883 docs: update Discord invite link to https://discord.gg/m4HZY7xNG3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:54:55 -06:00
Dotta
108bb9bd15 docs: update quickstart CTA to npx paperclipai onboard --yes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:47:33 -06:00
Dotta
6141d5c3f2 chore: release v0.2.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:14:33 -06:00
Dotta
a4da932d8d fix: remove stale server/ui-dist from git add in release.sh
The release script cleaned up temporary build artifacts (ui-dist, skills)
before staging files for the commit, then tried to git add server/ui-dist
which no longer existed.  Remove it from the git add list since these
artifacts should never be committed — they're only needed during publish.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:14:28 -06:00
Dotta
ab3b9ab19f chore: bump all packages to 0.2.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:10:52 -06:00
Dotta
f4a5b00116 fix: bundle skills directory into npm packages for runtime discovery
The claude-local, codex-local adapters and the server all resolve a
skills/ directory using __dirname-relative paths that only work inside
the monorepo.  When installed from npm the paths point outside the
package and cause ENOENT on readdir/readFile.

- Update both adapter execute.ts files to try a published-path
  candidate (../../skills from dist/) before falling back to the
  monorepo dev path (../../../../../skills from src/).
- Update server readSkillMarkdown() to try the published path first.
- Add "skills" to the files array in server, claude-local, and
  codex-local package.json so npm includes them.
- Update release.sh to copy the repo-root skills/ into each package
  before publish, and clean up after.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:06:12 -06:00
Dotta
09d2ef1a37 fix: restore docs deleted in v0.2.3 release, add Paperclip branding
- Restored docs/ directory that was accidentally deleted by `git add -A`
  in the v0.2.3 release script
- Replaced generic "P" favicon with actual paperclip icon using brand
  primary color (#2563EB)
- Added light/dark logo SVGs for Mintlify navbar (paperclip icon + wordmark)
- Updated docs.json with logo configuration for dark/light mode
- Fixed release.sh to stage only release-related files instead of `git add -A`
  to prevent sweeping unrelated changes into release commits

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:49:43 -06:00
Dotta
d18312d6de fix: bundle UI dist into server package for npm publishing
The server's static-ui mode resolves the UI dist path relative to its
own directory. In the monorepo it finds ../../ui/dist, but when published
to npm the UI package isn't available.

- server/src/app.ts: try ../ui-dist (published) then ../../ui/dist (dev),
  gracefully degrade to API-only if neither exists
- server/package.json: include ui-dist/ in published files
- scripts/release.sh: build UI and copy dist to server/ui-dist before
  publishing, clean up in restore step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:45:45 -06:00
Dotta
28bf5e9e9b chore: release v0.2.3 2026-03-03 15:39:13 -06:00
Dotta
925680f736 fix: include migration files in @paperclipai/db and improve server error msg
- db build now copies src/migrations/ to dist/migrations/ after tsc,
  so SQL + meta JSON files are included in the published package.
  Without this, `import("@paperclipai/server")` fails at runtime with
  ENOENT when scanning for migration files.

- CLI's importServerEntry() now distinguishes between "module not found"
  and "server crashed during startup" for clearer error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:30:50 -06:00
Dotta
4e9c1d83be Fix domain: paperclip.dev -> paperclip.ing
Updated all references across README and doc plans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:25:19 -06:00
Dotta
b2737b9571 chore: release v0.2.2 2026-03-03 15:10:25 -06:00
47 changed files with 771 additions and 158 deletions

40
Dockerfile.onboard-smoke Normal file
View File

@@ -0,0 +1,40 @@
FROM ubuntu:24.04
ARG NODE_MAJOR=20
ARG PAPERCLIPAI_VERSION=latest
ARG HOST_UID=10001
ENV DEBIAN_FRONTEND=noninteractive \
PAPERCLIP_HOME=/paperclip \
PAPERCLIP_OPEN_ON_LISTEN=false \
HOST=0.0.0.0 \
PORT=3100 \
HOME=/home/paperclip \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NPM_CONFIG_UPDATE_NOTIFIER=false \
NODE_MAJOR=${NODE_MAJOR} \
PAPERCLIPAI_VERSION=${PAPERCLIPAI_VERSION}
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg locales \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& locale-gen en_US.UTF-8 \
&& groupadd --gid 10001 paperclip \
&& useradd --create-home --shell /bin/bash --uid "${HOST_UID}" --gid 10001 paperclip \
&& mkdir -p /paperclip /home/paperclip/workspace \
&& chown -R paperclip:paperclip /paperclip /home/paperclip \
&& rm -rf /var/lib/apt/lists/*
VOLUME ["/paperclip"]
WORKDIR /home/paperclip/workspace
EXPOSE 3100
USER paperclip
CMD ["bash", "-lc", "set -euo pipefail; mkdir -p \"$PAPERCLIP_HOME\"; npx --yes \"paperclipai@${PAPERCLIPAI_VERSION}\" onboard --yes --data-dir \"$PAPERCLIP_HOME\""]

View File

@@ -4,15 +4,15 @@
<p align="center"> <p align="center">
<a href="#quickstart"><strong>Quickstart</strong></a> &middot; <a href="#quickstart"><strong>Quickstart</strong></a> &middot;
<a href="https://paperclip.dev/docs"><strong>Docs</strong></a> &middot; <a href="https://paperclip.ing/docs"><strong>Docs</strong></a> &middot;
<a href="https://github.com/paperclipai/paperclip"><strong>GitHub</strong></a> &middot; <a href="https://github.com/paperclipai/paperclip"><strong>GitHub</strong></a> &middot;
<a href="https://discord.gg/paperclip"><strong>Discord</strong></a> <a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/paperclipai/paperclip/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" /></a> <a href="https://github.com/paperclipai/paperclip/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" /></a>
<a href="https://github.com/paperclipai/paperclip/stargazers"><img src="https://img.shields.io/github/stars/paperclipai/paperclip?style=flat" alt="Stars" /></a> <a href="https://github.com/paperclipai/paperclip/stargazers"><img src="https://img.shields.io/github/stars/paperclipai/paperclip?style=flat" alt="Stars" /></a>
<a href="https://discord.gg/paperclip"><img src="https://img.shields.io/discord/000000000?label=discord" alt="Discord" /></a> <a href="https://discord.gg/m4HZY7xNG3"><img src="https://img.shields.io/discord/000000000?label=discord" alt="Discord" /></a>
</p> </p>
<br/> <br/>
@@ -174,7 +174,7 @@ Paperclip handles the hard orchestration details correctly.
Open source. Self-hosted. No Paperclip account required. Open source. Self-hosted. No Paperclip account required.
```bash ```bash
npx paperclipai onboard npx paperclipai onboard --yes
``` ```
Or manually: Or manually:
@@ -249,7 +249,7 @@ We welcome contributions. See the [contributing guide](CONTRIBUTING.md) for deta
## Community ## Community
- [Discord](#) — Coming soon - [Discord](https://discord.gg/m4HZY7xNG3) — Join the community
- [GitHub Issues](https://github.com/paperclipai/paperclip/issues) — bugs and feature requests - [GitHub Issues](https://github.com/paperclipai/paperclip/issues) — bugs and feature requests
- [GitHub Discussions](https://github.com/paperclipai/paperclip/discussions) — ideas and RFC - [GitHub Discussions](https://github.com/paperclipai/paperclip/discussions) — ideas and RFC

View File

@@ -1,5 +1,75 @@
# paperclipai # paperclipai
## 0.2.6
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.6
- @paperclipai/adapter-utils@0.2.6
- @paperclipai/db@0.2.6
- @paperclipai/adapter-claude-local@0.2.6
- @paperclipai/adapter-codex-local@0.2.6
- @paperclipai/adapter-openclaw@0.2.6
- @paperclipai/server@0.2.6
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.5
- @paperclipai/adapter-utils@0.2.5
- @paperclipai/db@0.2.5
- @paperclipai/adapter-claude-local@0.2.5
- @paperclipai/adapter-codex-local@0.2.5
- @paperclipai/adapter-openclaw@0.2.5
- @paperclipai/server@0.2.5
## 0.2.4
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.4
- @paperclipai/adapter-utils@0.2.4
- @paperclipai/db@0.2.4
- @paperclipai/adapter-claude-local@0.2.4
- @paperclipai/adapter-codex-local@0.2.4
- @paperclipai/adapter-openclaw@0.2.4
- @paperclipai/server@0.2.4
## 0.2.3
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.3
- @paperclipai/adapter-utils@0.2.3
- @paperclipai/db@0.2.3
- @paperclipai/adapter-claude-local@0.2.3
- @paperclipai/adapter-codex-local@0.2.3
- @paperclipai/adapter-openclaw@0.2.3
- @paperclipai/server@0.2.3
## 0.2.2
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.2
- @paperclipai/adapter-utils@0.2.2
- @paperclipai/db@0.2.2
- @paperclipai/adapter-claude-local@0.2.2
- @paperclipai/adapter-codex-local@0.2.2
- @paperclipai/adapter-openclaw@0.2.2
- @paperclipai/server@0.2.2
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "paperclipai", "name": "paperclipai",
"version": "0.2.1", "version": "0.2.6",
"description": "Paperclip CLI — orchestrate AI agent teams to run a business", "description": "Paperclip CLI — orchestrate AI agent teams to run a business",
"type": "module", "type": "module",
"bin": { "bin": {

View File

@@ -39,15 +39,7 @@ export async function databaseCheck(config: PaperclipConfig, configPath?: string
const dataDir = resolveRuntimeLikePath(config.database.embeddedPostgresDataDir, configPath); const dataDir = resolveRuntimeLikePath(config.database.embeddedPostgresDataDir, configPath);
const reportedPath = dataDir; const reportedPath = dataDir;
if (!fs.existsSync(dataDir)) { if (!fs.existsSync(dataDir)) {
return { fs.mkdirSync(reportedPath, { recursive: true });
name: "Database",
status: "warn",
message: `Embedded PostgreSQL data directory does not exist: ${reportedPath}`,
canRepair: true,
repair: () => {
fs.mkdirSync(reportedPath, { recursive: true });
},
};
} }
return { return {

View File

@@ -5,20 +5,16 @@ export async function llmCheck(config: PaperclipConfig): Promise<CheckResult> {
if (!config.llm) { if (!config.llm) {
return { return {
name: "LLM provider", name: "LLM provider",
status: "warn", status: "pass",
message: "No LLM provider configured", message: "No LLM provider configured (optional)",
canRepair: false,
repairHint: "Run `paperclipai configure --section llm` to set one up",
}; };
} }
if (!config.llm.apiKey) { if (!config.llm.apiKey) {
return { return {
name: "LLM provider", name: "LLM provider",
status: "warn", status: "pass",
message: `${config.llm.provider} configured but no API key set`, message: `${config.llm.provider} configured but no API key set (optional)`,
canRepair: false,
repairHint: "Run `paperclipai configure --section llm`",
}; };
} }

View File

@@ -8,15 +8,7 @@ export function logCheck(config: PaperclipConfig, configPath?: string): CheckRes
const reportedDir = logDir; const reportedDir = logDir;
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDir)) {
return { fs.mkdirSync(reportedDir, { recursive: true });
name: "Log directory",
status: "warn",
message: `Log directory does not exist: ${reportedDir}`,
canRepair: true,
repair: () => {
fs.mkdirSync(reportedDir, { recursive: true });
},
};
} }
try { try {

View File

@@ -7,16 +7,7 @@ export function storageCheck(config: PaperclipConfig, configPath?: string): Chec
if (config.storage.provider === "local_disk") { if (config.storage.provider === "local_disk") {
const baseDir = resolveRuntimeLikePath(config.storage.localDisk.baseDir, configPath); const baseDir = resolveRuntimeLikePath(config.storage.localDisk.baseDir, configPath);
if (!fs.existsSync(baseDir)) { if (!fs.existsSync(baseDir)) {
return { fs.mkdirSync(baseDir, { recursive: true });
name: "Storage",
status: "warn",
message: `Local storage directory does not exist: ${baseDir}`,
canRepair: true,
repair: () => {
fs.mkdirSync(baseDir, { recursive: true });
},
repairHint: "Run with --repair to create local storage directory",
};
} }
try { try {

View File

@@ -84,6 +84,15 @@ function isModuleNotFoundError(err: unknown): boolean {
return err.message.includes("Cannot find module"); return err.message.includes("Cannot find module");
} }
function getMissingModuleSpecifier(err: unknown): string | null {
if (!(err instanceof Error)) return null;
const packageMatch = err.message.match(/Cannot find package '([^']+)' imported from/);
if (packageMatch?.[1]) return packageMatch[1];
const moduleMatch = err.message.match(/Cannot find module '([^']+)'/);
if (moduleMatch?.[1]) return moduleMatch[1];
return null;
}
function maybeEnableUiDevMiddleware(entrypoint: string): void { function maybeEnableUiDevMiddleware(entrypoint: string): void {
if (process.env.PAPERCLIP_UI_DEV_MIDDLEWARE !== undefined) return; if (process.env.PAPERCLIP_UI_DEV_MIDDLEWARE !== undefined) return;
const normalized = entrypoint.replaceAll("\\", "/"); const normalized = entrypoint.replaceAll("\\", "/");
@@ -106,9 +115,17 @@ async function importServerEntry(): Promise<void> {
try { try {
await import("@paperclipai/server"); await import("@paperclipai/server");
} catch (err) { } catch (err) {
const missingSpecifier = getMissingModuleSpecifier(err);
const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@paperclipai/server";
if (isModuleNotFoundError(err) && missingServerEntrypoint) {
throw new Error(
`Could not locate a Paperclip server entrypoint.\n` +
`Tried: ${devEntry}, @paperclipai/server\n` +
`${formatError(err)}`,
);
}
throw new Error( throw new Error(
`Could not locate a Paperclip server entrypoint.\n` + `Paperclip server failed to start.\n` +
`Tried: ${devEntry}, @paperclipai/server\n` +
`${formatError(err)}`, `${formatError(err)}`,
); );
} }

View File

@@ -23,7 +23,7 @@ const DATA_DIR_OPTION_HELP =
program program
.name("paperclipai") .name("paperclipai")
.description("Paperclip CLI — setup, diagnose, and configure your instance") .description("Paperclip CLI — setup, diagnose, and configure your instance")
.version("0.2.0"); .version("0.2.6");
program.hook("preAction", (_thisCommand, actionCommand) => { program.hook("preAction", (_thisCommand, actionCommand) => {
const options = actionCommand.optsWithGlobals() as DataDirOptionLike; const options = actionCommand.optsWithGlobals() as DataDirOptionLike;

View File

@@ -66,3 +66,35 @@ Notes:
- Without API keys, the app still runs normally. - Without API keys, the app still runs normally.
- Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites. - Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites.
## Onboard Smoke Test (Ubuntu + npm only)
Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify:
- `npx paperclipai onboard --yes` completes
- the server binds to `0.0.0.0:3100` so host access works
- onboard/run banners and startup logs are visible in your terminal
Build + run:
```sh
./scripts/docker-onboard-smoke.sh
```
Open: `http://localhost:3131` (default smoke host port)
Useful overrides:
```sh
HOST_PORT=3200 PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
PAPERCLIP_DEPLOYMENT_MODE=authenticated PAPERCLIP_DEPLOYMENT_EXPOSURE=private ./scripts/docker-onboard-smoke.sh
```
Notes:
- Persistent data is mounted at `./data/docker-onboard-smoke` by default.
- Container runtime user id defaults to your local `id -u` so the mounted data dir stays writable while avoiding root runtime.
- Smoke script defaults to `authenticated/private` mode so `HOST=0.0.0.0` can be exposed to the host.
- Smoke script defaults host port to `3131` to avoid conflicts with local Paperclip on `3100`.
- Run the script in the foreground to watch the onboarding flow; stop with `Ctrl+C` after validation.
- The image definition is in `Dockerfile.onboard-smoke`.

View File

@@ -127,7 +127,7 @@ Response:
}, },
"onboarding": { "onboarding": {
"instructions": "You are being invited to join Acme Corp as an employee agent...", "instructions": "You are being invited to join Acme Corp as an employee agent...",
"skillUrl": "https://app.paperclip.dev/skills/paperclip/SKILL.md", "skillUrl": "https://app.paperclip.ing/skills/paperclip/SKILL.md",
"requiredFields": { "requiredFields": {
"name": "Your display name", "name": "Your display name",
"adapterType": "How Paperclip should send you heartbeats", "adapterType": "How Paperclip should send you heartbeats",

View File

@@ -577,7 +577,7 @@ The Company Store is a registry for discovering and installing modules and templ
"id": "startup-in-a-box", "id": "startup-in-a-box",
"name": "Startup in a Box", "name": "Startup in a Box",
"description": "5-agent startup team", "description": "5-agent startup team",
"url": "https://store.paperclip.dev/templates/startup-in-a-box.json", "url": "https://store.paperclip.ing/templates/startup-in-a-box.json",
"tags": ["startup", "team"] "tags": ["startup", "team"]
} }
] ]

View File

@@ -5,6 +5,10 @@ summary: Guide to building a custom adapter
Build a custom adapter to connect Paperclip to any agent runtime. Build a custom adapter to connect Paperclip to any agent runtime.
<Tip>
If you're using Claude Code, the `create-agent-adapter` skill can guide you through the full adapter creation process interactively. Just ask Claude to create a new adapter and it will walk you through each step.
</Tip>
## Package Structure ## Package Structure
``` ```

View File

@@ -9,6 +9,10 @@
"dark": "#1D4ED8" "dark": "#1D4ED8"
}, },
"favicon": "/favicon.svg", "favicon": "/favicon.svg",
"logo": {
"dark": "/images/logo-dark.svg",
"light": "/images/logo-light.svg"
},
"topbarLinks": [ "topbarLinks": [
{ {
"name": "GitHub", "name": "GitHub",

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 32 32" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect width="32" height="32" rx="6" fill="#2563EB"/> <rect x="-4" y="-4" width="32" height="32" rx="6" fill="#2563EB"/>
<path d="M10 8h6a6 6 0 0 1 0 12h-2v4h-4V8zm4 8h2a2 2 0 0 0 0-4h-2v4z" fill="white"/> <path stroke="#ffffff" stroke-width="2" d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="32" viewBox="0 0 140 32" fill="none">
<g stroke-linecap="round" stroke-linejoin="round">
<path stroke="#e4e4e7" stroke-width="2" d="m18 4-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"/>
</g>
<text x="32" y="22" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#e4e4e7">Paperclip</text>
</svg>

After

Width:  |  Height:  |  Size: 474 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="32" viewBox="0 0 140 32" fill="none">
<g stroke-linecap="round" stroke-linejoin="round">
<path stroke="#18181b" stroke-width="2" d="m18 4-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"/>
</g>
<text x="32" y="22" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#18181b">Paperclip</text>
</svg>

After

Width:  |  Height:  |  Size: 474 B

View File

@@ -5,24 +5,15 @@ summary: Get Paperclip running in minutes
Get Paperclip running locally in under 5 minutes. Get Paperclip running locally in under 5 minutes.
## Option 1: Docker Compose (Recommended) ## Quick Start (Recommended)
The fastest way to start. No Node.js install needed.
```sh ```sh
docker compose -f docker-compose.quickstart.yml up --build npx paperclipai onboard --yes
``` ```
Open [http://localhost:3100](http://localhost:3100). That's it. This walks you through setup, configures your environment, and gets Paperclip running.
The Docker image includes Claude Code CLI and Codex CLI pre-installed for local adapter runs. Pass API keys to enable them: ## Local Development
```sh
ANTHROPIC_API_KEY=sk-... OPENAI_API_KEY=sk-... \
docker compose -f docker-compose.quickstart.yml up --build
```
## Option 2: Local Development
Prerequisites: Node.js 20+ and pnpm 9+. Prerequisites: Node.js 20+ and pnpm 9+.
@@ -33,9 +24,9 @@ pnpm dev
This starts the API server and UI at [http://localhost:3100](http://localhost:3100). This starts the API server and UI at [http://localhost:3100](http://localhost:3100).
No Docker or external database required — Paperclip uses an embedded PostgreSQL instance by default. No external database required — Paperclip uses an embedded PostgreSQL instance by default.
## Option 3: One-Command Bootstrap ## One-Command Bootstrap
```sh ```sh
pnpm paperclipai run pnpm paperclipai run

View File

@@ -1,5 +1,35 @@
# @paperclipai/adapter-utils # @paperclipai/adapter-utils
## 0.2.6
### Patch Changes
- Version bump (patch)
## 0.2.5
### Patch Changes
- Version bump (patch)
## 0.2.4
### Patch Changes
- Version bump (patch)
## 0.2.3
### Patch Changes
- Version bump (patch)
## 0.2.2
### Patch Changes
- Version bump (patch)
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@paperclipai/adapter-utils", "name": "@paperclipai/adapter-utils",
"version": "0.2.1", "version": "0.2.6",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",

View File

@@ -1,5 +1,45 @@
# @paperclipai/adapter-claude-local # @paperclipai/adapter-claude-local
## 0.2.6
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.6
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.5
## 0.2.4
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.4
## 0.2.3
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.3
## 0.2.2
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.2
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@paperclipai/adapter-claude-local", "name": "@paperclipai/adapter-claude-local",
"version": "0.2.1", "version": "0.2.6",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
@@ -32,7 +32,8 @@
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
}, },
"files": [ "files": [
"dist" "dist",
"skills"
], ],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",

View File

@@ -27,10 +27,19 @@ import {
isClaudeUnknownSessionError, isClaudeUnknownSessionError,
} from "./parse.js"; } from "./parse.js";
const PAPERCLIP_SKILLS_DIR = path.resolve( const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
path.dirname(fileURLToPath(import.meta.url)), const PAPERCLIP_SKILLS_CANDIDATES = [
"../../../../../skills", path.resolve(__moduleDir, "../../skills"), // published: <pkg>/dist/server/ -> <pkg>/skills/
); path.resolve(__moduleDir, "../../../../../skills"), // dev: src/server/ -> repo root/skills/
];
async function resolvePaperclipSkillsDir(): Promise<string | null> {
for (const candidate of PAPERCLIP_SKILLS_CANDIDATES) {
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
if (isDir) return candidate;
}
return null;
}
/** /**
* Create a tmpdir with `.claude/skills/` containing symlinks to skills from * Create a tmpdir with `.claude/skills/` containing symlinks to skills from
@@ -41,11 +50,13 @@ async function buildSkillsDir(): Promise<string> {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-")); const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-"));
const target = path.join(tmp, ".claude", "skills"); const target = path.join(tmp, ".claude", "skills");
await fs.mkdir(target, { recursive: true }); await fs.mkdir(target, { recursive: true });
const entries = await fs.readdir(PAPERCLIP_SKILLS_DIR, { withFileTypes: true }); const skillsDir = await resolvePaperclipSkillsDir();
if (!skillsDir) return tmp;
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
if (entry.isDirectory()) { if (entry.isDirectory()) {
await fs.symlink( await fs.symlink(
path.join(PAPERCLIP_SKILLS_DIR, entry.name), path.join(skillsDir, entry.name),
path.join(target, entry.name), path.join(target, entry.name),
); );
} }

View File

@@ -1,5 +1,45 @@
# @paperclipai/adapter-codex-local # @paperclipai/adapter-codex-local
## 0.2.6
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.6
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.5
## 0.2.4
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.4
## 0.2.3
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.3
## 0.2.2
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.2
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@paperclipai/adapter-codex-local", "name": "@paperclipai/adapter-codex-local",
"version": "0.2.1", "version": "0.2.6",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
@@ -32,7 +32,8 @@
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
}, },
"files": [ "files": [
"dist" "dist",
"skills"
], ],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",

View File

@@ -19,10 +19,11 @@ import {
} from "@paperclipai/adapter-utils/server-utils"; } from "@paperclipai/adapter-utils/server-utils";
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js"; import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
const PAPERCLIP_SKILLS_DIR = path.resolve( const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
path.dirname(fileURLToPath(import.meta.url)), const PAPERCLIP_SKILLS_CANDIDATES = [
"../../../../../skills", path.resolve(__moduleDir, "../../skills"), // published: <pkg>/dist/server/ -> <pkg>/skills/
); path.resolve(__moduleDir, "../../../../../skills"), // dev: src/server/ -> repo root/skills/
];
const CODEX_ROLLOUT_NOISE_RE = const CODEX_ROLLOUT_NOISE_RE =
/^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i; /^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i;
@@ -66,19 +67,24 @@ function codexHomeDir(): string {
return path.join(os.homedir(), ".codex"); return path.join(os.homedir(), ".codex");
} }
async function resolvePaperclipSkillsDir(): Promise<string | null> {
for (const candidate of PAPERCLIP_SKILLS_CANDIDATES) {
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
if (isDir) return candidate;
}
return null;
}
async function ensureCodexSkillsInjected(onLog: AdapterExecutionContext["onLog"]) { async function ensureCodexSkillsInjected(onLog: AdapterExecutionContext["onLog"]) {
const sourceExists = await fs const skillsDir = await resolvePaperclipSkillsDir();
.stat(PAPERCLIP_SKILLS_DIR) if (!skillsDir) return;
.then((stats) => stats.isDirectory())
.catch(() => false);
if (!sourceExists) return;
const skillsHome = path.join(codexHomeDir(), "skills"); const skillsHome = path.join(codexHomeDir(), "skills");
await fs.mkdir(skillsHome, { recursive: true }); await fs.mkdir(skillsHome, { recursive: true });
const entries = await fs.readdir(PAPERCLIP_SKILLS_DIR, { withFileTypes: true }); const entries = await fs.readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
const source = path.join(PAPERCLIP_SKILLS_DIR, entry.name); const source = path.join(skillsDir, entry.name);
const target = path.join(skillsHome, entry.name); const target = path.join(skillsHome, entry.name);
const existing = await fs.lstat(target).catch(() => null); const existing = await fs.lstat(target).catch(() => null);
if (existing) continue; if (existing) continue;

View File

@@ -1,5 +1,45 @@
# @paperclipai/adapter-openclaw # @paperclipai/adapter-openclaw
## 0.2.6
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.6
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.5
## 0.2.4
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.4
## 0.2.3
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.3
## 0.2.2
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.2
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@paperclipai/adapter-openclaw", "name": "@paperclipai/adapter-openclaw",
"version": "0.2.1", "version": "0.2.6",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",

View File

@@ -1,5 +1,45 @@
# @paperclipai/db # @paperclipai/db
## 0.2.6
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.6
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.5
## 0.2.4
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.4
## 0.2.3
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.3
## 0.2.2
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.2
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@paperclipai/db", "name": "@paperclipai/db",
"version": "0.2.1", "version": "0.2.6",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
@@ -25,7 +25,7 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc && cp -r src/migrations dist/migrations",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"generate": "tsc -p tsconfig.json && drizzle-kit generate", "generate": "tsc -p tsconfig.json && drizzle-kit generate",

View File

@@ -1,5 +1,35 @@
# @paperclipai/shared # @paperclipai/shared
## 0.2.6
### Patch Changes
- Version bump (patch)
## 0.2.5
### Patch Changes
- Version bump (patch)
## 0.2.4
### Patch Changes
- Version bump (patch)
## 0.2.3
### Patch Changes
- Version bump (patch)
## 0.2.2
### Patch Changes
- Version bump (patch)
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@paperclipai/shared", "name": "@paperclipai/shared",
"version": "0.2.1", "version": "0.2.6",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",

36
pnpm-lock.yaml generated
View File

@@ -174,7 +174,7 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../packages/shared version: link:../packages/shared
better-auth: better-auth:
specifier: ^1.3.8 specifier: 1.4.18
version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
detect-port: detect-port:
specifier: ^2.1.0 specifier: ^2.1.0
@@ -185,6 +185,9 @@ importers:
drizzle-orm: drizzle-orm:
specifier: ^0.38.4 specifier: ^0.38.4
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
embedded-postgres:
specifier: ^18.1.0-beta.16
version: 18.1.0-beta.16
express: express:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.2.1 version: 5.2.1
@@ -209,10 +212,6 @@ importers:
zod: zod:
specifier: ^3.24.2 specifier: ^3.24.2
version: 3.25.76 version: 3.25.76
optionalDependencies:
embedded-postgres:
specifier: ^18.1.0-beta.16
version: 18.1.0-beta.16
devDependencies: devDependencies:
'@types/express': '@types/express':
specifier: ^5.0.0 specifier: ^5.0.0
@@ -8285,8 +8284,7 @@ snapshots:
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
async-exit-hook@2.0.1: async-exit-hook@2.0.1: {}
optional: true
asynckit@0.4.0: {} asynckit@0.4.0: {}
@@ -8615,7 +8613,6 @@ snapshots:
'@embedded-postgres/windows-x64': 18.1.0-beta.16 '@embedded-postgres/windows-x64': 18.1.0-beta.16
transitivePeerDependencies: transitivePeerDependencies:
- pg-native - pg-native
optional: true
encodeurl@2.0.0: {} encodeurl@2.0.0: {}
@@ -9808,19 +9805,15 @@ snapshots:
pg-cloudflare@1.3.0: pg-cloudflare@1.3.0:
optional: true optional: true
pg-connection-string@2.11.0: pg-connection-string@2.11.0: {}
optional: true
pg-int8@1.0.1: pg-int8@1.0.1: {}
optional: true
pg-pool@3.11.0(pg@8.18.0): pg-pool@3.11.0(pg@8.18.0):
dependencies: dependencies:
pg: 8.18.0 pg: 8.18.0
optional: true
pg-protocol@1.11.0: pg-protocol@1.11.0: {}
optional: true
pg-types@2.2.0: pg-types@2.2.0:
dependencies: dependencies:
@@ -9829,7 +9822,6 @@ snapshots:
postgres-bytea: 1.0.1 postgres-bytea: 1.0.1
postgres-date: 1.0.7 postgres-date: 1.0.7
postgres-interval: 1.2.0 postgres-interval: 1.2.0
optional: true
pg@8.18.0: pg@8.18.0:
dependencies: dependencies:
@@ -9840,12 +9832,10 @@ snapshots:
pgpass: 1.0.5 pgpass: 1.0.5
optionalDependencies: optionalDependencies:
pg-cloudflare: 1.3.0 pg-cloudflare: 1.3.0
optional: true
pgpass@1.0.5: pgpass@1.0.5:
dependencies: dependencies:
split2: 4.2.0 split2: 4.2.0
optional: true
picocolors@1.1.1: {} picocolors@1.1.1: {}
@@ -9913,19 +9903,15 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postgres-array@2.0.0: postgres-array@2.0.0: {}
optional: true
postgres-bytea@1.0.1: postgres-bytea@1.0.1: {}
optional: true
postgres-date@1.0.7: postgres-date@1.0.7: {}
optional: true
postgres-interval@1.2.0: postgres-interval@1.2.0:
dependencies: dependencies:
xtend: 4.0.2 xtend: 4.0.2
optional: true
postgres@3.4.8: {} postgres@3.4.8: {}

42
scripts/docker-onboard-smoke.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
IMAGE_NAME="${IMAGE_NAME:-paperclip-onboard-smoke}"
HOST_PORT="${HOST_PORT:-3131}"
PAPERCLIPAI_VERSION="${PAPERCLIPAI_VERSION:-latest}"
DATA_DIR="${DATA_DIR:-$REPO_ROOT/data/docker-onboard-smoke}"
HOST_UID="${HOST_UID:-$(id -u)}"
PAPERCLIP_DEPLOYMENT_MODE="${PAPERCLIP_DEPLOYMENT_MODE:-authenticated}"
PAPERCLIP_DEPLOYMENT_EXPOSURE="${PAPERCLIP_DEPLOYMENT_EXPOSURE:-private}"
DOCKER_TTY_ARGS=()
if [[ -t 0 && -t 1 ]]; then
DOCKER_TTY_ARGS=(-it)
fi
mkdir -p "$DATA_DIR"
echo "==> Building onboard smoke image"
docker build \
--build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \
--build-arg HOST_UID="$HOST_UID" \
-f "$REPO_ROOT/Dockerfile.onboard-smoke" \
-t "$IMAGE_NAME" \
"$REPO_ROOT"
echo "==> Running onboard smoke container"
echo " UI should be reachable at: http://localhost:$HOST_PORT"
echo " Data dir: $DATA_DIR"
echo " Deployment: $PAPERCLIP_DEPLOYMENT_MODE/$PAPERCLIP_DEPLOYMENT_EXPOSURE"
echo " Live output: onboard banner and server logs stream in this terminal (Ctrl+C to stop)"
docker run --rm \
"${DOCKER_TTY_ARGS[@]}" \
--name "${IMAGE_NAME//[^a-zA-Z0-9_.-]/-}" \
-p "$HOST_PORT:3100" \
-e HOST=0.0.0.0 \
-e PORT=3100 \
-e PAPERCLIP_DEPLOYMENT_MODE="$PAPERCLIP_DEPLOYMENT_MODE" \
-e PAPERCLIP_DEPLOYMENT_EXPOSURE="$PAPERCLIP_DEPLOYMENT_EXPOSURE" \
-v "$DATA_DIR:/paperclip" \
"$IMAGE_NAME"

View File

@@ -125,7 +125,7 @@ echo ""
echo "==> Step 4/7: Building all packages..." echo "==> Step 4/7: Building all packages..."
cd "$REPO_ROOT" cd "$REPO_ROOT"
# Build packages in dependency order (excluding UI and CLI) # Build packages in dependency order (excluding CLI)
pnpm --filter @paperclipai/shared build pnpm --filter @paperclipai/shared build
pnpm --filter @paperclipai/adapter-utils build pnpm --filter @paperclipai/adapter-utils build
pnpm --filter @paperclipai/db build pnpm --filter @paperclipai/db build
@@ -133,7 +133,18 @@ pnpm --filter @paperclipai/adapter-claude-local build
pnpm --filter @paperclipai/adapter-codex-local build pnpm --filter @paperclipai/adapter-codex-local build
pnpm --filter @paperclipai/adapter-openclaw build pnpm --filter @paperclipai/adapter-openclaw build
pnpm --filter @paperclipai/server build pnpm --filter @paperclipai/server build
echo " ✓ All packages built"
# Build UI and bundle into server package for static serving
pnpm --filter @paperclipai/ui build
rm -rf "$REPO_ROOT/server/ui-dist"
cp -r "$REPO_ROOT/ui/dist" "$REPO_ROOT/server/ui-dist"
# Bundle skills into packages that need them (adapters + server)
for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do
rm -rf "$REPO_ROOT/$pkg_dir/skills"
cp -r "$REPO_ROOT/skills" "$REPO_ROOT/$pkg_dir/skills"
done
echo " ✓ All packages built (including UI + skills)"
# ── Step 5: Build CLI bundle ───────────────────────────────────────────────── # ── Step 5: Build CLI bundle ─────────────────────────────────────────────────
@@ -183,8 +194,18 @@ if [ -f "$CLI_DIR/README.md" ]; then
rm "$CLI_DIR/README.md" rm "$CLI_DIR/README.md"
fi fi
# Commit all changes # Remove temporary build artifacts before committing (these are only needed during publish)
git add -A rm -rf "$REPO_ROOT/server/ui-dist"
for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do
rm -rf "$REPO_ROOT/$pkg_dir/skills"
done
# Stage only release-related files (avoid sweeping unrelated changes with -A)
git add \
.changeset/ \
'**/CHANGELOG.md' \
'**/package.json' \
cli/src/index.ts
git commit -m "chore: release v$NEW_VERSION" git commit -m "chore: release v$NEW_VERSION"
git tag "v$NEW_VERSION" git tag "v$NEW_VERSION"
echo " ✓ Committed and tagged v$NEW_VERSION" echo " ✓ Committed and tagged v$NEW_VERSION"

View File

@@ -1,5 +1,70 @@
# @paperclipai/server # @paperclipai/server
## 0.2.6
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.6
- @paperclipai/adapter-utils@0.2.6
- @paperclipai/db@0.2.6
- @paperclipai/adapter-claude-local@0.2.6
- @paperclipai/adapter-codex-local@0.2.6
- @paperclipai/adapter-openclaw@0.2.6
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.5
- @paperclipai/adapter-utils@0.2.5
- @paperclipai/db@0.2.5
- @paperclipai/adapter-claude-local@0.2.5
- @paperclipai/adapter-codex-local@0.2.5
- @paperclipai/adapter-openclaw@0.2.5
## 0.2.4
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.4
- @paperclipai/adapter-utils@0.2.4
- @paperclipai/db@0.2.4
- @paperclipai/adapter-claude-local@0.2.4
- @paperclipai/adapter-codex-local@0.2.4
- @paperclipai/adapter-openclaw@0.2.4
## 0.2.3
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.3
- @paperclipai/adapter-utils@0.2.3
- @paperclipai/db@0.2.3
- @paperclipai/adapter-claude-local@0.2.3
- @paperclipai/adapter-codex-local@0.2.3
- @paperclipai/adapter-openclaw@0.2.3
## 0.2.2
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.2
- @paperclipai/adapter-utils@0.2.2
- @paperclipai/db@0.2.2
- @paperclipai/adapter-claude-local@0.2.2
- @paperclipai/adapter-codex-local@0.2.2
- @paperclipai/adapter-openclaw@0.2.2
## 0.2.1 ## 0.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@paperclipai/server", "name": "@paperclipai/server",
"version": "0.2.1", "version": "0.2.6",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
@@ -17,7 +17,9 @@
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
}, },
"files": [ "files": [
"dist" "dist",
"ui-dist",
"skills"
], ],
"scripts": { "scripts": {
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",
@@ -35,10 +37,11 @@
"@paperclipai/adapter-utils": "workspace:*", "@paperclipai/adapter-utils": "workspace:*",
"@paperclipai/db": "workspace:*", "@paperclipai/db": "workspace:*",
"@paperclipai/shared": "workspace:*", "@paperclipai/shared": "workspace:*",
"better-auth": "^1.3.8", "better-auth": "1.4.18",
"detect-port": "^2.1.0", "detect-port": "^2.1.0",
"dotenv": "^17.0.1", "dotenv": "^17.0.1",
"drizzle-orm": "^0.38.4", "drizzle-orm": "^0.38.4",
"embedded-postgres": "^18.1.0-beta.16",
"express": "^5.1.0", "express": "^5.1.0",
"multer": "^2.0.2", "multer": "^2.0.2",
"open": "^11.0.0", "open": "^11.0.0",
@@ -48,9 +51,6 @@
"ws": "^8.19.0", "ws": "^8.19.0",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"optionalDependencies": {
"embedded-postgres": "^18.1.0-beta.16"
},
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/express-serve-static-core": "^5.0.0", "@types/express-serve-static-core": "^5.0.0",

View File

@@ -124,12 +124,20 @@ export async function createApp(
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
if (opts.uiMode === "static") { if (opts.uiMode === "static") {
// Serve built UI from ui/dist in production. // Try published location first (server/ui-dist/), then monorepo dev location (../../ui/dist)
const uiDist = path.resolve(__dirname, "../../ui/dist"); const candidates = [
app.use(express.static(uiDist)); path.resolve(__dirname, "../ui-dist"),
app.get(/.*/, (_req, res) => { path.resolve(__dirname, "../../ui/dist"),
res.sendFile(path.join(uiDist, "index.html")); ];
}); const uiDist = candidates.find((p) => fs.existsSync(path.join(p, "index.html")));
if (uiDist) {
app.use(express.static(uiDist));
app.get(/.*/, (_req, res) => {
res.sendFile(path.join(uiDist, "index.html"));
});
} else {
console.warn("[paperclip] UI dist not found; running in API-only mode");
}
} }
if (opts.uiMode === "vite-dev") { if (opts.uiMode === "vite-dev") {

View File

@@ -4,7 +4,7 @@ import { createServer } from "node:http";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { createInterface } from "node:readline/promises"; import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process"; import { stdin, stdout } from "node:process";
import type { Request as ExpressRequest } from "express"; import type { Request as ExpressRequest, RequestHandler } from "express";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
createDb, createDb,
@@ -26,12 +26,17 @@ import { heartbeatService } from "./services/index.js";
import { createStorageServiceFromConfig } from "./storage/index.js"; import { createStorageServiceFromConfig } from "./storage/index.js";
import { printStartupBanner } from "./startup-banner.js"; import { printStartupBanner } from "./startup-banner.js";
import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js"; import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js";
import {
createBetterAuthHandler, type BetterAuthSessionUser = {
createBetterAuthInstance, id: string;
resolveBetterAuthSession, email?: string | null;
resolveBetterAuthSessionFromHeaders, name?: string | null;
} from "./auth/better-auth.js"; };
type BetterAuthSessionResult = {
session: { id: string; userId: string } | null;
user: BetterAuthSessionUser | null;
};
type EmbeddedPostgresInstance = { type EmbeddedPostgresInstance = {
initialise(): Promise<void>; initialise(): Promise<void>;
@@ -232,7 +237,7 @@ if (config.databaseUrl) {
EmbeddedPostgres = mod.default as EmbeddedPostgresCtor; EmbeddedPostgres = mod.default as EmbeddedPostgresCtor;
} catch { } catch {
throw new Error( throw new Error(
"Embedded PostgreSQL mode requires optional dependency `embedded-postgres`. Install optional dependencies or set DATABASE_URL for external Postgres.", "Embedded PostgreSQL mode requires dependency `embedded-postgres`. Reinstall dependencies (without omitting required packages), or set DATABASE_URL for external Postgres.",
); );
} }
@@ -388,17 +393,23 @@ if (config.deploymentMode === "authenticated") {
} }
let authReady = config.deploymentMode === "local_trusted"; let authReady = config.deploymentMode === "local_trusted";
let betterAuthHandler: ReturnType<typeof createBetterAuthHandler> | undefined; let betterAuthHandler: RequestHandler | undefined;
let resolveSession: let resolveSession:
| ((req: ExpressRequest) => Promise<Awaited<ReturnType<typeof resolveBetterAuthSession>>>) | ((req: ExpressRequest) => Promise<BetterAuthSessionResult | null>)
| undefined; | undefined;
let resolveSessionFromHeaders: let resolveSessionFromHeaders:
| ((headers: Headers) => Promise<Awaited<ReturnType<typeof resolveBetterAuthSession>>>) | ((headers: Headers) => Promise<BetterAuthSessionResult | null>)
| undefined; | undefined;
if (config.deploymentMode === "local_trusted") { if (config.deploymentMode === "local_trusted") {
await ensureLocalTrustedBoardPrincipal(db as any); await ensureLocalTrustedBoardPrincipal(db as any);
} }
if (config.deploymentMode === "authenticated") { if (config.deploymentMode === "authenticated") {
const {
createBetterAuthHandler,
createBetterAuthInstance,
resolveBetterAuthSession,
resolveBetterAuthSessionFromHeaders,
} = await import("./auth/better-auth.js");
const betterAuthSecret = const betterAuthSecret =
process.env.BETTER_AUTH_SECRET?.trim() ?? process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim(); process.env.BETTER_AUTH_SECRET?.trim() ?? process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim();
if (!betterAuthSecret) { if (!betterAuthSecret) {

View File

@@ -59,8 +59,9 @@ function readSkillMarkdown(skillName: string): string | null {
if (normalized !== "paperclip" && normalized !== "paperclip-create-agent") return null; if (normalized !== "paperclip" && normalized !== "paperclip-create-agent") return null;
const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const candidates = [ const candidates = [
path.resolve(process.cwd(), "skills", normalized, "SKILL.md"), path.resolve(moduleDir, "../../skills", normalized, "SKILL.md"), // published: dist/routes/ -> <pkg>/skills/
path.resolve(moduleDir, "../../../skills", normalized, "SKILL.md"), path.resolve(process.cwd(), "skills", normalized, "SKILL.md"), // cwd (e.g. monorepo root)
path.resolve(moduleDir, "../../../skills", normalized, "SKILL.md"), // dev: src/routes/ -> repo root/skills/
]; ];
for (const skillPath of candidates) { for (const skillPath of candidates) {
try { try {

View File

@@ -207,6 +207,17 @@ PATCH /api/agents/{agentId}/instructions-path
| Release task | `POST /api/issues/:issueId/release` | | Release task | `POST /api/issues/:issueId/release` |
| List agents | `GET /api/companies/:companyId/agents` | | List agents | `GET /api/companies/:companyId/agents` |
| Dashboard | `GET /api/companies/:companyId/dashboard` | | Dashboard | `GET /api/companies/:companyId/dashboard` |
| Search issues | `GET /api/companies/:companyId/issues?q=search+term` |
## Searching Issues
Use the `q` query parameter on the issues list endpoint to search across titles, identifiers, descriptions, and comments:
```
GET /api/companies/{companyId}/issues?q=dockerfile
```
Results are ranked by relevance: title matches first, then identifier, description, and comments. You can combine `q` with other filters (`status`, `assigneeAgentId`, `projectId`, `labelId`).
## Full Reference ## Full Reference

View File

@@ -472,7 +472,7 @@ Terminal states: `done`, `cancelled`
| Method | Path | Description | | Method | Path | Description |
| ------ | ---------------------------------- | ---------------------------------------------------------------------------------------- | | ------ | ---------------------------------- | ---------------------------------------------------------------------------------------- |
| GET | `/api/companies/:companyId/issues` | List issues, sorted by priority. Filters: `?status=`, `?assigneeAgentId=`, `?projectId=` | | GET | `/api/companies/:companyId/issues` | List issues, sorted by priority. Filters: `?status=`, `?assigneeAgentId=`, `?assigneeUserId=`, `?projectId=`, `?labelId=`, `?q=` (full-text search across title, identifier, description, comments) |
| GET | `/api/issues/:issueId` | Issue details + ancestors | | GET | `/api/issues/:issueId` | Issue details + ancestors |
| POST | `/api/companies/:companyId/issues` | Create issue | | POST | `/api/companies/:companyId/issues` | Create issue |
| PATCH | `/api/issues/:issueId` | Update issue (optional `comment` field adds a comment in same call) | | PATCH | `/api/issues/:issueId` | Update issue (optional `comment` field adds a comment in same call) |

View File

@@ -38,7 +38,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) {
return ( return (
<div <div
className={cn( className={cn(
"prose prose-sm max-w-none prose-p:my-2 prose-p:leading-[1.4] prose-ul:my-1.5 prose-ol:my-1.5 prose-li:my-0.5 prose-li:leading-[1.4] prose-pre:my-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-headings:my-2 prose-headings:text-sm prose-blockquote:leading-[1.4] prose-table:my-2 prose-th:px-3 prose-th:py-1.5 prose-td:px-3 prose-td:py-1.5 prose-code:break-all", "prose prose-sm max-w-none prose-p:my-2 prose-p:leading-[1.4] prose-ul:my-1.5 prose-ol:my-1.5 prose-li:my-0.5 prose-li:leading-[1.4] prose-pre:my-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-headings:my-2 prose-headings:text-sm prose-blockquote:leading-[1.4] prose-table:my-2 prose-th:px-3 prose-th:py-1.5 prose-td:px-3 prose-td:py-1.5 prose-code:break-all [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:list-item",
theme === "dark" && "prose-invert", theme === "dark" && "prose-invert",
className, className,
)} )}

View File

@@ -563,7 +563,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
onBlur={() => onBlur?.()} onBlur={() => onBlur?.()}
className={cn("paperclip-mdxeditor", !bordered && "paperclip-mdxeditor--borderless")} className={cn("paperclip-mdxeditor", !bordered && "paperclip-mdxeditor--borderless")}
contentEditableClassName={cn( contentEditableClassName={cn(
"paperclip-mdxeditor-content focus:outline-none", "paperclip-mdxeditor-content focus:outline-none [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:list-item",
contentClassName, contentClassName,
)} )}
overlayContainer={containerRef.current} overlayContainer={containerRef.current}

View File

@@ -527,7 +527,7 @@ export function NewIssueDialog() {
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-48 p-1" align="start"> <PopoverContent className="w-48 p-1" align="start">
{companies.map((c) => ( {companies.filter((c) => c.status !== "archived").map((c) => (
<button <button
key={c.id} key={c.id}
className={cn( className={cn(

View File

@@ -89,6 +89,9 @@ export function OnboardingWizard() {
useState<AdapterEnvironmentTestResult | null>(null); useState<AdapterEnvironmentTestResult | null>(null);
const [adapterEnvError, setAdapterEnvError] = useState<string | null>(null); const [adapterEnvError, setAdapterEnvError] = useState<string | null>(null);
const [adapterEnvLoading, setAdapterEnvLoading] = useState(false); const [adapterEnvLoading, setAdapterEnvLoading] = useState(false);
const [forceUnsetAnthropicApiKey, setForceUnsetAnthropicApiKey] =
useState(false);
const [unsetAnthropicLoading, setUnsetAnthropicLoading] = useState(false);
// Step 3 // Step 3
const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md"); const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md");
@@ -159,6 +162,15 @@ export function OnboardingWizard() {
}, [step, adapterType, cwd, model, command, args, url]); }, [step, adapterType, cwd, model, command, args, url]);
const selectedModel = (adapterModels ?? []).find((m) => m.id === model); const selectedModel = (adapterModels ?? []).find((m) => m.id === model);
const hasAnthropicApiKeyOverrideCheck =
adapterEnvResult?.checks.some(
(check) =>
check.code === "claude_anthropic_api_key_overrides_subscription"
) ?? false;
const shouldSuggestUnsetAnthropicApiKey =
adapterType === "claude_local" &&
adapterEnvResult?.status === "fail" &&
hasAnthropicApiKeyOverrideCheck;
function reset() { function reset() {
setStep(1); setStep(1);
@@ -176,6 +188,8 @@ export function OnboardingWizard() {
setAdapterEnvResult(null); setAdapterEnvResult(null);
setAdapterEnvError(null); setAdapterEnvError(null);
setAdapterEnvLoading(false); setAdapterEnvLoading(false);
setForceUnsetAnthropicApiKey(false);
setUnsetAnthropicLoading(false);
setTaskTitle("Create your CEO HEARTBEAT.md"); setTaskTitle("Create your CEO HEARTBEAT.md");
setTaskDescription(DEFAULT_TASK_DESCRIPTION); setTaskDescription(DEFAULT_TASK_DESCRIPTION);
setCreatedCompanyId(null); setCreatedCompanyId(null);
@@ -191,7 +205,7 @@ export function OnboardingWizard() {
function buildAdapterConfig(): Record<string, unknown> { function buildAdapterConfig(): Record<string, unknown> {
const adapter = getUIAdapter(adapterType); const adapter = getUIAdapter(adapterType);
return adapter.buildAdapterConfig({ const config = adapter.buildAdapterConfig({
...defaultCreateValues, ...defaultCreateValues,
adapterType, adapterType,
cwd, cwd,
@@ -208,9 +222,22 @@ export function OnboardingWizard() {
? DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX ? DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX
: defaultCreateValues.dangerouslyBypassSandbox : defaultCreateValues.dangerouslyBypassSandbox
}); });
if (adapterType === "claude_local" && forceUnsetAnthropicApiKey) {
const env =
typeof config.env === "object" &&
config.env !== null &&
!Array.isArray(config.env)
? { ...(config.env as Record<string, unknown>) }
: {};
env.ANTHROPIC_API_KEY = { type: "plain", value: "" };
config.env = env;
}
return config;
} }
async function runAdapterEnvironmentTest(): Promise<AdapterEnvironmentTestResult | null> { async function runAdapterEnvironmentTest(
adapterConfigOverride?: Record<string, unknown>
): Promise<AdapterEnvironmentTestResult | null> {
if (!createdCompanyId) { if (!createdCompanyId) {
setAdapterEnvError( setAdapterEnvError(
"Create or select a company before testing adapter environment." "Create or select a company before testing adapter environment."
@@ -224,7 +251,7 @@ export function OnboardingWizard() {
createdCompanyId, createdCompanyId,
adapterType, adapterType,
{ {
adapterConfig: buildAdapterConfig() adapterConfig: adapterConfigOverride ?? buildAdapterConfig()
} }
); );
setAdapterEnvResult(result); setAdapterEnvResult(result);
@@ -276,12 +303,6 @@ export function OnboardingWizard() {
if (isLocalAdapter) { if (isLocalAdapter) {
const result = adapterEnvResult ?? (await runAdapterEnvironmentTest()); const result = adapterEnvResult ?? (await runAdapterEnvironmentTest());
if (!result) return; if (!result) return;
if (result.status === "fail") {
setError(
"Adapter environment test failed. Fix the errors and test again before continuing."
);
return;
}
} }
const agent = await agentsApi.create(createdCompanyId, { const agent = await agentsApi.create(createdCompanyId, {
@@ -311,6 +332,55 @@ export function OnboardingWizard() {
} }
} }
async function handleUnsetAnthropicApiKey() {
if (!createdCompanyId || unsetAnthropicLoading) return;
setUnsetAnthropicLoading(true);
setError(null);
setAdapterEnvError(null);
setForceUnsetAnthropicApiKey(true);
const configWithUnset = (() => {
const config = buildAdapterConfig();
const env =
typeof config.env === "object" &&
config.env !== null &&
!Array.isArray(config.env)
? { ...(config.env as Record<string, unknown>) }
: {};
env.ANTHROPIC_API_KEY = { type: "plain", value: "" };
config.env = env;
return config;
})();
try {
if (createdAgentId) {
await agentsApi.update(
createdAgentId,
{ adapterConfig: configWithUnset },
createdCompanyId
);
queryClient.invalidateQueries({
queryKey: queryKeys.agents.list(createdCompanyId)
});
}
const result = await runAdapterEnvironmentTest(configWithUnset);
if (result?.status === "fail") {
setError(
"Retried with ANTHROPIC_API_KEY unset in adapter config, but the environment test is still failing."
);
}
} catch (err) {
setError(
err instanceof Error
? err.message
: "Failed to unset ANTHROPIC_API_KEY and retry."
);
} finally {
setUnsetAnthropicLoading(false);
}
}
async function handleStep3Next() { async function handleStep3Next() {
if (!createdCompanyId || !createdAgentId) return; if (!createdCompanyId || !createdAgentId) return;
setLoading(true); setLoading(true);
@@ -673,6 +743,24 @@ export function OnboardingWizard() {
<AdapterEnvironmentResult result={adapterEnvResult} /> <AdapterEnvironmentResult result={adapterEnvResult} />
)} )}
{shouldSuggestUnsetAnthropicApiKey && (
<div className="rounded-md border border-amber-300/60 bg-amber-50/40 px-2.5 py-2 space-y-2">
<p className="text-[11px] text-amber-900/90 leading-relaxed">
Claude failed while <span className="font-mono">ANTHROPIC_API_KEY</span> is set.
You can clear it in this CEO adapter config and retry the probe.
</p>
<Button
size="sm"
variant="outline"
className="h-7 px-2.5 text-xs"
disabled={adapterEnvLoading || unsetAnthropicLoading}
onClick={() => void handleUnsetAnthropicApiKey()}
>
{unsetAnthropicLoading ? "Retrying..." : "Unset ANTHROPIC_API_KEY"}
</Button>
</div>
)}
<div className="rounded-md border border-border/70 bg-muted/20 px-2.5 py-2 text-[11px] space-y-1.5"> <div className="rounded-md border border-border/70 bg-muted/20 px-2.5 py-2 text-[11px] space-y-1.5">
<p className="font-medium">Manual debug</p> <p className="font-medium">Manual debug</p>
<p className="text-muted-foreground font-mono break-all"> <p className="text-muted-foreground font-mono break-all">