Compare commits

..

16 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
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
41 changed files with 545 additions and 117 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

@@ -4,15 +4,15 @@
<p align="center">
<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://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,61 @@
# 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
- 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
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "paperclipai",
"version": "0.2.1",
"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,9 +115,17 @@ async function importServerEntry(): Promise<void> {
try {
await import("@paperclipai/server");
} 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(
`Could not locate a Paperclip server entrypoint.\n` +
`Tried: ${devEntry}, @paperclipai/server\n` +
`Paperclip server failed to start.\n` +
`${formatError(err)}`,
);
}

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.0");
.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

@@ -127,7 +127,7 @@ Response:
},
"onboarding": {
"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": {
"name": "Your display name",
"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",
"name": "Startup in a Box",
"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"]
}
]

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

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

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect 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"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 32 32" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="-4" y="-4" width="32" height="32" rx="6" fill="#2563EB"/>
<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>

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.
## 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,29 @@
# @paperclipai/adapter-utils
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,37 @@
# @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
- 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
### Patch Changes

View File

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

View File

@@ -27,10 +27,19 @@ import {
isClaudeUnknownSessionError,
} from "./parse.js";
const PAPERCLIP_SKILLS_DIR = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../../../skills",
);
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
const PAPERCLIP_SKILLS_CANDIDATES = [
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
@@ -41,11 +50,13 @@ async function buildSkillsDir(): Promise<string> {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-"));
const target = path.join(tmp, ".claude", "skills");
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) {
if (entry.isDirectory()) {
await fs.symlink(
path.join(PAPERCLIP_SKILLS_DIR, entry.name),
path.join(skillsDir, entry.name),
path.join(target, entry.name),
);
}

View File

@@ -1,5 +1,37 @@
# @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
- 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
### Patch Changes

View File

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

View File

@@ -19,10 +19,11 @@ import {
} from "@paperclipai/adapter-utils/server-utils";
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
const PAPERCLIP_SKILLS_DIR = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../../../skills",
);
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
const PAPERCLIP_SKILLS_CANDIDATES = [
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 =
/^\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");
}
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"]) {
const sourceExists = await fs
.stat(PAPERCLIP_SKILLS_DIR)
.then((stats) => stats.isDirectory())
.catch(() => false);
if (!sourceExists) return;
const skillsDir = await resolvePaperclipSkillsDir();
if (!skillsDir) return;
const skillsHome = path.join(codexHomeDir(), "skills");
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) {
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 existing = await fs.lstat(target).catch(() => null);
if (existing) continue;

View File

@@ -1,5 +1,37 @@
# @paperclipai/adapter-openclaw
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,37 @@
# @paperclipai/db
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,29 @@
# @paperclipai/shared
## 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
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@paperclipai/shared",
"version": "0.2.1",
"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

@@ -125,7 +125,7 @@ echo ""
echo "==> Step 4/7: Building all packages..."
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/adapter-utils 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-openclaw 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 ─────────────────────────────────────────────────
@@ -183,8 +194,18 @@ if [ -f "$CLI_DIR/README.md" ]; then
rm "$CLI_DIR/README.md"
fi
# Commit all changes
git add -A
# Remove temporary build artifacts before committing (these are only needed during publish)
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 tag "v$NEW_VERSION"
echo " ✓ Committed and tagged v$NEW_VERSION"

View File

@@ -1,5 +1,57 @@
# @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
- 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
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@paperclipai/server",
"version": "0.2.1",
"version": "0.2.5",
"type": "module",
"exports": {
".": "./src/index.ts"
@@ -17,7 +17,9 @@
"types": "./dist/index.d.ts"
},
"files": [
"dist"
"dist",
"ui-dist",
"skills"
],
"scripts": {
"dev": "tsx src/index.ts",
@@ -35,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

@@ -124,12 +124,20 @@ export async function createApp(
const __dirname = path.dirname(fileURLToPath(import.meta.url));
if (opts.uiMode === "static") {
// Serve built UI from ui/dist in production.
const uiDist = path.resolve(__dirname, "../../ui/dist");
app.use(express.static(uiDist));
app.get(/.*/, (_req, res) => {
res.sendFile(path.join(uiDist, "index.html"));
});
// Try published location first (server/ui-dist/), then monorepo dev location (../../ui/dist)
const candidates = [
path.resolve(__dirname, "../ui-dist"),
path.resolve(__dirname, "../../ui/dist"),
];
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") {

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) {

View File

@@ -59,8 +59,9 @@ function readSkillMarkdown(skillName: string): string | null {
if (normalized !== "paperclip" && normalized !== "paperclip-create-agent") return null;
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const candidates = [
path.resolve(process.cwd(), "skills", normalized, "SKILL.md"),
path.resolve(moduleDir, "../../../skills", normalized, "SKILL.md"),
path.resolve(moduleDir, "../../skills", normalized, "SKILL.md"), // published: dist/routes/ -> <pkg>/skills/
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) {
try {