Compare commits

...

8 Commits

Author SHA1 Message Date
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
31 changed files with 221 additions and 82 deletions

29
Dockerfile.onboard-smoke Normal file
View File

@@ -0,0 +1,29 @@
FROM ubuntu:24.04
ARG NODE_MAJOR=20
ARG PAPERCLIPAI_VERSION=latest
ENV DEBIAN_FRONTEND=noninteractive \
PAPERCLIP_HOME=/paperclip \
PAPERCLIP_OPEN_ON_LISTEN=false \
HOST=0.0.0.0 \
PORT=3100 \
NODE_MAJOR=${NODE_MAJOR} \
PAPERCLIPAI_VERSION=${PAPERCLIPAI_VERSION}
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
&& 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 \
&& rm -rf /var/lib/apt/lists/*
VOLUME ["/paperclip"]
WORKDIR /workspace
EXPOSE 3100
CMD ["bash", "-lc", "set -euo pipefail; mkdir -p \"$PAPERCLIP_HOME\"; npx --yes \"paperclipai@${PAPERCLIPAI_VERSION}\" onboard --yes --data-dir \"$PAPERCLIP_HOME\""]

View File

@@ -6,13 +6,13 @@
<a href="#quickstart"><strong>Quickstart</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://discord.gg/paperclip"><strong>Discord</strong></a>
<a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a>
</p>
<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/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>
<br/>
@@ -174,7 +174,7 @@ Paperclip handles the hard orchestration details correctly.
Open source. Self-hosted. No Paperclip account required.
```bash
npx paperclipai onboard
npx paperclipai onboard --yes
```
Or manually:
@@ -249,7 +249,7 @@ We welcome contributions. See the [contributing guide](CONTRIBUTING.md) for deta
## 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 Discussions](https://github.com/paperclipai/paperclip/discussions) — ideas and RFC

View File

@@ -1,5 +1,19 @@
# paperclipai
## 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,16 +7,7 @@ export function storageCheck(config: PaperclipConfig, configPath?: string): Chec
if (config.storage.provider === "local_disk") {
const baseDir = resolveRuntimeLikePath(config.storage.localDisk.baseDir, configPath);
if (!fs.existsSync(baseDir)) {
return {
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",
};
fs.mkdirSync(baseDir, { recursive: true });
}
try {

View File

@@ -84,6 +84,15 @@ function isModuleNotFoundError(err: unknown): boolean {
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 {
if (process.env.PAPERCLIP_UI_DEV_MIDDLEWARE !== undefined) return;
const normalized = entrypoint.replaceAll("\\", "/");
@@ -106,7 +115,9 @@ async function importServerEntry(): Promise<void> {
try {
await import("@paperclipai/server");
} catch (err) {
if (isModuleNotFoundError(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` +

View File

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

View File

@@ -66,3 +66,29 @@ Notes:
- Without API keys, the app still runs normally.
- 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
Build + run:
```sh
./scripts/docker-onboard-smoke.sh
```
Open: `http://localhost:3100`
Useful overrides:
```sh
HOST_PORT=3200 PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
```
Notes:
- Persistent data is mounted at `./data/docker-onboard-smoke` by default.
- The image definition is in `Dockerfile.onboard-smoke`.

View File

@@ -5,6 +5,10 @@ summary: Guide to building a custom adapter
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
```

View File

@@ -5,24 +5,15 @@ summary: Get Paperclip running in minutes
Get Paperclip running locally in under 5 minutes.
## Option 1: Docker Compose (Recommended)
The fastest way to start. No Node.js install needed.
## Quick Start (Recommended)
```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:
```sh
ANTHROPIC_API_KEY=sk-... OPENAI_API_KEY=sk-... \
docker compose -f docker-compose.quickstart.yml up --build
```
## Option 2: Local Development
## Local Development
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).
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
pnpm paperclipai run

View File

@@ -1,5 +1,11 @@
# @paperclipai/adapter-utils
## 0.2.5
### Patch Changes
- Version bump (patch)
## 0.2.4
### Patch Changes

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@paperclipai/adapter-claude-local",
"version": "0.2.4",
"version": "0.2.5",
"type": "module",
"exports": {
".": "./src/index.ts",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@paperclipai/adapter-codex-local",
"version": "0.2.4",
"version": "0.2.5",
"type": "module",
"exports": {
".": "./src/index.ts",

View File

@@ -1,5 +1,13 @@
# @paperclipai/adapter-openclaw
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/adapter-utils@0.2.5
## 0.2.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,13 @@
# @paperclipai/db
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.5
## 0.2.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@paperclipai/db",
"version": "0.2.4",
"version": "0.2.5",
"type": "module",
"exports": {
".": "./src/index.ts",

View File

@@ -1,5 +1,11 @@
# @paperclipai/shared
## 0.2.5
### Patch Changes
- Version bump (patch)
## 0.2.4
### Patch Changes

View File

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

2
pnpm-lock.yaml generated
View File

@@ -174,7 +174,7 @@ importers:
specifier: workspace:*
version: link:../packages/shared
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))
detect-port:
specifier: ^2.1.0

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

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
IMAGE_NAME="${IMAGE_NAME:-paperclip-onboard-smoke}"
HOST_PORT="${HOST_PORT:-3100}"
PAPERCLIPAI_VERSION="${PAPERCLIPAI_VERSION:-latest}"
DATA_DIR="${DATA_DIR:-$REPO_ROOT/data/docker-onboard-smoke}"
mkdir -p "$DATA_DIR"
echo "==> Building onboard smoke image"
docker build \
--build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \
-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"
docker run --rm \
--name "${IMAGE_NAME//[^a-zA-Z0-9_.-]/-}" \
-p "$HOST_PORT:3100" \
-e HOST=0.0.0.0 \
-e PORT=3100 \
-v "$DATA_DIR:/paperclip" \
"$IMAGE_NAME"

View File

@@ -194,10 +194,8 @@ if [ -f "$CLI_DIR/README.md" ]; then
rm "$CLI_DIR/README.md"
fi
# Remove UI dist bundled into server for publishing
# Remove temporary build artifacts before committing (these are only needed during publish)
rm -rf "$REPO_ROOT/server/ui-dist"
# Remove skills bundled into packages for publishing
for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do
rm -rf "$REPO_ROOT/$pkg_dir/skills"
done
@@ -207,8 +205,7 @@ git add \
.changeset/ \
'**/CHANGELOG.md' \
'**/package.json' \
cli/src/index.ts \
server/ui-dist
cli/src/index.ts
git commit -m "chore: release v$NEW_VERSION"
git tag "v$NEW_VERSION"
echo " ✓ Committed and tagged v$NEW_VERSION"

