D-Pi (@sheason/d-pi)
D-Pi is the unified CLI for running a Pi hub and connecting terminal peers to it.
It contains the hub runtime and peer TUI directly:
d-pi hub ...runs the workspace-local backend that owns sessions, agents, model state, MCP servers, and sources.d-pi peer ...runs the terminal UI and local tool executor that connects to a hub.
Use D-Pi when you want one command surface for a multi-agent Pi workspace.
Quick Start
Install the CLI:
npm install -g @sheason/d-piCreate and start a hub in your project workspace:
cd /path/to/your/workspace
d-pi hub init
d-pi hub serveOpen another terminal and connect a peer:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptopYou now have:
- one hub process serving the workspace
- one terminal peer bound to the default
mainagent - a TUI where you can chat with the agent, select models, inspect sources, inspect MCP servers, and manage the current session
Installation
Global Install
npm install -g @sheason/d-piCheck the installed command:
d-pi helpLocal Development Install
From this monorepo:
npm install
npm run build --workspace @sheason/d-pi-web-ui
npm run build --workspace @sheason/d-pi
npm link --workspace @sheason/d-piHub and peer internals live inside the @sheason/d-pi package, so no separate hub or peer binaries need to be linked.
Core Commands
d-pi help
d-pi hub <command>
d-pi peer [options]Hub Commands
Run these from the workspace you want the hub to own.
d-pi hub init
d-pi hub add-skills
d-pi hub serve
d-pi hub export <archive.tar>
d-pi hub import <archive.tar> [--force]
d-pi hub status
d-pi hub cleand-pi hub init creates workspace state under:
.pi/agents.json.pi-hub/session.jsonl.pi-hub/session-meta.json
d-pi hub add-skills installs built-in guidance skills into .pi/skills.
d-pi hub serve starts the Socket.IO backend and opens the hub dashboard. By default it listens on 0.0.0.0:4317, so it is reachable from your LAN.
The same process also serves the built-in Web UI for the main agent:
http://127.0.0.1:4317/
Open a specific agent by path:
http://127.0.0.1:4317/
http://127.0.0.1:4317/agents/<child-agent-id>
The Web UI uses the same-origin Socket.IO/CRDT hub protocol, so no CORS setup is required. It connects as a hub host UI, not as a peer executor, so it is not counted in peerCount and cannot run peer-local tools. Use d-pi peer --agent <id> when you need peer-local tools.
Override the listen address with environment variables. Use 127.0.0.1 for local-only access:
PI_HUB_HOST=127.0.0.1 PI_HUB_PORT=4317 d-pi hub serveWhen using the default LAN binding, browse to http://<machine-lan-ip>:4317/ from another device.
d-pi hub export <archive.tar> writes a tar archive containing the workspace-local hub state and Pi configuration:
.pi-hub/for hub runtime state, session history, and agent state.pi/for workspace-local Pi configuration, sources, agents, and skills
d-pi hub import <archive.tar> restores those directories into the current workspace. It refuses to overwrite an existing .pi-hub or .pi directory unless --force is set:
d-pi hub import ./workspace.tar --forced-pi hub status prints current workspace hub metadata.
d-pi hub clean removes hub workspace state.
Peer Commands
Connect to a local hub:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptopConnect to a remote hub:
d-pi peer --hub http://HOSTNAME_OR_IP:4317 --peer-id laptopUse a display name:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptop --name "Laptop"Bind the peer to a child agent:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptop --agent obsidianThe peer is bound to one agent for the process lifetime. To switch agents, start another peer process with a different --agent.
Install peer-side guidance skills:
d-pi peer add-skillsTUI Commands
Inside d-pi peer, slash commands drive the hub-owned session:
| Command | Purpose |
|---|---|
/model |
Inspect or switch the active model |
/settings |
Inspect supported peer settings |
/settings thinking <level> |
Change reasoning level (off, minimal, low, medium, high, xhigh) |
/compact |
Ask the hub to compact the current session |
/reload |
Reload models, settings, sources, MCP servers, skills, and peer config |
/group |
Show main/child agents and available tool executors |
/session |
Show current session snapshot details |
/source |
Show hub and peer-local source processes and status |
/mcp |
Show MCP servers, capabilities, and status |
/skills |
Show available hub and peer skills |
Branch-oriented single-agent commands such as /new, /resume, /tree, /fork, and /clone are disabled in the hub/peer runtime.
Mental Model
One workspace has one hub. The hub owns durable state and all agent sessions.
Peers are frontends and executors:
- The hub stores session history and CRDT view state.
- A peer renders one agent session.
- Peer tools run on the peer machine.
- Hub tools run in the hub workspace.
- Sources and MCP servers are routed by stable
resourceIds, so different resources can share the same human name without ambiguous ownership.
Agents:
mainis created byd-pi hub init.- Child agents live under
.child-agent/<agent-id>/. - A peer connects to
mainunless--agent <id>is provided. - Use
/groupto see which agents and peers are available.
Workspace Configuration
D-Pi uses standard Pi workspace files.
Models
Workspace model config:
.pi/models.json
Models are merged into the hub model registry. Run /reload from a peer after editing model config.
Sources
Hub source config:
.pi/sources.json
A source is a long-running process that writes line-delimited JSON-RPC notifications to stdout.
Minimal source:
[
{
"name": "local-source",
"transport": "stdio",
"command": "node",
"args": [".pi/local-source.cjs"]
}
]Source stdout must use queue/write:
{"jsonrpc":"2.0","method":"queue/write","params":{"content":"hello from source"}}Rules:
- stdout is reserved for JSON-RPC notifications
- logs should go to stderr
params.contentmust be a stringparams.deliveryis not supported- the target agent defaults to
main
To target a child agent directly:
[
{
"name": "child-source",
"transport": "stdio",
"command": "node",
"args": [".pi/child-source.cjs"],
"agentId": "obsidian"
}
]To let a child agent inherit a host source as its own independent instance:
{
"extends": {
"host": {
"sources": ["lark-message-watcher"]
}
},
"sources": []
}Save that as:
.child-agent/<agent-id>/sources.json
After editing sources, run /reload from a peer or restart the hub.
MCP Servers
Hub MCP config:
.pi/mcp.json
Example stdio server:
{
"servers": [
{
"name": "filesystem",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
}
]
}Example HTTP server:
{
"servers": [
{
"name": "remote",
"transport": "http",
"url": "https://example.com/mcp"
}
]
}Use /mcp to inspect status and capabilities. Use /reload after editing MCP config.
Skills
Workspace skills:
.pi/skills/<skill-name>/SKILL.md
Child-local skills:
.child-agent/<agent-id>/skills/<skill-name>/SKILL.md
Install built-in guidance skills:
d-pi hub add-skills
d-pi peer add-skillsUse /skills in the peer to inspect the effective skill set.
Subscribe to Lark Messages
This section shows how to turn Lark messages into Pi source messages.
The pipeline is:
Lark Open Platform
-> lark-cli event +subscribe
-> .pi/lark-message-source.cjs
-> stdout queue/write
-> D-Pi hub source host
-> target agent queue
1. Install and Configure lark-cli
Install lark-cli following the Lark CLI documentation, then initialize app config:
lark-cli config init --newLark event subscription uses bot identity. User login is not required for the WebSocket event connection.
2. Configure Lark Open Platform
In the Lark Open Platform console:
- Open your app.
- Go to Events & Callbacks.
- Set the subscription method to long connection.
- Add the event type:
im.message.receive_v1
- Enable the required permission:
im:message:receive_as_bot
- Add the bot to the chats where it should receive messages.
3. Test the Lark Event Stream
Run:
lark-cli event +subscribe \
--as bot \
--event-types im.message.receive_v1 \
--compact \
--quietExpected output is NDJSON, one event per line:
{"type":"im.message.receive_v1","message_id":"om_xxx","chat_id":"oc_xxx","chat_type":"p2p","message_type":"text","content":"Hello","sender_id":"ou_xxx","timestamp":"1773491924409"}Important: do not run multiple subscribers for the same app unless you know what you are doing. Lark can split events across multiple long-connection clients. lark-cli event +subscribe protects this by default; avoid --force for Pi sources.
4. Create a Pi Source Wrapper
Create .pi/lark-message-source.cjs:
#!/usr/bin/env node
const { spawn } = require("node:child_process");
const eventTypes = process.env.LARK_EVENT_TYPES || "im.message.receive_v1";
const larkCli = process.env.LARK_CLI_PATH || "lark-cli";
function writeQueueMessage(content) {
process.stdout.write(
`${JSON.stringify({
jsonrpc: "2.0",
method: "queue/write",
params: { content },
})}\n`,
);
}
function start() {
const args = [
"event",
"+subscribe",
"--as",
"bot",
"--event-types",
eventTypes,
"--compact",
"--quiet",
];
console.error(`[lark-source] starting: ${larkCli} ${args.join(" ")}`);
const child = spawn(larkCli, args, {
stdio: ["ignore", "pipe", "pipe"],
});
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString("utf8");
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.trim()) continue;
try {
const event = JSON.parse(line);
const content = event.content || "";
if (!content) continue;
writeQueueMessage(
[
"Received Lark message",
`type: ${event.type || "unknown"}`,
`chat: ${event.chat_type || "unknown"}`,
`sender: ${event.sender_id || "unknown"}`,
`message_id: ${event.message_id || ""}`,
`content: ${content}`,
].join("\n"),
);
} catch (error) {
console.error(`[lark-source] failed to parse event: ${error instanceof Error ? error.message : String(error)}`);
}
}
});
child.stderr.on("data", (chunk) => {
const text = chunk.toString("utf8").trim();
if (text) console.error(`[lark-source] ${text}`);
});
child.once("error", (error) => {
console.error(`[lark-source] process error: ${error instanceof Error ? error.message : String(error)}`);
setTimeout(start, 3000);
});
child.once("exit", (code, signal) => {
console.error(`[lark-source] exited code=${code ?? ""} signal=${signal ?? ""}; restarting in 3s`);
setTimeout(start, 3000);
});
const stop = () => {
child.kill("SIGTERM");
setTimeout(() => process.exit(0), 1000);
};
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
}
start();Make it executable if you want to run it directly:
chmod +x .pi/lark-message-source.cjs5. Register the Source
Create or update .pi/sources.json:
[
{
"name": "lark-message-watcher",
"transport": "stdio",
"command": "node",
"args": [".pi/lark-message-source.cjs"],
"env": {
"LARK_EVENT_TYPES": "im.message.receive_v1"
}
}
]Start or reload the hub:
d-pi hub serveOr, from an existing peer:
/reload
Inspect the source:
/source
The source should move to running. When the bot receives a Lark message, the hub enqueues it as a user message with source metadata:
source/lark-message-watcher
6. Route Lark Messages to a Child Agent
To give a child agent its own Lark watcher instance, create:
.child-agent/obsidian/sources.json
with:
{
"extends": {
"host": {
"sources": ["lark-message-watcher"]
}
},
"sources": []
}Then reload or restart the hub. The hub will run a separate child-scoped source instance:
main: lark-message-watcher
obsidian: lark-message-watcher
Each instance has a distinct internal resourceId; the display name stays human-readable.
Connect a peer to that child agent:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptop --agent obsidianRun /source from that peer. It should show the child-owned lark-message-watcher, not the main agent's source.
Common Workflows
Start a Local Workspace
cd /path/to/workspace
d-pi hub init
d-pi hub add-skills
d-pi hub serveIn another terminal:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptopConnect from Another Machine
On the hub machine:
PI_HUB_HOST=0.0.0.0 PI_HUB_PORT=4317 d-pi hub serveOn the peer machine:
d-pi peer --hub http://HUB_IP_OR_DNS:4317 --peer-id remote-laptopMake sure your network and firewall allow access to the hub port.
Export and Import a Hub Workspace
Use a workspace archive to move or copy a hub between machines. The archive includes both durable hub state and workspace-local Pi configuration.
On the source machine:
cd /path/to/workspace
d-pi hub export ./workspace.tarCopy workspace.tar to the target machine, then import it from the target workspace directory:
mkdir -p /path/to/restored-workspace
cd /path/to/restored-workspace
d-pi hub import /path/to/workspace.tarIf the target already has .pi-hub or .pi, import fails by default. Use --force only when you want to replace the existing workspace state:
d-pi hub import /path/to/workspace.tar --forceAfter importing, start the hub and connect a peer:
d-pi hub serve
d-pi peer --hub http://127.0.0.1:4317 --peer-id restoredRun Hub Continuously with pm2
D-Pi does not publish an official Docker image yet. For a simple long-running hub process, use pm2 and keep the workspace directory mounted on the host machine.
Install pm2 if needed:
npm install -g pm2Initialize the workspace once:
cd /path/to/workspace
d-pi hub init
d-pi hub add-skillsStart the hub in the background:
pm2 start "$(which d-pi)" \
--name d-pi-hub \
--cwd /path/to/workspace \
-- hub serveFor remote peers, expose the hub on the host network:
PI_HUB_HOST=0.0.0.0 PI_HUB_PORT=4317 pm2 start "$(which d-pi)" \
--name d-pi-hub \
--cwd /path/to/workspace \
-- hub servePersist the process across machine restarts:
pm2 save
pm2 startupCommon operations:
pm2 logs d-pi-hub
pm2 restart d-pi-hub
pm2 stop d-pi-hubCreate and Use Child Agents
Use the main agent's child-agent tools from the conversation:
create_child_agentstop_child_agentstart_child_agentremove_child_agentsearch_memorylist_memory
Use /group to inspect all known agents and connected peers.
A child peer connects with:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptop --agent <child-id>Troubleshooting
d-pi peer cannot connect
Check that the hub is running:
d-pi hub status
d-pi hub serveCheck the peer URL:
d-pi peer --hub http://127.0.0.1:4317 --peer-id laptopFor remote peers, check PI_HUB_HOST, firewalls, and the hub machine's IP address.
/source shows a source but the child agent does not receive messages
The peer only receives sources owned by the bound agent.
For a child agent, either:
- set
agentIdon a source entry in.pi/sources.json, or - create
.child-agent/<agent-id>/sources.jsonwithextends.host.sources
Then run /reload or restart the hub.
Lark source starts but no events arrive
Check:
- the bot is added to the target chat
- long-connection subscription is enabled in the Lark Open Platform console
im.message.receive_v1is configured in the event listim:message:receive_as_botpermission is enabledlark-cli event +subscribe --as bot --event-types im.message.receive_v1 --compact --quietprints events outside Pi
Lark source exits or reports parse errors
Run the wrapper directly:
node .pi/lark-message-source.cjsLogs should appear on stderr. Stdout must contain only one-line JSON-RPC queue/write notifications.
Model, MCP, source, or skill changes do not appear
Run:
/reload
If the resource still does not appear, restart the hub.
Package Relationship
@sheason/d-pi is the only distributed package for the D-Pi runtime. The hub and peer implementations are maintained as internal modules and are invoked in-process by the d-pi binary.
Use D-Pi for the stable user-facing command surface.