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: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ cloning this repository:
npx @pascal-app/cli editor
```

The CLI starts the editor in the background at `http://pascal.localhost:<port>` and
keeps projects in `~/.pascal/data/pascal.db`. See [Run Pascal
locally](https://editor.pascal.app/docs/developers/local-editor) for pnpm/Bun commands,
lifecycle management, updates, storage paths, and troubleshooting.
The CLI starts the editor and an authenticated MCP service in the background, selects
collision-free loopback ports, and keeps projects in `~/.pascal/data/pascal.db`. Configure
an agent to launch `pascal mcp connect`. See [Run Pascal locally](https://editor.pascal.app/docs/developers/local-editor)
for pnpm/Bun commands, project management, MCP setup, updates, storage paths, and
troubleshooting.

## Using Published Packages

Expand Down
5 changes: 4 additions & 1 deletion SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ background, and open it in the browser without a repository checkout:
npx @pascal-app/cli editor
```

Use `npx @pascal-app/cli doctor` to check the runtime, storage, and process state. Saved
The command starts the editor and its authenticated local MCP service together. Configure
an agent to launch `pascal mcp connect`; for example, run `pascal mcp setup codex`.

Use `npx @pascal-app/cli doctor` to check the runtime, storage, editor, and MCP state. Saved
scenes live in `~/.pascal/data/pascal.db` independently from installed runtime versions.
The CLI retains old runtime versions for rollback and warns after more than three have
accumulated. It also replaces a damaged copy of its bundled runtime on the next start;
Expand Down
13 changes: 8 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 40 additions & 18 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ for `status`, `logs`, `stop`, and future sessions without another setup step. If
global installation is unavailable because of local npm permissions, the editor remains
running and the CLI shows the equivalent `npx` commands plus the manual install command.

The first run walks through local storage, runtime installation, automatic port
selection, process startup, and a health check with live terminal feedback. It then
opens `http://pascal.localhost:<port>`. Your projects are stored separately from the
runtime, so updating the CLI does not replace your work.
The first run walks through local storage, runtime installation, automatic editor and
MCP port selection, process startup, and both health checks with live terminal feedback.
It then opens `http://pascal.localhost:<port>`. Your projects are stored separately from
the runtime, so updating the CLI does not replace your work.

## Why use the CLI?

- Run a complete local Pascal editor with one command.
- Keep projects on your machine in a local SQLite database.
- Start and stop the editor independently from your terminal session.
- Inspect health, logs, versions, storage, and project state from scripts or agents.
- Connect Codex, Claude Code, Cursor, or another MCP client to the same local projects.
- Update through a health-checked activation that rolls back if the new runtime fails.

## Requirements
Expand Down Expand Up @@ -83,32 +84,40 @@ npx @pascal-app/cli editor --foreground --no-open
| --- | --- |
| `pascal editor` | Install if needed, ensure the editor is running, and open it. |
| `pascal start` | Ensure the editor is running without opening a browser. |
| `pascal stop [--force]` | Stop the managed process; `--force` is a guarded recovery path. |
| `pascal restart` | Restart the editor with its current configuration. |
| `pascal status [--json]` | Show health, version, PID, URL, and runtime metadata. |
| `pascal open` | Open the running editor in your default browser. |
| `pascal stop [--force]` | Stop the managed editor and MCP processes; `--force` is a guarded recovery path. |
| `pascal restart` | Restart the editor and MCP service with their current configuration. |
| `pascal status [--json]` | Show editor and MCP health, version, PIDs, ports, URL, and runtime metadata. |
| `pascal open [project]` | Start Pascal if needed, then open the editor or a project by ID, ID prefix, or unique name. |
| `pascal resume [project]` | Open the latest project, or a selected project. |
| `pascal projects [--json]` | List local projects. |
| `pascal logs [--follow]` | Read or follow the managed editor log. |
| `pascal update [--version <version>]` | Health-check and activate a published runtime. |
| `pascal doctor [--json]` | Diagnose Node.js, storage, runtime, process, and plugin state. |
| `pascal info [--json]` | Print platform, paths, runtime, and plugin context. |
| `pascal project list [--json]` | List projects in the running local editor. |
| `pascal project open <id>` | Open a local project in your browser. |
| `pascal project list [--json]` | Explicit form of `pascal projects`. |
| `pascal project open <id-or-name>` | Explicit form of `pascal open <project>`. |
| `pascal mcp connect` | Stable local connector for MCP clients; discovers the dynamic managed service. |
| `pascal mcp status [--json]` | Show managed MCP health. |
| `pascal mcp config [--json]` | Print generic MCP client configuration. |
| `pascal mcp setup <codex\|claude>` | Configure an installed client without overwriting existing entries. |
| `pascal plugin list [--json]` | Inspect the reserved managed-plugin lock. |

When you do not install globally, prefix commands with a runner—for example,
`npx @pascal-app/cli doctor`.

## Local data and security

Pascal binds only to `127.0.0.1` and uses the reserved `.localhost` hostname. The
initial CLI does not expose an unauthenticated editor to your network.
Pascal binds the editor and MCP service only to `127.0.0.1` and uses the reserved
`.localhost` hostname. MCP requires a random token stored in Pascal's private runtime
directory; client configuration never contains that token.

```text
~/.pascal/
runtime/<version>/ installed editor runtimes
data/pascal.db projects and scenes
logs/editor.log detached editor output
run/editor.json managed process identity
run/editor.json managed editor and MCP process identity
run/mcp-token private local MCP token
plugins/ reserved verified-plugin storage
pascal.plugins.lock reserved managed-plugin lock
```
Expand All @@ -118,17 +127,30 @@ The CLI does not include a command that deletes project data. Updates retain the
previous runtime for rollback, and `pascal doctor` warns when more than three versions
have accumulated.

## Plugins and AI agents
## Local AI agents

The MCP server starts automatically with `pascal editor`. Add the stable connector to
your client once:

```bash
pascal mcp setup codex
pascal mcp setup claude
```

Or use `pascal mcp config` for JSON-based clients. The connector also starts Pascal
when an agent connects while it is stopped. Ask the agent to read
`pascal://agent-guide`, list or load a scene, edit it, and return the `editorUrl`.

## Plugins

The current CLI manages the local editor runtime; it does not yet download plugin code
from GitHub or npm. Follow the [plugin authoring guide](https://editor.pascal.app/docs/developers/plugins)
and the standalone [Nature plugin](https://github.com/pascalorg/plugin-trees) when
building an extension today.

Pascal also exposes a hosted Model Context Protocol endpoint for Claude Code, Codex,
Cursor, OpenClaw, and other MCP clients. See [Connect an AI agent](https://editor.pascal.app/docs/developers/mcp)
for the hosted setup and the relationship between hosted projects, the local editor,
and `@pascal-app/mcp`.
Pascal also exposes a hosted Model Context Protocol endpoint for projects in a Pascal
account. See [Connect an AI agent](https://editor.pascal.app/docs/developers/mcp) for
the local and hosted workflows and the standalone `@pascal-app/mcp` package.

## Documentation and support

Expand Down
9 changes: 7 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@pascal-app/cli",
"version": "0.1.4",
"description": "Run and manage the open-source Pascal 3D building editor locally from your terminal",
"description": "Run the open-source Pascal 3D editor, local projects, and MCP agent tools from your terminal",
"type": "module",
"bin": {
"pascal": "dist/bin/pascal.js"
Expand Down Expand Up @@ -34,6 +34,9 @@
"@types/node": "^22.19.20",
"typescript": "6.0.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0"
},
"engines": {
"node": ">=22.13.0"
},
Expand All @@ -47,7 +50,9 @@
"cad",
"bim",
"local-first",
"cli"
"cli",
"mcp",
"ai-agents"
],
"repository": {
"type": "git",
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/scripts/smoke-packed-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import http from 'node:http'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'

const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const smokeRoot = await mkdtemp(path.join(os.tmpdir(), 'pascal-cli-smoke-'))
Expand Down Expand Up @@ -84,6 +86,40 @@ try {
undefined,
smokeEnvironment,
)
const mcpTransport = new StdioClientTransport({
command: process.execPath,
args: [smokeExecutable, 'mcp', 'connect'],
env: smokeEnvironment as Record<string, string>,
stderr: 'pipe',
})
const mcpClient = new Client({ name: 'pascal-cli-smoke', version: '0.0.0' })
try {
await mcpClient.connect(mcpTransport)
const tools = await mcpClient.listTools()
if (!tools.tools.some((tool) => tool.name === 'save_scene')) {
throw new Error('managed MCP did not expose save_scene')
}
const saved = await mcpClient.callTool({
name: 'save_scene',
arguments: { id: 'smoke-project', name: 'Smoke project' },
})
if (saved.isError) throw new Error(`managed MCP save_scene failed: ${JSON.stringify(saved)}`)
} finally {
await mcpClient.close()
}
const resumed = JSON.parse(
(
await run(
process.execPath,
[smokeExecutable, 'resume', 'Smoke project', '--json'],
undefined,
smokeEnvironment,
)
).stdout,
) as { project: { id: string }; url: string }
if (resumed.project.id !== 'smoke-project' || !resumed.url.endsWith('/scene/smoke-project')) {
throw new Error('CLI project resume did not resolve the MCP-saved project')
}
await run(process.execPath, [smokeExecutable, 'doctor', '--json'], undefined, smokeEnvironment)
await run(process.execPath, [smokeExecutable, 'stop', '--json'], undefined, smokeEnvironment)
smokeExecutable = null
Expand Down
34 changes: 34 additions & 0 deletions packages/cli/scripts/stage-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { spawn } from 'node:child_process'
import { chmod, cp, mkdir, readdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
Expand Down Expand Up @@ -30,6 +31,7 @@ await cp(
path.join(outputDirectory, 'apps/editor/.next/static'),
{ recursive: true, force: true },
)
await bundleMcpServer(outputDirectory, packageJson.version)

await removeUnusedSharp(outputDirectory)
await flattenBunNodeModules(outputDirectory)
Expand All @@ -47,7 +49,9 @@ await writeFile(
schemaVersion: 1,
version: packageJson.version,
entrypoint: 'apps/editor/server.js',
mcpEntrypoint: 'services/pascal-mcp.mjs',
healthPath: '/api/health',
mcpHealthPath: '/health',
},
null,
2,
Expand All @@ -56,6 +60,36 @@ await writeFile(

console.log(`Staged Pascal editor runtime ${packageJson.version} at ${outputDirectory}`)

async function bundleMcpServer(runtimeDirectory: string, version: string): Promise<void> {
const output = path.join(runtimeDirectory, 'services/pascal-mcp.mjs')
await mkdir(path.dirname(output), { recursive: true })
const child = spawn(
process.execPath,
[
'build',
path.join(repositoryRoot, 'packages/mcp/src/bin/pascal-mcp.ts'),
'--outfile',
output,
'--target',
'node',
'--format',
'esm',
'--define',
`process.env.PASCAL_MCP_VERSION=${JSON.stringify(version)}`,
],
{ stdio: ['ignore', 'ignore', 'pipe'] },
)
const stderr: Buffer[] = []
child.stderr.on('data', (chunk: Buffer) => stderr.push(chunk))
const exitCode = await new Promise<number>((resolve, reject) => {
child.once('error', reject)
child.once('exit', (code) => resolve(code ?? 1))
})
if (exitCode !== 0) {
throw new Error(`Unable to bundle the Pascal MCP server: ${Buffer.concat(stderr).toString()}`)
}
}

async function assertFile(filePath: string): Promise<void> {
try {
await readFile(filePath)
Expand Down
Loading
Loading