@juicesharp/rpiv-pi 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/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/codebase-analyzer.md +121 -0
- package/agents/codebase-locator.md +107 -0
- package/agents/codebase-pattern-finder.md +207 -0
- package/agents/integration-scanner.md +97 -0
- package/agents/precedent-locator.md +130 -0
- package/agents/test-case-locator.md +121 -0
- package/agents/thoughts-analyzer.md +147 -0
- package/agents/thoughts-locator.md +138 -0
- package/agents/web-search-researcher.md +107 -0
- package/extensions/rpiv-core/agents.ts +312 -0
- package/extensions/rpiv-core/git-context.ts +81 -0
- package/extensions/rpiv-core/guidance.ts +213 -0
- package/extensions/rpiv-core/index.ts +275 -0
- package/extensions/rpiv-core/package-checks.ts +51 -0
- package/package.json +36 -0
- package/scripts/migrate.js +242 -0
- package/scripts/types.js +1 -0
- package/skills/annotate-guidance/SKILL.md +303 -0
- package/skills/annotate-guidance/examples/root-dotnet-clean-arch.md +38 -0
- package/skills/annotate-guidance/examples/root-nodejs-monorepo.md +42 -0
- package/skills/annotate-guidance/examples/subfolder-database-layer.md +81 -0
- package/skills/annotate-guidance/examples/subfolder-dotnet-application.md +64 -0
- package/skills/annotate-guidance/examples/subfolder-schemas-layer.md +50 -0
- package/skills/annotate-guidance/templates/root-architecture.md +46 -0
- package/skills/annotate-guidance/templates/subfolder-architecture.md +57 -0
- package/skills/annotate-inline/SKILL.md +299 -0
- package/skills/annotate-inline/examples/root-dotnet-clean-arch.md +38 -0
- package/skills/annotate-inline/examples/root-nodejs-monorepo.md +42 -0
- package/skills/annotate-inline/examples/subfolder-database-layer.md +81 -0
- package/skills/annotate-inline/examples/subfolder-dotnet-application.md +64 -0
- package/skills/annotate-inline/examples/subfolder-schemas-layer.md +50 -0
- package/skills/annotate-inline/templates/root-claude-md.md +46 -0
- package/skills/annotate-inline/templates/subfolder-claude-md.md +57 -0
- package/skills/code-review/SKILL.md +184 -0
- package/skills/commit/SKILL.md +65 -0
- package/skills/create-handoff/SKILL.md +91 -0
- package/skills/design/SKILL.md +416 -0
- package/skills/discover/SKILL.md +242 -0
- package/skills/explore/SKILL.md +261 -0
- package/skills/implement/SKILL.md +74 -0
- package/skills/migrate-to-guidance/SKILL.md +88 -0
- package/skills/outline-test-cases/SKILL.md +348 -0
- package/skills/outline-test-cases/templates/feature-meta.md +43 -0
- package/skills/outline-test-cases/templates/outline-readme.md +36 -0
- package/skills/plan/SKILL.md +281 -0
- package/skills/research/SKILL.md +304 -0
- package/skills/resume-handoff/SKILL.md +207 -0
- package/skills/revise/SKILL.md +242 -0
- package/skills/validate/SKILL.md +175 -0
- package/skills/write-test-cases/SKILL.md +322 -0
- package/skills/write-test-cases/examples/customer-auth-flow.md +50 -0
- package/skills/write-test-cases/examples/order-management-suite.md +57 -0
- package/skills/write-test-cases/examples/order-placement-flow.md +54 -0
- package/skills/write-test-cases/examples/team-management-flow.md +56 -0
- package/skills/write-test-cases/examples/team-management-suite.md +54 -0
- package/skills/write-test-cases/templates/coverage-map.md +64 -0
- package/skills/write-test-cases/templates/regression-suite.md +63 -0
- package/skills/write-test-cases/templates/test-case.md +65 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guidance injection — resolves and injects subfolder guidance files.
|
|
3
|
+
*
|
|
4
|
+
* At each directory depth from project root down to the touched file's
|
|
5
|
+
* directory, picks the first existing of:
|
|
6
|
+
* AGENTS.md > CLAUDE.md > .rpiv/guidance/<sub>/architecture.md
|
|
7
|
+
*
|
|
8
|
+
* Depth 0 (project root) skips AGENTS.md/CLAUDE.md because Pi's own
|
|
9
|
+
* resource-loader (loadContextFileFromDir at resource-loader.js:30-46)
|
|
10
|
+
* already loads <cwd>/AGENTS.md or <cwd>/CLAUDE.md into the system
|
|
11
|
+
* prompt's # Project Context block. Depth 0 still checks
|
|
12
|
+
* <cwd>/.rpiv/guidance/architecture.md — Pi's loader does not see that
|
|
13
|
+
* path.
|
|
14
|
+
*
|
|
15
|
+
* `resolveGuidance` is pure logic with no ExtensionAPI references
|
|
16
|
+
* (utility-module rule from extensions/rpiv-core/CLAUDE.md). Side
|
|
17
|
+
* effects (sendMessage, in-memory dedup Set) live in
|
|
18
|
+
* `handleToolCallGuidance`, `injectRootGuidance`, and
|
|
19
|
+
* `clearInjectionState`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { dirname, relative, sep, isAbsolute, join } from "node:path";
|
|
24
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Guidance Resolution
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
type GuidanceKind = "agents" | "claude" | "architecture";
|
|
31
|
+
|
|
32
|
+
interface GuidanceFile {
|
|
33
|
+
/** Forward-slash-normalized path from project root — stable dedup key. */
|
|
34
|
+
relativePath: string;
|
|
35
|
+
absolutePath: string;
|
|
36
|
+
content: string;
|
|
37
|
+
kind: GuidanceKind;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve guidance files for a given file path.
|
|
42
|
+
*
|
|
43
|
+
* Walks from project root to the file's directory. At each depth, picks
|
|
44
|
+
* the first existing of AGENTS.md > CLAUDE.md > architecture.md (Pi's
|
|
45
|
+
* own per-dir precedence at resource-loader.js:30-46, extended with
|
|
46
|
+
* architecture.md as a third candidate). Depth 0 only checks
|
|
47
|
+
* architecture.md — Pi's loader already handles <cwd>/AGENTS.md and
|
|
48
|
+
* <cwd>/CLAUDE.md.
|
|
49
|
+
*
|
|
50
|
+
* Returns files root-first (general → specific), at most one per depth.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveGuidance(filePath: string, projectDir: string): GuidanceFile[] {
|
|
53
|
+
const fileDir = dirname(filePath);
|
|
54
|
+
const relativeDir = relative(projectDir, fileDir);
|
|
55
|
+
|
|
56
|
+
// Guard: file is outside project root
|
|
57
|
+
if (relativeDir.startsWith("..") || isAbsolute(relativeDir)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const parts = relativeDir ? relativeDir.split(sep) : [];
|
|
62
|
+
const results: GuidanceFile[] = [];
|
|
63
|
+
|
|
64
|
+
for (let depth = 0; depth <= parts.length; depth++) {
|
|
65
|
+
const subPath = parts.slice(0, depth).join(sep);
|
|
66
|
+
|
|
67
|
+
// Per-depth candidate ladder. First-match wins.
|
|
68
|
+
const candidates: Array<{ relative: string; kind: GuidanceKind }> = [];
|
|
69
|
+
|
|
70
|
+
// Depth 0: skip AGENTS/CLAUDE — Pi's loader handles <cwd> already.
|
|
71
|
+
if (depth > 0) {
|
|
72
|
+
candidates.push({ relative: join(subPath, "AGENTS.md"), kind: "agents" });
|
|
73
|
+
candidates.push({ relative: join(subPath, "CLAUDE.md"), kind: "claude" });
|
|
74
|
+
}
|
|
75
|
+
candidates.push({
|
|
76
|
+
relative: subPath
|
|
77
|
+
? join(".rpiv", "guidance", subPath, "architecture.md")
|
|
78
|
+
: join(".rpiv", "guidance", "architecture.md"),
|
|
79
|
+
kind: "architecture",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
for (const candidate of candidates) {
|
|
83
|
+
const absolute = join(projectDir, candidate.relative);
|
|
84
|
+
if (existsSync(absolute)) {
|
|
85
|
+
results.push({
|
|
86
|
+
relativePath: candidate.relative.split(sep).join("/"),
|
|
87
|
+
absolutePath: absolute,
|
|
88
|
+
content: readFileSync(absolute, "utf-8"),
|
|
89
|
+
kind: candidate.kind,
|
|
90
|
+
});
|
|
91
|
+
break; // first-match wins at this depth
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Session State
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/** In-memory set of injected guidance paths per session. */
|
|
104
|
+
const injectedGuidance = new Set<string>();
|
|
105
|
+
|
|
106
|
+
export function clearInjectionState() {
|
|
107
|
+
injectedGuidance.clear();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Root Guidance Injection (session_start)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Inject the root `.rpiv/guidance/architecture.md` at session start.
|
|
116
|
+
*
|
|
117
|
+
* Called from `session_start` so the root guidance is available before the
|
|
118
|
+
* first agent turn — without waiting for a read/edit/write tool_call.
|
|
119
|
+
* Uses the same `injectedGuidance` Set for dedup, so `handleToolCallGuidance`
|
|
120
|
+
* won't re-inject it later.
|
|
121
|
+
*/
|
|
122
|
+
export function injectRootGuidance(cwd: string, pi: ExtensionAPI): void {
|
|
123
|
+
const relativePath = ".rpiv/guidance/architecture.md";
|
|
124
|
+
|
|
125
|
+
if (injectedGuidance.has(relativePath)) return;
|
|
126
|
+
|
|
127
|
+
const absolutePath = join(cwd, relativePath);
|
|
128
|
+
if (!existsSync(absolutePath)) return;
|
|
129
|
+
|
|
130
|
+
let content: string;
|
|
131
|
+
try {
|
|
132
|
+
content = readFileSync(absolutePath, "utf-8");
|
|
133
|
+
} catch {
|
|
134
|
+
// Silent failure mirrors handleToolCallGuidance's posture — session_start
|
|
135
|
+
// runs before any UI is bound, so a permissions/race error here must not
|
|
136
|
+
// crash the hook. Don't mark as injected so a later tool_call can retry.
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
injectedGuidance.add(relativePath);
|
|
140
|
+
|
|
141
|
+
const label = formatLabel({
|
|
142
|
+
relativePath,
|
|
143
|
+
absolutePath,
|
|
144
|
+
content,
|
|
145
|
+
kind: "architecture",
|
|
146
|
+
});
|
|
147
|
+
pi.sendMessage({
|
|
148
|
+
customType: "rpiv-guidance",
|
|
149
|
+
content: `## Project Guidance: ${label}\n\n${content}`,
|
|
150
|
+
display: false,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Tool-call Handler
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle guidance injection on tool_call events for read/edit/write.
|
|
160
|
+
* Sends hidden messages via pi.sendMessage as a side effect.
|
|
161
|
+
*/
|
|
162
|
+
export function handleToolCallGuidance(
|
|
163
|
+
event: { toolName: string; input: Record<string, unknown> },
|
|
164
|
+
ctx: { cwd: string },
|
|
165
|
+
pi: ExtensionAPI,
|
|
166
|
+
): void {
|
|
167
|
+
if (!["read", "edit", "write"].includes(event.toolName)) return;
|
|
168
|
+
|
|
169
|
+
const filePath = (event.input as any).file_path ?? (event.input as any).path;
|
|
170
|
+
if (!filePath) return;
|
|
171
|
+
|
|
172
|
+
const resolved = resolveGuidance(filePath, ctx.cwd);
|
|
173
|
+
if (resolved.length === 0) return;
|
|
174
|
+
|
|
175
|
+
const newFiles = resolved.filter((g) => !injectedGuidance.has(g.relativePath));
|
|
176
|
+
if (newFiles.length === 0) return;
|
|
177
|
+
|
|
178
|
+
// Mark before sendMessage — idempotence > reliability.
|
|
179
|
+
for (const g of newFiles) {
|
|
180
|
+
injectedGuidance.add(g.relativePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const contextParts = newFiles.map(
|
|
184
|
+
(g) => `## Project Guidance: ${formatLabel(g)}\n\n${g.content}`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
pi.sendMessage({
|
|
188
|
+
customType: "rpiv-guidance",
|
|
189
|
+
content: contextParts.join("\n\n---\n\n"),
|
|
190
|
+
display: false,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Format a guidance file's heading label.
|
|
196
|
+
* extensions/rpiv-core/AGENTS.md → "extensions/rpiv-core (AGENTS.md)"
|
|
197
|
+
* scripts/CLAUDE.md → "scripts (CLAUDE.md)"
|
|
198
|
+
* .rpiv/guidance/scripts/architecture.md → "scripts (architecture.md)"
|
|
199
|
+
* .rpiv/guidance/architecture.md → "root (architecture.md)"
|
|
200
|
+
*/
|
|
201
|
+
function formatLabel(g: GuidanceFile): string {
|
|
202
|
+
if (g.kind === "architecture") {
|
|
203
|
+
const stripped = g.relativePath.replace(/^\.rpiv\/guidance\//, "");
|
|
204
|
+
const sub = stripped === "architecture.md"
|
|
205
|
+
? ""
|
|
206
|
+
: stripped.replace(/\/architecture\.md$/, "");
|
|
207
|
+
return `${sub || "root"} (architecture.md)`;
|
|
208
|
+
}
|
|
209
|
+
const fileName = g.kind === "agents" ? "AGENTS.md" : "CLAUDE.md";
|
|
210
|
+
const idx = g.relativePath.lastIndexOf("/");
|
|
211
|
+
const sub = idx > 0 ? g.relativePath.slice(0, idx) : "";
|
|
212
|
+
return `${sub || "root"} (${fileName})`;
|
|
213
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rpiv-core — Orchestrator extension for the rpiv-pi package
|
|
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
|
|
11
|
+
*
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
|
|
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 {
|
|
29
|
+
hasPiSubagentsInstalled,
|
|
30
|
+
hasRpivAskUserQuestionInstalled,
|
|
31
|
+
hasRpivTodoInstalled,
|
|
32
|
+
hasRpivAdvisorInstalled,
|
|
33
|
+
hasRpivWebToolsInstalled,
|
|
34
|
+
} from "./package-checks.js";
|
|
35
|
+
|
|
36
|
+
export default function (pi: ExtensionAPI) {
|
|
37
|
+
// ── Session Start ──────────────────────────────────────────────────────
|
|
38
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
39
|
+
clearInjectionState();
|
|
40
|
+
injectRootGuidance(ctx.cwd, pi);
|
|
41
|
+
|
|
42
|
+
// Scaffold thoughts/ directory structure (artifact chain)
|
|
43
|
+
const dirs = [
|
|
44
|
+
"thoughts/shared/research",
|
|
45
|
+
"thoughts/shared/questions",
|
|
46
|
+
"thoughts/shared/designs",
|
|
47
|
+
"thoughts/shared/plans",
|
|
48
|
+
"thoughts/shared/handoffs",
|
|
49
|
+
];
|
|
50
|
+
for (const dir of dirs) {
|
|
51
|
+
mkdirSync(join(ctx.cwd, dir), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Inject git context once into the transcript
|
|
55
|
+
const gitMsg = await takeGitContextIfChanged(pi);
|
|
56
|
+
if (gitMsg) {
|
|
57
|
+
pi.sendMessage({ customType: "rpiv-git-context", content: gitMsg, display: false });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Sync bundled agents into <cwd>/.pi/agents/
|
|
61
|
+
// Detect-only mode: adds new files, detects drift, does NOT overwrite or remove.
|
|
62
|
+
const agentResult = syncBundledAgents(ctx.cwd, false);
|
|
63
|
+
if (ctx.hasUI) {
|
|
64
|
+
if (agentResult.added.length > 0) {
|
|
65
|
+
ctx.ui.notify(
|
|
66
|
+
`Copied ${agentResult.added.length} rpiv-pi agent(s) to .pi/agents/`,
|
|
67
|
+
"info",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const driftCount = agentResult.pendingUpdate.length + agentResult.pendingRemove.length;
|
|
71
|
+
if (driftCount > 0) {
|
|
72
|
+
const parts: string[] = [];
|
|
73
|
+
if (agentResult.pendingUpdate.length > 0) {
|
|
74
|
+
parts.push(`${agentResult.pendingUpdate.length} outdated`);
|
|
75
|
+
}
|
|
76
|
+
if (agentResult.pendingRemove.length > 0) {
|
|
77
|
+
parts.push(`${agentResult.pendingRemove.length} removed from bundle`);
|
|
78
|
+
}
|
|
79
|
+
ctx.ui.notify(
|
|
80
|
+
`${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`,
|
|
81
|
+
"info",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Aggregated warning for any missing sibling plugins
|
|
87
|
+
if (ctx.hasUI) {
|
|
88
|
+
const missing: string[] = [];
|
|
89
|
+
if (!hasPiSubagentsInstalled()) missing.push("@tintinweb/pi-subagents");
|
|
90
|
+
if (!hasRpivAskUserQuestionInstalled()) missing.push("@juicesharp/rpiv-ask-user-question");
|
|
91
|
+
if (!hasRpivTodoInstalled()) missing.push("@juicesharp/rpiv-todo");
|
|
92
|
+
if (!hasRpivAdvisorInstalled()) missing.push("@juicesharp/rpiv-advisor");
|
|
93
|
+
if (!hasRpivWebToolsInstalled()) missing.push("@juicesharp/rpiv-web-tools");
|
|
94
|
+
if (missing.length > 0) {
|
|
95
|
+
ctx.ui.notify(
|
|
96
|
+
`rpiv-pi requires ${missing.length} sibling extension(s): ${missing.join(", ")}. Run /rpiv-setup to install them.`,
|
|
97
|
+
"warning",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── Session Compact — drop injection state, re-inject root guidance ────
|
|
104
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
105
|
+
clearInjectionState();
|
|
106
|
+
clearGitContextCache();
|
|
107
|
+
resetInjectedMarker();
|
|
108
|
+
injectRootGuidance(ctx.cwd, pi);
|
|
109
|
+
const gitMsg = await takeGitContextIfChanged(pi);
|
|
110
|
+
if (gitMsg) {
|
|
111
|
+
pi.sendMessage({ customType: "rpiv-git-context", content: gitMsg, display: false });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── Session Shutdown ───────────────────────────────────────────────────
|
|
116
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
117
|
+
clearInjectionState();
|
|
118
|
+
clearGitContextCache();
|
|
119
|
+
resetInjectedMarker();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── Guidance Injection + Git Cache Invalidation ────────────────────────
|
|
123
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
124
|
+
handleToolCallGuidance(event, ctx, pi);
|
|
125
|
+
if (isToolCallEventType("bash", event) && isGitMutatingCommand(event.input.command)) {
|
|
126
|
+
clearGitContextCache();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── Git Context Injection (only when cache diverges from transcript) ───
|
|
131
|
+
pi.on("before_agent_start", async (_event, _ctx) => {
|
|
132
|
+
const content = await takeGitContextIfChanged(pi);
|
|
133
|
+
if (!content) return;
|
|
134
|
+
return {
|
|
135
|
+
message: { customType: "rpiv-git-context", content, display: false },
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── /rpiv-update-agents Command ────────────────────────────────────────
|
|
140
|
+
pi.registerCommand("rpiv-update-agents", {
|
|
141
|
+
description: "Sync rpiv-pi bundled agents into .pi/agents/: add new, update changed, remove stale",
|
|
142
|
+
handler: async (_args, ctx) => {
|
|
143
|
+
const result = syncBundledAgents(ctx.cwd, true);
|
|
144
|
+
if (!ctx.hasUI) return;
|
|
145
|
+
|
|
146
|
+
const totalSynced = result.added.length + result.updated.length + result.removed.length;
|
|
147
|
+
if (totalSynced === 0 && result.errors.length === 0) {
|
|
148
|
+
ctx.ui.notify("All agents already up-to-date.", "info");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parts: string[] = [];
|
|
153
|
+
if (result.added.length > 0) parts.push(`${result.added.length} added`);
|
|
154
|
+
if (result.updated.length > 0) parts.push(`${result.updated.length} updated`);
|
|
155
|
+
if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);
|
|
156
|
+
|
|
157
|
+
const summary = parts.length > 0
|
|
158
|
+
? `Synced agents: ${parts.join(", ")}.`
|
|
159
|
+
: "No changes needed.";
|
|
160
|
+
|
|
161
|
+
if (result.errors.length > 0) {
|
|
162
|
+
ctx.ui.notify(
|
|
163
|
+
`${summary} ${result.errors.length} error(s): ${result.errors.map((e) => e.message).join("; ")}`,
|
|
164
|
+
"warning",
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
ctx.ui.notify(summary, "info");
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── /rpiv-setup Command ────────────────────────────────────────────────
|
|
173
|
+
pi.registerCommand("rpiv-setup", {
|
|
174
|
+
description: "Install rpiv-pi's sibling extension plugins",
|
|
175
|
+
handler: async (_args, ctx) => {
|
|
176
|
+
if (!ctx.hasUI) {
|
|
177
|
+
ctx.ui.notify("/rpiv-setup requires interactive mode", "error");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const missing: Array<{ pkg: string; reason: string }> = [];
|
|
182
|
+
if (!hasPiSubagentsInstalled()) {
|
|
183
|
+
missing.push({
|
|
184
|
+
pkg: "npm:@tintinweb/pi-subagents",
|
|
185
|
+
reason: "required — provides Agent / get_subagent_result / steer_subagent tools",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (!hasRpivAskUserQuestionInstalled()) {
|
|
189
|
+
missing.push({
|
|
190
|
+
pkg: "npm:@juicesharp/rpiv-ask-user-question",
|
|
191
|
+
reason: "required — provides the ask_user_question tool",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (!hasRpivTodoInstalled()) {
|
|
195
|
+
missing.push({
|
|
196
|
+
pkg: "npm:@juicesharp/rpiv-todo",
|
|
197
|
+
reason: "required — provides the todo tool + /todos command + overlay widget",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (!hasRpivAdvisorInstalled()) {
|
|
201
|
+
missing.push({
|
|
202
|
+
pkg: "npm:@juicesharp/rpiv-advisor",
|
|
203
|
+
reason: "required — provides the advisor tool + /advisor command",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (!hasRpivWebToolsInstalled()) {
|
|
207
|
+
missing.push({
|
|
208
|
+
pkg: "npm:@juicesharp/rpiv-web-tools",
|
|
209
|
+
reason: "required — provides web_search + web_fetch tools + /web-search-config",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (missing.length === 0) {
|
|
214
|
+
ctx.ui.notify(
|
|
215
|
+
"All rpiv-pi sibling dependencies already installed.",
|
|
216
|
+
"info",
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const lines = [
|
|
222
|
+
"rpiv-pi will install the following Pi packages via `pi install`:",
|
|
223
|
+
"",
|
|
224
|
+
...missing.map((m) => ` • ${m.pkg} (${m.reason})`),
|
|
225
|
+
"",
|
|
226
|
+
"Each install is a separate `pi install <pkg>` invocation. Your",
|
|
227
|
+
"~/.pi/agent/settings.json will be updated. Proceed?",
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const confirmed = await ctx.ui.confirm("Install rpiv-pi dependencies?", lines.join("\n"));
|
|
231
|
+
if (!confirmed) {
|
|
232
|
+
ctx.ui.notify("/rpiv-setup cancelled", "info");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const succeeded: string[] = [];
|
|
237
|
+
const failed: Array<{ pkg: string; error: string }> = [];
|
|
238
|
+
for (const { pkg } of missing) {
|
|
239
|
+
ctx.ui.notify(`Installing ${pkg}…`, "info");
|
|
240
|
+
try {
|
|
241
|
+
const result = await pi.exec("pi", ["install", pkg], { timeout: 120_000 });
|
|
242
|
+
if (result.code === 0) {
|
|
243
|
+
succeeded.push(pkg);
|
|
244
|
+
} else {
|
|
245
|
+
failed.push({
|
|
246
|
+
pkg,
|
|
247
|
+
error: (result.stderr || result.stdout || `exit ${result.code}`).trim().slice(0, 300),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
failed.push({
|
|
252
|
+
pkg,
|
|
253
|
+
error: err instanceof Error ? err.message : String(err),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const report: string[] = [];
|
|
259
|
+
if (succeeded.length > 0) {
|
|
260
|
+
report.push(`✓ Installed: ${succeeded.join(", ")}`);
|
|
261
|
+
}
|
|
262
|
+
if (failed.length > 0) {
|
|
263
|
+
report.push(`✗ Failed:`);
|
|
264
|
+
for (const { pkg, error } of failed) {
|
|
265
|
+
report.push(` ${pkg}: ${error}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (succeeded.length > 0) {
|
|
269
|
+
report.push("");
|
|
270
|
+
report.push("Restart your Pi session to load the newly-installed extensions.");
|
|
271
|
+
}
|
|
272
|
+
ctx.ui.notify(report.join("\n"), failed.length > 0 ? "warning" : "info");
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package presence checks — detects whether sibling pi packages are installed.
|
|
3
|
+
*
|
|
4
|
+
* Pure utility. No ExtensionAPI interactions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Paths
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const PI_AGENT_SETTINGS = join(homedir(), ".pi", "agent", "settings.json");
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Package Detection
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export function readInstalledPackages(): string[] {
|
|
22
|
+
if (!existsSync(PI_AGENT_SETTINGS)) return [];
|
|
23
|
+
try {
|
|
24
|
+
const raw = readFileSync(PI_AGENT_SETTINGS, "utf-8");
|
|
25
|
+
const settings = JSON.parse(raw) as { packages?: unknown };
|
|
26
|
+
if (!Array.isArray(settings.packages)) return [];
|
|
27
|
+
return settings.packages.filter((e): e is string => typeof e === "string");
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
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));
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juicesharp/rpiv-pi",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Skill-based development workflow for Pi — research, design, plan, implement, review",
|
|
5
|
+
"keywords": ["pi-package", "pi-extension", "rpiv", "skills", "workflow"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "juicesharp",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/juicesharp/rpiv-pi.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/juicesharp/rpiv-pi#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/juicesharp/rpiv-pi/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"files": ["extensions/", "skills/", "agents/", "scripts/", "README.md"],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": ["./extensions"],
|
|
23
|
+
"skills": ["./skills"]
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@mariozechner/pi-ai": "*",
|
|
27
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
28
|
+
"@mariozechner/pi-tui": "*",
|
|
29
|
+
"@sinclair/typebox": "*",
|
|
30
|
+
"@tintinweb/pi-subagents": "*",
|
|
31
|
+
"@juicesharp/rpiv-ask-user-question": "*",
|
|
32
|
+
"@juicesharp/rpiv-todo": "*",
|
|
33
|
+
"@juicesharp/rpiv-advisor": "*",
|
|
34
|
+
"@juicesharp/rpiv-web-tools": "*"
|
|
35
|
+
}
|
|
36
|
+
}
|