@juicesharp/rpiv-pi 0.4.4 → 0.4.5

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.
@@ -57,12 +57,6 @@ export interface SyncResult {
57
57
  pendingRemove: string[];
58
58
  /** Per-file errors collected during sync. */
59
59
  errors: SyncError[];
60
-
61
- // -- Legacy aliases (backward compat for existing callers) --
62
- /** Alias: added + updated (files written by this run). */
63
- copied: string[];
64
- /** Alias: unchanged + pendingUpdate + files that errored during read and were not written. */
65
- skipped: string[];
66
60
  }
67
61
 
68
62
  /** Create an empty SyncResult with all arrays initialized. */
@@ -75,8 +69,6 @@ function emptySyncResult(): SyncResult {
75
69
  pendingUpdate: [],
76
70
  pendingRemove: [],
77
71
  errors: [],
78
- copied: [],
79
- skipped: [],
80
72
  };
81
73
  }
82
74
 
@@ -219,7 +211,6 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
219
211
  op: "read-src",
220
212
  message: e instanceof Error ? e.message : String(e),
221
213
  });
222
- result.skipped.push(entry);
223
214
  continue;
224
215
  }
225
216
  try {
@@ -230,18 +221,15 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
230
221
  op: "read-dest",
231
222
  message: e instanceof Error ? e.message : String(e),
232
223
  });
233
- result.skipped.push(entry);
234
224
  continue;
235
225
  }
236
226
 
237
227
  if (Buffer.compare(srcContent, destContent) === 0) {
238
228
  result.unchanged.push(entry);
239
- result.skipped.push(entry);
240
229
  } else if (apply) {
241
230
  try {
242
231
  copyFileSync(src, dest);
243
232
  result.updated.push(entry);
244
- result.copied.push(entry);
245
233
  } catch (e) {
246
234
  result.errors.push({
247
235
  file: entry,
@@ -251,7 +239,6 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
251
239
  }
252
240
  } else {
253
241
  result.pendingUpdate.push(entry);
254
- result.skipped.push(entry);
255
242
  }
256
243
  }
257
244
 
@@ -287,26 +274,5 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
287
274
  : [...sourceEntries, ...result.pendingRemove];
288
275
  writeManifest(targetDir, manifestEntries);
289
276
 
290
- // 6. Populate legacy `copied` alias (added + updated)
291
- for (const name of result.added) {
292
- result.copied.push(name);
293
- }
294
- // updated files were pushed to `copied` inline in the loop above
295
-
296
277
  return result;
297
278
  }
298
-
299
- // ---------------------------------------------------------------------------
300
- // Backward-compatible wrapper
301
- // ---------------------------------------------------------------------------
302
-
303
- /**
304
- * Legacy entry point — delegates to syncBundledAgents.
305
- * Kept for backward compatibility; prefer syncBundledAgents for new callers.
306
- */
307
- export function copyBundledAgents(cwd: string, overwrite: boolean): {
308
- copied: string[];
309
- skipped: string[];
310
- } {
311
- return syncBundledAgents(cwd, overwrite);
312
- }
@@ -1,276 +1,19 @@
1
1
  /**
2
- * rpiv-core — Orchestrator extension for the rpiv-pi package
2
+ * rpiv-core — Pure-orchestrator extension for rpiv-pi.
3
3
  *
4
- * Provides:
5
- * - Guidance injection (replaces inject-guidance.js hook)
6
- * - Git context injection (replaces !`git ...` shell evaluation in skills)
7
- * - thoughts/ directory scaffolding on session start
8
- * - Bundled-agent auto-copy into <cwd>/.pi/agents/
9
- * - Aggregated session_start warning for missing sibling plugins
10
- * - /rpiv-update-agents, /rpiv-setup slash commands
4
+ * Composes session hooks and the two slash commands. All logic lives in the
5
+ * registrar modules; this file is the table of contents.
11
6
  *
12
- * Tool-owning plugins are siblings: @juicesharp/rpiv-ask-user-question,
13
- * @juicesharp/rpiv-todo, @juicesharp/rpiv-advisor, @juicesharp/rpiv-web-tools.
14
- * Install via /rpiv-setup.
7
+ * Tool-owning plugins are siblings (see siblings.ts); install via /rpiv-setup.
15
8
  */
