pi-profiles-manager 1.0.1

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.
Files changed (3) hide show
  1. package/README.md +96 -0
  2. package/index.ts +352 -0
  3. package/package.json +20 -0
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # pi-profiles-manager πŸš€
2
+
3
+ Interactive SDD model profile management built natively for the [Pi Coding Agent](https://github.com/earendil-works/pi-mono).
4
+
5
+ Manage your Gentle AI `models.json` configurations dynamically from a beautiful Terminal User Interface (TUI) without ever leaving your session.
6
+
7
+ ---
8
+
9
+ ## 🌟 Features
10
+
11
+ ### 1. Unified Profile Management
12
+ Manage all your SDD (Spec-Driven Development) profiles via a rich interface:
13
+ - **Create:** Read your current `models.json` setup and save it as a new profile instantly.
14
+ - **Activate:** Apply a saved profile into the global runtime config. Changes affect the executing session immediately with no restarts required.
15
+ - **Edit Agent Nodes:** Individually adjust `model` strings and `thinking` tiers (`low`, `medium`, `high`, `xhigh`, `max`) for any agent (`sdd-*`, `jd-*`, `review-*`, etc.).
16
+ - **Scaffold Empty Agents:** Add specific overrides for single agents right from the UI.
17
+ - **Delete:** Remove outdated tiers and profiles to keep your workspace tidy.
18
+
19
+ ### 2. Native Pi Integration
20
+ - Fully built on top of `@earendil-works/pi-tui`.
21
+ - Works flawlessly with overlay navigation, meaning your terminal output isn't erased when interacting with profiles.
22
+
23
+ ---
24
+
25
+ ## πŸ“Έ Interface Preview (Compact Format)
26
+
27
+ ```text
28
+ Profiles Manager
29
+ ──────────────────────────────────────────────────
30
+ > ✨ Create New Profile from Current Config
31
+ FREE
32
+ antigravity
33
+ deus
34
+
35
+ ↑↓ navigate β€’ enter select β€’ esc close
36
+ ```
37
+
38
+ ```text
39
+ Action for 'antigravity'
40
+ ──────────────────────────────────────────────────
41
+ > β–Ά Activate Apply this profile to models.json
42
+ ✎ Edit Modify agents in this profile
43
+ βœ– Delete Remove this profile
44
+ ← Back Return to profile list
45
+ ```
46
+
47
+ ```text
48
+ Edit Profile 'antigravity'
49
+ ──────────────────────────────────────────────────
50
+ > orchestrator omni/antigravity/gemini-pro-agent (medium)
51
+ jd-fix-agent omniroute/gemini-flash-low (high)
52
+ ...
53
+ βž• Add Subagent Add a specific configuration for a subagent
54
+ ```
55
+
56
+ ---
57
+
58
+ ## πŸ›  Installation
59
+
60
+ Clone this repository directly into your Pi extensions directory:
61
+
62
+ ```bash
63
+ git clone https://github.com/javinnav/pi-profiles-manager.git ~/.pi/agent/extensions/profiles-manager
64
+ ```
65
+
66
+ Apply the changes immediately by reloading the active session:
67
+
68
+ ```text
69
+ /reload
70
+ ```
71
+
72
+ ---
73
+
74
+ ## πŸš€ Usage
75
+
76
+ *Ensure the active Pi Session recognizes the newly linked extension context.*
77
+
78
+ Open the Profiles Manager by triggering its slash command:
79
+
80
+ ```text
81
+ /profiles
82
+ ```
83
+
84
+ Alternatively, to quickly snapshot your current session's `models.json` without routing through the UI:
85
+
86
+ ```text
87
+ /profiles save <profile-name>
88
+ ```
89
+
90
+ ---
91
+
92
+ ## πŸ’– Credits and Acknowledgments
93
+
94
+ - **Inspiration:** Developed mirroring the clean extension and TUI architecture provided by [opencode-sdd-engram-manage](https://github.com/j0k3r-dev-rgl/sdd-engram-plugin) from @j0k3r-dev-rgl.
95
+ - **Dependencies:** Built leveraging the core APIs of the [Pi Coding Agent framework](https://github.com/earendil-works/pi-mono).
96
+ - **Ecosystem:** Powered by [Gentle AI (OpenCode)](https://github.com/Gentleman-Programming/gentle-ai).
package/index.ts ADDED
@@ -0,0 +1,352 @@
1
+ import * as fs from "fs/promises";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import type { ExtensionAPI, TUIContext } from "@earendil-works/pi-coding-agent";
5
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
6
+ import { Container, SelectList, Text, Input } from "@earendil-works/pi-tui";
7
+
8
+ const GENTLE_DIR = path.join(os.homedir(), ".pi", "gentle-ai");
9
+ const MODELS_PATH = path.join(GENTLE_DIR, "models.json");
10
+ const PROFILES_PATH = path.join(GENTLE_DIR, "sdd-profiles-manager.json");
11
+
12
+ interface Profile {
13
+ name: string;
14
+ orchestrator: { model: string; thinking: string };
15
+ agents: Record<string, { model: string; thinking: string }>;
16
+ }
17
+
18
+ async function readJson(fp: string): Promise<any> {
19
+ try {
20
+ const data = await fs.readFile(fp, "utf-8");
21
+ return JSON.parse(data);
22
+ } catch (e: any) {
23
+ if (e.code === "ENOENT") return {};
24
+ throw e;
25
+ }
26
+ }
27
+
28
+ async function writeJson(fp: string, data: any): Promise<void> {
29
+ await fs.mkdir(path.dirname(fp), { recursive: true });
30
+ await fs.writeFile(fp, JSON.stringify(data, null, 2));
31
+ }
32
+
33
+ // Helper to ask user for a string
34
+ async function promptInput(ctx: any, title: string, initialValue: string = ""): Promise<string | null> {
35
+ return await ctx.ui.custom<string | null>((tui: any, theme: any, _kb: any, done: any) => {
36
+ const container = new Container() as any;
37
+ container.focused = true; // Make container focusable
38
+
39
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
40
+ container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
41
+
42
+ const input = new Input();
43
+ input.setValue(initialValue);
44
+ input.onSubmit = (val) => done(val.trim());
45
+ input.onEscape = () => done(null);
46
+ input.focused = true;
47
+
48
+ // Wire up focus propagation
49
+ container.handleInput = (data: string) => {
50
+ input.handleInput(data);
51
+ tui.requestRender();
52
+ };
53
+
54
+ container.addChild(input);
55
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
56
+
57
+ return {
58
+ render: (w: number) => container.render(w),
59
+ invalidate: () => container.invalidate(),
60
+ handleInput: (data: string) => container.handleInput(data),
61
+ };
62
+ }, { overlay: true });
63
+ }
64
+
65
+ export default function (pi: ExtensionAPI) {
66
+ pi.registerCommand("profiles", {
67
+ description: "Manage SDD model profiles",
68
+ handler: async (args, ctx) => {
69
+ // Subcommand for save
70
+ if (args[0] === "save") {
71
+ const profileName = args[1];
72
+ if (!profileName) {
73
+ ctx.ui.notify("Error: Provide a profile name (e.g. /profiles save my-profile)", "error");
74
+ return;
75
+ }
76
+
77
+ const models = await readJson(MODELS_PATH);
78
+ const orchestrator = models["gentle-orchestrator"] || { model: "", thinking: "" };
79
+ const agents: any = {};
80
+ for (const key of Object.keys(models)) {
81
+ if (key !== "gentle-orchestrator") {
82
+ agents[key] = models[key];
83
+ }
84
+ }
85
+ const profiles = await readJson(PROFILES_PATH);
86
+ profiles[profileName] = { name: profileName, orchestrator, agents };
87
+ await writeJson(PROFILES_PATH, profiles);
88
+ ctx.ui.notify(`Profile '${profileName}' saved`, "success");
89
+ return;
90
+ }
91
+
92
+ while (true) {
93
+ let profiles: Record<string, Profile> = await readJson(PROFILES_PATH);
94
+ const profileNames = Object.keys(profiles);
95
+
96
+ const items = [
97
+ { value: "__CREATE__", label: "✨ Create New Profile from Current Config", description: "Saves models.json into a new profile" },
98
+ ...profileNames.map((name) => ({
99
+ value: name,
100
+ label: name,
101
+ description: `Orchestrator: ${profiles[name].orchestrator?.model || "none"}`,
102
+ }))
103
+ ];
104
+
105
+ // Pick Profile
106
+ const selectedProfile = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
107
+ const container = new Container();
108
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
109
+ container.addChild(new Text(theme.fg("accent", theme.bold("Profiles Manager")), 1, 0));
110
+
111
+ const list = new SelectList(items, Math.min(items.length, 10), {
112
+ selectedPrefix: (t) => theme.fg("accent", t),
113
+ selectedText: (t) => theme.fg("accent", t),
114
+ description: (t) => theme.fg("muted", t),
115
+ scrollInfo: (t) => theme.fg("dim", t),
116
+ noMatch: (t) => theme.fg("warning", t),
117
+ });
118
+ list.onSelect = (item) => done(item.value);
119
+ list.onCancel = () => done(null);
120
+ container.addChild(list);
121
+
122
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate β€’ enter select β€’ esc close"), 1, 0));
123
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
124
+
125
+ return {
126
+ render: (w) => container.render(w),
127
+ invalidate: () => container.invalidate(),
128
+ handleInput: (data) => { list.handleInput(data); tui.requestRender(); },
129
+ };
130
+ }, { overlay: true });
131
+
132
+ if (!selectedProfile) break; // Finished and exit loop
133
+
134
+ if (selectedProfile === "__CREATE__") {
135
+ const newName = await promptInput(ctx, "Enter new profile name:");
136
+ if (newName) {
137
+ const models = await readJson(MODELS_PATH);
138
+ const orchestrator = models["gentle-orchestrator"] || { model: "", thinking: "" };
139
+ const agents: any = {};
140
+ for (const key of Object.keys(models)) {
141
+ if (key !== "gentle-orchestrator") {
142
+ agents[key] = models[key];
143
+ }
144
+ }
145
+ profiles[newName] = { name: newName, orchestrator, agents };
146
+ await writeJson(PROFILES_PATH, profiles);
147
+ ctx.ui.notify(`Created profile '${newName}'`, "success");
148
+ }
149
+ continue;
150
+ }
151
+
152
+ // Action menu for existing profile
153
+ while (true) {
154
+ const actionItems = [
155
+ { value: "activate", label: "β–Ά Activate", description: "Apply this profile to models.json" },
156
+ { value: "edit", label: "✎ Edit", description: "Modify agents in this profile" },
157
+ { value: "delete", label: "βœ– Delete", description: "Remove this profile" },
158
+ { value: "back", label: "← Back", description: "Return to profile list" },
159
+ ];
160
+
161
+ const selectedAction = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
162
+ const container = new Container();
163
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
164
+ container.addChild(new Text(theme.fg("accent", theme.bold(`Profile: '${selectedProfile}'`)), 1, 0));
165
+
166
+ const list = new SelectList(actionItems, 4, {
167
+ selectedPrefix: (t) => theme.fg("accent", t),
168
+ selectedText: (t) => theme.fg("accent", t),
169
+ description: (t) => theme.fg("muted", t),
170
+ scrollInfo: (t) => theme.fg("dim", t),
171
+ noMatch: (t) => theme.fg("warning", t),
172
+ });
173
+ list.onSelect = (item) => done(item.value);
174
+ list.onCancel = () => done(null);
175
+ container.addChild(list);
176
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
177
+
178
+ return {
179
+ render: (w) => container.render(w),
180
+ invalidate: () => container.invalidate(),
181
+ handleInput: (data) => { list.handleInput(data); tui.requestRender(); },
182
+ };
183
+ }, { overlay: true });
184
+
185
+ if (!selectedAction || selectedAction === "back") {
186
+ break; // Back to profile list
187
+ }
188
+
189
+ if (selectedAction === "activate") {
190
+ const profile = profiles[selectedProfile];
191
+ const newModels: any = {};
192
+ if (profile.orchestrator) {
193
+ newModels["gentle-orchestrator"] = profile.orchestrator;
194
+ }
195
+ if (profile.agents) {
196
+ for (const key of Object.keys(profile.agents)) {
197
+ newModels[key] = profile.agents[key];
198
+ }
199
+ }
200
+ await writeJson(MODELS_PATH, newModels);
201
+ ctx.ui.notify(`Activated profile '${selectedProfile}'. Run /reload if needed.`, "success");
202
+ break; // Back to profile list after activating
203
+ }
204
+
205
+ if (selectedAction === "delete") {
206
+ delete profiles[selectedProfile];
207
+ await writeJson(PROFILES_PATH, profiles);
208
+ ctx.ui.notify(`Deleted profile '${selectedProfile}'.`, "success");
209
+ break; // Back to profile list after deleting
210
+ }
211
+
212
+ if (selectedAction === "edit") {
213
+ // Edit Flow
214
+ while (true) {
215
+ const currentProfile = profiles[selectedProfile];
216
+ const agentKeys = ["orchestrator", ...Object.keys(currentProfile.agents || {})];
217
+
218
+ const editItems = agentKeys.map(k => {
219
+ const conf = k === "orchestrator" ? currentProfile.orchestrator : currentProfile.agents[k];
220
+ return {
221
+ value: k,
222
+ label: k,
223
+ description: `${conf?.model || "none"} (${conf?.thinking || "low"})`
224
+ };
225
+ });
226
+ editItems.push({ value: "__ADD__", label: "βž• Add Subagent", description: "Add a specific configuration for a subagent" });
227
+ editItems.push({ value: "back", label: "← Back", description: "Return to profile menu" });
228
+
229
+ const pickedAgent = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
230
+ const container = new Container();
231
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
232
+ container.addChild(new Text(theme.fg("accent", theme.bold(`Edit Profile '${selectedProfile}'`)), 1, 0));
233
+
234
+ const list = new SelectList(editItems, Math.min(editItems.length, 10), {
235
+ selectedPrefix: (t) => theme.fg("accent", t),
236
+ selectedText: (t) => theme.fg("accent", t),
237
+ description: (t) => theme.fg("muted", t),
238
+ scrollInfo: (t) => theme.fg("dim", t),
239
+ noMatch: (t) => theme.fg("warning", t),
240
+ });
241
+ list.onSelect = (item) => done(item.value);
242
+ list.onCancel = () => done(null);
243
+ container.addChild(list);
244
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
245
+
246
+ return {
247
+ render: (w) => container.render(w),
248
+ invalidate: () => container.invalidate(),
249
+ handleInput: (data) => { list.handleInput(data); tui.requestRender(); },
250
+ };
251
+ }, { overlay: true });
252
+
253
+ if (!pickedAgent || pickedAgent === "back") break;
254
+
255
+ let agentName = pickedAgent;
256
+ // If adding, ask for agent name
257
+ if (agentName === "__ADD__") {
258
+ const newName = await promptInput(ctx, "Subagent Name (e.g. sdd-apply):");
259
+ if (!newName) continue;
260
+ agentName = newName;
261
+ if (!currentProfile.agents) currentProfile.agents = {};
262
+ currentProfile.agents[agentName] = { model: "omni/antigravity/gemini-3.6-flash-low", thinking: "low" };
263
+ await writeJson(PROFILES_PATH, profiles);
264
+ }
265
+
266
+ // Edit Agent
267
+ while (true) {
268
+ const conf = agentName === "orchestrator" ? currentProfile.orchestrator : currentProfile.agents[agentName];
269
+
270
+ const modifierItems = [
271
+ { value: "model", label: "Modify Model", description: conf?.model || "none" },
272
+ { value: "thinking", label: "Modify Thinking", description: conf?.thinking || "low" },
273
+ { value: "delete", label: "βœ– Remove Agent from profile", description: "Delete this configuration" },
274
+ { value: "back", label: "← Back", description: "" },
275
+ ];
276
+
277
+ const pickedMod = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
278
+ const container = new Container();
279
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
280
+ container.addChild(new Text(theme.fg("accent", theme.bold(`Edit Agent '${agentName}'`)), 1, 0));
281
+ const list = new SelectList(modifierItems, 4, {
282
+ selectedPrefix: (t) => theme.fg("accent", t),
283
+ selectedText: (t) => theme.fg("accent", t),
284
+ description: (t) => theme.fg("muted", t),
285
+ scrollInfo: (t) => theme.fg("dim", t),
286
+ noMatch: (t) => theme.fg("warning", t),
287
+ });
288
+ list.onSelect = (item) => done(item.value);
289
+ list.onCancel = () => done(null);
290
+ container.addChild(list);
291
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
292
+ return {
293
+ render: (w) => container.render(w),
294
+ invalidate: () => container.invalidate(),
295
+ handleInput: (data) => { list.handleInput(data); tui.requestRender(); },
296
+ };
297
+ }, { overlay: true });
298
+
299
+ if (!pickedMod || pickedMod === "back") break;
300
+
301
+ if (pickedMod === "delete") {
302
+ if (agentName === "orchestrator") {
303
+ ctx.ui.notify("Cannot delete orchestrator", "error");
304
+ } else {
305
+ delete currentProfile.agents[agentName];
306
+ await writeJson(PROFILES_PATH, profiles);
307
+ ctx.ui.notify(`Removed ${agentName}`, "success");
308
+ break;
309
+ }
310
+ } else if (pickedMod === "model") {
311
+ const newModel = await promptInput(ctx, `Model for ${agentName}:`, conf.model);
312
+ if (newModel !== null) {
313
+ conf.model = newModel;
314
+ await writeJson(PROFILES_PATH, profiles);
315
+ }
316
+ } else if (pickedMod === "thinking") {
317
+ const thinkingLevels = ["low", "medium", "high", "xhigh", "max"].map(t => ({ value: t, label: t }));
318
+ const newThinking = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
319
+ const container = new Container();
320
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
321
+ container.addChild(new Text(theme.fg("accent", theme.bold(`Thinking for ${agentName}`)), 1, 0));
322
+ const list = new SelectList(thinkingLevels, 5, {
323
+ selectedPrefix: (t) => theme.fg("accent", t),
324
+ selectedText: (t) => theme.fg("accent", t),
325
+ description: (t) => theme.fg("muted", t),
326
+ scrollInfo: (t) => theme.fg("dim", t),
327
+ noMatch: (t) => theme.fg("warning", t),
328
+ });
329
+ list.onSelect = (item) => done(item.value);
330
+ list.onCancel = () => done(null);
331
+ container.addChild(list);
332
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
333
+ return {
334
+ render: (w) => container.render(w),
335
+ invalidate: () => container.invalidate(),
336
+ handleInput: (data) => { list.handleInput(data); tui.requestRender(); },
337
+ };
338
+ }, { overlay: true });
339
+
340
+ if (newThinking) {
341
+ conf.thinking = newThinking;
342
+ await writeJson(PROFILES_PATH, profiles);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ },
351
+ });
352
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "pi-profiles-manager",
3
+ "version": "1.0.1",
4
+ "description": "Interactive SDD model profile management built natively for the Pi Coding Agent.",
5
+ "main": "index.ts",
6
+ "type": "module",
7
+ "keywords": [
8
+ "pi-agent-extension",
9
+ "pi-package"
10
+ ],
11
+ "pi": {
12
+ "extensions": ["index.ts"]
13
+ },
14
+ "author": "javinnav",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/javinnav/pi-profiles-manager.git"
19
+ }
20
+ }