@jals1212/pi-spaces 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +26 -6
- package/dist/command.js +1 -1
- package/dist/context.js +63 -0
- package/dist/relaunch.js +21 -5
- package/dist/spaces.js +4 -0
- package/extensions/spaces.ts +6 -0
- package/package.json +9 -2
- package/skills/pi-spaces/SKILL.md +49 -0
- package/src/command.ts +1 -1
- package/src/context.ts +63 -0
- package/src/relaunch.ts +21 -5
- package/src/spaces.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@ commands and options and a patch version fixes.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pi --space <name> <command> [args...]` runs one of Pi's package commands
|
|
14
|
+
(`install`, `remove`, `uninstall`, `update`, `list`) in a space, or in the
|
|
15
|
+
base home with `base`, through the same Pi: `pi --space work update
|
|
16
|
+
--extensions`. Options before `--space`, such as a launcher's own `-e`, are
|
|
17
|
+
left out, since Pi runs a package command only as its first argument.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The command words `list`, `new`, `remove`, `rename`, `switch`, `update`,
|
|
22
|
+
`install` and `uninstall` are reserved and can no longer name a space, so
|
|
23
|
+
`pi-space <name>` never reads as a command. A folder with such a name in the
|
|
24
|
+
spaces folder is no longer listed as a space.
|
|
25
|
+
|
|
26
|
+
## 0.3.0
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Pi knows the space it runs in. Every Pi with the extension sets
|
|
31
|
+
`PI_SPACE` (unset in the base home), `PI_SPACE_HOME` (its agent home) and
|
|
32
|
+
`PI_SPACES_COMMAND` (its configured value, else `pi`), with or without a
|
|
33
|
+
switch, and the system prompt gets a `<pi_space>` section with the same
|
|
34
|
+
facts. `spaceInfo`, `exportSpaceInfo` and `spacePrompt` in the core build
|
|
35
|
+
them.
|
|
36
|
+
- A `pi-spaces` skill, declared in the package, that tells agents what a
|
|
37
|
+
space is, how to read the current space, home and command, and which
|
|
38
|
+
`/space` or `pi-space` action to use.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- The status line shows the active space as `◆ <name>` instead of
|
|
43
|
+
`space: <name>`, so it stands out among other extensions' statuses.
|
|
44
|
+
|
|
9
45
|
## 0.2.0
|
|
10
46
|
|
|
11
47
|
### Added
|
package/README.md
CHANGED
|
@@ -45,8 +45,9 @@ From your shell:
|
|
|
45
45
|
```sh
|
|
46
46
|
pi --space work # start Pi in "work"
|
|
47
47
|
pi --space work "review this branch" # with a prompt
|
|
48
|
-
pi-space
|
|
49
|
-
pi-space
|
|
48
|
+
pi-space new work # create a space (optional CLI)
|
|
49
|
+
pi-space remove work # delete it, after asking; keeps a backup unless told not to (optional CLI)
|
|
50
|
+
pi-space update --every # update the packages of base and of every space (optional CLI)
|
|
50
51
|
pi-space # pick from a numbered list (optional CLI)
|
|
51
52
|
```
|
|
52
53
|
|
|
@@ -64,16 +65,17 @@ A new space starts empty except for a `settings.json` that declares this package
|
|
|
64
65
|
| `/space rename <old> <new>` | Renames a space. The active space cannot be renamed. |
|
|
65
66
|
| `/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. |
|
|
66
67
|
| `pi --space <name>` | Starts Pi directly in a space, keeping the prompt and every other option. |
|
|
68
|
+
| `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>`. |
|
|
67
69
|
|
|
68
|
-
The status line shows
|
|
70
|
+
The status line shows `◆ <name>` while a space is active, and nothing in the base home.
|
|
69
71
|
|
|
70
72
|
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.
|
|
71
73
|
|
|
72
|
-
**Names**: lowercase letters, digits, `-` and `_`, starting with a letter or digit, at most 64 characters. `base` is reserved
|
|
74
|
+
**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` and `uninstall`.
|
|
73
75
|
|
|
74
76
|
## The `pi-space` command
|
|
75
77
|
|
|
76
|
-
The optional `@jals1212/pi-spaces-cli` package adds a `pi-space` command to your shell: `pi-space` picks a space from a numbered list and starts Pi in it, `pi-space <name> [pi args...]` starts Pi in a named space, `pi-space
|
|
78
|
+
The optional `@jals1212/pi-spaces-cli` package adds a `pi-space` command to your shell: `pi-space` picks a space from a numbered list and starts Pi in it, `pi-space <name> [pi args...]` starts Pi in a named space, `pi-space new <name>` creates one, `pi-space rename <old> <new>` renames one, `pi-space remove <name>` deletes one, and `pi-space list` lists them. `pi-space list|update|install|uninstall <name|--every> ...` runs Pi's package commands in a space, or in every space. `--command` and `--dir` (or `PI_SPACES_COMMAND` and `PI_SPACES_DIR`) make it run another program that runs Pi, with that program's spaces. It only runs `pi --space <name> [command]`, so this extension must be installed in the home it opens. See [`cli/README.md`](cli/README.md) for the commands, the options and the exit status.
|
|
77
79
|
|
|
78
80
|
## Configuration reference
|
|
79
81
|
|
|
@@ -99,6 +101,22 @@ Set for the Pi a switch starts:
|
|
|
99
101
|
| `PI_SPACE` | The space's name. Unset in the base home. |
|
|
100
102
|
| `PI_SPACES_BASE_HOME` | The home Pi started with, so `base` and the spaces folder survive switches. |
|
|
101
103
|
|
|
104
|
+
Set in every Pi this extension runs in, even one started without a switch, for its tools and the programs they start:
|
|
105
|
+
|
|
106
|
+
| Variable | Meaning |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `PI_SPACE` | The active space's name, unset in the base home: the same variable a switch sets. |
|
|
109
|
+
| `PI_SPACE_HOME` | The agent home this Pi runs with: the space's folder, or the base home. |
|
|
110
|
+
| `PI_SPACES_COMMAND` | Kept when it has a value; set to `pi` when it is unset or blank. |
|
|
111
|
+
|
|
112
|
+
### What the model is told
|
|
113
|
+
|
|
114
|
+
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. The model knows where it runs without reading the environment. A Pi without structured prompt sections gets no section; the variables are still set.
|
|
115
|
+
|
|
116
|
+
### The `pi-spaces` skill
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
102
120
|
### Exit status
|
|
103
121
|
|
|
104
122
|
| Status | `pi --space` |
|
|
@@ -119,13 +137,15 @@ Pi reads its agent home once, when it starts, so a running session cannot move t
|
|
|
119
137
|
|
|
120
138
|
`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.
|
|
121
139
|
|
|
140
|
+
`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.
|
|
141
|
+
|
|
122
142
|
## What it leaves on your machine
|
|
123
143
|
|
|
124
144
|
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.
|
|
125
145
|
|
|
126
146
|
## Safety
|
|
127
147
|
|
|
128
|
-
`/space remove`, `/space rename` and `pi-space
|
|
148
|
+
`/space remove`, `/space rename`, `pi-space remove` and `pi-space rename` refuse, before they ask:
|
|
129
149
|
|
|
130
150
|
- `base` and the active space;
|
|
131
151
|
- symlinks, and anything that is not a real folder directly inside the spaces folder;
|
package/dist/command.js
CHANGED
|
@@ -157,7 +157,7 @@ function switchTo(name , deps ) {
|
|
|
157
157
|
/** Status line text: the active space, or nothing in the base home. */
|
|
158
158
|
export function statusText(dir , currentHome ) {
|
|
159
159
|
const active = activeSpace(dir, currentHome);
|
|
160
|
-
return active === undefined ? undefined :
|
|
160
|
+
return active === undefined ? undefined : `◆ ${active}`;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
export function completeSpaceArgs(prefix , dir ) {
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What this Pi process knows about its space: the active space, the agent home and the command
|
|
2
|
+
// that starts Pi. It is exported to the environment (for tools and child processes) and told to the model.
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { activeSpace, } from "./spaces.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read once, when the extension loads: Pi keeps its agent home for the whole process, and
|
|
17
|
+
* the active space cannot be renamed or removed, so the facts hold until Pi exits.
|
|
18
|
+
*/
|
|
19
|
+
export function spaceInfo(dir , agentHome , env ) {
|
|
20
|
+
return {
|
|
21
|
+
space: activeSpace(dir, agentHome),
|
|
22
|
+
home: resolve(agentHome),
|
|
23
|
+
// A blank value counts as unset, so the default is pi, as in the pi-space command.
|
|
24
|
+
command: env.PI_SPACES_COMMAND?.trim() || "pi",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sets PI_SPACE (removed in the base home), PI_SPACE_HOME and PI_SPACES_COMMAND. */
|
|
29
|
+
export function exportSpaceInfo(env , info ) {
|
|
30
|
+
if (info.space === undefined) delete env.PI_SPACE;
|
|
31
|
+
else env.PI_SPACE = info.space;
|
|
32
|
+
env.PI_SPACE_HOME = info.home;
|
|
33
|
+
env.PI_SPACES_COMMAND = info.command;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The system prompt section that tells the model where it runs. */
|
|
37
|
+
export function spacePrompt(info ) {
|
|
38
|
+
const where = info.space === undefined
|
|
39
|
+
? "This Pi runs in the base home, outside any space."
|
|
40
|
+
: `This Pi runs in the space "${info.space}" (PI_SPACE).`;
|
|
41
|
+
const home = `Agent home: ${info.home} (PI_SPACE_HOME).`;
|
|
42
|
+
const start = `\`${info.command} --space <name>\` starts it in a space`;
|
|
43
|
+
const manage = "`/space` lists, creates, removes and switches spaces";
|
|
44
|
+
const command = `Command that starts Pi: ${info.command} (PI_SPACES_COMMAND); ${start}, and ${manage}.`;
|
|
45
|
+
return [where, home, command].join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The part of a run's structured prompt options this package writes. */
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sets the <pi_space> section in a run's structured prompt options, creating the section map when
|
|
55
|
+
* it is missing and replacing an earlier pi_space section. Other sections and the rest of the
|
|
56
|
+
* prompt are left alone. Without structured options (an older Pi) nothing is added; the
|
|
57
|
+
* environment still has the facts.
|
|
58
|
+
*/
|
|
59
|
+
export function addSpaceSection(options , info ) {
|
|
60
|
+
if (options === undefined) return;
|
|
61
|
+
options.sections ??= {};
|
|
62
|
+
options.sections.pi_space = spacePrompt(info);
|
|
63
|
+
}
|
package/dist/relaunch.js
CHANGED
|
@@ -123,10 +123,14 @@ export function relaunchCommand(proc = process, args = filterArg
|
|
|
123
123
|
|
|
124
124
|
const SPACE_FLAG_ERROR = "--space needs a space name: pi --space <name>.";
|
|
125
125
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Finds --space <name> or --space=<name> before "--", and returns the other arguments unchanged,
|
|
128
|
+
* and the arguments that follow the name.
|
|
129
|
+
*/
|
|
130
|
+
export function parseSpaceFlag(args ) {
|
|
128
131
|
const rest = [];
|
|
129
132
|
let name ;
|
|
133
|
+
let afterName = [];
|
|
130
134
|
for (let i = 0; i < args.length; i++) {
|
|
131
135
|
const arg = args[i];
|
|
132
136
|
if (arg === "--") {
|
|
@@ -138,36 +142,48 @@ export function parseSpaceFlag(args )
|
|
|
138
142
|
if (value === undefined || value === "" || value.startsWith("-")) return { error: SPACE_FLAG_ERROR };
|
|
139
143
|
name = value;
|
|
140
144
|
i++;
|
|
145
|
+
afterName = args.slice(i + 1);
|
|
141
146
|
} else if (arg.startsWith("--space=")) {
|
|
142
147
|
name = arg.slice("--space=".length);
|
|
143
148
|
if (name === "") return { error: SPACE_FLAG_ERROR };
|
|
149
|
+
afterName = args.slice(i + 1);
|
|
144
150
|
} else {
|
|
145
151
|
rest.push(arg);
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
|
-
return { name, rest };
|
|
154
|
+
return { name, rest, afterName };
|
|
149
155
|
}
|
|
150
156
|
|
|
157
|
+
// Pi runs these only as its first argument, before extensions load.
|
|
158
|
+
const PACKAGE_COMMANDS = new Set(["install", "remove", "uninstall", "update", "list"]);
|
|
159
|
+
|
|
151
160
|
|
|
152
161
|
|
|
153
162
|
/**
|
|
154
163
|
* What `pi --space <name>` does at startup: nothing without the flag or when Pi already runs in that
|
|
155
164
|
* space; otherwise run Pi again in the space with every other argument, since nothing has run yet.
|
|
165
|
+
* `pi --space <name> <package command> ...` always runs Pi again, even in the current home, because
|
|
166
|
+
* Pi runs a package command (install, remove, uninstall, update, list) only as its first argument.
|
|
156
167
|
*/
|
|
157
168
|
export function planStartup(input ) {
|
|
158
169
|
const { proc, dir, currentHome, env } = input;
|
|
159
170
|
const parsed = parseSpaceFlag(proc.argv.slice(2));
|
|
160
171
|
if ("error" in parsed) return { kind: "error", message: parsed.error };
|
|
161
172
|
if (parsed.name === undefined) return { kind: "none" };
|
|
173
|
+
// A package command runs alone: options before --space (a launcher may add its own) are for a session.
|
|
174
|
+
const packageCommand = PACKAGE_COMMANDS.has(parsed.afterName[0] ?? "");
|
|
175
|
+
const args = packageCommand ? parsed.afterName : parsed.rest;
|
|
162
176
|
let target ;
|
|
163
177
|
try {
|
|
164
178
|
target = resolveSwitch({ dir, name: parsed.name, currentHome, env });
|
|
165
179
|
} catch (error) {
|
|
166
180
|
if (!(error instanceof SpaceError)) throw error;
|
|
167
|
-
if (error.message.startsWith("Already in"))
|
|
181
|
+
if (error.message.startsWith("Already in")) {
|
|
182
|
+
return packageCommand ? { kind: "relaunch", command: relaunchCommand(proc, args), env } : { kind: "none" };
|
|
183
|
+
}
|
|
168
184
|
return { kind: "error", message: error.message };
|
|
169
185
|
}
|
|
170
|
-
return { kind: "relaunch", command: relaunchCommand(proc,
|
|
186
|
+
return { kind: "relaunch", command: relaunchCommand(proc, args), env: relaunchEnv(env, { ...target, currentHome }) };
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
/** Runs the command now with inherited stdio and returns its exit code (1 when it cannot start). */
|
package/dist/spaces.js
CHANGED
|
@@ -37,9 +37,13 @@ export function spacesDir(env = process.env, home = osHomedir())
|
|
|
37
37
|
return join(dirname(resolve(baseHome)), "spaces");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// The words of the pi-space and /space commands, so a space name never reads as one.
|
|
41
|
+
const COMMANDS = new Set(["list", "new", "remove", "rename", "switch", "update", "install", "uninstall"]);
|
|
42
|
+
|
|
40
43
|
/** Returns the error message for an unusable name, or undefined when the name is valid. */
|
|
41
44
|
export function validateName(name ) {
|
|
42
45
|
if (name === BASE) return `"${BASE}" is reserved for the home Pi started with.`;
|
|
46
|
+
if (COMMANDS.has(name)) return `"${name}" is reserved: it is a pi-space or /space command.`;
|
|
43
47
|
if (!NAME.test(name)) {
|
|
44
48
|
return `Invalid space name "${name}": use lowercase letters, digits, "-" and "_", starting with a letter or digit (at most 64 characters).`;
|
|
45
49
|
}
|
package/extensions/spaces.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { completeSpaceArgs, runSpaceCommand, statusText } from "../src/command.ts";
|
|
7
|
+
import { addSpaceSection, exportSpaceInfo, spaceInfo } from "../src/context.ts";
|
|
7
8
|
import { planStartup, relaunchCommand, runNow, scheduleRelaunch } from "../src/relaunch.ts";
|
|
8
9
|
import { findOwnDeclaration, spacesDir } from "../src/spaces.ts";
|
|
9
10
|
|
|
@@ -20,6 +21,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
20
21
|
}
|
|
21
22
|
if (plan.kind === "relaunch") process.exit(runNow(plan.command, plan.env));
|
|
22
23
|
|
|
24
|
+
// Where this Pi runs: exported for tools and the programs they start, and told to the model before each run.
|
|
25
|
+
const info = spaceInfo(spacesDir(), getAgentDir(), process.env);
|
|
26
|
+
exportSpaceInfo(process.env, info);
|
|
27
|
+
pi.on("before_agent_start", (event) => addSpaceSection(event.systemPromptOptions, info));
|
|
28
|
+
|
|
23
29
|
pi.registerFlag("space", { description: "Start Pi in this space (see /space list)", type: "string" });
|
|
24
30
|
|
|
25
31
|
pi.on("session_start", (_event, ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jals1212/pi-spaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Named spaces for Pi: keep several agent homes (sign-ins, packages, settings) and pick one by name.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "jals1212",
|
|
@@ -25,12 +25,16 @@
|
|
|
25
25
|
"pi": {
|
|
26
26
|
"extensions": [
|
|
27
27
|
"./extensions/spaces.ts"
|
|
28
|
+
],
|
|
29
|
+
"skills": [
|
|
30
|
+
"./skills"
|
|
28
31
|
]
|
|
29
32
|
},
|
|
30
33
|
"files": [
|
|
31
34
|
"dist",
|
|
32
35
|
"src",
|
|
33
36
|
"extensions",
|
|
37
|
+
"skills",
|
|
34
38
|
"README.md",
|
|
35
39
|
"CHANGELOG.md",
|
|
36
40
|
"LICENSE"
|
|
@@ -41,7 +45,10 @@
|
|
|
41
45
|
"scripts": {
|
|
42
46
|
"build": "node --disable-warning=ExperimentalWarning scripts/build.mjs",
|
|
43
47
|
"prepack": "npm run build",
|
|
44
|
-
"test": "node scripts/link-self.mjs && npm run build && node --test
|
|
48
|
+
"test": "node scripts/link-self.mjs && npm run build && node --test \"tests/*.test.ts\" && npm test --prefix cli"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"cross-spawn": "^7.0.6"
|
|
45
52
|
},
|
|
46
53
|
"peerDependencies": {
|
|
47
54
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-spaces
|
|
3
|
+
description: "Trigger: pi-spaces, space, spaces, /space, pi-space, pi --space, PI_SPACE, PI_SPACE_HOME, PI_SPACES_COMMAND, which space, agent home, base home, update, install, uninstall. Know which Pi space this session runs in, its home and start command, and how spaces and their packages are listed, created, updated, switched and removed."
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: jals1212
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Activation Contract
|
|
11
|
+
|
|
12
|
+
Use when a task depends on which Pi agent home this session uses (settings, sign-ins, installed packages, sessions), or when the user asks about spaces. A space is a separate Pi agent home kept in a folder of the spaces folder; the base home is the one Pi started with. Pi reads its home once at startup, so one process stays in one space.
|
|
13
|
+
|
|
14
|
+
## Hard Rules
|
|
15
|
+
|
|
16
|
+
- Read where you run from the `<pi_space>` system prompt section; in a shell, from `PI_SPACE` (unset in the base home), `PI_SPACE_HOME` (this Pi's agent home) and `PI_SPACES_COMMAND` (the program that starts Pi, default `pi`). Never guess from `~/.pi/agent`.
|
|
17
|
+
- Files of this session's home (`settings.json`, `extensions/`, `sessions/`, installed packages) live under `PI_SPACE_HOME`; `pi install` and settings changes apply to this space only.
|
|
18
|
+
- Never read `auth.json` or other credential files in any home.
|
|
19
|
+
- You cannot move this session to another space. Ask the user to run `/space switch <name>` (interactive terminal only; Pi restarts), or start another Pi with `$PI_SPACES_COMMAND --space <name>`.
|
|
20
|
+
- Never set `PI_CODING_AGENT_DIR` yourself, and never create, rename or delete space folders with file commands; use `/space` or `pi-space`.
|
|
21
|
+
- Removing a space deletes everything in it: only on the user's explicit request, and keep the backup unless they say otherwise.
|
|
22
|
+
|
|
23
|
+
## Decision Gates
|
|
24
|
+
|
|
25
|
+
| Need | Action |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Which space, home, command | `<pi_space>` section, or `echo "$PI_SPACE|$PI_SPACE_HOME|$PI_SPACES_COMMAND"` |
|
|
28
|
+
| List spaces from a shell | `pi-space list` (optional `@jals1212/pi-spaces-cli`) |
|
|
29
|
+
| Spaces folder | `PI_SPACES_DIR`, else `spaces` next to the base home (`PI_SPACES_BASE_HOME`, else this home) |
|
|
30
|
+
| Create a space | `pi-space new <name>`, or ask the user for `/space new <name>` |
|
|
31
|
+
| Work in another space | User runs `/space switch <name>`, or `$PI_SPACES_COMMAND --space <name> "<prompt>"` |
|
|
32
|
+
| Remove a space | User request only: `pi-space remove <name> --yes` keeps a backup in `.backups` |
|
|
33
|
+
| Packages of a space | `pi-space update <name|base|--every> --extensions`, `pi-space install <name> <source>`, `pi-space uninstall <name> <source>`, `pi-space list <name>`; without the CLI, `$PI_SPACES_COMMAND --space <name|base> update --extensions` |
|
|
34
|
+
|
|
35
|
+
Names: lowercase letters, digits, `-` and `_`, starting with a letter or digit, at most 64 characters; `base` (the base home) and the command words (`list`, `new`, `remove`, `rename`, `switch`, `update`, `install`, `uninstall`) are reserved.
|
|
36
|
+
|
|
37
|
+
## Execution Steps
|
|
38
|
+
|
|
39
|
+
1. Read the `<pi_space>` section or the three variables before touching any home file.
|
|
40
|
+
2. Pick the action from Decision Gates; prefer telling the user the `/space` command when it needs the interactive terminal.
|
|
41
|
+
3. Report the space and home you acted on.
|
|
42
|
+
|
|
43
|
+
## Output Contract
|
|
44
|
+
|
|
45
|
+
State the space (or "base home"), the agent home path, and the command or `/space` action used or proposed.
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
|
|
49
|
+
- `../../README.md` — commands, environment variables and how switching works.
|
package/src/command.ts
CHANGED
|
@@ -157,7 +157,7 @@ function switchTo(name: string, deps: SpaceCommandDeps): void {
|
|
|
157
157
|
/** Status line text: the active space, or nothing in the base home. */
|
|
158
158
|
export function statusText(dir: string, currentHome: string): string | undefined {
|
|
159
159
|
const active = activeSpace(dir, currentHome);
|
|
160
|
-
return active === undefined ? undefined :
|
|
160
|
+
return active === undefined ? undefined : `◆ ${active}`;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
export function completeSpaceArgs(prefix: string, dir: string): { value: string; label: string }[] | null {
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What this Pi process knows about its space: the active space, the agent home and the command
|
|
2
|
+
// that starts Pi. It is exported to the environment (for tools and child processes) and told to the model.
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { activeSpace, type Env } from "./spaces.ts";
|
|
5
|
+
|
|
6
|
+
export interface SpaceInfo {
|
|
7
|
+
/** The active space, or undefined in the base home. */
|
|
8
|
+
space: string | undefined;
|
|
9
|
+
/** The agent home this Pi runs with. */
|
|
10
|
+
home: string;
|
|
11
|
+
/** PI_SPACES_COMMAND, the program that starts Pi (default pi). */
|
|
12
|
+
command: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read once, when the extension loads: Pi keeps its agent home for the whole process, and
|
|
17
|
+
* the active space cannot be renamed or removed, so the facts hold until Pi exits.
|
|
18
|
+
*/
|
|
19
|
+
export function spaceInfo(dir: string, agentHome: string, env: Env): SpaceInfo {
|
|
20
|
+
return {
|
|
21
|
+
space: activeSpace(dir, agentHome),
|
|
22
|
+
home: resolve(agentHome),
|
|
23
|
+
// A blank value counts as unset, so the default is pi, as in the pi-space command.
|
|
24
|
+
command: env.PI_SPACES_COMMAND?.trim() || "pi",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sets PI_SPACE (removed in the base home), PI_SPACE_HOME and PI_SPACES_COMMAND. */
|
|
29
|
+
export function exportSpaceInfo(env: Env, info: SpaceInfo): void {
|
|
30
|
+
if (info.space === undefined) delete env.PI_SPACE;
|
|
31
|
+
else env.PI_SPACE = info.space;
|
|
32
|
+
env.PI_SPACE_HOME = info.home;
|
|
33
|
+
env.PI_SPACES_COMMAND = info.command;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The system prompt section that tells the model where it runs. */
|
|
37
|
+
export function spacePrompt(info: SpaceInfo): string {
|
|
38
|
+
const where = info.space === undefined
|
|
39
|
+
? "This Pi runs in the base home, outside any space."
|
|
40
|
+
: `This Pi runs in the space "${info.space}" (PI_SPACE).`;
|
|
41
|
+
const home = `Agent home: ${info.home} (PI_SPACE_HOME).`;
|
|
42
|
+
const start = `\`${info.command} --space <name>\` starts it in a space`;
|
|
43
|
+
const manage = "`/space` lists, creates, removes and switches spaces";
|
|
44
|
+
const command = `Command that starts Pi: ${info.command} (PI_SPACES_COMMAND); ${start}, and ${manage}.`;
|
|
45
|
+
return [where, home, command].join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The part of a run's structured prompt options this package writes. */
|
|
49
|
+
export interface PromptSections {
|
|
50
|
+
sections?: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sets the <pi_space> section in a run's structured prompt options, creating the section map when
|
|
55
|
+
* it is missing and replacing an earlier pi_space section. Other sections and the rest of the
|
|
56
|
+
* prompt are left alone. Without structured options (an older Pi) nothing is added; the
|
|
57
|
+
* environment still has the facts.
|
|
58
|
+
*/
|
|
59
|
+
export function addSpaceSection(options: PromptSections | undefined, info: SpaceInfo): void {
|
|
60
|
+
if (options === undefined) return;
|
|
61
|
+
options.sections ??= {};
|
|
62
|
+
options.sections.pi_space = spacePrompt(info);
|
|
63
|
+
}
|
package/src/relaunch.ts
CHANGED
|
@@ -123,10 +123,14 @@ export function relaunchCommand(proc: Proc = process, args: string[] = filterArg
|
|
|
123
123
|
|
|
124
124
|
const SPACE_FLAG_ERROR = "--space needs a space name: pi --space <name>.";
|
|
125
125
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Finds --space <name> or --space=<name> before "--", and returns the other arguments unchanged,
|
|
128
|
+
* and the arguments that follow the name.
|
|
129
|
+
*/
|
|
130
|
+
export function parseSpaceFlag(args: string[]): { name: string | undefined; rest: string[]; afterName: string[] } | { error: string } {
|
|
128
131
|
const rest: string[] = [];
|
|
129
132
|
let name: string | undefined;
|
|
133
|
+
let afterName: string[] = [];
|
|
130
134
|
for (let i = 0; i < args.length; i++) {
|
|
131
135
|
const arg = args[i];
|
|
132
136
|
if (arg === "--") {
|
|
@@ -138,36 +142,48 @@ export function parseSpaceFlag(args: string[]): { name: string | undefined; rest
|
|
|
138
142
|
if (value === undefined || value === "" || value.startsWith("-")) return { error: SPACE_FLAG_ERROR };
|
|
139
143
|
name = value;
|
|
140
144
|
i++;
|
|
145
|
+
afterName = args.slice(i + 1);
|
|
141
146
|
} else if (arg.startsWith("--space=")) {
|
|
142
147
|
name = arg.slice("--space=".length);
|
|
143
148
|
if (name === "") return { error: SPACE_FLAG_ERROR };
|
|
149
|
+
afterName = args.slice(i + 1);
|
|
144
150
|
} else {
|
|
145
151
|
rest.push(arg);
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
|
-
return { name, rest };
|
|
154
|
+
return { name, rest, afterName };
|
|
149
155
|
}
|
|
150
156
|
|
|
157
|
+
// Pi runs these only as its first argument, before extensions load.
|
|
158
|
+
const PACKAGE_COMMANDS = new Set(["install", "remove", "uninstall", "update", "list"]);
|
|
159
|
+
|
|
151
160
|
export type StartupPlan = { kind: "none" } | { kind: "error"; message: string } | { kind: "relaunch"; command: Command; env: Env };
|
|
152
161
|
|
|
153
162
|
/**
|
|
154
163
|
* What `pi --space <name>` does at startup: nothing without the flag or when Pi already runs in that
|
|
155
164
|
* space; otherwise run Pi again in the space with every other argument, since nothing has run yet.
|
|
165
|
+
* `pi --space <name> <package command> ...` always runs Pi again, even in the current home, because
|
|
166
|
+
* Pi runs a package command (install, remove, uninstall, update, list) only as its first argument.
|
|
156
167
|
*/
|
|
157
168
|
export function planStartup(input: { proc: Proc; dir: string; currentHome: string; env: Env }): StartupPlan {
|
|
158
169
|
const { proc, dir, currentHome, env } = input;
|
|
159
170
|
const parsed = parseSpaceFlag(proc.argv.slice(2));
|
|
160
171
|
if ("error" in parsed) return { kind: "error", message: parsed.error };
|
|
161
172
|
if (parsed.name === undefined) return { kind: "none" };
|
|
173
|
+
// A package command runs alone: options before --space (a launcher may add its own) are for a session.
|
|
174
|
+
const packageCommand = PACKAGE_COMMANDS.has(parsed.afterName[0] ?? "");
|
|
175
|
+
const args = packageCommand ? parsed.afterName : parsed.rest;
|
|
162
176
|
let target: SwitchTarget;
|
|
163
177
|
try {
|
|
164
178
|
target = resolveSwitch({ dir, name: parsed.name, currentHome, env });
|
|
165
179
|
} catch (error) {
|
|
166
180
|
if (!(error instanceof SpaceError)) throw error;
|
|
167
|
-
if (error.message.startsWith("Already in"))
|
|
181
|
+
if (error.message.startsWith("Already in")) {
|
|
182
|
+
return packageCommand ? { kind: "relaunch", command: relaunchCommand(proc, args), env } : { kind: "none" };
|
|
183
|
+
}
|
|
168
184
|
return { kind: "error", message: error.message };
|
|
169
185
|
}
|
|
170
|
-
return { kind: "relaunch", command: relaunchCommand(proc,
|
|
186
|
+
return { kind: "relaunch", command: relaunchCommand(proc, args), env: relaunchEnv(env, { ...target, currentHome }) };
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
/** Runs the command now with inherited stdio and returns its exit code (1 when it cannot start). */
|
package/src/spaces.ts
CHANGED
|
@@ -37,9 +37,13 @@ export function spacesDir(env: Env = process.env, home: string = osHomedir()): s
|
|
|
37
37
|
return join(dirname(resolve(baseHome)), "spaces");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// The words of the pi-space and /space commands, so a space name never reads as one.
|
|
41
|
+
const COMMANDS = new Set(["list", "new", "remove", "rename", "switch", "update", "install", "uninstall"]);
|
|
42
|
+
|
|
40
43
|
/** Returns the error message for an unusable name, or undefined when the name is valid. */
|
|
41
44
|
export function validateName(name: string): string | undefined {
|
|
42
45
|
if (name === BASE) return `"${BASE}" is reserved for the home Pi started with.`;
|
|
46
|
+
if (COMMANDS.has(name)) return `"${name}" is reserved: it is a pi-space or /space command.`;
|
|
43
47
|
if (!NAME.test(name)) {
|
|
44
48
|
return `Invalid space name "${name}": use lowercase letters, digits, "-" and "_", starting with a letter or digit (at most 64 characters).`;
|
|
45
49
|
}
|