@dougbots/pi-agents 0.1.1 → 0.2.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/README.md CHANGED
@@ -49,6 +49,42 @@ Profile fields:
49
49
  - `excludeTools` — denylist of tool names (removed from available set)
50
50
  - `permissions.bash` — `{ allow?: string[], deny?: string[] }` with glob patterns
51
51
 
52
+ ### Runtime agent profiles
53
+
54
+ A runtime agent profile is a session-scoped overlay for role profiles. It can
55
+ change execution settings without changing the role's system prompt, tools, or
56
+ permission boundary. Define global profiles in `~/.pi/agent/agent-profiles.json`
57
+ and project overrides in `.pi/agent-profiles.json`:
58
+
59
+ ```json
60
+ {
61
+ "cloud": {
62
+ "description": "Temporary cloud fallbacks",
63
+ "agents": {
64
+ "explore": { "model": "sparky/deepseek-flash" },
65
+ "mule": { "model": "sparky/deepseek-flash" },
66
+ "reviewer": { "model": "sparky/deepseek-flash" }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ Only `model` and `thinkingLevel` may be overridden. Project profile entries
73
+ merge over global entries by profile and agent name. Malformed entries are
74
+ ignored and reported as a Pi warning. A selected profile is persisted in the Pi
75
+ session and restored by `/resume`.
76
+
77
+ `PI_AGENT_PROFILE=<name>` is an explicit launch-time override; `PROFILE=<name>`
78
+ is accepted as a convenience alias. Both take precedence over a saved session
79
+ selection. Explicit `--model` still takes precedence over an agent-profile's
80
+ model.
81
+
82
+ While a session has a profile active, the extension reflects it into
83
+ `process.env.PI_AGENT_PROFILE` (cleared on `/agent-profile none`). Child
84
+ processes inherit it, so tooling like `agent-model.sh <role> pi` resolves the
85
+ same model the session runs, and fresh dispatches from the session stay on the
86
+ active profile.
87
+
52
88
  ## Commands
53
89
 
54
90
  | Command | Description |
@@ -57,12 +93,16 @@ Profile fields:
57
93
  | `/agent none` | Deactivate agent, restore all tools |
58
94
  | `/agent` | List available agents |
59
95
  | `/agents` | Same as `/agent` |
96
+ | `/agent-profile <name>` | Select a session-scoped runtime agent profile |
97
+ | `/agent-profile none` | Clear the session runtime agent profile |
98
+ | `/agent-profile` | Show the active and available runtime agent profiles |
60
99
 
61
100
  ## Boot with agent
62
101
 
63
102
  ```bash
64
103
  PI_AGENT=reviewer pi
65
104
  PI_AGENT=jockey pi -c
