Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Server Configuration
NODE_ENV=development
# Optional base port for local multi-instance runs:
# MCP_HTTP_PORT=PCP_PORT_BASE
# WEB_PORT=PCP_PORT_BASE+1
# MYRA_HTTP_PORT=PCP_PORT_BASE+2
# PORT defaults to PCP_PORT_BASE-1 (if needed)
PCP_PORT_BASE=3001
PORT=3000

# Database - Supabase (use newer naming convention)
Expand All @@ -24,6 +30,9 @@ MCP_AUTH_TOKEN=your-secret-token-for-http-transport
# Myra (persistent messaging process)
MYRA_HTTP_PORT=3003

# Web dashboard (used by PM2 ecosystem config)
WEB_PORT=3002

# Authentication
JWT_SECRET=your-jwt-secret-key-min-32-chars-long
JWT_EXPIRES_IN=7d
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,6 @@ vite.config.ts.timestamp-*
# Backend-specific config (generated by sb mcp sync)
.codex/
.gemini/

# Local PCP identity should be machine/user-specific
.pcp/identity.json
5 changes: 0 additions & 5 deletions .pcp/identity.json

This file was deleted.

60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,66 @@ sb

See [packages/cli/README.md](./packages/cli/README.md) for full CLI documentation.

## Database Setup (Supabase)

PCP supports both:

- **Remote Supabase** (hosted Supabase project)
- **Local Supabase** (Docker + Supabase CLI)

### Option A: Remote Supabase (quickest to start)

1. Create/select a Supabase project.
2. Copy your project URL + API keys.
3. Fill `.env.local` from `.env.example`:
- `SUPABASE_URL`
- `SUPABASE_PUBLISHABLE_KEY`
- `SUPABASE_SECRET_KEY`
4. Start PCP:

```bash
yarn dev
```

### Option B: Local Supabase (best for offline/dev parity)

1. Install Supabase CLI and Docker:
- Supabase CLI install docs: https://supabase.com/docs/guides/cli/getting-started
2. Start local Supabase from this repo root:

```bash
supabase start
```

3. Reset/apply migrations + seed data:

```bash
supabase db reset
```

4. Print local env values:

```bash
supabase status -o env
```

5. Map local values into `.env.local`:
- `API_URL` → `SUPABASE_URL`
- `ANON_KEY` → `SUPABASE_PUBLISHABLE_KEY`
- `SERVICE_ROLE_KEY` → `SUPABASE_SECRET_KEY`

6. Start PCP:

```bash
yarn dev
```

Useful Supabase docs:

- Local development workflow: https://supabase.com/docs/guides/cli/local-development
- CLI reference (`start`, `status`, `db reset`, etc.): https://supabase.com/docs/reference/cli/start
- API key types and guidance: https://supabase.com/docs/guides/api/api-keys

## Project Structure

