@dougbots/pi-agents 0.1.1 → 0.2.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/README.md CHANGED
@@ -49,6 +49,36 @@ 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
+
52
82
  ## Commands
53
83
 
54
84
  | Command | Description |
@@ -57,12 +87,16 @@ Profile fields:
57
87
  | `/agent none` | Deactivate agent, restore all tools |
58
88
  | `/agent` | List available agents |
59
89
  | `/agents` | Same as `/agent` |
90
+ | `/agent-profile <name>` | Select a session-scoped runtime agent profile |
91
+ | `/agent-profile none` | Clear the session runtime agent profile |
92
+ | `/agent-profile` | Show the active and available runtime agent profiles |
60
93
 
61
94
  ## Boot with agent
62
95
 
63
96
  ```bash
64
97
  PI_AGENT=reviewer pi
65
98
  PI_AGENT=jockey pi -c
99
+ PI_AGENT=explore PI_AGENT_PROFILE=cloud pi
66
100
  ```
67
101
 
68
102
  ## 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,62 @@ 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
+ /** Apply a named agent-profile, optionally saving the choice in this session. */
165
+ async function selectAgentProfile(
166
+ name: string | undefined,
167
+ ctx: ExtensionContext,
168
+ persist: boolean,
169
+ ): Promise<boolean> {
170
+ if (name && !agentProfiles[name]) {
171
+ ctx.ui.notify(`Agent profile "${name}" not found in agent-profiles.json`, "error");
172
+ return false;
173
+ }
174
+
175
+ const previousName = activeAgentProfileName;
176
+ activeAgentProfileName = name;
177
+ setAgentProfileStatus(ctx);
178
+
179
+ // The active role must receive a new effective model/thinking level now;
180
+ // future roles resolve the overlay when /agent is invoked. Do not persist
181
+ // a selection that cannot be applied to the current active role.
182
+ if (activeAgent && !(await applyAgent(activeAgent.name, ctx))) {
183
+ activeAgentProfileName = previousName;
184
+ setAgentProfileStatus(ctx);
185
+ return false;
186
+ }
187
+
188
+ if (persist) pi.appendEntry("pi-agents:profile", { name: name ?? null });
189
+ return true;
190
+ }
107
191
 
108
192
  /* ---- Apply agent profile to the current session ---- */
109
193
  async function applyAgent(name: string, ctx: ExtensionContext): Promise<boolean> {
110
- const profile = agents[name];
194
+ const profile = getEffectiveAgentProfile(name);
111
195
  if (!profile) {
112
196
  ctx.ui.notify(`Agent "${name}" not found in agents.json`, "error");
113
197
  return false;
@@ -131,10 +215,7 @@ export default async function (pi: ExtensionAPI) {
131
215
  await pi.setModel(model);
132
216
  }
133
217
 
134
- // Thinking level
135
- if (profile.thinkingLevel) {
136
- pi.setThinkingLevel(profile.thinkingLevel);
137
- }
218
+ if (profile.thinkingLevel) pi.setThinkingLevel(profile.thinkingLevel);
138
219
 
139
220
  // Tool restrictions — best-effort against currently registered tools.
140
221
  // tool_call handler enforces restrictions regardless of registration timing.
@@ -142,23 +223,25 @@ export default async function (pi: ExtensionAPI) {
142
223
  const allTools = pi.getAllTools();
143
224
  let allowed: typeof allTools;
144
225
  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)
226
+ allowed = allTools.filter((tool) => profile.tools!.includes(tool.name));
148
227
  } else {
149
228
  allowed = [...allTools];
150
229
  }
151
230
  if (profile.excludeTools) {
152
- allowed = allowed.filter((t) => !profile.excludeTools!.includes(t.name));
231
+ allowed = allowed.filter((tool) => !profile.excludeTools!.includes(tool.name));
153
232
  }
154
- pi.setActiveTools(allowed.map((t) => t.name));
233
+ pi.setActiveTools(allowed.map((tool) => tool.name));
155
234
  } catch {
156
- // setActiveTools may fail if called too early; tool_call gate is the fallback
235
+ // setActiveTools may fail if called too early; tool_call gate is the fallback.
157
236
  }
158
237
 
159
238
  activeAgent = { name, profile };
160
239
  ctx.ui.setStatus("agent", `agent:${name}`);