View File

@@ -1,5 +1,18 @@
# @paperclipai/server
## 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

View File

@@ -1,6 +1,6 @@
{
"name": "@paperclipai/server",
"version": "0.2.4",
"version": "0.2.5",
"type": "module",
"exports": {
".": "./src/index.ts"
@@ -37,7 +37,7 @@
"@paperclipai/adapter-utils": "workspace:*",
"@paperclipai/db": "workspace:*",
"@paperclipai/shared": "workspace:*",
"better-auth": "^1.3.8",
"better-auth": "1.4.18",
"detect-port": "^2.1.0",
"dotenv": "^17.0.1",
"drizzle-orm": "^0.38.4",

View File

@@ -4,7 +4,7 @@ import { createServer } from "node:http";
import { resolve } from "node:path";
import { createInterface } from "node:readline/promises";
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 {
createDb,
@@ -26,12 +26,17 @@ import { heartbeatService } from "./services/index.js";
import { createStorageServiceFromConfig } from "./storage/index.js";
import { printStartupBanner } from "./startup-banner.js";
import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js";
import {
createBetterAuthHandler,
createBetterAuthInstance,
resolveBetterAuthSession,
resolveBetterAuthSessionFromHeaders,
} from "./auth/better-auth.js";
type BetterAuthSessionUser = {
id: string;
email?: string | null;
name?: string | null;
};
type BetterAuthSessionResult = {
session: { id: string; userId: string } | null;
user: BetterAuthSessionUser | null;
};
type EmbeddedPostgresInstance = {
initialise(): Promise<void>;
@@ -388,17 +393,23 @@ if (config.deploymentMode === "authenticated") {
}
let authReady = config.deploymentMode === "local_trusted";
let betterAuthHandler: ReturnType<typeof createBetterAuthHandler> | undefined;
let betterAuthHandler: RequestHandler | undefined;
let resolveSession:
| ((req: ExpressRequest) => Promise<Awaited<ReturnType<typeof resolveBetterAuthSession>>>)
| ((req: ExpressRequest) => Promise<BetterAuthSessionResult | null>)
| undefined;
let resolveSessionFromHeaders:
| ((headers: Headers) => Promise<Awaited<ReturnType<typeof resolveBetterAuthSession>>>)
| ((headers: Headers) => Promise<BetterAuthSessionResult | null>)
| undefined;
if (config.deploymentMode === "local_trusted") {
await ensureLocalTrustedBoardPrincipal(db as any);
}
if (config.deploymentMode === "authenticated") {
const {
createBetterAuthHandler,
createBetterAuthInstance,
resolveBetterAuthSession,
resolveBetterAuthSessionFromHeaders,
} = await import("./auth/better-auth.js");
const betterAuthSecret =
process.env.BETTER_AUTH_SECRET?.trim() ?? process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim();
if (!betterAuthSecret) {