```
Expand Down
10 changes: 9 additions & 1 deletion ecosystem.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const path = require('path');
const rootDir = __dirname;
const apiDir = path.join(rootDir, 'packages/api');
const webDir = path.join(rootDir, 'packages/web');
const basePort = Number(process.env.PCP_PORT_BASE || 3001); // MCP-first base
const apiPort = Number(process.env.PORT || basePort - 1);
const mcpPort = Number(process.env.MCP_HTTP_PORT || basePort);
const webPort = Number(process.env.WEB_PORT || basePort + 1);
const myraPort = Number(process.env.MYRA_HTTP_PORT || basePort + 2);

// Yarn workspaces hoists dependencies to root node_modules
const tsxBin = path.join(rootDir, 'node_modules/.bin/tsx');
Expand All @@ -44,6 +49,9 @@ module.exports = {
env: {
NODE_ENV: 'development',
MCP_TRANSPORT: 'http',
PORT: String(apiPort),
MCP_HTTP_PORT: String(mcpPort),
MYRA_HTTP_PORT: String(myraPort),
ENABLE_WHATSAPP: 'true',
AGENT_ID: 'myra', // Identity for the Claude Code backend
},
Expand All @@ -64,7 +72,7 @@ module.exports = {
name: 'web',
cwd: webDir,
script: nextBin,
args: 'dev -p 3002',
args: `dev -p ${webPort}`,
watch: false,
env: {
NODE_ENV: 'development',
Expand Down
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test": "vitest run",
"test:watch": "vitest",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:codex-e2e": "RUN_CODEX_E2E=1 vitest run --config vitest.integration.config.ts src/services/sessions/codex-runner.integration.test.ts",
"test:coverage": "vitest run --coverage",
"test:connection": "tsx src/test-connection.ts",
"test:channels": "tsx src/test-channels.ts",
Expand Down
34 changes: 31 additions & 3 deletions packages/api/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const optionalUrl = z.string().url().optional().or(z.literal('')).transform(val
const envSchema = z.object({
// Server
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().transform(Number).default('3000'),
PCP_PORT_BASE: z.string().transform(Number).optional(),
PORT: z.string().transform(Number).optional(),

// Database - Supabase (supports both old and new naming conventions)
SUPABASE_URL: z.string().url(),
Expand All @@ -43,12 +44,12 @@ const envSchema = z.object({

// MCP Server
MCP_TRANSPORT: z.enum(['stdio', 'http']).default('stdio'),
MCP_HTTP_PORT: z.string().transform(Number).default('3001'),
MCP_HTTP_PORT: z.string().transform(Number).optional(),
MCP_BASE_URL: optionalUrl, // Public base URL (e.g., https://pcp.example.com). Defaults to http://localhost:{MCP_HTTP_PORT}
MCP_AUTH_TOKEN: optionalString,

// Myra (persistent messaging process)
MYRA_HTTP_PORT: z.string().transform(Number).default('3003'),
MYRA_HTTP_PORT: z.string().transform(Number).optional(),

// Authentication
JWT_SECRET: z.string().min(32),
Expand Down Expand Up @@ -85,11 +86,38 @@ const parseEnv = () => {
}

// Create normalized keys (prefer new naming)
const hasBaseOverride = parsed.PCP_PORT_BASE !== undefined;
// Base is MCP-first: MCP=base, WEB=base+1, MYRA=base+2
const portBase = parsed.PCP_PORT_BASE ?? 3001;

// If PCP_PORT_BASE is provided and legacy defaults are still present,
// treat those defaults as unset so the base can drive derived ports.
const port =
parsed.PORT === undefined || (hasBaseOverride && parsed.PORT === 3000)
? portBase - 1
: parsed.PORT;
const mcpHttpPort =
parsed.MCP_HTTP_PORT === undefined || (hasBaseOverride && parsed.MCP_HTTP_PORT === 3001)
? portBase
: parsed.MCP_HTTP_PORT;
const myraHttpPort =
parsed.MYRA_HTTP_PORT === undefined || (hasBaseOverride && parsed.MYRA_HTTP_PORT === 3003)
? portBase + 2
: parsed.MYRA_HTTP_PORT;

return {
...parsed,
PCP_PORT_BASE: portBase,
PORT: port,
MCP_HTTP_PORT: mcpHttpPort,
MYRA_HTTP_PORT: myraHttpPort,
SUPABASE_PUBLISHABLE_KEY: parsed.SUPABASE_PUBLISHABLE_KEY || parsed.SUPABASE_ANON_KEY,
SUPABASE_SECRET_KEY: parsed.SUPABASE_SECRET_KEY || parsed.SUPABASE_SERVICE_KEY,
} as typeof parsed & {
PCP_PORT_BASE: number;
PORT: number;
MCP_HTTP_PORT: number;
MYRA_HTTP_PORT: number;
SUPABASE_PUBLISHABLE_KEY: string;
SUPABASE_SECRET_KEY: string;
};
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/data/supabase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export type Database = {
agent_identities: {
Row: {
agent_id: string
backend: string | null
capabilities: Json | null
created_at: string | null
description: string | null
Expand All @@ -138,6 +139,7 @@ export type Database = {
}
Insert: {
agent_id: string
backend?: string | null
capabilities?: Json | null
created_at?: string | null
description?: string | null
Expand All @@ -155,6 +157,7 @@ export type Database = {
}
Update: {
agent_id?: string
backend?: string | null
capabilities?: Json | null
created_at?: string | null
description?: string | null
Expand Down Expand Up @@ -184,6 +187,7 @@ export type Database = {
Row: {
agent_id: string
archived_at: string | null
backend: string | null
capabilities: Json | null
change_type: string
created_at: string
Expand All @@ -203,6 +207,7 @@ export type Database = {
Insert: {
agent_id: string
archived_at?: string | null
backend?: string | null
capabilities?: Json | null
change_type?: string
created_at: string
Expand All @@ -222,6 +227,7 @@ export type Database = {
Update: {
agent_id?: string
archived_at?: string | null
backend?: string | null
capabilities?: Json | null
change_type?: string
created_at?: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* CodexRunner E2E Integration Test
*
* This test invokes the real Codex CLI and therefore requires:
* - codex installed
* - codex login (or API-key login) configured
*
* It is gated behind RUN_CODEX_E2E=1 to avoid accidental usage/cost.
*/

import { describe, it, expect, beforeAll } from 'vitest';
import { execSync } from 'child_process';
import { CodexRunner } from './codex-runner.js';

let shouldRun = false;
let skipReason = '';

describe('CodexRunner E2E (real codex cli)', () => {
beforeAll(() => {
if (process.env.RUN_CODEX_E2E !== '1') {
skipReason = 'Set RUN_CODEX_E2E=1 to run real Codex E2E tests';
// eslint-disable-next-line no-console
console.warn(`[codex-e2e] Skipping: ${skipReason}`);
return;
}

try {
execSync('codex --version', { stdio: 'pipe' });
} catch {
skipReason = 'codex binary is not installed';
// eslint-disable-next-line no-console
console.warn(`[codex-e2e] Skipping: ${skipReason}`);
return;
}

try {
// codex currently prints login status to stderr, so capture both streams
const status = execSync('codex login status 2>&1', { stdio: 'pipe', encoding: 'utf-8' });
const loggedIn = /logged in/i.test(status);
if (!loggedIn) {
skipReason = 'codex login is required (run `codex login` or `codex login --with-api-key`)';
// eslint-disable-next-line no-console
console.warn(`[codex-e2e] Skipping: ${skipReason}`);
return;
}
} catch {
skipReason = 'codex login status check failed';
// eslint-disable-next-line no-console
console.warn(`[codex-e2e] Skipping: ${skipReason}`);
return;
}

shouldRun = true;
});

it('can run a simple prompt via real codex exec --json', async () => {
if (!shouldRun) {
expect(skipReason.length).toBeGreaterThan(0);
return;
}

const runner = new CodexRunner();
const result = await runner.run('Reply with exactly: CODEX_E2E_OK', {
config: {
workingDirectory: process.cwd(),
mcpConfigPath: '',
model: process.env.CODEX_E2E_MODEL || 'gpt-5-codex',
appendSystemPrompt: 'You are running an integration test. Keep responses brief.',
},
});

expect(result.success).toBe(true);
expect(typeof result.claudeSessionId).toBe('string');
expect(result.claudeSessionId.length).toBeGreaterThan(0);
expect(result.finalTextResponse).toBeTruthy();
});
});
Loading