@juicesharp/rpiv-pi 1.12.0 → 1.13.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/README.md CHANGED
@@ -223,6 +223,7 @@ Pi Agent discovers extensions via `"extensions": ["./extensions"]` and skills vi
223
223
  - **UI language** - run `/languages` to pick the locale for rpiv-* TUI strings, or pass `pi --locale <code>` at startup. Detection priority: flag → `~/.config/rpiv-i18n/locale.json` → `LANG` / `LC_ALL` → English. LLM-facing copy stays English by design
224
224
  - **Agent concurrency** - open the `/agents` overlay and tune `Settings → Max concurrency` to match your provider's rate limits. `@tintinweb/pi-subagents` owns this setting; rpiv-pi does not seed it.
225
225
  - **Agent profiles** - synced to `~/.pi/agent/agents/` from bundled defaults; refresh with `/rpiv-update-agents` (overwrites rpiv-managed files, preserves your custom agents).
226
+ - **Non-default agent directory** - if you set `PI_CODING_AGENT_DIR` (e.g. `~/.config/pi/agent` for an XDG-style layout), rpiv-pi reads and writes the same `settings.json` Pi does — sibling detection, `/rpiv-setup`, and `/rpiv-update-agents` all follow the env var. Leading `~` is expanded.
226
227
 
227
228
  ## Uninstall
228
229
 
