diff --git a/README.md b/README.md index f7ade1b3..42f24cd1 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,8 @@ Open source. Self-hosted. No Paperclip account required. npx paperclipai onboard --yes ``` +If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings. + Or manually: ```bash diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..1826e376 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,292 @@ +
+
+
+ Quickstart · + Docs · + GitHub · + Discord +
+ + + +| Works with |
+ OpenClaw |
+ Claude Code |
+ Codex |
+ Cursor |
+ Bash |
+ HTTP |
+
+🔌 Bring Your Own Agent+Any agent, any runtime, one org chart. If it can receive a heartbeat, it's hired. + |
+
+🎯 Goal Alignment+Every task traces back to the company mission. Agents know what to do and why. + |
+
+💓 Heartbeats+Agents wake on a schedule, check work, and act. Delegation flows up and down the org chart. + |
+
+💰 Cost Control+Monthly budgets per agent. When they hit the limit, they stop. No runaway costs. + |
+
+🏢 Multi-Company+One deployment, many companies. Complete data isolation. One control plane for your portfolio. + |
+
+🎫 Ticket System+Every conversation traced. Every decision explained. Full tool-call tracing and immutable audit log. + |
+
+🛡️ Governance+You're the board. Approve hires, override strategy, pause or terminate any agent — at any time. + |
+
+📊 Org Chart+Hierarchies, roles, reporting lines. Your agents have a boss, a title, and a job description. + |
+
+📱 Mobile Ready+Monitor and manage your autonomous businesses from anywhere. + |
+
+
+
+ Open source under MIT. Built for people who want to run companies, not babysit agents. +
diff --git a/cli/src/__tests__/onboard.test.ts b/cli/src/__tests__/onboard.test.ts new file mode 100644 index 00000000..a5ffe44a --- /dev/null +++ b/cli/src/__tests__/onboard.test.ts @@ -0,0 +1,105 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { onboard } from "../commands/onboard.js"; +import type { PaperclipConfig } from "../config/schema.js"; + +const ORIGINAL_ENV = { ...process.env }; + +function createExistingConfigFixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-onboard-")); + const runtimeRoot = path.join(root, "runtime"); + const configPath = path.join(root, ".paperclip", "config.json"); + const config: PaperclipConfig = { + $meta: { + version: 1, + updatedAt: "2026-03-29T00:00:00.000Z", + source: "configure", + }, + database: { + mode: "embedded-postgres", + embeddedPostgresDataDir: path.join(runtimeRoot, "db"), + embeddedPostgresPort: 54329, + backup: { + enabled: true, + intervalMinutes: 60, + retentionDays: 30, + dir: path.join(runtimeRoot, "backups"), + }, + }, + logging: { + mode: "file", + logDir: path.join(runtimeRoot, "logs"), + }, + server: { + deploymentMode: "local_trusted", + exposure: "private", + host: "127.0.0.1", + port: 3100, + allowedHostnames: [], + serveUi: true, + }, + auth: { + baseUrlMode: "auto", + disableSignUp: false, + }, + storage: { + provider: "local_disk", + localDisk: { + baseDir: path.join(runtimeRoot, "storage"), + }, + s3: { + bucket: "paperclip", + region: "us-east-1", + prefix: "", + forcePathStyle: false, + }, + }, + secrets: { + provider: "local_encrypted", + strictMode: false, + localEncrypted: { + keyFilePath: path.join(runtimeRoot, "secrets", "master.key"), + }, + }, + }; + + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }); + + return { configPath, configText: fs.readFileSync(configPath, "utf8") }; +} + +describe("onboard", () => { + beforeEach(() => { + process.env = { ...ORIGINAL_ENV }; + delete process.env.PAPERCLIP_AGENT_JWT_SECRET; + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY; + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + }); + + afterEach(() => { + process.env = { ...ORIGINAL_ENV }; + }); + + it("preserves an existing config when rerun without flags", async () => { + const fixture = createExistingConfigFixture(); + + await onboard({ config: fixture.configPath }); + + expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText); + expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false); + expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true); + }); + + it("preserves an existing config when rerun with --yes", async () => { + const fixture = createExistingConfigFixture(); + + await onboard({ config: fixture.configPath, yes: true, invokedByRun: true }); + + expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText); + expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false); + expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true); + }); +}); diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index 523484f3..d470354f 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -244,11 +244,12 @@ export async function onboard(opts: OnboardOptions): Promise