Final rebrand: rename remaining Rust source file to complete the gsd → forge transition. All parser references already use forge_parser after earlier commits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
24 KiB
Building Branded Apps on Top of Pi
This document covers the part that the extension docs, SDK docs, RPC docs, and package docs only imply when read together:
How do you build your own product on top of pi so users run your app, your command, and your UI rather than installing and managing pi directly?
Examples:
- a branded CLI like
sf - a desktop app that uses pi as its backend engine
- a web or Electron app that uses pi sessions, tools, and event streaming
- an internal company agent product built on pi primitives
The short answer is:
- Yes, you can build your own branded app on top of pi
- No, end users do not need to install pi globally if you ship your own app that depends on pi packages
- No, you do not have to rely on
~/.sfif you embed pi with custom paths and storage - Yes, you can bundle your own extensions, prompts, themes, skills, and providers inside your app
The rest of this document explains the architecture choices, storage choices, packaging strategies, and practical tradeoffs.
19.1 The Three Ways to Use Pi as a Foundation
There are really three layers you can build on:
@mariozechner/pi-coding-agent- Highest-level embedding API
- Best when you want pi's session system, resource loading, tools, extension model, and coding-agent behaviors
- Pi CLI in RPC mode
- Best when you want process isolation or language-agnostic integration
@mariozechner/pi-agent-core- Lower-level agent loop without the full pi coding-agent shell
- Best when you want more of the engine than the product surface
For most branded CLI or desktop app use cases, start with @mariozechner/pi-coding-agent.
Rule of thumb
- Want your own CLI/TUI with pi behavior under the hood -> use SDK embedding via
createAgentSession() - Want your own app in a different language or want a subprocess boundary -> use RPC mode
- Want a more generic agent engine and will build more infrastructure yourself -> use
@mariozechner/pi-agent-core
19.2 The Biggest Misconception: Pi Does Not Require a Global pi Install
If you are building a product on top of pi, your users do not need to install pi globally with npm.
You can ship your own app that depends on:
@mariozechner/pi-coding-agent@mariozechner/pi-agent-core@mariozechner/pi-ai@mariozechner/pi-tui@mariozechner/pi-web-ui
That means a branded command like:
sf
can be your executable, backed by pi internals, without asking users to separately install and run pi.
What this means in practice
Instead of telling users:
npm install -g @mariozechner/pi-coding-agent
pi
you can ship:
npm install -g my-sf
# or a standalone binary / packaged desktop app
sf
And inside sf, you import pi packages and create your own session, UI, storage, and resource loading behavior.
19.3 The Second Biggest Misconception: ~/.sf Is a Default, Not a Requirement
Pi CLI defaults to ~/.sf/agent, but embedded applications are not forced to use it.
When you use createAgentSession(), you can control:
agentDircwdauthStoragemodelRegistryresourceLoadersessionManagersettingsManager
That means your app can store state under:
~/.sf/agent~/Library/Application Support/SF%APPDATA%/SF- an app-local portable directory
- a project-local directory
instead of ~/.sf.
Things you can relocate
- auth and OAuth credentials
- settings
- models config
- sessions
- extensions
- prompt templates
- themes
- AGENTS-style context files
Important nuance
If you use the default resource loader and default managers, pi behaves like pi:
- standard discovery
- standard config locations
- standard session directories
If you pass custom managers and loaders, pi becomes an engine inside your app.
19.4 Choose an Architecture First
Before writing code, decide which of these architectures you actually want.
Architecture A: Branded Node CLI or TUI using the SDK
This is the most natural fit for tools like sf.
You create your own executable and call createAgentSession() directly.
Good for
- a branded terminal tool
- a custom TUI
- internal company coding agents
- a CLI with pi sessions, tools, and extensions under the hood
Benefits
- type-safe
- no subprocess management
- easy to customize storage and discovery
- easiest way to remove dependency on
~/.sf - easiest way to bundle built-in resources
Typical stack
@mariozechner/pi-coding-agent- optionally
@mariozechner/pi-tui - your own entrypoint and app directories
Architecture B: Branded App + Pi RPC subprocess
Here your app spawns pi as a subprocess and talks to it over JSON lines.
Good for
- non-Node host applications
- desktop shells with a strict engine boundary
- process isolation
- integrations where restarting the engine independently is useful
Benefits
- language-agnostic
- process isolation
- JSON protocol is explicit and stream-friendly
Costs
- you must manage subprocess lifecycle
- some UI features are degraded compared to pi's native TUI
- extension UI works through a request/response sub-protocol, not full TUI embedding
Architecture C: App built on pi-agent-core or pi-web-ui
This is for cases where you want pi's model and agent infrastructure but not necessarily pi's full coding-agent product surface.
Good for
- browser apps
- web chat products
- custom artifact workflows
- custom message types and renderers
Benefits
- lower-level control
- more app-specific freedom
- easier fit for non-terminal interfaces
Costs
- you build more yourself
- fewer coding-agent-specific conveniences out of the box
19.5 SDK vs RPC vs Agent-Core
Use this decision table.
| Goal | Best Starting Point |
|---|---|
Branded CLI like sf |
@mariozechner/pi-coding-agent SDK |
| Branded TUI with coding tools | @mariozechner/pi-coding-agent SDK |
| Desktop app with subprocess boundary | pi RPC mode |
| Non-Node integration | pi RPC mode |
| Browser chat app | @mariozechner/pi-web-ui + @mariozechner/pi-agent-core |
| Generic agent engine with custom infrastructure | @mariozechner/pi-agent-core |
| Want pi sessions/resources/extensions but app-owned directories | @mariozechner/pi-coding-agent SDK |
More detailed tradeoff matrix
| Concern | SDK | RPC | agent-core |
|---|---|---|---|
| Type safety | Excellent | Weak at protocol boundary | Excellent |
| Process isolation | No | Yes | No |
| Language agnostic | No | Yes | No |
| Full pi session/resource system | Yes | Yes | No |
| App-owned storage | Yes | Partial / external orchestration | Yes |
| Rich custom UI | Strong | Moderate | Strong |
| Uses pi extension ecosystem easily | Yes | Yes | No, not directly |
| Simplest branded CLI path | Yes | No | No |
19.6 The Recommended Path for a Branded CLI Like sf
If you want users to run:
sf
and you want it to feel like your product rather than "pi but renamed," the default recommendation is:
- Build a Node/TypeScript app
- Depend on
@mariozechner/pi-coding-agent - Create your own executable entrypoint
- Use
createAgentSession()directly - Set custom directories for config/auth/sessions
- Bundle your own extensions/prompts/themes/providers
- Expose only the commands and UX you want
That gives you the best control over:
- branding
- defaults
- storage layout
- startup behavior
- extension loading
- model/provider setup
19.7 App-Owned Storage Layout
A branded app should usually own its own storage hierarchy.
Example:
~/.sf/
agent/
auth.json
models.json
settings.json
extensions/
prompts/
themes/
skills/
sessions/
Or on macOS:
~/Library/Application Support/SF/
agent/
sessions/
Why this matters
If your product uses ~/.sf, then:
- it shares state with the user's pi installation
- branding becomes muddy
- support/debugging becomes more confusing
- product boundaries become less clear
Use app-specific directories unless you intentionally want interoperability with a user's pi environment.
Minimal example
import path from "node:path";
import os from "node:os";
import {
AuthStorage,
createAgentSession,
ModelRegistry,
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
const appRoot = path.join(os.homedir(), ".sf");
const agentDir = path.join(appRoot, "agent");
const sessionsDir = path.join(appRoot, "sessions");
const authStorage = AuthStorage.create(path.join(agentDir, "auth.json"));
const modelRegistry = new ModelRegistry(authStorage, path.join(agentDir, "models.json"));
const settingsManager = SettingsManager.create(process.cwd(), agentDir);
const sessionManager = SessionManager.create(process.cwd(), sessionsDir);
const { session } = await createAgentSession({
cwd: process.cwd(),
agentDir,
authStorage,
modelRegistry,
settingsManager,
sessionManager,
});
This is the core pattern for “my app uses pi, but not as global pi.”
19.8 Bundling Resources Inside Your App
This is another place where people often assume they must rely on discovery from ~/.sf or .sf/.
You do not.
Your app can bundle:
- extensions
- prompts
- themes
- skills
- AGENTS-style context
- provider registrations
inside your own package or app bundle.
Strategy 1: Use custom paths with DefaultResourceLoader
import { DefaultResourceLoader } from "@mariozechner/pi-coding-agent";
const loader = new DefaultResourceLoader({
cwd: process.cwd(),
agentDir,
additionalExtensionPaths: [
"/absolute/path/to/bundled/extension.ts",
],
});
await loader.reload();
Strategy 2: Use inline extension factories
const loader = new DefaultResourceLoader({
cwd: process.cwd(),
agentDir,
extensionFactories: [
(pi) => {
pi.registerCommand("hello", {
description: "My branded command",
handler: async (_args, ctx) => ctx.ui.notify("Hello from SF", "info"),
});
},
],
});
Strategy 3: Override discovered resources entirely
const loader = new DefaultResourceLoader({
cwd: process.cwd(),
agentDir,
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
skillsOverride: () => ({ skills: [], diagnostics: [] }),
agentsFilesOverride: () => ({ agentsFiles: [] }),
systemPromptOverride: () => "You are SF, a specialized software delivery agent.",
});
Why this matters
For a branded product, it is often better to think in terms of:
- bundled built-ins shipped by your app
- optional plugin support later
rather than:
- user-managed global pi resources first
19.9 Discovery vs Bundling
These are different product strategies.
Discovery-driven product
You intentionally load from:
~/.sf/agent/....sf/...- installed pi packages
Good when
- your product is basically pi with additions
- you want compatibility with existing pi user workflows
Bundled-app product
You intentionally ship your own resources and avoid implicit user-level discovery.
Good when
- you want strong branding
- you want predictable behavior
- you want supportability and reproducibility
- you do not want random user extensions affecting behavior
Recommendation
For a branded tool like sf, default to bundled-app product behavior.
If you later add plugin support, make it explicit.
19.10 Using Pi Packages Internally vs Externally
Pi packages are a sharing mechanism for extensions, prompts, skills, and themes.
But when you are building your own app, there are two separate questions:
- Should your app itself be distributed as a pi package?
- Should your app internally use pi-package-style resource organization?
Usually, for a branded app:
- No on #1
- Maybe on #2
If your users run your app directly, your app is usually a normal Node package, binary, or desktop app, not a pi package.
But internally, you may still organize resources in a pi-friendly structure:
src/
resources/
extensions/
prompts/
themes/
skills/
and load them through your resource loader.
When pi packages still matter
Pi packages are still useful when:
- you want optional add-ons
- you want to reuse existing pi ecosystem resources
- you want third parties to extend your app through pi-compatible bundles
19.11 RPC Mode for Branded Apps
RPC mode is the right answer when your product wants pi as a subprocess engine.
Start it with:
pi --mode rpc
or programmatically by calling runRpcMode(session) in your own Node process.
RPC is good for
- non-Node clients
- desktop shells in other runtimes
- separate engine process architecture
- explicit JSON protocol boundaries
What RPC gives you
- prompt / steer / follow_up / abort
- model selection
- state inspection
- session operations
- bash execution
- event streaming
- extension UI request/response protocol
Important limitation
RPC is not the same thing as embedding pi's full native TUI.
Some extension UI methods degrade in RPC mode.
Dialogs still work
selectconfirminputeditor
Fire-and-forget UI signals still work
- notifications
- status
- widgets
- title
- editor text setting
Some richer TUI behaviors do not map cleanly
- full
custom()component workflows - some footer/header/editor replacement behavior
- some theme-specific TUI behavior
If your branded app needs a deeply custom UI, SDK embedding or direct app-level UI integration is usually better.
19.12 Extension UI in RPC Mode
One subtle but important point: extensions with user interaction are still possible in RPC mode, but through a protocol, not by directly rendering pi TUI components.
The client receives extension_ui_request messages and must answer with extension_ui_response for blocking dialogs.
This means you can build your own frontend and still support many extension-driven workflows.
But know the boundary
RPC mode supports:
- interaction patterns
- not full TUI component identity
If your extension assumes pi's exact terminal UI surface, it may need adaptation.
19.13 Web and Browser Apps
If your app is a web app or browser-hosted UI, look closely at:
@mariozechner/pi-agent-core@mariozechner/pi-web-ui
pi-web-ui already provides:
- chat UI
- session storage
- provider key storage
- attachments
- artifacts
- model selection
- settings dialogs
- renderers and tool renderers
This is effectively a starter kit for a branded web app using pi-related primitives.
Use pi-web-ui when
- you want a browser or Electron-friendly UI surface
- you want a ready-made chat shell
- you do not specifically want pi's TUI
Use pi-coding-agent SDK when
- you want coding-agent-specific resource loading, sessions, extensions, and coding tool behaviors
- your app is terminal-first or Node-first
19.14 Branding Boundaries: What Still Feels Like Pi?
This matters if you are building a white-labeled or branded product.
If you spawn the pi CLI directly
Your product is closer to “pi as a subprocess.” That is fine, but many pi-level assumptions remain nearby.
If you embed @mariozechner/pi-coding-agent
You can hide most pi branding and product surface decisions. You keep the coding-agent infrastructure but own the app UX.
If you use @mariozechner/pi-agent-core
You are even lower-level. Pi becomes more of a library source than a user-visible product.
Practical recommendation
If branding matters, do not treat the pi CLI binary as your product surface unless you truly want pi semantics exposed.
Use the SDK or lower-level packages and build your own interface.
19.15 Session Strategy for a Branded App
Decide whether your app wants:
- persistent sessions with app-owned storage
- ephemeral sessions only
- project-local sessions
- branching session history exposed to users
Persistent app-owned sessions
Most natural for a CLI or desktop app.
const sessionManager = SessionManager.create(process.cwd(), sessionsDir);
Ephemeral mode
Useful for task-runner or automation workflows.
const sessionManager = SessionManager.inMemory();
Important question
Do you want your app to share session files with pi itself?
Usually the answer should be no unless interoperability is an explicit feature.
19.16 Settings Strategy for a Branded App
You should decide whether settings are:
- file-backed
- in-memory
- app-global
- project-local
- user-editable
- controlled only by your product UI
App-owned settings
const settingsManager = SettingsManager.create(projectCwd, agentDir);
with agentDir pointing into your app-owned config directory.
Fully controlled settings
const settingsManager = SettingsManager.inMemory({
compaction: { enabled: true },
retry: { enabled: true, maxRetries: 2 },
});
Use in-memory settings when you want the host app to own the config model entirely.
19.17 Provider and Auth Strategy
A branded app should decide whether users:
- bring their own API keys
- use OAuth through pi provider support
- connect to your proxy/backend
- use your own registered providers
App-owned auth paths
Use custom AuthStorage paths.
const authStorage = AuthStorage.create("/path/to/sf/auth.json");
App-owned model config
Use your own models.json location or register providers dynamically.
const modelRegistry = new ModelRegistry(authStorage, "/path/to/sf/models.json");
Custom provider strategy
If your app talks to a proxy or company backend, register providers from your app or bundled extensions.
That keeps the app experience aligned with your branding and infrastructure.
19.18 Building a Branded sf CLI: Recommended Shape
A practical architecture looks like this:
my-sf/
package.json
src/
cli.ts
app-paths.ts
session.ts
resource-loader.ts
ui/
resources/
extensions/
prompts/
themes/
skills/
In cli.ts
- parse your app flags
- compute app directories
- create auth/model/settings/session managers
- create resource loader
- create agent session
- run your own mode (custom TUI, print mode, or RPC bridge)
In resource-loader.ts
- load bundled resources
- optionally disable ambient pi discovery
- add your branded system prompt and context files
In bundled extensions
- add your commands
- register your custom tools
- control your app-specific behaviors
19.19 Minimal SDK Skeleton for a Branded CLI
import path from "node:path";
import os from "node:os";
import {
AuthStorage,
createAgentSession,
DefaultResourceLoader,
ModelRegistry,
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
const appRoot = path.join(os.homedir(), ".sf");
const agentDir = path.join(appRoot, "agent");
const sessionsDir = path.join(appRoot, "sessions");
const authStorage = AuthStorage.create(path.join(agentDir, "auth.json"));
const modelRegistry = new ModelRegistry(authStorage, path.join(agentDir, "models.json"));
const settingsManager = SettingsManager.create(process.cwd(), agentDir);
const sessionManager = SessionManager.create(process.cwd(), sessionsDir);
const resourceLoader = new DefaultResourceLoader({
cwd: process.cwd(),
agentDir,
settingsManager,
systemPromptOverride: () =>
"You are SF, a branded software delivery agent. Prefer project-specific workflows and terminology.",
additionalExtensionPaths: [
path.resolve("resources/extensions/index.ts"),
],
});
await resourceLoader.reload();
const { session } = await createAgentSession({
cwd: process.cwd(),
agentDir,
authStorage,
modelRegistry,
settingsManager,
sessionManager,
resourceLoader,
});
session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await session.prompt("Help me understand this repo.");
This is not yet a full product, but it is the correct starting shape for one.
19.20 When to Reuse Pi's Interactive Mode
The SDK exports InteractiveMode, runPrintMode, and runRpcMode.
These are useful if you want to reuse existing pi surfaces while changing the surrounding setup.
Reuse InteractiveMode when
- you want pi's TUI mostly intact
- but with app-owned storage, extensions, defaults, and resources
Do not reuse it when
- you want a strongly branded UI
- you want different commands or layout metaphors
- you want your app to feel fundamentally different from pi
For a white-labeled product, InteractiveMode is a good prototyping step, not always the final product surface.
19.21 What to Avoid in a Branded Product
Avoid accidental dependence on ambient user state
If your app silently loads from a user's ~/.sf, you may get:
- surprising extensions
- strange prompts
- odd themes
- hard-to-debug behavior differences
Avoid mixing branding and storage casually
If your app is called sf, but state lives in ~/.sf, users will notice.
Avoid choosing RPC just because it sounds generic
If your app is already Node/TypeScript, SDK embedding is usually simpler and more powerful.
Avoid exposing every pi concept unless you want to
A branded product should choose what the user sees. You do not need to expose:
- all slash commands
- all extension loading paths
- all package concepts
- all theme/customization behaviors
19.22 Suggested Product Postures
Posture A: “Pi-compatible branded shell”
- Uses pi concepts openly
- Supports pi packages and pi-style discovery
- Good for power users
Posture B: “Branded app powered by pi”
- Uses pi internally
- App-owned directories and resources
- Explicit plugins only
- Good for productized tools like
sf
Posture C: “Custom agent product using pi primitives”
- Uses
pi-agent-coreor selective libraries - Pi itself is mostly invisible
- Good for SaaS or browser products
For most branded command-line products, posture B is the best fit.
19.23 Recommended Documentation Reading Order for This Use Case
If you are building a branded app on top of pi, read in this order:
what-is-pi/14-the-sdk-rpc-embedding-pi.md- this file
extending-pi/19-packaging-distribution.mdextending-pi/04-extension-locations-discovery.mdextending-pi/05-extension-structure-styles.mdextending-pi/12-custom-ui-visual-components.mdpi-ui-tui/01-the-ui-architecture.mdpi-ui-tui/03-entry-points-how-ui-gets-on-screen.mdpi-ui-tui/22-quick-reference-all-ui-apis.md
Then read the source package docs for exact API details:
packages/coding-agent/docs/sdk.mdpackages/coding-agent/docs/rpc.mdpackages/coding-agent/docs/extensions.mdpackages/coding-agent/docs/packages.mdpackages/web-ui/README.md
19.24 Bottom Line
If your goal is:
“I want users to download and run
sf, and have it use pi internally without requiring a separate pi install or~/.sfsetup.”
Then the answer is:
- Yes, that is a supported architecture
- Use the SDK first unless you have a strong reason to choose RPC
- Use app-owned storage directories
- Bundle your own resources instead of relying on global discovery
- Use pi packages as an ecosystem mechanism, not as a requirement for your app's internal structure
- Treat pi as a foundation layer, not necessarily the product surface
That is the difference between:
- “using pi as a user tool”
- and “building your own product on top of pi.”