@@ -3,12 +3,12 @@ import { dirname, join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { findMissingSiblings } from "./package-checks.js";
5
5
  import { SIBLINGS } from "./siblings.js";
6
-
7
- const SETTINGS_PATH = join(process.env.HOME!, ".pi", "agent", "settings.json");
6
+ import { getPiAgentSettingsPath } from "./utils.js";
8
7
 
9
8
  function writeSettings(contents: unknown) {
10
- mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
11
- writeFileSync(SETTINGS_PATH, JSON.stringify(contents), "utf-8");
9
+ const settingsPath = getPiAgentSettingsPath();
10
+ mkdirSync(dirname(settingsPath), { recursive: true });
11
+ writeFileSync(settingsPath, JSON.stringify(contents), "utf-8");
12
12
  }
13
13
 
14
14
  describe("findMissingSiblings", () => {
@@ -17,8 +17,9 @@ describe("findMissingSiblings", () => {
17
17
  });
18
18
 
19
19
  it("returns all 7 siblings when JSON is invalid", () => {
20
- mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
21
- writeFileSync(SETTINGS_PATH, "{not json", "utf-8");
20
+ const settingsPath = getPiAgentSettingsPath();
21
+ mkdirSync(dirname(settingsPath), { recursive: true });
22
+ writeFileSync(settingsPath, "{not json", "utf-8");
22
23
  expect(findMissingSiblings()).toHaveLength(SIBLINGS.length);
23
24
  });
24
25
 
@@ -56,4 +57,12 @@ describe("findMissingSiblings", () => {
56
57
  });
57
58
  expect(findMissingSiblings()).toEqual([]);
58
59
  });
60
+
61
+ it("reads settings from PI_CODING_AGENT_DIR when configured", () => {
62
+ process.env.PI_CODING_AGENT_DIR = join(process.env.HOME!, ".config", "pi", "agent");
63
+ writeSettings({
64
+ packages: SIBLINGS.map((s) => s.pkg.replace(/^npm:/, "")),
65
+ });
66
+ expect(findMissingSiblings()).toEqual([]);
67
+ });
59
68
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Detect which SIBLINGS are installed by reading ~/.pi/agent/settings.json.
2
+ * Detect which SIBLINGS are installed by reading the active Pi settings file.
3
3
  * Pure utility — no ExtensionAPI.
4
4
  */
5
5
 
@@ -8,7 +8,7 @@ import { readPiAgentSettings } from "./utils.js";
8
8
 
9
9
  /**
10
10
  * Return the SIBLINGS not currently installed.
11
- * Reads ~/.pi/agent/settings.json once per call — callers that need both the
11
+ * Reads the active Pi settings file once per call — callers that need both the
12
12
  * full snapshot and the missing subset should call this once and filter.
13
13
  */
14
14
  export function findMissingSiblings(): SiblingPlugin[] {
@@ -2,16 +2,24 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { findLegacySiblings, pruneLegacySiblings } from "./prune-legacy-siblings.js";
5
+ import { getPiAgentSettingsPath } from "./utils.js";
5
6
 
6
- const SETTINGS_PATH = join(process.env.HOME!, ".pi", "agent", "settings.json");
7
+ function writeSettingsRaw(raw: string): void {
8
+ const settingsPath = getPiAgentSettingsPath();
9
+ mkdirSync(dirname(settingsPath), { recursive: true });
10
+ writeFileSync(settingsPath, raw, "utf-8");
11
+ }
7
12
 
8
13
  function writeSettings(contents: unknown): void {
9
- mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
10
- writeFileSync(SETTINGS_PATH, JSON.stringify(contents), "utf-8");
14
+ writeSettingsRaw(JSON.stringify(contents));
15
+ }
16
+
17
+ function readSettingsRaw(): string {
18
+ return readFileSync(getPiAgentSettingsPath(), "utf-8");
11
19
  }
12
20
 
13
21
  function readSettings(): unknown {
14
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
22
+ return JSON.parse(readSettingsRaw());
15
23
  }
16
24
 
17
25
  describe("pruneLegacySiblings", () => {
@@ -20,10 +28,9 @@ describe("pruneLegacySiblings", () => {
20
28
  });
21
29
 
22
30
  it("invalid JSON → pruned: [], file byte-exact unchanged", () => {
23
- mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
24
- writeFileSync(SETTINGS_PATH, "{not json", "utf-8");
31
+ writeSettingsRaw("{not json");
25
32
  expect(pruneLegacySiblings()).toEqual({ pruned: [] });
26
- expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe("{not json");
33
+ expect(readSettingsRaw()).toBe("{not json");
27
34
  });
28
35
 
29
36
  it("non-object top-level (array) → pruned: [], file unchanged", () => {
@@ -47,9 +54,9 @@ describe("pruneLegacySiblings", () => {
47
54
  writeSettings({
48
55
  packages: ["npm:pi-perplexity", "npm:@juicesharp/rpiv-todo", "npm:@tintinweb/pi-subagents"],
49
56
  });
50
- const before = readFileSync(SETTINGS_PATH, "utf-8");
57
+ const before = readSettingsRaw();
51
58
  expect(pruneLegacySiblings()).toEqual({ pruned: [] });
52
- expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe(before);
59
+ expect(readSettingsRaw()).toBe(before);
53
60
  });
54
61
 
55
62
  it("legacy-only: removes pi-subagents (nicobailon fork), preserves other top-level keys", () => {
@@ -93,6 +100,15 @@ describe("pruneLegacySiblings", () => {
93
100
  });
94
101
  });
95
102
 
103
+ it("prunes settings from PI_CODING_AGENT_DIR when configured", () => {
104
+ process.env.PI_CODING_AGENT_DIR = join(process.env.HOME!, ".config", "pi", "agent");
105
+ writeSettings({
106
+ packages: ["npm:pi-subagents"],
107
+ });
108
+ expect(pruneLegacySiblings().pruned).toEqual(["npm:pi-subagents"]);
109
+ expect(readSettings()).toEqual({ packages: [] });
110
+ });
111
+
96
112
  it("idempotent: second call after prune is a no-op", () => {
97
113
  writeSettings({
98
114
  packages: ["npm:pi-subagents"],
@@ -115,8 +131,7 @@ describe("findLegacySiblings (read-only scan)", () => {
115
131
  });
116
132
 
117
133
  it("invalid JSON → []", () => {
118
- mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
119
- writeFileSync(SETTINGS_PATH, "{not json", "utf-8");
134
+ writeSettingsRaw("{not json");
120
135
  expect(findLegacySiblings()).toEqual([]);
121
136
  });
122
137
 
@@ -147,16 +162,22 @@ describe("findLegacySiblings (read-only scan)", () => {
147
162
  defaultProvider: "zai",
148
163
  packages: ["npm:pi-subagents", "npm:@juicesharp/rpiv-todo"],
149
164
  });
150
- const before = readFileSync(SETTINGS_PATH, "utf-8");
165
+ const before = readSettingsRaw();
166
+ expect(findLegacySiblings()).toEqual(["npm:pi-subagents"]);
167
+ expect(readSettingsRaw()).toBe(before);
168
+ });
169
+
170
+ it("reads settings from PI_CODING_AGENT_DIR when configured", () => {
171
+ process.env.PI_CODING_AGENT_DIR = join(process.env.HOME!, ".config", "pi", "agent");
172
+ writeSettings({ packages: ["npm:pi-subagents"] });
151
173
  expect(findLegacySiblings()).toEqual(["npm:pi-subagents"]);
152
- expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe(before);
153
174
  });
154
175
 
155
176
  it("idempotent: repeat call returns the same list and does not mutate", () => {
156
177
  writeSettings({ packages: ["npm:pi-subagents"] });
157
- const before = readFileSync(SETTINGS_PATH, "utf-8");
178
+ const before = readSettingsRaw();
158
179
  expect(findLegacySiblings()).toEqual(["npm:pi-subagents"]);
159
180
  expect(findLegacySiblings()).toEqual(["npm:pi-subagents"]);
160
- expect(readFileSync(SETTINGS_PATH, "utf-8")).toBe(before);
181
+ expect(readSettingsRaw()).toBe(before);
161
182
  });
162
183
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Detect + remove deprecated sibling package entries from
3
- * ~/.pi/agent/settings.json.
3
+ * the active Pi agent settings file.
4
4
  *
5
5
  * Split into two phases so /rpiv-setup can preview pending changes in the
6
6
  * confirmation dialog and apply the mutation only after the user agrees:
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { writeFileSync } from "node:fs";
25
25
  import { LEGACY_SIBLINGS } from "./siblings.js";
26
- import { PI_AGENT_SETTINGS, readPiAgentSettings } from "./utils.js";
26
+ import { getPiAgentSettingsPath, readPiAgentSettings } from "./utils.js";
27
27
 
28
28
  export interface PruneLegacySiblingsResult {
29
29
  /** settings.json `packages[]` entries that were removed (empty = no-op). */
@@ -65,7 +65,7 @@ export function pruneLegacySiblings(): PruneLegacySiblingsResult {
65
65
 
66
66
  parsed.settings.packages = kept;
67
67
  try {
68
- writeFileSync(PI_AGENT_SETTINGS, `${JSON.stringify(parsed.settings, null, 2)}\n`, "utf-8");
68
+ writeFileSync(getPiAgentSettingsPath(), `${JSON.stringify(parsed.settings, null, 2)}\n`, "utf-8");
69
69
  } catch {
70
70
  return { pruned: [] };
71
71
  }
@@ -1,3 +1,4 @@
1
+ import { join } from "node:path";
1
2
  import { createMockCtx, createMockPi } from "@juicesharp/rpiv-test-utils";
2
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
4
 
@@ -85,6 +86,18 @@ describe("/rpiv-setup — pre-confirm read-only contract", () => {
85
86
  expect(confirmCall[1]).toContain("npm:pi-subagents");
86
87
  });
87
88
 
89
+ it("shows the PI_CODING_AGENT_DIR settings path in the confirmation body", async () => {
90
+ process.env.PI_CODING_AGENT_DIR = join(process.env.HOME!, ".config", "pi", "agent");
91
+ vi.mocked(findMissingSiblings).mockReturnValue([]);
92
+ vi.mocked(findLegacySiblings).mockReturnValue(["npm:pi-subagents"]);
93
+ const { pi, captured } = createMockPi();
94
+ registerSetupCommand(pi);
95
+ const ctx = createMockCtx({ hasUI: true });
96
+ await captured.commands.get("rpiv-setup")?.handler("", ctx as never);
97
+ const confirmCall = (ctx.ui.confirm as ReturnType<typeof vi.fn>).mock.calls[0]!;
98
+ expect(confirmCall[1]).toContain(join(process.env.HOME!, ".config", "pi", "agent", "settings.json"));
99
+ });
100
+
88
101
  it("includes pending installs in the confirmation body", async () => {
89
102
  vi.mocked(findMissingSiblings).mockReturnValue([{ pkg: "npm:@x/a", matches: /./, provides: "A" }]);
90
103
  vi.mocked(findLegacySiblings).mockReturnValue([]);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * /rpiv-setup — installs any SIBLINGS not present in ~/.pi/agent/settings.json
2
+ * /rpiv-setup — installs any SIBLINGS not present in the active Pi settings file
3
3
  * and prunes deprecated entries (e.g. the unscoped `npm:pi-subagents` from
4
4
  * the rpiv-pi 0.12.x → 0.14.0 line). Both mutations are previewed in the
5
5
  * confirmation dialog and only executed after the user agrees.
@@ -13,7 +13,7 @@ import { findMissingSiblings } from "./package-checks.js";
13
13
  import { spawnPiInstall } from "./pi-installer.js";
14
14
  import { findLegacySiblings, pruneLegacySiblings } from "./prune-legacy-siblings.js";
15
15
  import type { SiblingPlugin } from "./siblings.js";
16
- import { toErrorMessage } from "./utils.js";
16
+ import { getPiAgentSettingsPath, toErrorMessage } from "./utils.js";
17
17
 
18
18
  const INSTALL_TIMEOUT_MS = 120_000;
19
19
  const STDERR_SNIPPET_CHARS = 300;
@@ -43,12 +43,13 @@ function buildConfirmBody(missing: SiblingPlugin[], legacyEntries: string[]): st
43
43
  for (const m of missing) lines.push(` • ${m.pkg} (required — provides ${m.provides})`);
44
44
  lines.push("");
45
45
  }
46
+ const settingsPath = getPiAgentSettingsPath();
46
47
  if (legacyEntries.length > 0) {
47
- lines.push("Remove from `~/.pi/agent/settings.json` (deprecated):");
48
+ lines.push(`Remove from \`${settingsPath}\` (deprecated):`);
48
49
  for (const entry of legacyEntries) lines.push(` • ${entry}`);
49
50
  lines.push("");
50
51
  }
51
- lines.push("Your `~/.pi/agent/settings.json` will be updated. Proceed?");
52
+ lines.push(`Your \`${settingsPath}\` will be updated. Proceed?`);
52
53
  return lines.join("\n");
53
54
  }
54
55
 
@@ -6,14 +6,14 @@
6
6
  * /rpiv-setup installer (setup-command.ts). Add a sibling here and every
7
7
  * consumer picks it up automatically.
8
8
  *
9
- * Detection is filesystem-based via a regex over ~/.pi/agent/settings.json
9
+ * Detection is filesystem-based via a regex over the active Pi settings file
10
10
  * — no runtime import of sibling packages (keeps rpiv-core pure-orchestrator).
11
11
  */
12
12
 
13
13
  export interface SiblingPlugin {
14
14
  /** Install spec passed to `pi install`. Prefixed with `npm:` for Pi's installer. */
15
15
  readonly pkg: string;
16
- /** Case-insensitive regex that matches the package in ~/.pi/agent/settings.json. */
16
+ /** Case-insensitive regex that matches the package in settings.json. */
17
17
  readonly matches: RegExp;
18
18
  /** What the sibling provides — shown in /rpiv-setup confirmation and reports. */
19
19
  readonly provides: string;
@@ -59,7 +59,7 @@ export const SIBLINGS: readonly SiblingPlugin[] = [
59
59
 
60
60
  /**
61
61
  * Deprecated sibling packages that `/rpiv-setup` actively prunes from
62
- * ~/.pi/agent/settings.json (so upgraders don't end up with superseded
62
+ * the active Pi settings file (so upgraders don't end up with superseded
63
63
  * libraries loaded alongside their replacements). Single source of truth
64
64
  * for `prune-legacy-siblings.ts`.
65
65
  */
@@ -1,15 +1,21 @@
1
1
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
- import { isPlainObject, PI_AGENT_SETTINGS, readPiAgentSettings, toErrorMessage } from "./utils.js";
5
-
6
- function writeSettingsRaw(raw: string): void {
7
- mkdirSync(dirname(PI_AGENT_SETTINGS), { recursive: true });
8
- writeFileSync(PI_AGENT_SETTINGS, raw, "utf-8");
4
+ import {
5
+ getPiAgentSettingsPath,
6
+ isPlainObject,
7
+ PI_AGENT_SETTINGS,
8
+ readPiAgentSettings,
9
+ toErrorMessage,
10
+ } from "./utils.js";
11
+
12
+ function writeSettingsRaw(raw: string, path = getPiAgentSettingsPath()): void {
13
+ mkdirSync(dirname(path), { recursive: true });
14
+ writeFileSync(path, raw, "utf-8");
9
15
  }
10
16
 
11
- function writeSettings(contents: unknown): void {
12
- writeSettingsRaw(JSON.stringify(contents));
17
+ function writeSettings(contents: unknown, path = getPiAgentSettingsPath()): void {
18
+ writeSettingsRaw(JSON.stringify(contents), path);
13
19
  }
14
20
 
15
21
  describe("isPlainObject", () => {
@@ -65,6 +71,22 @@ describe("toErrorMessage", () => {
65
71
  });
66
72
  });
67
73
 
74
+ describe("getPiAgentSettingsPath", () => {
75
+ it("uses ~/.pi/agent/settings.json when PI_CODING_AGENT_DIR is unset", () => {
76
+ expect(getPiAgentSettingsPath()).toBe(PI_AGENT_SETTINGS);
77
+ });
78
+
79
+ it("uses PI_CODING_AGENT_DIR/settings.json when configured", () => {
80
+ process.env.PI_CODING_AGENT_DIR = join(process.env.HOME!, ".config", "pi", "agent");
81
+ expect(getPiAgentSettingsPath()).toBe(join(process.env.HOME!, ".config", "pi", "agent", "settings.json"));
82
+ });
83
+
84
+ it("expands a leading ~ in PI_CODING_AGENT_DIR", () => {
85
+ process.env.PI_CODING_AGENT_DIR = "~/.config/pi/agent";
86
+ expect(getPiAgentSettingsPath()).toBe(join(process.env.HOME!, ".config", "pi", "agent", "settings.json"));
87
+ });
88
+ });
89
+
68
90
  describe("readPiAgentSettings", () => {
69
91
  it("returns undefined when the settings file is missing", () => {
70
92
  rmSync(PI_AGENT_SETTINGS, { force: true });
@@ -105,6 +127,12 @@ describe("readPiAgentSettings", () => {
105
127
  });
106
128
  });
107
129
 
130
+ it("reads from PI_CODING_AGENT_DIR/settings.json when configured", () => {
131
+ process.env.PI_CODING_AGENT_DIR = join(process.env.HOME!, ".config", "pi", "agent");
132
+ writeSettings({ packages: ["npm:@juicesharp/rpiv-todo"] });
133
+ expect(readPiAgentSettings()?.packages).toEqual(["npm:@juicesharp/rpiv-todo"]);
134
+ });
135
+
108
136
  it("preserves non-string entries inside packages (caller responsibility to filter)", () => {
109
137
  writeSettings({ packages: [null, 42, "npm:pi-subagents"] });
110
138
  const result = readPiAgentSettings();
@@ -7,14 +7,24 @@
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
10
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // PI Agent Settings path
13
14
  // ---------------------------------------------------------------------------
14
15
 
15
- /** Path to the Pi agent settings file. Shared by package-checks and prune-legacy-siblings. */
16
+ /** Default Pi agent settings path when PI_CODING_AGENT_DIR is not configured. */
16
17
  export const PI_AGENT_SETTINGS = join(homedir(), ".pi", "agent", "settings.json");
17
18
 
19
+ /**
20
+ * Resolve the active Pi agent settings file. Delegates the agent-dir lookup to
21
+ * Pi's `getAgentDir()` so PI_CODING_AGENT_DIR handling (including tilde
22
+ * expansion) stays in one place across rpiv-pi and Pi itself.
23
+ */
24
+ export function getPiAgentSettingsPath(): string {
25
+ return join(getAgentDir(), "settings.json");
26
+ }
27
+
18
28
  // ---------------------------------------------------------------------------
19
29
  // Type guards
20
30
  // ---------------------------------------------------------------------------
@@ -48,15 +58,16 @@ interface PiAgentSettingsResult {
48
58
  }
49
59
 
50
60
  /**
51
- * Read and parse ~/.pi/agent/settings.json.
61
+ * Read and parse the active Pi agent settings file.
52
62
  * Returns undefined if the file is missing, has invalid JSON, or is not a plain object
53
63
  * with a packages array. Fail-soft — never throws.
54
64
  */
55
65
  export function readPiAgentSettings(): PiAgentSettingsResult | undefined {
56
- if (!existsSync(PI_AGENT_SETTINGS)) return undefined;
66
+ const settingsPath = getPiAgentSettingsPath();
67
+ if (!existsSync(settingsPath)) return undefined;
57
68
  let parsed: unknown;
58
69
  try {
59
- parsed = JSON.parse(readFileSync(PI_AGENT_SETTINGS, "utf-8"));
70
+ parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
60
71
  } catch {
61
72
  return undefined;
62
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "A skill-based development workflow for Pi Agent. Five skills (research, design, plan, implement, validate) and the shared subagents that compose its ship-loop.",
5
5
  "keywords": [
6
6
  "pi-package",