161
- ctx.ui.notify(`Agent "${name}" active${profile.description ? ": " + profile.description : ""}`, "info");
240
+ const selected = activeAgentProfileName ? ` [${activeAgentProfileName}]` : "";
241
+ ctx.ui.notify(
242
+ `Agent "${name}" active${selected}${profile.description ? ": " + profile.description : ""}`,
243
+ "info",
244
+ );
162
245
  return true;
163
246
  }
164
247
 
@@ -166,44 +249,55 @@ export default async function (pi: ExtensionAPI) {
166
249
  function deactivateAgent(ctx: ExtensionContext) {
167
250
  activeAgent = null;
168
251
  try {
169
- pi.setActiveTools(pi.getAllTools().map((t) => t.name));
252
+ pi.setActiveTools(pi.getAllTools().map((tool) => tool.name));
170
253
  } catch { /* no-op if tools not fully registered */ }
171
254
  ctx.ui.setStatus("agent", "");
172
255
  ctx.ui.notify("Agent deactivated — all tools restored", "info");
173
256
  }
174
257
 
175
- /* ---- session_start: apply PI_AGENT env var ---- */
258
+ /* ---- session_start: restore profile, then apply PI_AGENT ---- */
176
259
  pi.on("session_start", async (_event, ctx) => {
177
- const envAgent = process.env.PI_AGENT;
178
- if (envAgent && agents[envAgent]) {
179
- await applyAgent(envAgent, ctx);
260
+ if (agentProfileConfigErrors.length) {
261
+ ctx.ui.notify(
262
+ `Ignored malformed agent-profile configuration:\n${agentProfileConfigErrors.join("\n")}`,
263
+ "warning",
264
+ );
180
265
  }
266
+
267
+ // The environment is an explicit launch-time override. A persisted command
268
+ // selection is restored otherwise, including a saved `none` selection.
269
+ const environmentProfile = process.env.PI_AGENT_PROFILE ?? process.env.PROFILE;
270
+ const persistedProfile = findPersistedProfileName(ctx.sessionManager.getEntries());
271
+ const selectedProfile = environmentProfile ?? persistedProfile;
272
+ if (selectedProfile) await selectAgentProfile(selectedProfile, ctx, false);
273
+ else setAgentProfileStatus(ctx);
274
+
275
+ const envAgent = process.env.PI_AGENT;
276
+ if (envAgent && agents[envAgent]) await applyAgent(envAgent, ctx);
181
277
  });
182
278
 
183
279
  /* ---- before_agent_start: inject agent system prompt ---- */
184
280
  pi.on("before_agent_start", async (event, _ctx) => {
185
281
  if (!activeAgent) return undefined;
186
282
 
187
- // Re-apply tool restrictions each turn (catches late-registered tools)
283
+ // Re-apply tool restrictions each turn (catches late-registered tools).
188
284
  try {
189
285
  const profile = activeAgent.profile;
190
286
  const allTools = pi.getAllTools();
191
287
  let allowed: typeof allTools;
192
288
  if (profile.tools) {
193
- allowed = allTools.filter((t) => profile.tools.includes(t.name));
289
+ allowed = allTools.filter((tool) => profile.tools.includes(tool.name));
194
290
  } else {
195
291
  allowed = [...allTools];
196
292
  }
197
293
  if (profile.excludeTools) {
198
- allowed = allowed.filter((t) => !profile.excludeTools.includes(t.name));
294
+ allowed = allowed.filter((tool) => !profile.excludeTools.includes(tool.name));
199
295
  }
200
- pi.setActiveTools(allowed.map((t) => t.name));
296
+ pi.setActiveTools(allowed.map((tool) => tool.name));
201
297
  } catch { /* best effort */ }
202
298
 
203
299
  const agentPrompt = resolveSystemPrompt(activeAgent.profile.systemPrompt);
204
- return {
205
- systemPrompt: agentPrompt + "\n\n" + event.systemPrompt,
206
- };
300
+ return { systemPrompt: agentPrompt + "\n\n" + event.systemPrompt };
207
301
  });
208
302
 
209
303
  /* ---- tool_call: enforce tool allowlist/denylist + bash permissions ---- */
@@ -211,22 +305,18 @@ export default async function (pi: ExtensionAPI) {
211
305
  if (!activeAgent) return;
212
306
  const profile = activeAgent.profile;
213
307
 
214
- // Tool allowlist gate
215
308
  if (profile.tools && !profile.tools.includes(event.toolName)) {
216
309
  return { block: true, reason: `Tool "${event.toolName}" not allowed for agent "${activeAgent.name}"` };
217
310
  }
218
311
 
219
- // Tool denylist gate
220
312
  if (profile.excludeTools && profile.excludeTools.includes(event.toolName)) {
221
313
  ctx.ui.notify(`Blocked tool: ${event.toolName}`, "warning");
222
314
  return { block: true, reason: `Tool "${event.toolName}" excluded for agent "${activeAgent.name}"` };
223
315
  }
224
316
 
225
- // Bash permission gate
226
317
  if (event.toolName === "bash" && profile.permissions?.bash) {
227
318
  const command = (event.input as { command?: string })?.command ?? "";
228
- const verdict = checkBashPermission(profile.permissions.bash, command);
229
- if (verdict === "deny") {
319
+ if (checkBashPermission(profile.permissions.bash, command) === "deny") {
230
320
  ctx.ui.notify(`Blocked: ${command.slice(0, 80)}`, "warning");
231
321
  return { block: true, reason: `Denied by ${activeAgent.name} bash permissions` };
232
322
  }
@@ -237,12 +327,12 @@ export default async function (pi: ExtensionAPI) {
237
327
  pi.registerCommand("agent", {
238
328
  description: "Switch to an agent profile (or 'none' to deactivate)",
239
329
  getArgumentCompletions: (prefix) => {
240
- const matches = Object.keys(agents).filter((n) => n.startsWith(prefix ?? ""));
330
+ const matches = Object.keys(agents).filter((name) => name.startsWith(prefix ?? ""));
241
331
  return matches.length
242
- ? matches.map((n) => ({
243
- value: n,
244
- label: n,
245
- description: agents[n].description ?? "",
332
+ ? matches.map((name) => ({
333
+ value: name,
334
+ label: name,
335
+ description: agents[name].description ?? "",
246
336
  }))
247
337
  : null;
248
338
  },
@@ -251,7 +341,7 @@ export default async function (pi: ExtensionAPI) {
251
341
  if (!name) {
252
342
  const current = activeAgent ? ` (active: ${activeAgent.name})` : "";
253
343
  const list = Object.entries(agents)
254
- .map(([n, p]) => ` ${n}${p.description ? ": " + p.description : ""}`)
344
+ .map(([agentName, profile]) => ` ${agentName}${profile.description ? ": " + profile.description : ""}`)
255
345
  .join("\n");
256
346
  ctx.ui.notify(`Agent profiles${current}:\n${list}`, "info");
257
347
  return;
@@ -270,9 +360,39 @@ export default async function (pi: ExtensionAPI) {
270
360
  handler: async (_args, ctx) => {
271
361
  const current = activeAgent ? ` [active: ${activeAgent.name}]` : "";
272
362
  const list = Object.entries(agents)
273
- .map(([n, p]) => ` ${n}${p.description ? ": " + p.description : ""}`)
363
+ .map(([name, profile]) => ` ${name}${profile.description ? ": " + profile.description : ""}`)
274
364
  .join("\n");
275
365
  ctx.ui.notify(`Agent profiles${current}:\n${list}`, "info");
276
366
  },
277
367
  });
368
+
369
+ /* ---- /agent-profile <name> — select a session-scoped runtime overlay ---- */
370
+ pi.registerCommand("agent-profile", {
371
+ description: "Select a session-scoped agent runtime profile (or 'none' to clear)",
372
+ getArgumentCompletions: (prefix) => {
373
+ const names = ["none", ...Object.keys(agentProfiles)];
374
+ const matches = names.filter((name) => name.startsWith(prefix ?? ""));
375
+ return matches.length
376
+ ? matches.map((name) => ({
377
+ value: name,
378
+ label: name,
379
+ description: name === "none"
380
+ ? "Clear the session agent profile"
381
+ : agentProfiles[name]?.description ?? "",
382
+ }))
383
+ : null;
384
+ },
385
+ handler: async (args, ctx) => {
386
+ const name = args?.trim();
387
+ if (!name) {
388
+ const current = activeAgentProfileName ?? "none";
389
+ const list = Object.entries(agentProfiles)
390
+ .map(([profileName, profile]) => ` ${profileName}${profile.description ? ": " + profile.description : ""}`)
391
+ .join("\n");
392
+ ctx.ui.notify(`Agent runtime profiles (active: ${current})${list ? `:\n${list}` : ""}`, "info");
393
+ return;
394
+ }
395
+ await selectAgentProfile(name === "none" ? undefined : name, ctx, true);
396
+ },
397
+ });
278
398
  }
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.0",
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"