@narumitw/pi-subagents 0.14.1 → 0.15.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
@@ -210,7 +210,7 @@ When `subagent_wait` starts while a turn is running, that wait consumes the turn
210
210
 
211
211
  The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history. Set `transport` to `in-process` to retain one public Pi SDK `AgentSession` per stateful `agentId`, avoiding repeated process startup while preserving native child history in memory.
212
212
 
213
- Configure the runtime in `~/.pi/agent/pi-subagents-config.json`, then reload Pi:
213
+ Configure the runtime in `~/.pi/agent/pi-subagents.json`, then reload Pi:
214
214
 
215
215
  ```json
216
216
  {
@@ -301,7 +301,9 @@ Built-in agents inherit the active/default Pi model instead of forcing a provide
301
301
  ## ⚙️ Configure agent tools
302
302
 
303
303
  Run `/subagents:config` in an interactive Pi session to edit the tools each subagent may use.
304
- The command stores settings in `~/.pi/agent/pi-subagents-config.json`.
304
+ The command stores settings in `~/.pi/agent/pi-subagents.json`.
305
+
306
+ Compatibility: a valid legacy `pi-subagents-config.json` is migrated automatically to `pi-subagents.json`. If both files exist, the new filename takes precedence.
305
307
 
306
308
  - Select an agent, then press Enter or Space to toggle tools.
307
309
  - Press `S` to save, or Esc to cancel and return to agent selection.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/settings.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
@@ -118,22 +119,151 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
118
119
  return settings;
119
120
  }
120
121
 
122
+ const SETTINGS_FILE = "pi-subagents.json";
123
+ const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
124
+ let pendingSettingsNotice: string | undefined;
125
+
121
126
  export function readSubagentSettings(): SubagentSettings | undefined {
122
- const configPath = path.join(getAgentDir(), "pi-subagents-config.json");
123
- if (!fs.existsSync(configPath)) return undefined;
127
+ pendingSettingsNotice = undefined;
128
+ const canonicalPath = path.join(getAgentDir(), SETTINGS_FILE);
129
+ const legacyPath = path.join(getAgentDir(), LEGACY_SETTINGS_FILE);
130
+ if (fs.existsSync(canonicalPath)) {
131
+ const canonical = readSettingsFile(canonicalPath);
132
+ const notices: string[] = [];
133
+ if (!canonical) notices.push(`${SETTINGS_FILE} is invalid and was ignored.`);
134
+ if (fs.existsSync(legacyPath)) {
135
+ notices.push(`${LEGACY_SETTINGS_FILE} ignored because ${SETTINGS_FILE} takes precedence.`);
136
+ }
137
+ if (notices.length > 0) pendingSettingsNotice = notices.join("\n");
138
+ return canonical;
139
+ }
140
+ if (!fs.existsSync(legacyPath)) return undefined;
141
+ const legacySnapshot = readSettingsSnapshot(legacyPath);
142
+ const legacy = legacySnapshot.settings;
143
+ if (!legacy) {
144
+ pendingSettingsNotice = `${LEGACY_SETTINGS_FILE} is invalid and was ignored.`;
145
+ return undefined;
146
+ }
147
+ let installedIdentity: FileIdentity;
148
+ try {
149
+ installedIdentity = installFileExclusively(canonicalPath, legacySnapshot.contents ?? "");
150
+ } catch (error) {
151
+ if (fs.existsSync(canonicalPath)) {
152
+ const canonical = readSettingsFile(canonicalPath);
153
+ pendingSettingsNotice = [
154
+ ...(!canonical ? [`${SETTINGS_FILE} is invalid and was ignored.`] : []),
155
+ `${LEGACY_SETTINGS_FILE} ignored because ${SETTINGS_FILE} was created concurrently.`,
156
+ ].join("\n");
157
+ return canonical;
158
+ }
159
+ pendingSettingsNotice = `Subagent settings migration failed: ${formatError(error)}. The legacy file was used for this session.`;
160
+ return legacy;
161
+ }
162
+ if (!fileContentsEqual(legacyPath, legacySnapshot.contents ?? "")) {
163
+ pendingSettingsNotice = removeFileIfIdentityMatches(
164
+ canonicalPath,
165
+ installedIdentity,
166
+ legacySnapshot.contents ?? "",
167
+ )
168
+ ? `${LEGACY_SETTINGS_FILE} changed during migration; the stale ${SETTINGS_FILE} snapshot was removed.`
169
+ : `${LEGACY_SETTINGS_FILE} changed during migration, but ${SETTINGS_FILE} was replaced concurrently and takes precedence on the next load.`;
170
+ return legacy;
171
+ }
172
+ try {
173
+ fs.rmSync(legacyPath);
174
+ pendingSettingsNotice = `Subagent settings migrated from ${LEGACY_SETTINGS_FILE} to ${SETTINGS_FILE}.`;
175
+ } catch (error) {
176
+ pendingSettingsNotice = `Subagent settings migrated to ${SETTINGS_FILE}, but ${LEGACY_SETTINGS_FILE} could not be removed: ${formatError(error)}.`;
177
+ }
178
+ return legacy;
179
+ }
180
+
181
+ type FileIdentity = { dev: number; ino: number };
182
+
183
+ function installFileExclusively(filePath: string, contents: string): FileIdentity {
184
+ const tempFile = path.join(path.dirname(filePath), `.${SETTINGS_FILE}.${randomUUID()}.tmp`);
185
+ try {
186
+ fs.writeFileSync(tempFile, contents, { encoding: "utf8", flag: "wx" });
187
+ const identity = fs.lstatSync(tempFile);
188
+ fs.linkSync(tempFile, filePath);
189
+ return { dev: identity.dev, ino: identity.ino };
190
+ } finally {
191
+ try {
192
+ fs.rmSync(tempFile, { force: true });
193
+ } catch {
194
+ // Preserve the migration result if best-effort temp cleanup fails.
195
+ }
196
+ }
197
+ }
198
+
199
+ function removeFileIfIdentityMatches(
200
+ filePath: string,
201
+ expected: FileIdentity,
202
+ expectedContents: string,
203
+ ) {
124
204
  try {
125
- return normalizeSubagentSettings(JSON.parse(fs.readFileSync(configPath, "utf-8")));
205
+ const current = fs.lstatSync(filePath);
206
+ if (current.dev !== expected.dev || current.ino !== expected.ino) return false;
207
+ if (fs.readFileSync(filePath, "utf8") !== expectedContents) return false;
208
+ fs.rmSync(filePath);
209
+ return true;
126
210
  } catch {
127
- return undefined;
211
+ return false;
128
212
  }
129
213
  }
130
214
 
215
+ function fileContentsEqual(filePath: string, expected: string) {
216
+ try {
217
+ return fs.readFileSync(filePath, "utf8") === expected;
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ export function consumeSubagentSettingsNotice() {
224
+ const notice = pendingSettingsNotice;
225
+ pendingSettingsNotice = undefined;
226
+ return notice;
227
+ }
228
+
131
229
  export function saveSubagentConfig(settings: SubagentSettings): void {
132
230
  const agentDir = getAgentDir();
133
231
  fs.mkdirSync(agentDir, { recursive: true });
232
+ const configPath = path.join(agentDir, SETTINGS_FILE);
233
+ const tempFile = path.join(agentDir, `.${SETTINGS_FILE}.${randomUUID()}.tmp`);
234
+ try {
235
+ fs.writeFileSync(tempFile, `${JSON.stringify(settings, null, "\t")}\n`, {
236
+ encoding: "utf8",
237
+ flag: "wx",
238
+ });
239
+ fs.renameSync(tempFile, configPath);
240
+ } finally {
241
+ try {
242
+ fs.rmSync(tempFile, { force: true });
243
+ } catch {
244
+ // Preserve the save result if best-effort temp cleanup fails.
245
+ }
246
+ }
247
+ }
248
+
249
+ function readSettingsFile(configPath: string): SubagentSettings | undefined {
250
+ return readSettingsSnapshot(configPath).settings;
251
+ }
252
+
253
+ function readSettingsSnapshot(configPath: string): {
254
+ settings?: SubagentSettings;
255
+ contents?: string;
256
+ } {
257
+ try {
258
+ const contents = fs.readFileSync(configPath, "utf8");
259
+ return { settings: normalizeSubagentSettings(JSON.parse(contents)), contents };
260
+ } catch {
261
+ return {};
262
+ }
263
+ }
134
264
 
135
- const configPath = path.join(agentDir, "pi-subagents-config.json");
136
- fs.writeFileSync(configPath, `${JSON.stringify(settings, null, "\t")}\n`, "utf-8");
265
+ function formatError(error: unknown) {
266
+ return error instanceof Error ? error.message : String(error);
137
267
  }
138
268
 
139
269
  export function uniqueToolNames(tools: string[]): string[] {
@@ -153,7 +283,11 @@ export function resolveSubagentThinkingLevel(
153
283
  topLevelThinkingLevel?: SubagentThinkingLevel,
154
284
  localThinkingLevel?: SubagentThinkingLevel,
155
285
  ): SubagentThinkingLevel | undefined {
156
- return localThinkingLevel ?? topLevelThinkingLevel ?? agents.find((agent) => agent.name === agentName)?.thinkingLevel;
286
+ return (
287
+ localThinkingLevel ??
288
+ topLevelThinkingLevel ??
289
+ agents.find((agent) => agent.name === agentName)?.thinkingLevel
290
+ );
157
291
  }
158
292
 
159
293
  export function hasAnyAgentOverride(config: SubagentAgentConfig): boolean {
package/src/subagents.ts CHANGED
@@ -19,6 +19,7 @@ import { executeSubagent } from "./execution.js";
19
19
  import { renderSubagentCall, renderSubagentResult } from "./render.js";
20
20
  import type { SubagentDetails } from "./runner.js";
21
21
  import { registerStatefulSubagents } from "./stateful.js";
22
+ import { consumeSubagentSettingsNotice, readSubagentSettings } from "./settings.js";
22
23
 
23
24
  export default function (pi: ExtensionAPI) {
24
25
  pi.registerTool<typeof SubagentParams, SubagentDetails>({
@@ -67,6 +68,15 @@ export default function (pi: ExtensionAPI) {
67
68
  if ((event.details as (SubagentDetails & { isError?: boolean }) | undefined)?.isError) return { isError: true };
68
69
  });
69
70
 
71
+ pi.on("session_start", (_event, ctx) => {
72
+ let notice = consumeSubagentSettingsNotice();
73
+ if (!notice) {
74
+ readSubagentSettings();
75
+ notice = consumeSubagentSettingsNotice();
76
+ }
77
+ if (notice) ctx.ui.notify(notice, "warning");
78
+ });
79
+
70
80
  registerSubagentConfigCommand(pi);
71
81
  registerStatefulSubagents(pi);
72
82
  }
@@ -75,7 +85,9 @@ export { buildPiArgs } from "./runner.js";
75
85
  export {
76
86
  normalizeAgentSettings,
77
87
  normalizeSubagentSettings,
88
+ readSubagentSettings,
78
89
  resolveSubagentThinkingLevel,
90
+ saveSubagentConfig,
79
91
  sameToolSet,
80
92
  uniqueToolNames,
81
93
  } from "./settings.js";