@pi-archimedes/subagent 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/subagent",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "main": "./src/index.ts",
13
13
  "dependencies": {
14
- "@pi-archimedes/core": "1.0.0"
14
+ "@pi-archimedes/core": "1.1.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@earendil-works/pi-coding-agent": ">=0.1.0",
@@ -54,8 +54,10 @@ interface ToolInfo {
54
54
  interface ManagerState {
55
55
  screen: "list" | "detail" | "edit" | "name-input" | "confirm-delete";
56
56
  agents: AgentConfig[];
57
+ globalAgents: AgentConfig[];
57
58
  userAgents: AgentConfig[];
58
59
  projectAgents: AgentConfig[];
60
+ globalDir: string | null;
59
61
  userDir: string;
60
62
  projectDir: string | null;
61
63
 
@@ -86,7 +88,7 @@ interface ManagerState {
86
88
  // Name input state
87
89
  nameInputBuffer: string;
88
90
  nameInputCursor: number;
89
- nameInputScope: "user" | "project";
91
+ nameInputScope: "global" | "user" | "project";
90
92
  nameInputMode: "new" | "clone";
91
93
  nameInputSource: AgentConfig | null;
92
94
  nameInputError: string | null;
@@ -188,7 +190,8 @@ function renderFooter(text: string, width: number, theme: Theme): string {
188
190
  return theme.fg("dim", padEnd(text, width));
189
191
  }
190
192
 
191
- function scopeLabel(source: "user" | "project"): string {
193
+ function scopeLabel(source: "global" | "user" | "project"): string {
194
+ if (source === "global") return "home";
192
195
  return source === "user" ? "user" : "proj";
193
196
  }
194
197
 
@@ -1225,7 +1228,9 @@ function saveAgent(state: ManagerState, requestRender: () => void): void {
1225
1228
  }
1226
1229
 
1227
1230
  // Determine target directory
1228
- const dir = agent.source === "user" ? state.userDir : state.projectDir;
1231
+ const dir = agent.source === "global" ? state.globalDir
1232
+ : agent.source === "user" ? state.userDir
1233
+ : state.projectDir;
1229
1234
  if (!dir) {
1230
1235
  state.editError = "Target directory not available";
1231
1236
  requestRender();
@@ -1259,9 +1264,11 @@ function saveAgent(state: ManagerState, requestRender: () => void): void {
1259
1264
  // Refresh agents list
1260
1265
  const cwd = process.cwd();
1261
1266
  const discovery = discoverAgentsAll(cwd);
1267
+ state.globalAgents = discovery.global;
1262
1268
  state.userAgents = discovery.user;
1263
1269
  state.projectAgents = discovery.project;
1264
- state.agents = [...discovery.user, ...discovery.project];
1270
+ state.globalDir = discovery.globalDir;
1271
+ state.agents = [...discovery.global, ...discovery.user, ...discovery.project];
1265
1272
 
1266
1273
  // Find the saved agent and switch to detail
1267
1274
  const savedAgent = state.agents.find((a) => a.name === agent.name && a.source === agent.source);
@@ -1305,12 +1312,22 @@ function renderNameInput(state: ManagerState, width: number, theme: Theme): stri
1305
1312
  const scopeText = `Scope: [${state.nameInputScope}] [tab] toggle`;
1306
1313
  lines.push(padEnd(theme.fg("dim", scopeText), width));
1307
1314
 
1308
- // Cross-scope collision warning
1309
- const otherScope = state.nameInputScope === "user" ? "project" : "user";
1310
- const otherScopeAgents = otherScope === "user" ? state.userAgents : state.projectAgents;
1311
- const collisionAgent = otherScopeAgents.find((a) => a.name === state.nameInputBuffer.trim());
1315
+ // Cross-scope collision warning — check higher-precedence scopes
1316
+ const scopeOrder = ["global", "user", "project"];
1317
+ const currentIdx = scopeOrder.indexOf(state.nameInputScope);
1318
+ let collisionAgent: AgentConfig | undefined;
1319
+ let collisionScope: string | undefined;
1320
+ for (const scope of scopeOrder.slice(currentIdx + 1)) {
1321
+ const scopeAgents = scope === "global" ? state.globalAgents : scope === "user" ? state.userAgents : state.projectAgents;
1322
+ const found = scopeAgents.find((a) => a.name === state.nameInputBuffer.trim());
1323
+ if (found) {
1324
+ collisionAgent = found;
1325
+ collisionScope = scope;
1326
+ break;
1327
+ }
1328
+ }
1312
1329
  if (collisionAgent) {
1313
- lines.push(padEnd(theme.fg("warning", `Warning: a ${otherScope} agent "${collisionAgent.name}" exists and will take precedence`), width));
1330
+ lines.push(padEnd(theme.fg("warning", `Warning: a ${collisionScope} agent "${collisionAgent.name}" exists and will take precedence`), width));
1314
1331
  } else if (state.nameInputError) {
1315
1332
  lines.push(padEnd(theme.fg("error", ` ${state.nameInputError}`), width));
1316
1333
  } else {
@@ -1329,8 +1346,13 @@ function handleNameInput(
1329
1346
  requestRender: () => void,
1330
1347
  ): void {
1331
1348
  if (matchesKey(data, Key.tab)) {
1332
- state.nameInputScope = state.nameInputScope === "user" ? "project" : "user";
1333
- if (state.nameInputScope === "project" && !state.projectDir) {
1349
+ const scopes = ["global", "user", "project"];
1350
+ const currentIdx = scopes.indexOf(state.nameInputScope);
1351
+ const nextIdx = (currentIdx + 1) % scopes.length;
1352
+ state.nameInputScope = scopes[nextIdx] as "global" | "user" | "project";
1353
+ if (state.nameInputScope === "global" && !state.globalDir) {
1354
+ state.nameInputError = "No global agents directory found";
1355
+ } else if (state.nameInputScope === "project" && !state.projectDir) {
1334
1356
  state.nameInputError = "No project agents directory found";
1335
1357
  } else {
1336
1358
  state.nameInputError = null;
@@ -1363,6 +1385,11 @@ function handleNameInput(
1363
1385
  requestRender();
1364
1386
  return;
1365
1387
  }
1388
+ if (state.nameInputScope === "global" && !state.globalDir) {
1389
+ state.nameInputError = "No global agents directory found";
1390
+ requestRender();
1391
+ return;
1392
+ }
1366
1393
  if (state.nameInputScope === "project" && !state.projectDir) {
1367
1394
  state.nameInputError = "No project agents directory found";
1368
1395
  requestRender();
@@ -1379,7 +1406,9 @@ function handleNameInput(
1379
1406
  return;
1380
1407
  }
1381
1408
 
1382
- const dir = state.nameInputScope === "user" ? state.userDir : state.projectDir;
1409
+ const dir = state.nameInputScope === "global" ? state.globalDir
1410
+ : state.nameInputScope === "user" ? state.userDir
1411
+ : state.projectDir;
1383
1412
  if (!dir) {
1384
1413
  state.nameInputError = "Target directory not available";
1385
1414
  requestRender();
@@ -1486,9 +1515,11 @@ function handleConfirmDelete(
1486
1515
  // Refresh agents list
1487
1516
  const cwd = process.cwd();
1488
1517
  const discovery = discoverAgentsAll(cwd);
1518
+ state.globalAgents = discovery.global;
1489
1519
  state.userAgents = discovery.user;
1490
1520
  state.projectAgents = discovery.project;
1491
- state.agents = [...discovery.user, ...discovery.project];
1521
+ state.globalDir = discovery.globalDir;
1522
+ state.agents = [...discovery.global, ...discovery.user, ...discovery.project];
1492
1523
 
1493
1524
  state.listCursor = 0;
1494
1525
  state.listScroll = 0;
@@ -1506,8 +1537,10 @@ function handleConfirmDelete(
1506
1537
  // ── Main factory ────────────────────────────────────────────────────────────
1507
1538
 
1508
1539
  export function createAgentManager(
1540
+ globalAgents: AgentConfig[],
1509
1541
  userAgents: AgentConfig[],
1510
1542
  projectAgents: AgentConfig[],
1543
+ globalDir: string | null,
1511
1544
  userDir: string,
1512
1545
  projectDir: string | null,
1513
1546
  tui: TUIContext,
@@ -1518,9 +1551,11 @@ export function createAgentManager(
1518
1551
  ): Component {
1519
1552
  const state: ManagerState = {
1520
1553
  screen: "list",
1521
- agents: [...userAgents, ...projectAgents],
1554
+ agents: [...globalAgents, ...userAgents, ...projectAgents],
1555
+ globalAgents,
1522
1556
  userAgents,
1523
1557
  projectAgents,
1558
+ globalDir,
1524
1559
  userDir,
1525
1560
  projectDir,
1526
1561
 
package/src/agents.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as fs from "node:fs";
7
+ import * as os from "node:os";
7
8
  import * as path from "node:path";
8
9
  import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
9
10
 
@@ -14,13 +15,13 @@ export interface AgentConfig {
14
15
  model?: string;
15
16
  thinking?: string;
16
17
  systemPrompt: string;
17
- source: "user" | "project";
18
+ source: "global" | "user" | "project";
18
19
  filePath: string;
19
20
  // Extra fields preserved from frontmatter but not editable in TUI
20
21
  extraFields?: Record<string, unknown>;
21
22
  }
22
23
 
23
- function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
24
+ function loadAgentsFromDir(dir: string, source: "global" | "user" | "project"): AgentConfig[] {
24
25
  const agents: AgentConfig[] = [];
25
26
 
26
27
  if (!fs.existsSync(dir)) return agents;
@@ -102,28 +103,55 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
102
103
  }
103
104
  }
104
105
 
106
+ function findNearestAgentsDir(cwd: string): string | null {
107
+ // Walk up looking for .agents/agents inside a git repo
108
+ let currentDir = cwd;
109
+ while (true) {
110
+ const gitPath = path.join(currentDir, ".git");
111
+ if (fs.existsSync(gitPath)) {
112
+ const projectAgentsDir = path.join(currentDir, ".agents", "agents");
113
+ if (fs.existsSync(projectAgentsDir)) return projectAgentsDir;
114
+ }
115
+
116
+ const parentDir = path.dirname(currentDir);
117
+ if (parentDir === currentDir) break;
118
+ currentDir = parentDir;
119
+ }
120
+
121
+ // Fallback: ~/.agents/agents
122
+ const homeAgentsDir = path.join(os.homedir(), ".agents", "agents");
123
+ if (fs.existsSync(homeAgentsDir)) return homeAgentsDir;
124
+ return null;
125
+ }
126
+
105
127
  /**
106
128
  * Result of discovering agents — separated by source with directory paths.
107
129
  */
108
130
  export interface AgentsDiscoveryResult {
131
+ global: AgentConfig[];
109
132
  user: AgentConfig[];
110
133
  project: AgentConfig[];
111
- userDir: string; // e.g., ~/.pi/agent/agents
112
- projectDir: string | null; // e.g., .pi/agents or null if not found
134
+ globalDir: string | null; // e.g., .agents/agents or null if not found
135
+ userDir: string; // e.g., ~/.pi/agent/agents
136
+ projectDir: string | null; // e.g., .pi/agents or null if not found
113
137
  }
114
138
 
115
139
  /**
116
- * Discover all available agents from user and/or project directories.
140
+ * Discover all available agents from global, user and/or project directories.
141
+ * Precedence (highest last): global < user < project
117
142
  */
118
143
  export function discoverAgents(cwd: string): AgentConfig[] {
144
+ const globalDir = findNearestAgentsDir(cwd);
119
145
  const userDir = path.join(getAgentDir(), "agents");
120
146
  const projectDir = findNearestProjectAgentsDir(cwd);
121
147
 
148
+ const globalAgents = globalDir ? loadAgentsFromDir(globalDir, "global") : [];
122
149
  const userAgents = loadAgentsFromDir(userDir, "user");
123
150
  const projectAgents = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
124
151
 
125
- // Project agents override user agents with the same name
152
+ // Later sources override earlier: global < user < project
126
153
  const agentMap = new Map<string, AgentConfig>();
154
+ for (const agent of globalAgents) agentMap.set(agent.name, agent);
127
155
  for (const agent of userAgents) agentMap.set(agent.name, agent);
128
156
  for (const agent of projectAgents) agentMap.set(agent.name, agent);
129
157
 
@@ -134,15 +162,19 @@ export function discoverAgents(cwd: string): AgentConfig[] {
134
162
  * Discover agents and return them separated by source with directory paths.
135
163
  */
136
164
  export function discoverAgentsAll(cwd: string): AgentsDiscoveryResult {
165
+ const globalDir = findNearestAgentsDir(cwd);
137
166
  const userDir = path.join(getAgentDir(), "agents");
138
167
  const projectDir = findNearestProjectAgentsDir(cwd);
139
168
 
169
+ const globalAgents = globalDir ? loadAgentsFromDir(globalDir, "global") : [];
140
170
  const userAgents = loadAgentsFromDir(userDir, "user");
141
171
  const projectAgents = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
142
172
 
143
173
  return {
174
+ global: globalAgents,
144
175
  user: userAgents,
145
176
  project: projectAgents,
177
+ globalDir,
146
178
  userDir,
147
179
  projectDir,
148
180
  };
package/src/index.ts CHANGED
@@ -266,7 +266,7 @@ export function registerAgentsCommand(pi: ExtensionAPI): void {
266
266
  pi.registerCommand("agents", {
267
267
  description: "Open the Agents Manager",
268
268
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
269
- const { user, project, userDir, projectDir } = discoverAgentsAll(ctx.cwd);
269
+ const { global: globalAgents, user, project, globalDir, userDir, projectDir } = discoverAgentsAll(ctx.cwd);
270
270
 
271
271
  const availableModels = ctx.modelRegistry.getAvailable().map((m) => ({
272
272
  id: m.id,
@@ -281,7 +281,7 @@ export function registerAgentsCommand(pi: ExtensionAPI): void {
281
281
 
282
282
  await ctx.ui.custom<void>(
283
283
  (tui: TUI, theme: Theme, _keybindings, done: () => void) => {
284
- return createAgentManager(user, project, userDir, projectDir, tui, theme, done, availableModels, availableTools);
284
+ return createAgentManager(globalAgents, user, project, globalDir, userDir, projectDir, tui, theme, done, availableModels, availableTools);
285
285
  },
286
286
  { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
287
287
  );