16
9
 
17
- import { mkdirSync } from "node:fs";
18
- import { join } from "node:path";
19
- import { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
- import { clearInjectionState, handleToolCallGuidance, injectRootGuidance } from "./guidance.js";
21
- import {
22
- clearGitContextCache,
23
- isGitMutatingCommand,
24
- resetInjectedMarker,
25
- takeGitContextIfChanged,
26
- } from "./git-context.js";
27
- import { syncBundledAgents } from "./agents.js";
28
- import { spawnPiInstall } from "./pi-installer.js";
29
- import {
30
- hasPiSubagentsInstalled,
31
- hasRpivAskUserQuestionInstalled,
32
- hasRpivTodoInstalled,
33
- hasRpivAdvisorInstalled,
34
- hasRpivWebToolsInstalled,
35
- } from "./package-checks.js";
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { registerSessionHooks } from "./session-hooks.js";
12
+ import { registerSetupCommand } from "./setup-command.js";
13
+ import { registerUpdateAgentsCommand } from "./update-agents-command.js";
36
14
 
37
15
  export default function (pi: ExtensionAPI) {
38
- // ── Session Start ──────────────────────────────────────────────────────
39
- pi.on("session_start", async (_event, ctx) => {
40
- clearInjectionState();
41
- injectRootGuidance(ctx.cwd, pi);
42
-
43
- // Scaffold thoughts/ directory structure (artifact chain)
44
- const dirs = [
45
- "thoughts/shared/research",
46
- "thoughts/shared/questions",
47
- "thoughts/shared/designs",
48
- "thoughts/shared/plans",
49
- "thoughts/shared/handoffs",
50
- ];
51
- for (const dir of dirs) {
52
- mkdirSync(join(ctx.cwd, dir), { recursive: true });
53
- }
54
-
55
- // Inject git context once into the transcript
56
- const gitMsg = await takeGitContextIfChanged(pi);
57
- if (gitMsg) {
58
- pi.sendMessage({ customType: "rpiv-git-context", content: gitMsg, display: false });
59
- }
60
-
61
- // Sync bundled agents into <cwd>/.pi/agents/
62
- // Detect-only mode: adds new files, detects drift, does NOT overwrite or remove.
63
- const agentResult = syncBundledAgents(ctx.cwd, false);
64
- if (ctx.hasUI) {
65
- if (agentResult.added.length > 0) {
66
- ctx.ui.notify(
67
- `Copied ${agentResult.added.length} rpiv-pi agent(s) to .pi/agents/`,
68
- "info",
69
- );
70
- }
71
- const driftCount = agentResult.pendingUpdate.length + agentResult.pendingRemove.length;
72
- if (driftCount > 0) {
73
- const parts: string[] = [];
74
- if (agentResult.pendingUpdate.length > 0) {
75
- parts.push(`${agentResult.pendingUpdate.length} outdated`);
76
- }
77
- if (agentResult.pendingRemove.length > 0) {
78
- parts.push(`${agentResult.pendingRemove.length} removed from bundle`);
79
- }
80
- ctx.ui.notify(
81
- `${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`,
82
- "info",
83
- );
84
- }
85
- }
86
-
87
- // Aggregated warning for any missing sibling plugins
88
- if (ctx.hasUI) {
89
- const missing: string[] = [];
90
- if (!hasPiSubagentsInstalled()) missing.push("@tintinweb/pi-subagents");
91
- if (!hasRpivAskUserQuestionInstalled()) missing.push("@juicesharp/rpiv-ask-user-question");
92
- if (!hasRpivTodoInstalled()) missing.push("@juicesharp/rpiv-todo");
93
- if (!hasRpivAdvisorInstalled()) missing.push("@juicesharp/rpiv-advisor");
94
- if (!hasRpivWebToolsInstalled()) missing.push("@juicesharp/rpiv-web-tools");
95
- if (missing.length > 0) {
96
- ctx.ui.notify(
97
- `rpiv-pi requires ${missing.length} sibling extension(s): ${missing.join(", ")}. Run /rpiv-setup to install them.`,
98
- "warning",
99
- );
100
- }
101
- }
102
- });
103
-
104
- // ── Session Compact — drop injection state, re-inject root guidance ────
105
- pi.on("session_compact", async (_event, ctx) => {
106
- clearInjectionState();
107
- clearGitContextCache();
108
- resetInjectedMarker();
109
- injectRootGuidance(ctx.cwd, pi);
110
- const gitMsg = await takeGitContextIfChanged(pi);
111
- if (gitMsg) {
112
- pi.sendMessage({ customType: "rpiv-git-context", content: gitMsg, display: false });
113
- }
114
- });
115
-
116
- // ── Session Shutdown ───────────────────────────────────────────────────
117
- pi.on("session_shutdown", async (_event, _ctx) => {
118
- clearInjectionState();
119
- clearGitContextCache();
120
- resetInjectedMarker();
121
- });
122
-
123
- // ── Guidance Injection + Git Cache Invalidation ────────────────────────
124
- pi.on("tool_call", async (event, ctx) => {
125
- handleToolCallGuidance(event, ctx, pi);
126
- if (isToolCallEventType("bash", event) && isGitMutatingCommand(event.input.command)) {
127
- clearGitContextCache();
128
- }
129
- });
130
-
131
- // ── Git Context Injection (only when cache diverges from transcript) ───
132
- pi.on("before_agent_start", async (_event, _ctx) => {
133
- const content = await takeGitContextIfChanged(pi);
134
- if (!content) return;
135
- return {
136
- message: { customType: "rpiv-git-context", content, display: false },
137
- };
138
- });
139
-
140
- // ── /rpiv-update-agents Command ────────────────────────────────────────
141
- pi.registerCommand("rpiv-update-agents", {
142
- description: "Sync rpiv-pi bundled agents into .pi/agents/: add new, update changed, remove stale",
143
- handler: async (_args, ctx) => {
144
- const result = syncBundledAgents(ctx.cwd, true);
145
- if (!ctx.hasUI) return;
146
-
147
- const totalSynced = result.added.length + result.updated.length + result.removed.length;
148
- if (totalSynced === 0 && result.errors.length === 0) {
149
- ctx.ui.notify("All agents already up-to-date.", "info");
150
- return;
151
- }
152
-
153
- const parts: string[] = [];
154
- if (result.added.length > 0) parts.push(`${result.added.length} added`);
155
- if (result.updated.length > 0) parts.push(`${result.updated.length} updated`);
156
- if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);
157
-
158
- const summary = parts.length > 0
159
- ? `Synced agents: ${parts.join(", ")}.`
160
- : "No changes needed.";
161
-
162
- if (result.errors.length > 0) {
163
- ctx.ui.notify(
164
- `${summary} ${result.errors.length} error(s): ${result.errors.map((e) => e.message).join("; ")}`,
165
- "warning",
166
- );
167
- } else {
168
- ctx.ui.notify(summary, "info");
169
- }
170
- },
171
- });
172
-
173
- // ── /rpiv-setup Command ────────────────────────────────────────────────
174
- pi.registerCommand("rpiv-setup", {
175
- description: "Install rpiv-pi's sibling extension plugins",
176
- handler: async (_args, ctx) => {
177
- if (!ctx.hasUI) {
178
- ctx.ui.notify("/rpiv-setup requires interactive mode", "error");
179
- return;
180
- }
181
-
182
- const missing: Array<{ pkg: string; reason: string }> = [];
183
- if (!hasPiSubagentsInstalled()) {
184
- missing.push({
185
- pkg: "npm:@tintinweb/pi-subagents",
186
- reason: "required — provides Agent / get_subagent_result / steer_subagent tools",
187
- });
188
- }
189
- if (!hasRpivAskUserQuestionInstalled()) {
190
- missing.push({
191
- pkg: "npm:@juicesharp/rpiv-ask-user-question",
192
- reason: "required — provides the ask_user_question tool",
193
- });
194
- }
195
- if (!hasRpivTodoInstalled()) {
196
- missing.push({
197
- pkg: "npm:@juicesharp/rpiv-todo",
198
- reason: "required — provides the todo tool + /todos command + overlay widget",
199
- });
200
- }
201
- if (!hasRpivAdvisorInstalled()) {
202
- missing.push({
203
- pkg: "npm:@juicesharp/rpiv-advisor",
204
- reason: "required — provides the advisor tool + /advisor command",
205
- });
206
- }
207
- if (!hasRpivWebToolsInstalled()) {
208
- missing.push({
209
- pkg: "npm:@juicesharp/rpiv-web-tools",
210
- reason: "required — provides web_search + web_fetch tools + /web-search-config",
211
- });
212
- }
213
-
214
- if (missing.length === 0) {
215
- ctx.ui.notify(
216
- "All rpiv-pi sibling dependencies already installed.",
217
- "info",
218
- );
219
- return;
220
- }
221
-
222
- const lines = [
223
- "rpiv-pi will install the following Pi packages via `pi install`:",
224
- "",
225
- ...missing.map((m) => ` • ${m.pkg} (${m.reason})`),
226
- "",
227
- "Each install is a separate `pi install <pkg>` invocation. Your",
228
- "~/.pi/agent/settings.json will be updated. Proceed?",
229
- ];
230
-
231
- const confirmed = await ctx.ui.confirm("Install rpiv-pi dependencies?", lines.join("\n"));
232
- if (!confirmed) {
233
- ctx.ui.notify("/rpiv-setup cancelled", "info");
234
- return;
235
- }
236
-
237
- const succeeded: string[] = [];
238
- const failed: Array<{ pkg: string; error: string }> = [];
239
- for (const { pkg } of missing) {
240
- ctx.ui.notify(`Installing ${pkg}…`, "info");
241
- try {
242
- const result = await spawnPiInstall(pkg, 120_000);
243
- if (result.code === 0) {
244
- succeeded.push(pkg);
245
- } else {
246
- failed.push({
247
- pkg,
248
- error: (result.stderr || result.stdout || `exit ${result.code}`).trim().slice(0, 300),
249
- });
250
- }
251
- } catch (err) {
252
- failed.push({
253
- pkg,
254
- error: err instanceof Error ? err.message : String(err),
255
- });
256
- }
257
- }
258
-
259
- const report: string[] = [];
260
- if (succeeded.length > 0) {
261
- report.push(`✓ Installed: ${succeeded.join(", ")}`);
262
- }
263
- if (failed.length > 0) {
264
- report.push(`✗ Failed:`);
265
- for (const { pkg, error } of failed) {
266
- report.push(` ${pkg}: ${error}`);
267
- }
268
- }
269
- if (succeeded.length > 0) {
270
- report.push("");
271
- report.push("Restart your Pi session to load the newly-installed extensions.");
272
- }
273
- ctx.ui.notify(report.join("\n"), failed.length > 0 ? "warning" : "info");
274
- },
275
- });
16
+ registerSessionHooks(pi);
17
+ registerUpdateAgentsCommand(pi);
18
+ registerSetupCommand(pi);
276
19
  }
