pi-profiles-manager 1.0.1 → 1.1.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.
- package/LICENSE +21 -0
- package/README.md +66 -62
- package/index.ts +1 -352
- package/package.json +52 -19
- package/src/commands.test.ts +289 -0
- package/src/commands.ts +115 -0
- package/src/config.test.ts +251 -0
- package/src/config.ts +264 -0
- package/src/constants.ts +5 -0
- package/src/extension.test.ts +750 -0
- package/src/extension.ts +568 -0
- package/src/profile-manager.test.ts +274 -0
- package/src/profile-manager.ts +171 -0
- package/src/session-state.test.ts +331 -0
- package/src/session-state.ts +138 -0
- package/src/sync.test.ts +89 -0
- package/src/sync.ts +68 -0
- package/src/types.ts +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 javinnav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,96 +1,100 @@
|
|
|
1
|
-
# pi-profiles-manager
|
|
1
|
+
# pi-profiles-manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`pi-profiles-manager` is a [Pi Coding Agent](https://github.com/badlogic/pi-mono) extension for saving, selecting, editing, importing, and exporting SDD model profiles. A profile can set an orchestrator model and thinking level, plus routes for managed subagents.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Prerequisites:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- Pi Coding Agent
|
|
10
|
+
- Node.js 22.19.0 or later (for development)
|
|
10
11
|
|
|
11
|
-
|
|
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.
|
|
12
|
+
Install the published package:
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
```bash
|
|
15
|
+
pi install npm:pi-profiles-manager
|
|
16
|
+
```
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
Reload an active Pi session with `/reload` if needed.
|
|
24
19
|
|
|
25
|
-
##
|
|
20
|
+
## Pi command: `/profiles`
|
|
26
21
|
|
|
27
|
-
|
|
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
|
-
```
|
|
22
|
+
Pi registers one command: `/profiles`. Its actions are arguments to that command, not separate Pi commands such as `/profiles:sync`.
|
|
37
23
|
|
|
38
24
|
```text
|
|
39
|
-
|
|
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
|
|
25
|
+
/profiles
|
|
45
26
|
```
|
|
46
27
|
|
|
47
|
-
|
|
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
|
-
```
|
|
28
|
+
Open the interactive manager with no arguments. When entering an argument, Pi completion suggests supported actions; after `use` or `save`, it suggests matching saved profile names. Profile names may contain spaces.
|
|
55
29
|
|
|
56
|
-
|
|
30
|
+
| Command | Action |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `/profiles` | Open the interactive profile manager. |
|
|
33
|
+
| `/profiles list` | List saved profiles. |
|
|
34
|
+
| `/profiles status` | Show the active profile for the current session. |
|
|
35
|
+
| `/profiles use <name>` | Activate a saved profile. |
|
|
36
|
+
| `/profiles save <name>` | Save the current configuration as a profile. |
|
|
37
|
+
| `/profiles next` | Activate the next profile in the configured cycle. |
|
|
38
|
+
| `/profiles off` | Disable the active profile for the current session. |
|
|
39
|
+
| `/profiles sync` | Discover managed `agents/*.md` files and reconcile every saved profile's subagent entries. It does not create Pi commands. |
|
|
57
40
|
|
|
58
|
-
|
|
41
|
+
The default shortcut, `ctrl+shift+p`, activates the next profile in the configured cycle.
|
|
59
42
|
|
|
60
|
-
|
|
43
|
+
## Where data lives
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
45
|
+
These locations have different purposes:
|
|
46
|
+
|
|
47
|
+
### Saved profile persistence
|
|
65
48
|
|
|
66
|
-
|
|
49
|
+
Saved profiles, the default profile, cycle, and shortcut preference are persisted in:
|
|
67
50
|
|
|
68
51
|
```text
|
|
69
|
-
/
|
|
52
|
+
~/.pi/agent/pi-profiles/config.json
|
|
70
53
|
```
|
|
71
54
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## 🚀 Usage
|
|
55
|
+
The extension creates this file and its parent directory when it saves a profile.
|
|
75
56
|
|
|
76
|
-
|
|
57
|
+
### Managed subagent definitions
|
|
77
58
|
|
|
78
|
-
|
|
59
|
+
Pi agent markdown files live in the Pi agent directory:
|
|
79
60
|
|
|
80
61
|
```text
|
|
81
|
-
/
|
|
62
|
+
~/.pi/agent/agents/*.md
|
|
82
63
|
```
|
|
83
64
|
|
|
84
|
-
|
|
65
|
+
Run `/profiles sync` after adding or removing these files. Sync discovers their names and reconciles each saved profile: new managed agents receive a profile entry, and entries for removed markdown files are removed. Sync updates profile data; it does not register additional Pi slash commands.
|
|
66
|
+
|
|
67
|
+
### Applied subagent routes
|
|
68
|
+
|
|
69
|
+
When a profile is activated, its subagent routes are applied to Pi's global agent-route configuration:
|
|
85
70
|
|
|
86
71
|
```text
|
|
87
|
-
/
|
|
72
|
+
~/.pi/agent/subagents.json
|
|
88
73
|
```
|
|
89
74
|
|
|
90
|
-
|
|
75
|
+
This is separate from `pi-profiles/config.json`: the former is the active routing configuration Pi uses, while the latter stores the profiles that can be selected later.
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
- Create, activate, edit, and delete profiles.
|
|
80
|
+
- Import and export versioned profile strings.
|
|
81
|
+
- Set model and thinking routes for the orchestrator and managed subagents.
|
|
82
|
+
- Mark a default profile for session startup and view active/default status in the UI.
|
|
83
|
+
- Cycle profiles with a keyboard shortcut.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
Clone this repository into your Pi extensions directory to develop it locally:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/javinnav/pi-profiles-manager.git ~/.pi/agent/extensions/profiles-manager
|
|
91
|
+
cd ~/.pi/agent/extensions/profiles-manager
|
|
92
|
+
npm ci
|
|
93
|
+
npm test
|
|
94
|
+
npm run typecheck
|
|
95
|
+
npm run package-check
|
|
96
|
+
```
|
|
91
97
|
|
|
92
|
-
##
|
|
98
|
+
## License
|
|
93
99
|
|
|
94
|
-
|
|
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).
|
|
100
|
+
MIT © javinnav
|
package/index.ts
CHANGED
|
@@ -1,352 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { default } from "./src/extension.js";
|
package/package.json
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
2
|
+
"name": "pi-profiles-manager",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Interactive SDD model profile management built natively for the Pi Coding Agent.",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "npm run build -- --watch",
|
|
13
|
+
"build": "tsup index.ts --format esm --clean",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"lint": "biome check .",
|
|
18
|
+
"format": "biome format --write .",
|
|
19
|
+
"package-check": "node -e \"const pkg = require('./package.json'); if (!pkg.pi || !pkg.pi.extensions) { console.error('Missing pi.extensions in package.json'); process.exit(1); }\""
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.19.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"pi-agent-extension",
|
|
26
|
+
"pi-package"
|
|
27
|
+
],
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"author": "javinnav",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"homepage": "https://github.com/javinnav/pi-profiles-manager#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/javinnav/pi-profiles-manager/issues"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/javinnav/pi-profiles-manager.git"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
45
|
+
"@earendil-works/pi-tui": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "^2.5.11",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
50
|
+
"typescript": "^7.0.2",
|
|
51
|
+
"vitest": "^4.1.11"
|
|
52
|
+
}
|
|
53
|
+
}
|