105
+ PI_AGENT=explore PI_AGENT_PROFILE=cloud pi
66
106
  ```
67
107
 
68
108
  ## Avenor integration
@@ -0,0 +1,107 @@
1
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
2
+
3
+ /** Runtime-safe fields a named agent-profile may override on a base agent. */
4
+ export interface AgentProfileOverride {
5
+ model?: string;
6
+ thinkingLevel?: ThinkingLevel;
7
+ }
8
+
9
+ export interface AgentProfileSet {
10
+ description?: string;
11
+ agents?: Record<string, AgentProfileOverride>;
12
+ }
13
+
14
+ export interface AgentProfilesConfig {
15
+ [name: string]: AgentProfileSet;
16
+ }
17
+
18
+ const thinkingLevels = new Set<ThinkingLevel>([
19
+ "off", "minimal", "low", "medium", "high", "xhigh",
20
+ ]);
21
+
22
+ function isRecord(value: unknown): value is Record<string, unknown> {
23
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+
26
+ /** Discard unsupported fields from untrusted JSON config. */
27
+ export function executionOverride(value: unknown): AgentProfileOverride | undefined {
28
+ if (!isRecord(value)) return undefined;
29
+ const override: AgentProfileOverride = {};
30
+ if (typeof value.model === "string") override.model = value.model;
31
+ if (typeof value.thinkingLevel === "string" && thinkingLevels.has(value.thinkingLevel as ThinkingLevel)) {
32
+ override.thinkingLevel = value.thinkingLevel as ThinkingLevel;
33
+ }
34
+ return Object.keys(override).length ? override : undefined;
35
+ }
36
+
37
+ /**
38
+ * Merge profile config sources: project entries override global entries by
39
+ * profile and agent name. Invalid JSON shapes are ignored rather than making
40
+ * the extension fail to load.
41
+ */
42
+ export function mergeAgentProfilesConfigs(
43
+ configs: readonly unknown[],
44
+ onInvalid?: (message: string) => void,
45
+ ): AgentProfilesConfig {
46
+ const merged: AgentProfilesConfig = {};
47
+
48
+ for (const config of configs) {
49
+ if (!isRecord(config)) {
50
+ onInvalid?.("top-level value must be an object");
51
+ continue;
52
+ }
53
+
54
+ for (const [name, rawProfile] of Object.entries(config)) {
55
+ if (!isRecord(rawProfile)) {
56
+ onInvalid?.(`profile "${name}" must be an object`);
57
+ continue;
58
+ }
59
+ if (rawProfile.agents !== undefined && !isRecord(rawProfile.agents)) {
60
+ onInvalid?.(`profile "${name}" has a non-object agents value`);
61
+ }
62
+ if (rawProfile.description !== undefined && typeof rawProfile.description !== "string") {
63
+ onInvalid?.(`profile "${name}" has a non-string description`);
64
+ }
65
+
66
+ const current = Object.hasOwn(merged, name) ? merged[name] : undefined;
67
+ const rawAgents = isRecord(rawProfile.agents) ? rawProfile.agents : {};
68
+ const profile: AgentProfileSet = {
69
+ ...(typeof current?.description === "string" ? { description: current.description } : {}),
70
+ ...(typeof rawProfile.description === "string" ? { description: rawProfile.description } : {}),
71
+ agents: {
72
+ ...current?.agents,
73
+ ...rawAgents as Record<string, AgentProfileOverride>,
74
+ },
75
+ };
76
+
77
+ // defineProperty avoids the __proto__ setter on normal objects and makes
78
+ // profile-name lookups consistently own-property-only.
79
+ Object.defineProperty(merged, name, {
80
+ value: profile,
81
+ enumerable: true,
82
+ configurable: true,
83
+ writable: true,
84
+ });
85
+ }
86
+ }
87
+
88
+ return merged;
89
+ }
90
+
91
+ /** Return the latest persisted profile selection, including an explicit clear. */
92
+ export function findPersistedProfileName(entries: readonly unknown[]): string | undefined {
93
+ let profileName: string | undefined;
94
+
95
+ for (const entry of entries) {
96
+ if (!entry || typeof entry !== "object") continue;
97
+ const customEntry = entry as { type?: unknown; customType?: unknown; data?: unknown };
98
+ if (customEntry.type !== "custom" || customEntry.customType !== "pi-agents:profile") continue;
99
+ if (!customEntry.data || typeof customEntry.data !== "object") continue;
100
+
101
+ const name = (customEntry.data as { name?: unknown }).name;
102
+ if (typeof name === "string") profileName = name;
103
+ if (name === null) profileName = undefined;
104
+ }
105
+
106
+ return profileName;
107
+ }
@@ -2,6 +2,13 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
3
  import { readFileSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import {
6
+ executionOverride,
7
+ findPersistedProfileName,
8
+ mergeAgentProfilesConfigs,
9
+ type AgentProfilesConfig,
10
+ type ThinkingLevel,
11
+ } from "./agent-profile.ts";
5
12
  import { findCliModelOverride, parseModelSpecifier } from "./model-selection.ts";
6
13
 
7
14
  /* ------------------------------------------------------------------ */
@@ -17,7 +24,7 @@ interface AgentProfile {
17
24
  description?: string;
18
25
  model: string; // "provider/model-id"
19
26
  systemPrompt: string; // inline text or "file:/absolute/path/to/prompt.md"
20
- thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
27
+ thinkingLevel?: ThinkingLevel;
21
28
  tools?: string[]; // allowlist — if set, only these tools are callable
22
29
  excludeTools?: string[]; // denylist — removed from available tools
23
30
  permissions?: {
@@ -30,10 +37,10 @@ interface AgentsConfig {
30
37
  }
31
38
 
32
39
  /* ------------------------------------------------------------------ */
33
- /* Config loading — merges global (~/.pi/agent/agents.json) */
34
- /* with project (.pi/agents.json), project wins. */
40
+ /* Config loading */
35
41
  /* ------------------------------------------------------------------ */
36
42
 
43
+ /** Merge global ~/.pi/agent/agents.json with project .pi/agents.json. */
37
44
  function loadAgentsConfig(): AgentsConfig {
38
45
  const agentDir = getAgentDir();
39
46
  const globalPath = join(agentDir, "agents.json");
@@ -51,10 +58,40 @@ function loadAgentsConfig(): AgentsConfig {
51
58
  console.error(`[agents] Failed to parse ${label} agents.json (${path}):`, err);
52
59
  }
53
60
  }
54
- // Later entries override earlier — project wins over global
61
+ // Later entries override earlier — project wins over global.
55
62
  return configs.reduce<AgentsConfig>((merged, cfg) => ({ ...merged, ...cfg }), {});
56
63
  }
57
64
 
65
+ /**
66
+ * Load named runtime overlays. These can change only execution settings,
67
+ * leaving an agent's prompt, tools, and permission boundary authoritative.
68
+ */
69
+ function loadAgentProfilesConfig(): { profiles: AgentProfilesConfig; errors: string[] } {
70
+ const agentDir = getAgentDir();
71
+ const globalPath = join(agentDir, "agent-profiles.json");
72
+ const projectPath = join(process.cwd(), ".pi", "agent-profiles.json");
73
+
74
+ let profiles: AgentProfilesConfig = {};
75
+ const errors: string[] = [];
76
+ for (const [label, path] of [
77
+ ["global", globalPath],
78
+ ["project", projectPath],
79
+ ] as const) {
80
+ if (!existsSync(path)) continue;
81
+ try {
82
+ const config = JSON.parse(readFileSync(path, "utf8"));
83
+ profiles = mergeAgentProfilesConfigs([profiles, config], (message) => {
84
+ errors.push(`${label} ${path}: ${message}`);
85
+ });
86
+ } catch (err) {
87
+ const message = `Failed to parse ${label} agent-profiles.json (${path}): ${String(err)}`;
88
+ console.error(`[agents] ${message}`);
89
+ errors.push(message);
90
+ }
91
+ }
92
+ return { profiles, errors };
93
+ }
94
+
58
95
  function resolveSystemPrompt(raw: string): string {
59
96
  if (raw.startsWith("file:")) {
60
97
  const path = raw.slice(5);
@@ -82,7 +119,7 @@ function checkBashPermission(
82
119
  command: string,
83
120
  ): "allow" | "deny" {
84
121
  if (!perms) return "allow";
85
- // Deny wins over allow
122
+ // Deny wins over allow.
86
123
  for (const pattern of perms.deny ?? []) {
87
124
  if (matchPattern(pattern, command)) return "deny";
88
125
  }
@@ -99,15 +136,78 @@ function checkBashPermission(
99
136
 
100
137
  export default async function (pi: ExtensionAPI) {
101
138
  const agents = loadAgentsConfig();
139
+ const { profiles: agentProfiles, errors: agentProfileConfigErrors } = loadAgentProfilesConfig();
102
140
  // Built-in flags are not exposed through pi.getFlag(). Avenor forwards its
103
141
  // model option to pi as --model, so argv is the extension-level signal that
104
142
  // the caller's model should take precedence over the profile default.
105
143
  const cliModelOverride = findCliModelOverride(process.argv.slice(2));
106
144
  let activeAgent: { name: string; profile: AgentProfile } | null = null;
145
+ let activeAgentProfileName: string | undefined;
146
+
147
+ function getEffectiveAgentProfile(name: string): AgentProfile | undefined {
148
+ const base = agents[name];
149
+ if (!base) return undefined;
150
+
151
+ const override = activeAgentProfileName
152
+ ? executionOverride(agentProfiles[activeAgentProfileName]?.agents?.[name])
153
+ : undefined;
154
+ return { ...base, ...override };
155
+ }
156
+
157
+ function setAgentProfileStatus(ctx: ExtensionContext) {
158
+ ctx.ui.setStatus(
159
+ "agent-profile",
160
+ activeAgentProfileName ? `agent-profile:${activeAgentProfileName}` : "",
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Surface the session's active agent-profile to child processes.
166
+ *
167
+ * agent-model.sh resolves a role's effective model by reading
168
+ * $PI_AGENT_PROFILE; absent a launch-time override, the interactive
169
+ * /agent-profile selection is otherwise invisible to anything pi spawns.
170
+ * Mirroring it into the env lets the resolver agree with the session and,
171
+ * since children inherit it, keeps fresh dispatches on the same profile.
172
+ */
173
+ function syncProfileEnv(name: string | undefined) {
174
+ if (name) process.env.PI_AGENT_PROFILE = name;
175
+ else delete process.env.PI_AGENT_PROFILE;
176
+ }
177
+
178
+ /** Apply a named agent-profile, optionally saving the choice in this session. */
179
+ async function selectAgentProfile(
180
+ name: string | undefined,
181
+ ctx: ExtensionContext,
182
+ persist: boolean,
183
+ ): Promise<boolean> {
184
+ if (name && !agentProfiles[name]) {
185
+ ctx.ui.notify(`Agent profile "${name}" not found in agent-profiles.json`, "error");
186
+ return false;
187
+ }
188
+
189
+ const previousName = activeAgentProfileName;
190
+ activeAgentProfileName = name;
191
+ setAgentProfileStatus(ctx);
192
+ syncProfileEnv(name);
193
+
194
+ // The active role must receive a new effective model/thinking level now;
195
+ // future roles resolve the overlay when /agent is invoked. Do not persist
196
+ // a selection that cannot be applied to the current active role.
197
+ if (activeAgent && !(await applyAgent(activeAgent.name, ctx))) {
198
+ activeAgentProfileName = previousName;
199
+ setAgentProfileStatus(ctx);
200
+ syncProfileEnv(previousName);
201
+ return false;
202
+ }
203
+
204
+ if (persist) pi.appendEntry("pi-agents:profile", { name: name ?? null });
205
+ return true;
206
+ }
107
207
 
108
208
  /* ---- Apply agent profile to the current session ---- */
109
209
  async function applyAgent(name: string, ctx: ExtensionContext): Promise<boolean> {
110
- const profile = agents[name];
210
+ const profile = getEffectiveAgentProfile(name);
111
211
  if (!profile) {
112
212
  ctx.ui.notify(`Agent "${name}" not found in agents.json`, "error");
113
213
  return false;
@@ -131,10 +231,7 @@ export default async function (pi: ExtensionAPI) {
131
231
  await pi.setModel(model);
132
232
  }
133
233
 
134
- // Thinking level
135
- if (profile.thinkingLevel) {
136
- pi.setThinkingLevel(profile.thinkingLevel);
137
- }
234
+ if (profile.thinkingLevel) pi.setThinkingLevel(profile.thinkingLevel);
138
235
 
139
236
  // Tool restrictions — best-effort against currently registered tools.
140
237
  // tool_call handler enforces restrictions regardless of registration timing.
@@ -142,23 +239,25 @@ export default async function (pi: ExtensionAPI) {
142
239
  const allTools = pi.getAllTools();
143
240
  let allowed: typeof allTools;
144
241
  if (profile.tools) {
145
- // Allowlist: only these tool names
146
- allowed = allTools.filter((t) => profile.tools!.includes(t.name));
147
- // Also include any future tools whose name matches (checked at call time)
242
+ allowed = allTools.filter((tool) => profile.tools!.includes(tool.name));
148
243
  } else {
149
244
  allowed = [...allTools];
150
245
  }
151
246
  if (profile.excludeTools) {
152
- allowed = allowed.filter((t) => !profile.excludeTools!.includes(t.name));
247
+ allowed = allowed.filter((tool) => !profile.excludeTools!.includes(tool.name));
153
248
  }
154
- pi.setActiveTools(allowed.map((t) => t.name));
249
+ pi.setActiveTools(allowed.map((tool) => tool.name));
155
250
  } catch {
156
- // setActiveTools may fail if called too early; tool_call gate is the fallback
251
+ // setActiveTools may fail if called too early; tool_call gate is the fallback.
157
252
  }
158
253
 
159
254
  activeAgent = { name, profile };
160
255
  ctx.ui.setStatus("agent", `agent:${name}`);
161
- ctx.ui.notify(`Agent "${name}" active${profile.description ? ": " + profile.description : ""}`, "info");
256
+ const selected = activeAgentProfileName ? ` [${activeAgentProfileName}]` : "";
257
+ ctx.ui.notify(
258
+ `Agent "${name}" active${selected}${profile.description ? ": " + profile.description : ""}`,
259
+ "info",
260
+ );
162
261
  return true;
163
262
  }
164
263
 
@@ -166,44 +265,55 @@ export default async function (pi: ExtensionAPI) {
166
265
  function deactivateAgent(ctx: ExtensionContext) {
167
266
  activeAgent = null;
168
267
  try {
169
- pi.setActiveTools(pi.getAllTools().map((t) => t.name));
268
+ pi.setActiveTools(pi.getAllTools().map((tool) => tool.name));
170
269
  } catch { /* no-op if tools not fully registered */ }
171
270
  ctx.ui.setStatus("agent", "");
172
271
  ctx.ui.notify("Agent deactivated — all tools restored", "info");
173
272
  }
174
273
 
175
- /* ---- session_start: apply PI_AGENT env var ---- */
274
+ /* ---- session_start: restore profile, then apply PI_AGENT ---- */
176
275
  pi.on("session_start", async (_event, ctx) => {
177
- const envAgent = process.env.PI_AGENT;
178
- if (envAgent && agents[envAgent]) {
179
- await applyAgent(envAgent, ctx);
276
+ if (agentProfileConfigErrors.length) {
277
+ ctx.ui.notify(
278
+ `Ignored malformed agent-profile configuration:\n${agentProfileConfigErrors.join("\n")}`,
279
+ "warning",
280
+ );
180
281
  }
282
+
283
+ // The environment is an explicit launch-time override. A persisted command
284
+ // selection is restored otherwise, including a saved `none` selection.
285
+ const environmentProfile = process.env.PI_AGENT_PROFILE ?? process.env.PROFILE;
286
+ const persistedProfile = findPersistedProfileName(ctx.sessionManager.getEntries());
287
+ const selectedProfile = environmentProfile ?? persistedProfile;
288
+ if (selectedProfile) await selectAgentProfile(selectedProfile, ctx, false);
289
+ else setAgentProfileStatus(ctx);
290
+
291
+ const envAgent = process.env.PI_AGENT;
292
+ if (envAgent && agents[envAgent]) await applyAgent(envAgent, ctx);
181
293
  });
182
294
 
183
295
  /* ---- before_agent_start: inject agent system prompt ---- */
184
296
  pi.on("before_agent_start", async (event, _ctx) => {
185
297
  if (!activeAgent) return undefined;
186
298
 
187
- // Re-apply tool restrictions each turn (catches late-registered tools)
299
+ // Re-apply tool restrictions each turn (catches late-registered tools).
188
300
  try {
189
301
  const profile = activeAgent.profile;
190
302
  const allTools = pi.getAllTools();
191
303
  let allowed: typeof allTools;
192
304
  if (profile.tools) {
193
- allowed = allTools.filter((t) => profile.tools.includes(t.name));
305
+ allowed = allTools.filter((tool) => profile.tools.includes(tool.name));
194
306
  } else {
195
307
  allowed = [...allTools];
196
308
  }
197
309
  if (profile.excludeTools) {
198
- allowed = allowed.filter((t) => !profile.excludeTools.includes(t.name));
310
+ allowed = allowed.filter((tool) => !profile.excludeTools.includes(tool.name));
199
311
  }
200
- pi.setActiveTools(allowed.map((t) => t.name));
312
+ pi.setActiveTools(allowed.map((tool) => tool.name));
201
313
  } catch { /* best effort */ }
202
314
 
203
315
  const agentPrompt = resolveSystemPrompt(activeAgent.profile.systemPrompt);
204
- return {
205
- systemPrompt: agentPrompt + "\n\n" + event.systemPrompt,
206
- };
316
+ return { systemPrompt: agentPrompt + "\n\n" + event.systemPrompt };
207
317
  });
208
318
 
209
319
  /* ---- tool_call: enforce tool allowlist/denylist + bash permissions ---- */
@@ -211,22 +321,18 @@ export default async function (pi: ExtensionAPI) {
211
321
  if (!activeAgent) return;
212
322
  const profile = activeAgent.profile;
213
323
 
214
- // Tool allowlist gate
215
324
  if (profile.tools && !profile.tools.includes(event.toolName)) {
216
325
  return { block: true, reason: `Tool "${event.toolName}" not allowed for agent "${activeAgent.name}"` };
217
326
  }
218
327
 
219
- // Tool denylist gate
220
328
  if (profile.excludeTools && profile.excludeTools.includes(event.toolName)) {
221
329
  ctx.ui.notify(`Blocked tool: ${event.toolName}`, "warning");
222
330
  return { block: true, reason: `Tool "${event.toolName}" excluded for agent "${activeAgent.name}"` };
223
331
  }
224
332
 
225
- // Bash permission gate
226
333
  if (event.toolName === "bash" && profile.permissions?.bash) {
227
334
  const command = (event.input as { command?: string })?.command ?? "";
228
- const verdict = checkBashPermission(profile.permissions.bash, command);
229
- if (verdict === "deny") {
335
+ if (checkBashPermission(profile.permissions.bash, command) === "deny") {
230
336
  ctx.ui.notify(`Blocked: ${command.slice(0, 80)}`, "warning");
231
337
  return { block: true, reason: `Denied by ${activeAgent.name} bash permissions` };
232
338
  }
@@ -237,12 +343,12 @@ export default async function (pi: ExtensionAPI) {
237
343
  pi.registerCommand("agent", {
238
344
  description: "Switch to an agent profile (or 'none' to deactivate)",
239
345
  getArgumentCompletions: (prefix) => {
240
- const matches = Object.keys(agents).filter((n) => n.startsWith(prefix ?? ""));
346
+ const matches = Object.keys(agents).filter((name) => name.startsWith(prefix ?? ""));
241
347
  return matches.length
242
- ? matches.map((n) => ({
243
- value: n,
244
- label: n,
245
- description: agents[n].description ?? "",
348
+ ? matches.map((name) => ({
349
+ value: name,
350
+ label: name,
351
+ description: agents[name].description ?? "",
246
352
  }))
247
353
  : null;
248
354
  },
@@ -251,7 +357,7 @@ export default async function (pi: ExtensionAPI) {
251
357
  if (!name) {
252
358
  const current = activeAgent ? ` (active: ${activeAgent.name})` : "";
253
359
  const list = Object.entries(agents)
254
- .map(([n, p]) => ` ${n}${p.description ? ": " + p.description : ""}`)
360
+ .map(([agentName, profile]) => ` ${agentName}${profile.description ? ": " + profile.description : ""}`)
255
361
  .join("\n");
256
362
  ctx.ui.notify(`Agent profiles${current}:\n${list}`, "info");
257
363
  return;
@@ -270,9 +376,39 @@ export default async function (pi: ExtensionAPI) {
270
376
  handler: async (_args, ctx) => {
271
377
  const current = activeAgent ? ` [active: ${activeAgent.name}]` : "";
272
378
  const list = Object.entries(agents)
273
- .map(([n, p]) => ` ${n}${p.description ? ": " + p.description : ""}`)
379
+ .map(([name, profile]) => ` ${name}${profile.description ? ": " + profile.description : ""}`)
274
380
  .join("\n");
275
381
  ctx.ui.notify(`Agent profiles${current}:\n${list}`, "info");
276
382
  },
277
383
  });
384
+
385
+ /* ---- /agent-profile <name> — select a session-scoped runtime overlay ---- */
386
+ pi.registerCommand("agent-profile", {
387
+ description: "Select a session-scoped agent runtime profile (or 'none' to clear)",
388
+ getArgumentCompletions: (prefix) => {
389
+ const names = ["none", ...Object.keys(agentProfiles)];
390
+ const matches = names.filter((name) => name.startsWith(prefix ?? ""));
391
+ return matches.length
392
+ ? matches.map((name) => ({
393
+ value: name,
394
+ label: name,
395
+ description: name === "none"
396
+ ? "Clear the session agent profile"
397
+ : agentProfiles[name]?.description ?? "",
398
+ }))
399
+ : null;
400
+ },
401
+ handler: async (args, ctx) => {
402
+ const name = args?.trim();
403
+ if (!name) {
404
+ const current = activeAgentProfileName ?? "none";
405
+ const list = Object.entries(agentProfiles)
406
+ .map(([profileName, profile]) => ` ${profileName}${profile.description ? ": " + profile.description : ""}`)
407
+ .join("\n");
408
+ ctx.ui.notify(`Agent runtime profiles (active: ${current})${list ? `:\n${list}` : ""}`, "info");
409
+ return;
410
+ }
411
+ await selectAgentProfile(name === "none" ? undefined : name, ctx, true);
412
+ },
413
+ });
278
414
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/pi-agents",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Agent profiles for pi — preconfigured model, system prompt, tool restrictions, and permission gates",
5
5
  "scripts": {
6
6
  "test": "node --experimental-strip-types --test tests/*.test.mts"