@jals1212/pi-spaces
Named spaces for Pi: keep several agent homes (sign-ins, packages, settings) and pick one by name.
Package details
Install @jals1212/pi-spaces from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@jals1212/pi-spaces- Package
@jals1212/pi-spaces- Version
0.9.0- Published
- Nov 4, 2026
- Downloads
- 2,091/mo · 1,716/wk
- Author
- jals1212
- License
- MIT
- Types
- extension, skill
- Size
- 137 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"skills": [
"./skills"
],
"extensions": [
"./extensions/spaces.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
🪐 pi-spaces
Named spaces for Pi: keep several agent homes side by side and switch between them from inside Pi.
A space is a complete Pi agent home: its own sign-ins, packages, settings, skills and sessions. Use one per client, per experiment, or to try a package without touching your everyday setup.
/space new work Created space "work" at ~/.pi/spaces/work.
/space Switch to space
→ base
work
Pi restarts in "work"; the status line shows space: work
It relies only on Pi's own PI_CODING_AGENT_DIR variable, so it works with plain Pi and with any program that runs Pi.
Requirements
- Pi 1.4 or later.
Install
pi install npm:@jals1212/pi-spaces
That adds /space and the --space option to Pi. A program that runs Pi with its own home installs it the same way, through its own install command.
The pi-space shell command is optional and is a separate package, so it is installed and updated on its own; pi install does not put commands on your PATH, so it comes from npm:
npm install -g @jals1212/pi-spaces-cli
Quick start
/space new work create a space
/space pick a space; Pi restarts in it
/space switch base back to the home Pi started with
From your shell:
pi --space work # start Pi in "work"
pi --space work "review this branch" # with a prompt
pi-space new pi/work # create a space of the profile "pi" (optional CLI)
pi-space remove pi/work # delete it, after asking; keeps a backup unless told not to (optional CLI)
pi-space update --every # update the packages of base and of every space of every profile (optional CLI)
pi-space # pick a space of any profile: arrows, Enter, type to filter (optional CLI)
A new space starts empty except for a settings.json that declares this package, so /space works inside it. Sign in and install packages there as you would in a fresh Pi.
Commands in Pi
| Command | What it does |
|---|---|
/space |
Opens a list of the other spaces and restarts Pi in the one you pick. Without the interactive terminal it lists them instead. |
/space list |
Lists base and every space, with * on the active one. |
/space new <name> |
Creates an empty space. When a setup command is set (the profile's setup, or PI_SPACES_SETUP), it names pi-space setup <profile>/<name> to run from a shell, with this Pi's profile (pi when none matches). |
/space switch <name> |
Restarts Pi in that space. base is the home Pi started with. |
/space switch |
Same as /space. |
/space rename <old> <new> |
Renames a space. The active space cannot be renamed. |
/space remove <name> [--yes] [--no-backup] |
Deletes a space after asking, then asks whether to keep a backup in <spaces folder>/.backups (keeping one is first in the list; dismissing the question keeps the space). --yes skips the questions and keeps a backup; it is required without a dialog (print and JSON modes). --no-backup deletes without one. |
pi --space <name> |
Starts Pi directly in a space, keeping the prompt and every other option. |
pi --space <name> <command> [args...] |
Runs one of Pi's package commands (install, remove, uninstall, update, list) in a space, or in the base home with base: pi --space work update --extensions, pi --space work install npm:<package>. |
The status line shows 🪐 <profile>/<name> while a space is active: the name pi-space takes, with the profile of the running Pi (pi when no profile matches its base home), so 🪐 gentle/work and 🪐 pi/work are told apart. /space cannot change the profile: a switch starts the same program again. In the base home it shows 🪐 <profile>/base when pi-spaces opened it (pi --space base, pi-space pi/base, or /space switch base), and nothing when Pi was started plainly. 🪐 is pi-spaces' own emoji.
Choosing an argument such as switch from the /space suggestions only fills the line; press Enter again to run it. /space and Enter opens the picker in one step.
Names: lowercase letters, digits, - and _, starting with a letter or digit, at most 64 characters. base is reserved, and so are the command words list, new, remove, rename, switch, update, install, uninstall, setup and completion.
The pi-space command
The optional @jals1212/pi-spaces-cli package adds a pi-space command to your shell. It names every space as <profile>/<name> (pi/work, my/base): the profile is an entry of the config file, and pi (plain Pi) is always there. pi-space opens a picker of the spaces of every profile and starts Pi in the one you choose, pi-space <profile>/<name> [pi args...] starts Pi in a named space, pi-space new <profile>/<name> creates one, pi-space rename <profile>/<old> <new> renames one, pi-space remove <profile>/<name> deletes one, and pi-space list lists every profile's spaces. pi-space list|update|install|uninstall <profile>/<name|--every> ... runs Pi's package commands in a space, in a profile's spaces, or (--every alone) in every profile's. The profile says which program to run, where its spaces live and how to prepare a new space; --command, --dir and --setup override those for one call, for one named profile. pi-space setup <profile>/<name|--every> runs the setup command again. A profile command with {home} is run directly in the space's home instead (pi-space gentle/base upgrade runs the program's own upgrade; see Profiles). The command does not read PI_SPACES_COMMAND, PI_SPACES_DIR or PI_SPACES_SETUP as settings: they are what it hands to the Pi it starts. Unless the profile's command has {home}, it only runs pi --space <name> [command], so this extension must be installed in the home it opens. See cli/README.md for the commands, the options and the exit status.
Profiles
A profile describes one program that runs Pi (plain Pi is one too): where its agent home is, the command that starts it, where its spaces live, how to prepare a new space, and the variables it needs per space. Profiles live in an optional JSON file, so you do not need shell variables or functions to tell pi-spaces about a program. Without the file, nothing changes: this is exactly how pi-spaces behaves without any configuration.
The extension uses the profile whose home is the base home Pi started with (the one base goes back to); inside a space that is the home the space came from. The optional pi-space command (cli/README.md) reads the same file, and the profile is the first word of every space it takes: pi-space ml/work. It does not use default: the profile is always named.
The file
| Location | $XDG_CONFIG_HOME/pi-spaces/config.json when that file exists (XDG_CONFIG_HOME unset, blank or relative means ~/.config, so ~/.config/pi-spaces/config.json: easy to keep in your dotfiles), else ~/.pi-spaces/config.json; on Windows %USERPROFILE%\.config\pi-spaces\config.json (C:\Users\<you>\.config\pi-spaces\config.json) unless XDG_CONFIG_HOME is set, else %USERPROFILE%\.pi-spaces\config.json. With both files the first wins. Nothing moves or creates either file. |
| Change it | PI_SPACES_CONFIG=<path> (a leading ~ is your home folder; a relative path is taken from it). The pi-space command also takes --config <path> (see cli/README.md). |
| Missing | No profiles: Pi, spaces and pi-space behave as without this feature. |
| Invalid | See When the file is wrong. |
A complete example for a program called my-launcher, which keeps its own configuration outside its agent home:
{
"schema": "pi-spaces.config/v1",
"profiles": {
"ml": {
"home": "~/.my-launcher/agent",
"command": "my-launcher --",
"setup": "my-launcher --home {home} setup",
"spaceEnv": { "MY_LAUNCHER_CONFIG_HOME": "{home}/config" }
}
}
}
With that file, a space of my-launcher lives in ~/.my-launcher/spaces/<name>, /space new <name> points you at pi-space setup ml/<name>, and entering the space work starts Pi with MY_LAUNCHER_CONFIG_HOME=~/.my-launcher/spaces/work/config, while the base home gets no such variable from pi-spaces. From a shell, pi-space ml/work starts that space, and an alias such as alias mlw='pi-space ml/work' is optional sugar on top.
A program that takes its home with an option can be the whole command, with {home} in it. Then pi-space runs it in the space's home with no --space, and the program's own commands work through the profile (pi-space gentle/base upgrade runs gentle-shell --home ~/.gentle-shell/agent upgrade):
{
"schema": "pi-spaces.config/v1",
"profiles": {
"gentle": {
"home": "~/.gentle-shell/agent",
"command": "gentle-shell --home {home}",
"spaceEnv": { "GENTLE_PI_CONFIG_HOME": "{home}/gentle-config" }
}
}
}
Keep the spaceEnv: a --home outside a .gentle-shell folder without GENTLE_PI_CONFIG_HOME makes gentle-shell leave an empty ~/.pi/gentle-ai behind. The details are in cli/README.md.
Schema pi-spaces.config/v1
Top level (every other key is refused):
| Key | Type | Required | Meaning |
|---|---|---|---|
schema |
string | yes | Exactly "pi-spaces.config/v1". |
default |
string | no | Accepted and validated (it must name a profile of the file, or pi), but not used: neither the extension nor pi-space falls back to it. pi-space warns when it reads a file that has it; remove the key. |
profiles |
object | no | Profiles by name. Names: lowercase letters, digits, - and _, starting with a letter or digit, at most 64 characters. |
A profile (every other key is refused; every key is optional):
| Key | Type | Default | Meaning |
|---|---|---|---|
home |
string, not empty | PI_CODING_AGENT_DIR, else ~/.pi/agent |
The base agent home of the program: the home it starts Pi with, which base goes back to. ~ is your home folder; a relative path is taken from it. |
command |
string, not empty | pi for the profile pi; none for the others |
The command that starts the program (my-launcher --), in front of --space <name>. It may contain {home} and {name} (gentle-shell --home {home}): pi-space then runs it directly in the space's home, without --space, filling them in with the space's folder and name (in base, the profile's home and base), and the program's own commands work: pi-space gentle/base upgrade runs gentle-shell --home ~/.gentle-shell/agent upgrade. See A command with {home}. |
dir |
string, not empty | spaces next to home (~/.my-launcher/agent gives ~/.my-launcher/spaces) |
The folder that holds the profile's spaces. ~ and relative paths as in home. |
setup |
string, not empty | none | The command that prepares a new space; {home} and {name} become the space's folder and name. With it, /space new names pi-space setup <profile>/<name>. |
spaceEnv |
object of strings | none | Variables that exist only in spaces; see below. Names: letters, digits and _, not starting with a digit; not PI_CODING_AGENT_DIR, PI_SPACE, PI_SPACE_HOME or any PI_SPACES_*, which pi-spaces sets itself. |
All strings are trimmed. {home} and {name} are replaced everywhere they appear in a spaceEnv value (and setup, and command when pi-space runs it with {home}), and $ in a value stays literal.
spaceEnv: what is separate and what is shared
Pi keeps everything in its agent home, so a space separates it completely with no configuration. A program that runs Pi may also keep something outside that home (a configuration folder, say) and hand its location to Pi in an environment variable. When you switch, pi-spaces moves only the variables that point inside the old home (see How switching works); a variable that points elsewhere is not recognised as part of the home and stays as it was. So without spaceEnv, whatever the program keeps outside the agent home is shared between the base home and every space. That is the default on purpose: nothing is separated that you did not ask to separate.
To separate it, name the variable in spaceEnv, with a value built from the space:
- Entering a space sets the variable, from the template, with
{home}the space's folder and{name}its name. - Going back to
baseputs back the value the base home had when you left it, or removes the variable when it had none, so the base home runs exactly as it was started. The base's values travel inPI_SPACES_BASE_ENVwhile you are in spaces. - Going from one space to another sets it again for the target; a value that pointed into the old space is replaced, not moved.
pi --space <name>does the same at startup.
pi-spaces does not create what a variable points to; setup (or the program itself) does.
The built-in pi profile
Plain Pi is always a profile named pi: command pi, no setup, no spaceEnv. Its home is PI_SPACES_BASE_HOME (set inside a space), else PI_CODING_AGENT_DIR, else ~/.pi/agent.
Inside another program's Pi those variables hold that program's home. When a profile of the file names that folder as its home, pi does not take it and uses ~/.pi/agent, so pi/<name> is still plain Pi there.
Naming pi in profiles replaces it as a whole; what that entry leaves out still takes the defaults above for home and command, never anything from the old built-in's other fields.
Schema versions
The file says which format it uses in schema, so the format can grow without breaking files. This pi-spaces reads pi-spaces.config/v1 only. A file with a newer pi-spaces.config/vN is refused with a message that it needs a newer pi-spaces (update the package); anything else in schema, or no schema, is refused as invalid. A newer format is never half-read.
When the file is wrong
Unknown keys, wrong types, bad names, a default that names no profile, an unreadable file and invalid JSON are all refused with a message that names the file and the problem, for example ~/.pi-spaces/config.json: unknown key "hmoe" in profile "ml"; allowed keys are "home", "command", "dir", "setup" and "spaceEnv". Pi never stops for it: the extension shows the message once as a warning when the session starts (on stderr without a terminal UI) and then behaves as if there were no file. A missing file is not an error.
Configuration reference
Where spaces live
In a spaces folder next to the home Pi starts with: ~/.pi/agent keeps its spaces in ~/.pi/spaces/<name>/. A program that runs Pi with its own home gets its own folder too, for example ~/.tool/agent → ~/.tool/spaces. The folder stays the same while you switch between spaces. A profile's dir replaces it for that profile's home, and PI_SPACES_DIR replaces both. The pi-space command does not read PI_SPACES_DIR (or PI_SPACES_COMMAND and PI_SPACES_SETUP) as a setting: it takes the folder from the profile or --dir, and sets the variable for the Pi it starts.
Environment variables
Read:
| Variable | Meaning |
|---|---|
PI_SPACES_DIR |
The spaces folder. A relative value is taken from your home folder. Default: the matching profile's dir, else spaces next to the base home. |
PI_SPACES_CONFIG |
The config file with the profiles. A leading ~ is your home folder and a relative value is taken from it. Default: ~/.config/pi-spaces/config.json (or $XDG_CONFIG_HOME/pi-spaces/config.json) when that file exists, else ~/.pi-spaces/config.json. |
PI_SPACES_COMMAND |
The program that starts Pi (default pi): told to the model and exported (see below). A switch starts Pi again with the command line it already has, so this is information, not a setting. |
PI_SPACES_SETUP |
The command that prepares a new space. /space new names pi-space setup <profile>/<name> when it is set. The pi-space command sets it for the Pi it starts, from the profile or --setup; it does not read it from your shell. |
PI_CODING_AGENT_DIR |
Pi's agent home; its parent holds the default spaces folder. Default: ~/.pi/agent. |
Set for the Pi a switch starts:
| Variable | Meaning |
|---|---|
PI_CODING_AGENT_DIR |
The space's folder, or the base home for base. |
PI_SPACE |
The space's name. Unset in the base home. |
PI_SPACES_BASE_HOME |
The home Pi started with, so base and the spaces folder survive switches. |
PI_SPACES_BASE_ENV |
Only with a profile's spaceEnv, and only in a space: the base home's own values of those variables (JSON), put back when you return to base. |
Set in every Pi this extension runs in, even one started without a switch, for its tools and the programs they start:
| Variable | Meaning |
|---|---|
PI_SPACE |
The active space's name, unset in the base home: the same variable a switch sets. |
PI_SPACE_HOME |
The agent home this Pi runs with: the space's folder, or the base home. |
PI_SPACES_COMMAND |
Kept when it has a value; set to pi when it is unset or blank. The pi-space command sets it for the Pi it starts (the profile's command, or --command), and the extension reads it to tell the model and the tools how Pi was started (see below); pi-space itself does not read it from your shell. |
What the model is told
Before each run the system prompt gets a <pi_space> section with the same three facts: the space (or the base home), the agent home, and the command, with <command> --space <name> and /space as the ways to change space (when the command has {home}, pi-space <profile>/<name> instead of <command> --space <name>). The model knows where it runs without reading the environment. A Pi without structured prompt sections gets no section; the variables are still set.
The pi-spaces skill
The package also ships a pi-spaces skill (skills/pi-spaces/SKILL.md). It tells agents what a space is, how to read the current space, home and command, and which /space or pi-space action to use or propose for listing, creating, switching and removing spaces. Pi loads it when a task is about spaces; /skill:pi-spaces loads it by hand.
Exit status
| Status | pi --space |
|---|---|
| 0 … | Pi's own exit status. |
| 1 | An unknown or invalid space, or no name after --space. |
The pi-space command has its own: see cli/README.md.
How switching works
Pi reads its agent home once, when it starts, so a running session cannot move to another home. /space switch closes Pi and starts the same command again with the space's home:
PI_CODING_AGENT_DIR,PI_SPACEandPI_SPACES_BASE_HOMEare set as in Environment variables.- Any other variable whose value is the old home, or a path inside it, moves to the same path in the new home; in a list such as
PATH, each entry moves on its own. This keeps variables that a launcher derived from the home consistent.PWDandOLDPWDare left alone. - With a profile that has
spaceEnv, those variables are then set, with{home}and{name}filled in, when the target is a space; when it isbase, they get back the values the base home had (PI_SPACES_BASE_ENV). - The same options are kept, such as
--model,-e,--toolsand other extensions' options. The initial prompt,@filearguments, everything after--, and the session options (-c,-r,--session,--session-id,--fork,--session-dir,--no-session,-n) are dropped, because they belong to the home being left. - Switching works only in the interactive terminal.
pi --space <name> acts before Pi opens its interface or runs anything, so it keeps the prompt and the session options too, in every mode.
pi --space <name> <command> ..., where <command> is one of Pi's package commands right after the name, runs Pi again in that home with only <command> ...: Pi runs a package command only as its first argument. Options before --space, such as a launcher's own -e, are left out. The same Pi runs it, so a launcher's own Pi manages the space's packages; base and the current home work too.
What it leaves on your machine
Only the spaces you create, each a folder in the spaces folder, and its settings.json. Pi fills a space as it would any home. Removing a space moves its folder to .backups/<name>-<UTC time> in the spaces folder, or deletes it with --no-backup or when you choose so; nothing else is written outside it. A backup is a whole space: move it back to restore it (mv ~/.pi/spaces/.backups/work-2026-10-28T14-30-05Z ~/.pi/spaces/work), or delete it when you no longer need it. Backups are never removed for you, and .backups is never listed as a space.
Safety
/space remove, /space rename, pi-space remove and pi-space rename refuse, before they ask:
baseand the active space;- symlinks, and anything that is not a real folder directly inside the spaces folder;
- a space that holds the current or base home;
- a spaces folder that is the filesystem root, your home folder, or the agent home (or inside it).
A backup also needs .backups to be a real folder, not a symlink or a file.
Development
Two packages live in this repository: the Pi extension and its shared core at the root (@jals1212/pi-spaces), and the pi-space command in cli/ (@jals1212/pi-spaces-cli).
npm test # links cli/ to this checkout, builds both, runs both test suites (Node 22.18 or later)
pi -e ./extensions/spaces.ts # try the extension without installing it
src/ is TypeScript. Pi loads the extension's TypeScript itself. The command in cli/ imports the core as @jals1212/pi-spaces/spaces (and the profiles code as @jals1212/pi-spaces/config): the plain JavaScript in dist/ that npm run build makes (Node does not strip types from files under node_modules). npm pack and npm publish build it first. scripts/link-self.mjs makes cli/node_modules/@jals1212/pi-spaces a link to this checkout, so the command's tests use the core from the working tree instead of from npm.
Branches: work goes to develop through pull requests; main receives develop when a release closes.
Releasing
Each package has its own version, its own CHANGELOG.md and its own tag prefix:
| Package | Directory | Tag | Version and notes |
|---|---|---|---|
@jals1212/pi-spaces (the extension) |
. |
vX.Y.Z |
package.json, CHANGELOG.md |
@jals1212/pi-spaces-cli (pi-space) |
cli/ |
cli-vX.Y.Z |
cli/package.json, cli/CHANGELOG.md |
- On a
release/X.Y.Zbranch fromdevelop: setversionin the package'spackage.json, rename## Unreleasedin itsCHANGELOG.mdto## X.Y.Z, and commitrelease X.Y.Z: <summary>. Merge it intodevelop, thendevelopintomain. - Tag
mainand push the tag:vX.Y.Zfor the extension,cli-vX.Y.Zfor the command. - The release workflow checks the tag against that package's
package.jsonandCHANGELOG.md, runs the tests of both packages, and stages the version on npm through trusted publishing, with provenance. Approve it with 2FA:npm stage list, thennpm stage approve <id>, or on npmjs.com. - The workflow then creates the GitHub release, with the version's section of that package's
CHANGELOG.mdas notes.
The command takes its core from the extension package (the @jals1212/pi-spaces range in cli/package.json). A CLI release needs a version in that range already on npm, so release the extension first and approve it; the workflow refuses a cli-v* tag otherwise. When the command needs a newer core, raise the range in cli/package.json in the same release.
License
MIT