pi-session-cleanup 1.1.0 → 1.1.2

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.
@@ -1,361 +1,361 @@
1
- import { readFileSync, readdirSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
7
-
8
- const DEFAULT_PROJECT_SOURCE_DIRS = [".omp/agents", ".pi/agents", ".claude/agents"];
9
- const DEFAULT_USER_SOURCE_DIRS = ["{home}/.omp/agents", "{agentDir}/agents", "{home}/.claude/agents"];
10
- const ROUTER_CONFIG_FILE_NAME = "config.json";
11
- const ROUTER_EXTENSION_NAME = "pi-agent-router";
12
- const PI_AGENT_DIR_ENV_VAR = "PI_CODING_AGENT_DIR";
13
-
14
- const SESSION_EXTENSION_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
-
16
- type AgentMode = "primary" | "subagent" | "all";
17
-
18
- interface AgentDiscoveryConfig {
19
- projectSourceDirs: string[];
20
- userSourceDirs: string[];
21
- }
22
-
23
- export interface SelectableAgent {
24
- name: string;
25
- description: string;
26
- mode?: AgentMode;
27
- }
28
-
29
- interface AgentSelectionMenu {
30
- labels: string[];
31
- valueByLabel: Map<string, string>;
32
- }
33
-
34
- function normalizeStringArray(value: unknown): string[] | null {
35
- if (!Array.isArray(value)) {
36
- return null;
37
- }
38
-
39
- const normalized = value
40
- .filter((entry): entry is string => typeof entry === "string")
41
- .map((entry) => entry.trim())
42
- .filter(Boolean);
43
-
44
- return normalized.length > 0 ? normalized : null;
45
- }
46
-
47
- function toRecord(value: unknown): Record<string, unknown> | null {
48
- return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
49
- }
50
-
51
- function expandHomeDirectory(configuredDir: string, homeDirectory: string): string {
52
- if (configuredDir === "~") {
53
- return homeDirectory;
54
- }
55
-
56
- if (configuredDir.startsWith("~/") || configuredDir.startsWith("~\\")) {
57
- return join(homeDirectory, stripLeadingPathSeparators(configuredDir.slice(1)));
58
- }
59
-
60
- return configuredDir;
61
- }
62
-
63
- function resolvePiAgentDir(): string {
64
- const configuredDir = process.env[PI_AGENT_DIR_ENV_VAR]?.trim();
65
- if (configuredDir) {
66
- return expandHomeDirectory(configuredDir, homedir());
67
- }
68
-
69
- return join(homedir(), ".pi", "agent");
70
- }
71
-
72
- function resolveRouterConfigCandidates(): string[] {
73
- const agentDir = resolvePiAgentDir();
74
- const extensionParentDir = dirname(SESSION_EXTENSION_ROOT);
75
-
76
- return [
77
- join(extensionParentDir, ROUTER_EXTENSION_NAME, ROUTER_CONFIG_FILE_NAME),
78
- join(agentDir, "extensions", ROUTER_EXTENSION_NAME, ROUTER_CONFIG_FILE_NAME),
79
- ];
80
- }
81
-
82
- function loadAgentDiscoveryConfig(): AgentDiscoveryConfig {
83
- for (const configPath of resolveRouterConfigCandidates()) {
84
- try {
85
- const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
86
- const config = toRecord(parsed);
87
- const agentDiscovery = toRecord(config?.agentDiscovery);
88
- const projectSourceDirs = normalizeStringArray(agentDiscovery?.projectSourceDirs);
89
- const userSourceDirs = normalizeStringArray(agentDiscovery?.userSourceDirs);
90
-
91
- return {
92
- projectSourceDirs: projectSourceDirs ?? [...DEFAULT_PROJECT_SOURCE_DIRS],
93
- userSourceDirs: userSourceDirs ?? [...DEFAULT_USER_SOURCE_DIRS],
94
- };
95
- } catch {
96
- continue;
97
- }
98
- }
99
-
100
- return {
101
- projectSourceDirs: [...DEFAULT_PROJECT_SOURCE_DIRS],
102
- userSourceDirs: [...DEFAULT_USER_SOURCE_DIRS],
103
- };
104
- }
105
-
106
- function stripLeadingPathSeparators(value: string): string {
107
- return value.replace(/^[\\/]+/, "");
108
- }
109
-
110
- function resolveConfiguredUserPath(rawPath: string): string {
111
- const trimmed = rawPath.trim();
112
- if (!trimmed) {
113
- return resolve(resolvePiAgentDir(), "agents");
114
- }
115
-
116
- if (trimmed === "~") {
117
- return homedir();
118
- }
119
-
120
- if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
121
- return join(homedir(), stripLeadingPathSeparators(trimmed.slice(1)));
122
- }
123
-
124
- if (trimmed === "{home}") {
125
- return homedir();
126
- }
127
-
128
- if (trimmed.startsWith("{home}/") || trimmed.startsWith("{home}\\")) {
129
- return join(homedir(), stripLeadingPathSeparators(trimmed.slice("{home}".length)));
130
- }
131
-
132
- if (trimmed === "{agentDir}") {
133
- return resolvePiAgentDir();
134
- }
135
-
136
- if (trimmed.startsWith("{agentDir}/") || trimmed.startsWith("{agentDir}\\")) {
137
- return join(resolvePiAgentDir(), stripLeadingPathSeparators(trimmed.slice("{agentDir}".length)));
138
- }
139
-
140
- return resolve(trimmed);
141
- }
142
-
143
- function isDirectory(path: string): boolean {
144
- try {
145
- readdirSync(path);
146
- return true;
147
- } catch {
148
- return false;
149
- }
150
- }
151
-
152
- function findNearestProjectAgentDirs(cwd: string, projectSourceDirs: readonly string[]): string[] {
153
- let currentDir = resolve(cwd);
154
-
155
- while (true) {
156
- const candidates = projectSourceDirs
157
- .map((sourceDir) => resolve(currentDir, sourceDir))
158
- .filter((candidate) => isDirectory(candidate));
159
-
160
- if (candidates.length > 0) {
161
- return candidates;
162
- }
163
-
164
- const parentDir = dirname(currentDir);
165
- if (parentDir === currentDir) {
166
- return [];
167
- }
168
-
169
- currentDir = parentDir;
170
- }
171
- }
172
-
173
- function parseAgentMode(value: unknown): AgentMode | undefined {
174
- if (typeof value !== "string") {
175
- return undefined;
176
- }
177
-
178
- const normalized = value.trim().toLowerCase();
179
- if (normalized === "primary" || normalized === "subagent" || normalized === "all") {
180
- return normalized;
181
- }
182
-
183
- return undefined;
184
- }
185
-
186
- function parseFrontmatter(content: string): Record<string, string> | null {
187
- const normalized = content.replace(/\r\n/g, "\n");
188
- if (!normalized.startsWith("---\n")) {
189
- return null;
190
- }
191
-
192
- const end = normalized.indexOf("\n---", 4);
193
- if (end === -1) {
194
- return null;
195
- }
196
-
197
- const frontmatter: Record<string, string> = {};
198
- const lines = normalized.slice(4, end).split("\n");
199
-
200
- for (const line of lines) {
201
- const separatorIndex = line.indexOf(":");
202
- if (separatorIndex === -1) {
203
- continue;
204
- }
205
-
206
- const key = line.slice(0, separatorIndex).trim();
207
- const value = line.slice(separatorIndex + 1).trim().replace(/^['\"]|['\"]$/g, "");
208
- if (key) {
209
- frontmatter[key] = value;
210
- }
211
- }
212
-
213
- return frontmatter;
214
- }
215
-
216
- function parseAgentFile(filePath: string): SelectableAgent | null {
217
- try {
218
- const content = readFileSync(filePath, "utf-8");
219
- const frontmatter = parseFrontmatter(content);
220
- if (!frontmatter?.name) {
221
- return null;
222
- }
223
-
224
- return {
225
- name: frontmatter.name,
226
- description: frontmatter.description || `Agent ${frontmatter.name}`,
227
- mode: parseAgentMode(frontmatter.mode),
228
- };
229
- } catch {
230
- return null;
231
- }
232
- }
233
-
234
- function loadAgentsFromDir(dirPath: string): SelectableAgent[] {
235
- try {
236
- return readdirSync(dirPath)
237
- .filter((entry) => entry.endsWith(".md"))
238
- .map((entry) => parseAgentFile(join(dirPath, entry)))
239
- .filter((agent): agent is SelectableAgent => Boolean(agent));
240
- } catch {
241
- return [];
242
- }
243
- }
244
-
245
- function truncateDescription(description: string, maxLength = 72): string {
246
- const normalized = description.trim().replace(/\s+/g, " ");
247
- if (normalized.length <= maxLength) {
248
- return normalized;
249
- }
250
-
251
- return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
252
- }
253
-
254
- function formatModeBadge(agent: SelectableAgent): string {
255
- return `[${agent.mode ?? "primary"}]`;
256
- }
257
-
258
- function formatCurrentMarker(currentAgentName: string | null, candidateAgentName: string): string {
259
- return currentAgentName === candidateAgentName ? "●" : "○";
260
- }
261
-
262
- function buildAgentSelectionLabel(
263
- agent: SelectableAgent,
264
- currentAgentName: string | null,
265
- ): string {
266
- return [
267
- formatCurrentMarker(currentAgentName, agent.name),
268
- agent.name,
269
- formatModeBadge(agent),
270
- "—",
271
- truncateDescription(agent.description),
272
- ].join(" ");
273
- }
274
-
275
- export function buildAgentSelectionMenu(
276
- agents: readonly SelectableAgent[],
277
- currentAgentName: string | null,
278
- ): AgentSelectionMenu {
279
- const labels: string[] = [];
280
- const valueByLabel = new Map<string, string>();
281
-
282
- for (const agent of agents) {
283
- const label = buildAgentSelectionLabel(agent, currentAgentName);
284
- labels.push(label);
285
- valueByLabel.set(label, agent.name);
286
- }
287
-
288
- return {
289
- labels,
290
- valueByLabel,
291
- };
292
- }
293
-
294
- export function discoverSelectableAgents(cwd: string): SelectableAgent[] {
295
- const config = loadAgentDiscoveryConfig();
296
- const projectAgentDirs = findNearestProjectAgentDirs(cwd, config.projectSourceDirs);
297
- const userAgentDirs = config.userSourceDirs
298
- .map((sourceDir) => resolveConfiguredUserPath(sourceDir))
299
- .filter((candidate) => isDirectory(candidate));
300
-
301
- const byName = new Map<string, SelectableAgent>();
302
- const precedenceOrder = [
303
- ...userAgentDirs.slice().reverse(),
304
- ...projectAgentDirs.slice().reverse(),
305
- ];
306
-
307
- for (const sourceDir of precedenceOrder) {
308
- const agents = loadAgentsFromDir(sourceDir);
309
- for (const agent of agents) {
310
- byName.set(agent.name, agent);
311
- }
312
- }
313
-
314
- return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
315
- }
316
-
317
- export async function resolveTargetAgentForSessionNix(
318
- ctx: ExtensionCommandContext,
319
- input: string | undefined,
320
- currentAgentName: string | null,
321
- ): Promise<SelectableAgent | null | undefined> {
322
- const agents = discoverSelectableAgents(ctx.cwd);
323
- if (agents.length === 0) {
324
- ctx.ui.notify(
325
- "No agents were discovered. Check pi-agent-router agent directories before using /nix agent.",
326
- "warning",
327
- );
328
- return undefined;
329
- }
330
-
331
- if (input) {
332
- const matchedAgent = agents.find((agent) => agent.name === input);
333
- if (!matchedAgent) {
334
- const agentNames = agents.map((agent) => agent.name).join(", ");
335
- ctx.ui.notify(`Unknown agent: ${input}\nAvailable agents: ${agentNames}`, "warning");
336
- return undefined;
337
- }
338
-
339
- return matchedAgent;
340
- }
341
-
342
- if (!ctx.hasUI) {
343
- ctx.ui.notify("/nix agent requires an explicit agent name in non-interactive mode.", "warning");
344
- return undefined;
345
- }
346
-
347
- const { showAgentTargetPicker } = await import("./tui/agent-target-picker.js");
348
- const selectedAgentName = await showAgentTargetPicker(ctx, agents, currentAgentName);
349
-
350
- if (!selectedAgentName) {
351
- return null;
352
- }
353
-
354
- const selectedAgent = agents.find((agent) => agent.name === selectedAgentName);
355
- if (!selectedAgent) {
356
- ctx.ui.notify("Unknown agent selection. Please try again.", "warning");
357
- return undefined;
358
- }
359
-
360
- return selectedAgent;
361
- }
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
7
+
8
+ const DEFAULT_PROJECT_SOURCE_DIRS = [".omp/agents", ".pi/agents", ".claude/agents"];
9
+ const DEFAULT_USER_SOURCE_DIRS = ["{home}/.omp/agents", "{agentDir}/agents", "{home}/.claude/agents"];
10
+ const ROUTER_CONFIG_FILE_NAME = "config.json";
11
+ const ROUTER_EXTENSION_NAME = "pi-agent-router";
12
+ const PI_AGENT_DIR_ENV_VAR = "PI_CODING_AGENT_DIR";
13
+
14
+ const SESSION_EXTENSION_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
+
16
+ type AgentMode = "primary" | "subagent" | "all";
17
+
18
+ interface AgentDiscoveryConfig {
19
+ projectSourceDirs: string[];
20
+ userSourceDirs: string[];
21
+ }
22
+
23
+ export interface SelectableAgent {
24
+ name: string;
25
+ description: string;
26
+ mode?: AgentMode;
27
+ }
28
+
29
+ interface AgentSelectionMenu {
30
+ labels: string[];
31
+ valueByLabel: Map<string, string>;
32
+ }
33
+
34
+ function normalizeStringArray(value: unknown): string[] | null {
35
+ if (!Array.isArray(value)) {
36
+ return null;
37
+ }
38
+
39
+ const normalized = value
40
+ .filter((entry): entry is string => typeof entry === "string")
41
+ .map((entry) => entry.trim())
42
+ .filter(Boolean);
43
+
44
+ return normalized.length > 0 ? normalized : null;
45
+ }
46
+
47
+ function toRecord(value: unknown): Record<string, unknown> | null {
48
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
49
+ }
50
+
51
+ function expandHomeDirectory(configuredDir: string, homeDirectory: string): string {
52
+ if (configuredDir === "~") {
53
+ return homeDirectory;
54
+ }
55
+
56
+ if (configuredDir.startsWith("~/") || configuredDir.startsWith("~\\")) {
57
+ return join(homeDirectory, stripLeadingPathSeparators(configuredDir.slice(1)));
58
+ }
59
+
60
+ return configuredDir;
61
+ }
62
+
63
+ function resolvePiAgentDir(): string {
64
+ const configuredDir = process.env[PI_AGENT_DIR_ENV_VAR]?.trim();
65
+ if (configuredDir) {
66
+ return expandHomeDirectory(configuredDir, homedir());
67
+ }
68
+
69
+ return join(homedir(), ".pi", "agent");
70
+ }
71
+
72
+ function resolveRouterConfigCandidates(): string[] {
73
+ const agentDir = resolvePiAgentDir();
74
+ const extensionParentDir = dirname(SESSION_EXTENSION_ROOT);
75
+
76
+ return [
77
+ join(extensionParentDir, ROUTER_EXTENSION_NAME, ROUTER_CONFIG_FILE_NAME),
78
+ join(agentDir, "extensions", ROUTER_EXTENSION_NAME, ROUTER_CONFIG_FILE_NAME),
79
+ ];
80
+ }
81
+
82
+ function loadAgentDiscoveryConfig(): AgentDiscoveryConfig {
83
+ for (const configPath of resolveRouterConfigCandidates()) {
84
+ try {
85
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
86
+ const config = toRecord(parsed);
87
+ const agentDiscovery = toRecord(config?.agentDiscovery);
88
+ const projectSourceDirs = normalizeStringArray(agentDiscovery?.projectSourceDirs);
89
+ const userSourceDirs = normalizeStringArray(agentDiscovery?.userSourceDirs);
90
+
91
+ return {
92
+ projectSourceDirs: projectSourceDirs ?? [...DEFAULT_PROJECT_SOURCE_DIRS],
93
+ userSourceDirs: userSourceDirs ?? [...DEFAULT_USER_SOURCE_DIRS],
94
+ };
95
+ } catch {
96
+ continue;
97
+ }
98
+ }
99
+
100
+ return {
101
+ projectSourceDirs: [...DEFAULT_PROJECT_SOURCE_DIRS],
102
+ userSourceDirs: [...DEFAULT_USER_SOURCE_DIRS],
103
+ };
104
+ }
105
+
106
+ function stripLeadingPathSeparators(value: string): string {
107
+ return value.replace(/^[\\/]+/, "");
108
+ }
109
+
110
+ function resolveConfiguredUserPath(rawPath: string): string {
111
+ const trimmed = rawPath.trim();
112
+ if (!trimmed) {
113
+ return resolve(resolvePiAgentDir(), "agents");
114
+ }
115
+
116
+ if (trimmed === "~") {
117
+ return homedir();
118
+ }
119
+
120
+ if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
121
+ return join(homedir(), stripLeadingPathSeparators(trimmed.slice(1)));
122
+ }
123
+
124
+ if (trimmed === "{home}") {
125
+ return homedir();
126
+ }
127
+
128
+ if (trimmed.startsWith("{home}/") || trimmed.startsWith("{home}\\")) {
129
+ return join(homedir(), stripLeadingPathSeparators(trimmed.slice("{home}".length)));
130
+ }
131
+
132
+ if (trimmed === "{agentDir}") {
133
+ return resolvePiAgentDir();
134
+ }
135
+
136
+ if (trimmed.startsWith("{agentDir}/") || trimmed.startsWith("{agentDir}\\")) {
137
+ return join(resolvePiAgentDir(), stripLeadingPathSeparators(trimmed.slice("{agentDir}".length)));
138
+ }
139
+
140
+ return resolve(trimmed);
141
+ }
142
+
143
+ function isDirectory(path: string): boolean {
144
+ try {
145
+ readdirSync(path);
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ function findNearestProjectAgentDirs(cwd: string, projectSourceDirs: readonly string[]): string[] {
153
+ let currentDir = resolve(cwd);
154
+
155
+ while (true) {
156
+ const candidates = projectSourceDirs
157
+ .map((sourceDir) => resolve(currentDir, sourceDir))
158
+ .filter((candidate) => isDirectory(candidate));
159
+
160
+ if (candidates.length > 0) {
161
+ return candidates;
162
+ }
163
+
164
+ const parentDir = dirname(currentDir);
165
+ if (parentDir === currentDir) {
166
+ return [];
167
+ }
168
+
169
+ currentDir = parentDir;
170
+ }
171
+ }
172
+
173
+ function parseAgentMode(value: unknown): AgentMode | undefined {
174
+ if (typeof value !== "string") {
175
+ return undefined;
176
+ }
177
+
178
+ const normalized = value.trim().toLowerCase();
179
+ if (normalized === "primary" || normalized === "subagent" || normalized === "all") {
180
+ return normalized;
181
+ }
182
+
183
+ return undefined;
184
+ }
185
+
186
+ function parseFrontmatter(content: string): Record<string, string> | null {
187
+ const normalized = content.replace(/\r\n/g, "\n");
188
+ if (!normalized.startsWith("---\n")) {
189
+ return null;
190
+ }
191
+
192
+ const end = normalized.indexOf("\n---", 4);
193
+ if (end === -1) {
194
+ return null;
195
+ }
196
+
197
+ const frontmatter: Record<string, string> = {};
198
+ const lines = normalized.slice(4, end).split("\n");
199
+
200
+ for (const line of lines) {
201
+ const separatorIndex = line.indexOf(":");
202
+ if (separatorIndex === -1) {
203
+ continue;
204
+ }
205
+
206
+ const key = line.slice(0, separatorIndex).trim();
207
+ const value = line.slice(separatorIndex + 1).trim().replace(/^['\"]|['\"]$/g, "");
208
+ if (key) {
209
+ frontmatter[key] = value;
210
+ }
211
+ }
212
+
213
+ return frontmatter;
214
+ }
215
+
216
+ function parseAgentFile(filePath: string): SelectableAgent | null {
217
+ try {
218
+ const content = readFileSync(filePath, "utf-8");
219
+ const frontmatter = parseFrontmatter(content);
220
+ if (!frontmatter?.name) {
221
+ return null;
222
+ }
223
+
224
+ return {
225
+ name: frontmatter.name,
226
+ description: frontmatter.description || `Agent ${frontmatter.name}`,
227
+ mode: parseAgentMode(frontmatter.mode),
228
+ };
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+
234
+ function loadAgentsFromDir(dirPath: string): SelectableAgent[] {
235
+ try {
236
+ return readdirSync(dirPath)
237
+ .filter((entry) => entry.endsWith(".md"))
238
+ .map((entry) => parseAgentFile(join(dirPath, entry)))
239
+ .filter((agent): agent is SelectableAgent => Boolean(agent));
240
+ } catch {
241
+ return [];
242
+ }
243
+ }
244
+
245
+ function truncateDescription(description: string, maxLength = 72): string {
246
+ const normalized = description.trim().replace(/\s+/g, " ");
247
+ if (normalized.length <= maxLength) {
248
+ return normalized;
249
+ }
250
+
251
+ return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
252
+ }
253
+
254
+ function formatModeBadge(agent: SelectableAgent): string {
255
+ return `[${agent.mode ?? "primary"}]`;
256
+ }
257
+
258
+ function formatCurrentMarker(currentAgentName: string | null, candidateAgentName: string): string {
259
+ return currentAgentName === candidateAgentName ? "●" : "○";
260
+ }
261
+
262
+ function buildAgentSelectionLabel(
263
+ agent: SelectableAgent,
264
+ currentAgentName: string | null,
265
+ ): string {
266
+ return [
267
+ formatCurrentMarker(currentAgentName, agent.name),
268
+ agent.name,
269
+ formatModeBadge(agent),
270
+ "—",
271
+ truncateDescription(agent.description),
272
+ ].join(" ");
273
+ }
274
+
275
+ export function buildAgentSelectionMenu(
276
+ agents: readonly SelectableAgent[],
277
+ currentAgentName: string | null,
278
+ ): AgentSelectionMenu {
279
+ const labels: string[] = [];
280
+ const valueByLabel = new Map<string, string>();
281
+
282
+ for (const agent of agents) {
283
+ const label = buildAgentSelectionLabel(agent, currentAgentName);
284
+ labels.push(label);
285
+ valueByLabel.set(label, agent.name);
286
+ }
287
+
288
+ return {
289
+ labels,
290
+ valueByLabel,
291
+ };
292
+ }
293
+
294
+ export function discoverSelectableAgents(cwd: string): SelectableAgent[] {
295
+ const config = loadAgentDiscoveryConfig();
296
+ const projectAgentDirs = findNearestProjectAgentDirs(cwd, config.projectSourceDirs);
297
+ const userAgentDirs = config.userSourceDirs
298
+ .map((sourceDir) => resolveConfiguredUserPath(sourceDir))
299
+ .filter((candidate) => isDirectory(candidate));
300
+
301
+ const byName = new Map<string, SelectableAgent>();
302
+ const precedenceOrder = [
303
+ ...userAgentDirs.slice().reverse(),
304
+ ...projectAgentDirs.slice().reverse(),
305
+ ];
306
+
307
+ for (const sourceDir of precedenceOrder) {
308
+ const agents = loadAgentsFromDir(sourceDir);
309
+ for (const agent of agents) {
310
+ byName.set(agent.name, agent);
311
+ }
312
+ }
313
+
314
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
315
+ }
316
+
317
+ export async function resolveTargetAgentForSessionNix(
318
+ ctx: ExtensionCommandContext,
319
+ input: string | undefined,
320
+ currentAgentName: string | null,
321
+ ): Promise<SelectableAgent | null | undefined> {
322
+ const agents = discoverSelectableAgents(ctx.cwd);
323
+ if (agents.length === 0) {
324
+ ctx.ui.notify(
325
+ "No agents were discovered. Check pi-agent-router agent directories before using /nix agent.",
326
+ "warning",
327
+ );
328
+ return undefined;
329
+ }
330
+
331
+ if (input) {
332
+ const matchedAgent = agents.find((agent) => agent.name === input);
333
+ if (!matchedAgent) {
334
+ const agentNames = agents.map((agent) => agent.name).join(", ");
335
+ ctx.ui.notify(`Unknown agent: ${input}\nAvailable agents: ${agentNames}`, "warning");
336
+ return undefined;
337
+ }
338
+
339
+ return matchedAgent;
340
+ }
341
+
342
+ if (!ctx.hasUI) {
343
+ ctx.ui.notify("/nix agent requires an explicit agent name in non-interactive mode.", "warning");
344
+ return undefined;
345
+ }
346
+
347
+ const { showAgentTargetPicker } = await import("./tui/agent-target-picker.js");
348
+ const selectedAgentName = await showAgentTargetPicker(ctx, agents, currentAgentName);
349
+
350
+ if (!selectedAgentName) {
351
+ return null;
352
+ }
353
+
354
+ const selectedAgent = agents.find((agent) => agent.name === selectedAgentName);
355
+ if (!selectedAgent) {
356
+ ctx.ui.notify("Unknown agent selection. Please try again.", "warning");
357
+ return undefined;
358
+ }
359
+
360
+ return selectedAgent;
361
+ }