@@ -1,24 +1,16 @@
1
1
  /**
2
- * Package presence checks detects whether sibling pi packages are installed.
3
- *
4
- * Pure utility. No ExtensionAPI interactions.
2
+ * Detect which SIBLINGS are installed by reading ~/.pi/agent/settings.json.
3
+ * Pure utility — no ExtensionAPI.
5
4
  */
6
5
 
7
6
  import { existsSync, readFileSync } from "node:fs";
8
7
  import { join } from "node:path";
9
8
  import { homedir } from "node:os";
10
-
11
- // ---------------------------------------------------------------------------
12
- // Paths
13
- // ---------------------------------------------------------------------------
9
+ import { SIBLINGS, type SiblingPlugin } from "./siblings.js";
14
10
 
15
11
  const PI_AGENT_SETTINGS = join(homedir(), ".pi", "agent", "settings.json");
16
12
 
17
- // ---------------------------------------------------------------------------
18
- // Package Detection
19
- // ---------------------------------------------------------------------------
20
-
21
- export function readInstalledPackages(): string[] {
13
+ function readInstalledPackages(): string[] {
22
14
  if (!existsSync(PI_AGENT_SETTINGS)) return [];
23
15
  try {
24
16
  const raw = readFileSync(PI_AGENT_SETTINGS, "utf-8");
@@ -30,22 +22,12 @@ export function readInstalledPackages(): string[] {
30
22
  }
31
23
  }
32
24
 
33
- export function hasPiSubagentsInstalled(): boolean {
34
- return readInstalledPackages().some((entry) => /@tintinweb\/pi-subagents/i.test(entry));
35
- }
36
-
37
- export function hasRpivAskUserQuestionInstalled(): boolean {
38
- return readInstalledPackages().some((entry) => /rpiv-ask-user-question/i.test(entry));
39
- }
40
-
41
- export function hasRpivTodoInstalled(): boolean {
42
- return readInstalledPackages().some((entry) => /rpiv-todo/i.test(entry));
43
- }
44
-
45
- export function hasRpivAdvisorInstalled(): boolean {
46
- return readInstalledPackages().some((entry) => /rpiv-advisor/i.test(entry));
47
- }
48
-
49
- export function hasRpivWebToolsInstalled(): boolean {
50
- return readInstalledPackages().some((entry) => /rpiv-web-tools/i.test(entry));
25
+ /**
26
+ * Return the SIBLINGS not currently installed.
27
+ * Reads ~/.pi/agent/settings.json once per call — callers that need both the
28
+ * full snapshot and the missing subset should call this once and filter.
29
+ */
30
+ export function findMissingSiblings(): SiblingPlugin[] {
31
+ const installed = readInstalledPackages();
32
+ return SIBLINGS.filter((s) => !installed.some((entry) => s.matches.test(entry)));
51
33
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Session lifecycle wiring for rpiv-core.
3
+ *
4
+ * Each handler body is a named helper; pi.on(...) lines are pure wiring.
5
+ * Ordering and invariants preserved verbatim from the pre-refactor index.ts.
6
+ */
7
+
8
+ import { mkdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { clearInjectionState, handleToolCallGuidance, injectRootGuidance } from "./guidance.js";
12
+ import {
13
+ clearGitContextCache,
14
+ isGitMutatingCommand,
15
+ resetInjectedMarker,
16
+ takeGitContextIfChanged,
17
+ } from "./git-context.js";
18
+ import { syncBundledAgents, type SyncResult } from "./agents.js";
19
+ import { findMissingSiblings } from "./package-checks.js";
20
+
21
+ const THOUGHTS_DIRS = [
22
+ "thoughts/shared/research",
23
+ "thoughts/shared/questions",
24
+ "thoughts/shared/designs",
25
+ "thoughts/shared/plans",
26
+ "thoughts/shared/handoffs",
27
+ ] as const;
28
+
29
+ const msgAgentsAdded = (n: number) => `Copied ${n} rpiv-pi agent(s) to .pi/agents/`;
30
+ const msgAgentsDrift = (parts: string[]) =>
31
+ `${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`;
32
+ const msgMissingSiblings = (n: number, list: string) =>
33
+ `rpiv-pi requires ${n} sibling extension(s): ${list}. Run /rpiv-setup to install them.`;
34
+
35
+ type UI = { notify: (msg: string, sev: "info" | "warning" | "error") => void };
36
+
37
+ export function registerSessionHooks(pi: ExtensionAPI): void {
38
+ pi.on("session_start", async (_event, ctx) => {
39
+ resetInjectionState();
40
+ injectRootGuidance(ctx.cwd, pi);
41
+ scaffoldThoughtsDirs(ctx.cwd);
42
+ await injectGitContext(pi, (msg) =>
43
+ pi.sendMessage({ customType: "rpiv-git-context", content: msg, display: false }),
44
+ );
45
+ const agents = syncBundledAgents(ctx.cwd, false);
46
+ if (ctx.hasUI) {
47
+ notifyAgentSyncDrift(ctx.ui, agents);
48
+ warnMissingSiblings(ctx.ui);
49
+ }
50
+ });
51
+
52
+ pi.on("session_compact", async (_event, ctx) => {
53
+ resetInjectionState();
54
+ clearGitContextCache();
55
+ resetInjectedMarker();
56
+ injectRootGuidance(ctx.cwd, pi);
57
+ await injectGitContext(pi, (msg) =>
58
+ pi.sendMessage({ customType: "rpiv-git-context", content: msg, display: false }),
59
+ );
60
+ });
61
+
62
+ pi.on("session_shutdown", async () => {
63
+ resetInjectionState();
64
+ clearGitContextCache();
65
+ resetInjectedMarker();
66
+ });
67
+
68
+ pi.on("tool_call", async (event, ctx) => {
69
+ handleToolCallGuidance(event, ctx, pi);
70
+ if (isToolCallEventType("bash", event) && isGitMutatingCommand(event.input.command)) {
71
+ clearGitContextCache();
72
+ }
73
+ });
74
+
75
+ pi.on("before_agent_start", async () => {
76
+ const content = await takeGitContextIfChanged(pi);
77
+ if (!content) return;
78
+ return { message: { customType: "rpiv-git-context", content, display: false } };
79
+ });
80
+ }
81
+
82
+ function resetInjectionState(): void {
83
+ clearInjectionState();
84
+ }
85
+
86
+ function scaffoldThoughtsDirs(cwd: string): void {
87
+ for (const dir of THOUGHTS_DIRS) {
88
+ mkdirSync(join(cwd, dir), { recursive: true });
89
+ }
90
+ }
91
+
92
+ async function injectGitContext(
93
+ pi: ExtensionAPI,
94
+ send: (msg: string) => void,
95
+ ): Promise<void> {
96
+ const msg = await takeGitContextIfChanged(pi);
97
+ if (msg) send(msg);
98
+ }
99
+
100
+ function notifyAgentSyncDrift(ui: UI, result: SyncResult): void {
101
+ if (result.added.length > 0) {
102
+ ui.notify(msgAgentsAdded(result.added.length), "info");
103
+ }
104
+ const parts: string[] = [];
105
+ if (result.pendingUpdate.length > 0) parts.push(`${result.pendingUpdate.length} outdated`);
106
+ if (result.pendingRemove.length > 0) parts.push(`${result.pendingRemove.length} removed from bundle`);
107
+ if (parts.length > 0) {
108
+ ui.notify(msgAgentsDrift(parts), "info");
109
+ }
110
+ }
111
+
112
+ function warnMissingSiblings(ui: UI): void {
113
+ const missing = findMissingSiblings();
114
+ if (missing.length === 0) return;
115
+ ui.notify(
116
+ msgMissingSiblings(missing.length, missing.map((m) => m.pkg.replace(/^npm:/, "")).join(", ")),
117
+ "warning",
118
+ );
119
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * /rpiv-setup — installs any SIBLINGS not present in ~/.pi/agent/settings.json.
3
+ *
4
+ * Serial `pi install <pkg>` loop via spawnPiInstall (Windows-safe).
5
+ * Reports succeeded/failed split; prompts the user to restart Pi on success.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { findMissingSiblings } from "./package-checks.js";
10
+ import { spawnPiInstall } from "./pi-installer.js";
11
+ import { type SiblingPlugin } from "./siblings.js";
12
+
13
+ const INSTALL_TIMEOUT_MS = 120_000;
14
+ const STDERR_SNIPPET_CHARS = 300;
15
+
16
+ const MSG_INTERACTIVE_ONLY = "/rpiv-setup requires interactive mode";
17
+ const MSG_ALL_INSTALLED = "All rpiv-pi sibling dependencies already installed.";
18
+ const MSG_CANCELLED = "/rpiv-setup cancelled";
19
+ const MSG_CONFIRM_TITLE = "Install rpiv-pi dependencies?";
20
+ const MSG_RESTART = "Restart your Pi session to load the newly-installed extensions.";
21
+
22
+ const msgInstalling = (pkg: string) => `Installing ${pkg}…`;
23
+ const msgInstalledLine = (pkgs: string[]) => `✓ Installed: ${pkgs.join(", ")}`;
24
+ const msgFailedHeader = () => `✗ Failed:`;
25
+ const msgFailedLine = (pkg: string, err: string) => ` ${pkg}: ${err}`;
26
+
27
+ type UI = {
28
+ notify: (msg: string, sev: "info" | "warning" | "error") => void;
29
+ confirm: (title: string, body: string) => Promise<boolean>;
30
+ };
31
+
32
+ function buildConfirmBody(missing: SiblingPlugin[]): string {
33
+ return [
34
+ "rpiv-pi will install the following Pi packages via `pi install`:",
35
+ "",
36
+ ...missing.map((m) => ` • ${m.pkg} (required — provides ${m.provides})`),
37
+ "",
38
+ "Each install is a separate `pi install <pkg>` invocation. Your",
39
+ "~/.pi/agent/settings.json will be updated. Proceed?",
40
+ ].join("\n");
41
+ }
42
+
43
+ export function registerSetupCommand(pi: ExtensionAPI): void {
44
+ pi.registerCommand("rpiv-setup", {
45
+ description: "Install rpiv-pi's sibling extension plugins",
46
+ handler: async (_args, ctx) => {
47
+ if (!ctx.hasUI) {
48
+ ctx.ui.notify(MSG_INTERACTIVE_ONLY, "error");
49
+ return;
50
+ }
51
+
52
+ const missing = findMissingSiblings();
53
+ if (missing.length === 0) {
54
+ ctx.ui.notify(MSG_ALL_INSTALLED, "info");
55
+ return;
56
+ }
57
+
58
+ const confirmed = await ctx.ui.confirm(MSG_CONFIRM_TITLE, buildConfirmBody(missing));
59
+ if (!confirmed) {
60
+ ctx.ui.notify(MSG_CANCELLED, "info");
61
+ return;
62
+ }
63
+
64
+ const { succeeded, failed } = await installMissing(ctx.ui, missing);
65
+ ctx.ui.notify(buildReport(succeeded, failed), failed.length > 0 ? "warning" : "info");
66
+ },
67
+ });
68
+ }
69
+
70
+ async function installMissing(
71
+ ui: UI,
72
+ missing: SiblingPlugin[],
73
+ ): Promise<{ succeeded: string[]; failed: Array<{ pkg: string; error: string }> }> {
74
+ const succeeded: string[] = [];
75
+ const failed: Array<{ pkg: string; error: string }> = [];
76
+ for (const { pkg } of missing) {
77
+ ui.notify(msgInstalling(pkg), "info");
78
+ try {
79
+ const result = await spawnPiInstall(pkg, INSTALL_TIMEOUT_MS);
80
+ if (result.code === 0) {
81
+ succeeded.push(pkg);
82
+ } else {
83
+ failed.push({
84
+ pkg,
85
+ error: (result.stderr || result.stdout || `exit ${result.code}`)
86
+ .trim()
87
+ .slice(0, STDERR_SNIPPET_CHARS),
88
+ });
89
+ }
90
+ } catch (err) {
91
+ failed.push({ pkg, error: err instanceof Error ? err.message : String(err) });
92
+ }
93
+ }
94
+ return { succeeded, failed };
95
+ }
96
+
97
+ function buildReport(succeeded: string[], failed: Array<{ pkg: string; error: string }>): string {
98
+ const lines: string[] = [];
99
+ if (succeeded.length > 0) lines.push(msgInstalledLine(succeeded));
100
+ if (failed.length > 0) {
101
+ lines.push(msgFailedHeader());
102
+ for (const { pkg, error } of failed) lines.push(msgFailedLine(pkg, error));
103
+ }
104
+ if (succeeded.length > 0) {
105
+ lines.push("");
106
+ lines.push(MSG_RESTART);
107
+ }
108
+ return lines.join("\n");
109
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Declarative registry of rpiv-pi's sibling Pi plugins.
3
+ *
4
+ * Single source of truth for: presence detection (package-checks.ts),
5
+ * session_start "missing plugins" warning (session-hooks.ts), and
6
+ * /rpiv-setup installer (setup-command.ts). Add a sibling here and every
7
+ * consumer picks it up automatically.
8
+ *
9
+ * Detection is filesystem-based via a regex over ~/.pi/agent/settings.json
10
+ * — no runtime import of sibling packages (keeps rpiv-core pure-orchestrator).
11
+ */
12
+
13
+ export interface SiblingPlugin {
14
+ /** Install spec passed to `pi install`. Prefixed with `npm:` for Pi's installer. */
15
+ readonly pkg: string;
16
+ /** Case-insensitive regex that matches the package in ~/.pi/agent/settings.json. */
17
+ readonly matches: RegExp;
18
+ /** What the sibling provides — shown in /rpiv-setup confirmation and reports. */
19
+ readonly provides: string;
20
+ }
21
+
22
+ export const SIBLINGS: readonly SiblingPlugin[] = [
23
+ {
24
+ pkg: "npm:@tintinweb/pi-subagents",
25
+ matches: /@tintinweb\/pi-subagents/i,
26
+ provides: "Agent / get_subagent_result / steer_subagent tools",
27
+ },
28
+ {
29
+ pkg: "npm:@juicesharp/rpiv-ask-user-question",
30
+ matches: /rpiv-ask-user-question/i,
31
+ provides: "ask_user_question tool",
32
+ },
33
+ {
34
+ pkg: "npm:@juicesharp/rpiv-todo",
35
+ matches: /rpiv-todo/i,
36
+ provides: "todo tool + /todos command + overlay widget",
37
+ },
38
+ {
39
+ pkg: "npm:@juicesharp/rpiv-advisor",
40
+ matches: /rpiv-advisor/i,
41
+ provides: "advisor tool + /advisor command",
42
+ },
43
+ {
44
+ pkg: "npm:@juicesharp/rpiv-web-tools",
45
+ matches: /rpiv-web-tools/i,
46
+ provides: "web_search + web_fetch tools + /web-search-config",
47
+ },
48
+ ];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * /rpiv-update-agents — apply-mode sync of bundled agents into <cwd>/.pi/agents/.
3
+ * Adds new, overwrites changed managed files, removes stale managed files.
4
+ */
5
+
6
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+ import { syncBundledAgents, type SyncResult } from "./agents.js";
8
+
9
+ const MSG_UP_TO_DATE = "All agents already up-to-date.";
10
+ const MSG_NO_CHANGES = "No changes needed.";
11
+
12
+ const msgSynced = (parts: string[]) => `Synced agents: ${parts.join(", ")}.`;
13
+ const msgSyncedWithErrors = (summary: string, errors: string[]) =>
14
+ `${summary} ${errors.length} error(s): ${errors.join("; ")}`;
15
+
16
+ export function registerUpdateAgentsCommand(pi: ExtensionAPI): void {
17
+ pi.registerCommand("rpiv-update-agents", {
18
+ description:
19
+ "Sync rpiv-pi bundled agents into .pi/agents/: add new, update changed, remove stale",
20
+ handler: async (_args, ctx) => {
21
+ const result = syncBundledAgents(ctx.cwd, true);
22
+ if (!ctx.hasUI) return;
23
+ ctx.ui.notify(formatSyncReport(result), result.errors.length > 0 ? "warning" : "info");
24
+ },
25
+ });
26
+ }
27
+
28
+ function formatSyncReport(result: SyncResult): string {
29
+ const totalSynced = result.added.length + result.updated.length + result.removed.length;
30
+ if (totalSynced === 0 && result.errors.length === 0) return MSG_UP_TO_DATE;
31
+
32
+ const parts: string[] = [];
33
+ if (result.added.length > 0) parts.push(`${result.added.length} added`);
34
+ if (result.updated.length > 0) parts.push(`${result.updated.length} updated`);
35
+ if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);
36
+
37
+ const summary = parts.length > 0 ? msgSynced(parts) : MSG_NO_CHANGES;
38
+ if (result.errors.length > 0) {
39
+ return msgSyncedWithErrors(summary, result.errors.map((e) => e.message));
40
+ }
41
+ return summary;
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Skill-based development workflow for Pi Agent — discover, research, design, plan, implement, validate",
5
5
  "keywords": ["pi-package", "pi-extension", "rpiv", "skills", "workflow"],
6
6
  "license": "MIT",
@@ -17,16 +17,13 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
- "files": ["extensions/", "skills/", "agents/", "scripts/", "README.md"],
20
+ "files": ["extensions/", "skills/", "agents/", "scripts/", "README.md", "LICENSE"],
21
21
  "pi": {
22
22
  "extensions": ["./extensions"],
23
23
  "skills": ["./skills"]
24
24
  },
25
25
  "peerDependencies": {
26
- "@mariozechner/pi-ai": "*",
27
26
  "@mariozechner/pi-coding-agent": "*",
28
- "@mariozechner/pi-tui": "*",
29
- "@sinclair/typebox": "*",
30
27
  "@tintinweb/pi-subagents": "*",
31
28
  "@juicesharp/rpiv-ask-user-question": "*",
32
29
  "@juicesharp/rpiv-todo": "*",
package/scripts/types.js DELETED
@@ -1 +0,0 @@
1
- export {};