pi-teams 0.9.13 → 0.9.14
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/extensions/index.ts +1 -1
- package/package.json +1 -1
- package/src/adapters/cmux-adapter.test.ts +70 -35
- package/src/adapters/cmux-adapter.ts +70 -15
- package/src/utils/models.test.ts +8 -0
- package/src/utils/models.ts +5 -1
- package/src/utils/predefined-teams.test.ts +123 -1
- package/src/utils/predefined-teams.ts +22 -14
package/extensions/index.ts
CHANGED
|
@@ -554,7 +554,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
554
554
|
prompt: Type.String(),
|
|
555
555
|
cwd: Type.String(),
|
|
556
556
|
model: Type.Optional(Type.String()),
|
|
557
|
-
thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high"])),
|
|
557
|
+
thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"])),
|
|
558
558
|
plan_mode_required: Type.Optional(Type.Boolean({ default: false })),
|
|
559
559
|
separate_window: Type.Optional(Type.Boolean({ default: false })),
|
|
560
560
|
}),
|
package/package.json
CHANGED
|
@@ -62,12 +62,16 @@ describe("CmuxAdapter", () => {
|
|
|
62
62
|
process.env.CMUX_SOCKET_PATH = "/tmp/cmux.sock";
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it("should spawn
|
|
66
|
-
mockExecCommand
|
|
67
|
-
|
|
68
|
-
stderr: "",
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
it("should spawn via new-split + poll + respawn-pane and return the new surface", () => {
|
|
66
|
+
mockExecCommand
|
|
67
|
+
// 1. listSurfaceRefs (before snapshot) — realistic cmux output
|
|
68
|
+
.mockReturnValueOnce({ stdout: "* surface:1 Terminal [selected]\n surface:2 Terminal\n", stderr: "", status: 0 })
|
|
69
|
+
// 2. new-split right
|
|
70
|
+
.mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 })
|
|
71
|
+
// 3. listSurfaceRefs (poll — finds new surface:42)
|
|
72
|
+
.mockReturnValueOnce({ stdout: "* surface:1 Terminal [selected]\n surface:2 Terminal\n surface:42 Terminal\n", stderr: "", status: 0 })
|
|
73
|
+
// 4. respawn-pane
|
|
74
|
+
.mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 });
|
|
71
75
|
|
|
72
76
|
const result = adapter.spawn({
|
|
73
77
|
name: "test-agent",
|
|
@@ -76,19 +80,25 @@ describe("CmuxAdapter", () => {
|
|
|
76
80
|
env: { PI_AGENT_ID: "test-123" },
|
|
77
81
|
});
|
|
78
82
|
|
|
79
|
-
expect(result).toBe("surface
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
expect(result).toBe("surface:42");
|
|
84
|
+
|
|
85
|
+
// Verify new-split was called without --command
|
|
86
|
+
expect(mockExecCommand).toHaveBeenCalledWith("cmux", ["new-split", "right"]);
|
|
87
|
+
|
|
88
|
+
// Verify respawn-pane was called with the new surface and full command
|
|
89
|
+
expect(mockExecCommand).toHaveBeenCalledWith("cmux", [
|
|
90
|
+
"respawn-pane",
|
|
91
|
+
"--surface", "surface:42",
|
|
92
|
+
"--command", "env PI_AGENT_ID=test-123 pi --agent test",
|
|
93
|
+
]);
|
|
84
94
|
});
|
|
85
95
|
|
|
86
96
|
it("should spawn without env prefix when no PI_ vars", () => {
|
|
87
|
-
mockExecCommand
|
|
88
|
-
stdout: "
|
|
89
|
-
stderr: "",
|
|
90
|
-
status: 0
|
|
91
|
-
|
|
97
|
+
mockExecCommand
|
|
98
|
+
.mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
|
|
99
|
+
.mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 })
|
|
100
|
+
.mockReturnValueOnce({ stdout: " surface:1 Terminal\n surface:99 Terminal\n", stderr: "", status: 0 })
|
|
101
|
+
.mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 });
|
|
92
102
|
|
|
93
103
|
const result = adapter.spawn({
|
|
94
104
|
name: "test-agent",
|
|
@@ -97,19 +107,20 @@ describe("CmuxAdapter", () => {
|
|
|
97
107
|
env: { OTHER: "ignored" },
|
|
98
108
|
});
|
|
99
109
|
|
|
100
|
-
expect(result).toBe("surface
|
|
101
|
-
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
expect(result).toBe("surface:99");
|
|
111
|
+
expect(mockExecCommand).toHaveBeenCalledWith("cmux", [
|
|
112
|
+
"respawn-pane",
|
|
113
|
+
"--surface", "surface:99",
|
|
114
|
+
"--command", "pi",
|
|
115
|
+
]);
|
|
105
116
|
});
|
|
106
117
|
|
|
107
|
-
it("should throw on
|
|
108
|
-
mockExecCommand
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
it("should throw on new-split failure", () => {
|
|
119
|
+
mockExecCommand
|
|
120
|
+
// listSurfaceRefs (before)
|
|
121
|
+
.mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
|
|
122
|
+
// new-split fails
|
|
123
|
+
.mockReturnValueOnce({ stdout: "", stderr: "cmux not found", status: 1 });
|
|
113
124
|
|
|
114
125
|
expect(() => adapter.spawn({
|
|
115
126
|
name: "test-agent",
|
|
@@ -119,19 +130,43 @@ describe("CmuxAdapter", () => {
|
|
|
119
130
|
})).toThrow("cmux new-split failed with status 1");
|
|
120
131
|
});
|
|
121
132
|
|
|
122
|
-
it("should throw
|
|
123
|
-
mockExecCommand
|
|
124
|
-
|
|
125
|
-
stderr: "",
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
it("should throw when new surface is not found after polling", () => {
|
|
134
|
+
mockExecCommand
|
|
135
|
+
// listSurfaceRefs (before)
|
|
136
|
+
.mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
|
|
137
|
+
// new-split OK
|
|
138
|
+
.mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 });
|
|
139
|
+
|
|
140
|
+
// All subsequent polls return the same surfaces (no new one appears)
|
|
141
|
+
// Each poll cycle = listSurfaceRefs + sleep
|
|
142
|
+
for (let i = 0; i < 20; i++) {
|
|
143
|
+
mockExecCommand
|
|
144
|
+
.mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 }) // listSurfaceRefs
|
|
145
|
+
.mockReturnValueOnce({ stdout: "", stderr: "", status: 0 }); // sleep
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
expect(() => adapter.spawn({
|
|
149
|
+
name: "test-agent",
|
|
150
|
+
cwd: "/home/user/project",
|
|
151
|
+
command: "pi",
|
|
152
|
+
env: {},
|
|
153
|
+
})).toThrow("new surface was not found");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should throw when respawn-pane fails", () => {
|
|
157
|
+
mockExecCommand
|
|
158
|
+
.mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
|
|
159
|
+
.mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 })
|
|
160
|
+
.mockReturnValueOnce({ stdout: " surface:1 Terminal\n surface:50 Terminal\n", stderr: "", status: 0 })
|
|
161
|
+
// respawn-pane fails
|
|
162
|
+
.mockReturnValueOnce({ stdout: "", stderr: "respawn error", status: 1 });
|
|
128
163
|
|
|
129
164
|
expect(() => adapter.spawn({
|
|
130
165
|
name: "test-agent",
|
|
131
166
|
cwd: "/home/user/project",
|
|
132
167
|
command: "pi",
|
|
133
168
|
env: {},
|
|
134
|
-
})).toThrow("cmux
|
|
169
|
+
})).toThrow("cmux respawn-pane failed with status 1");
|
|
135
170
|
});
|
|
136
171
|
});
|
|
137
172
|
|
|
@@ -306,4 +341,4 @@ describe("CmuxAdapter", () => {
|
|
|
306
341
|
expect(adapter.isWindowAlive(undefined as unknown as string)).toBe(false);
|
|
307
342
|
});
|
|
308
343
|
});
|
|
309
|
-
});
|
|
344
|
+
});
|
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
* CMUX Terminal Adapter
|
|
3
3
|
*
|
|
4
4
|
* Implements the TerminalAdapter interface for CMUX (cmux.dev).
|
|
5
|
+
*
|
|
6
|
+
* Spawn strategy: cmux's `new-split` does not support a `--command` flag.
|
|
7
|
+
* We follow the proven pattern from pi-cmux (npm:pi-cmux):
|
|
8
|
+
* 1. Snapshot existing surfaces
|
|
9
|
+
* 2. `cmux new-split <direction>`
|
|
10
|
+
* 3. Poll `cmux list-pane-surfaces` to find the newly created surface
|
|
11
|
+
* 4. `cmux respawn-pane --surface <id> --command <cmd>` to run the command
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
14
|
import { TerminalAdapter, SpawnOptions, execCommand } from "../utils/terminal-adapter";
|
|
8
15
|
|
|
16
|
+
const SURFACE_POLL_ATTEMPTS = 20;
|
|
17
|
+
const SURFACE_POLL_DELAY_MS = 150;
|
|
18
|
+
|
|
9
19
|
export class CmuxAdapter implements TerminalAdapter {
|
|
10
20
|
readonly name = "cmux";
|
|
11
21
|
|
|
@@ -18,34 +28,79 @@ export class CmuxAdapter implements TerminalAdapter {
|
|
|
18
28
|
return !!process.env.CMUX_SOCKET_PATH || !!process.env.CMUX_WORKSPACE_ID;
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
/**
|
|
32
|
+
* List all surface refs currently visible in the workspace.
|
|
33
|
+
*/
|
|
34
|
+
private listSurfaceRefs(): Set<string> {
|
|
35
|
+
const refs = new Set<string>();
|
|
36
|
+
try {
|
|
37
|
+
const result = execCommand("cmux", ["list-pane-surfaces"]);
|
|
38
|
+
if (result.status === 0) {
|
|
39
|
+
for (const line of result.stdout.split("\n")) {
|
|
40
|
+
// Output lines look like: "* surface:5 ⠹ π · ziahmco [selected]"
|
|
41
|
+
// Extract the surface:N ref from each line.
|
|
42
|
+
const match = line.match(/\b(surface:\d+)\b/);
|
|
43
|
+
if (match) refs.add(match[1]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Ignore
|
|
48
|
+
}
|
|
49
|
+
return refs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Block until a new surface appears that was not in `before`, or give up.
|
|
54
|
+
*/
|
|
55
|
+
private waitForNewSurface(before: Set<string>): string | null {
|
|
56
|
+
for (let i = 0; i < SURFACE_POLL_ATTEMPTS; i++) {
|
|
57
|
+
const current = this.listSurfaceRefs();
|
|
58
|
+
for (const ref of current) {
|
|
59
|
+
if (!before.has(ref)) return ref;
|
|
60
|
+
}
|
|
61
|
+
// spawnSync-based sleep — keeps the adapter synchronous
|
|
62
|
+
execCommand("sleep", [String(SURFACE_POLL_DELAY_MS / 1000)]);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
21
67
|
spawn(options: SpawnOptions): string {
|
|
22
|
-
//
|
|
23
|
-
// CMUX doesn't have a direct 'spawn' that returns a pane ID and runs a command
|
|
24
|
-
// in one go while also returning the ID in a way we can easily capture for 'isAlive'.
|
|
25
|
-
// However, 'new-split' returns the new surface ID.
|
|
26
|
-
|
|
27
|
-
// Construct the command with environment variables
|
|
68
|
+
// Construct the full command with PI_ environment variables
|
|
28
69
|
const envPrefix = Object.entries(options.env)
|
|
29
70
|
.filter(([k]) => k.startsWith("PI_"))
|
|
30
71
|
.map(([k, v]) => `${k}=${v}`)
|
|
31
72
|
.join(" ");
|
|
32
|
-
|
|
73
|
+
|
|
33
74
|
const fullCommand = envPrefix ? `env ${envPrefix} ${options.command}` : options.command;
|
|
34
75
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
37
|
-
|
|
76
|
+
// 1. Snapshot existing surfaces before the split
|
|
77
|
+
const before = this.listSurfaceRefs();
|
|
78
|
+
|
|
79
|
+
// 2. Create the split (without --command, which is not supported)
|
|
80
|
+
const splitResult = execCommand("cmux", ["new-split", "right"]);
|
|
81
|
+
|
|
38
82
|
if (splitResult.status !== 0) {
|
|
39
83
|
throw new Error(`cmux new-split failed with status ${splitResult.status}: ${splitResult.stderr}`);
|
|
40
84
|
}
|
|
41
85
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
86
|
+
// 3. Poll for the newly created surface
|
|
87
|
+
const newSurface = this.waitForNewSurface(before);
|
|
88
|
+
if (!newSurface) {
|
|
89
|
+
throw new Error("cmux new-split succeeded but new surface was not found");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. Use respawn-pane to run the command in the new surface
|
|
93
|
+
const respawnResult = execCommand("cmux", [
|
|
94
|
+
"respawn-pane",
|
|
95
|
+
"--surface", newSurface,
|
|
96
|
+
"--command", fullCommand,
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
if (respawnResult.status !== 0) {
|
|
100
|
+
throw new Error(`cmux respawn-pane failed with status ${respawnResult.status}: ${respawnResult.stderr}`);
|
|
46
101
|
}
|
|
47
102
|
|
|
48
|
-
|
|
103
|
+
return newSurface;
|
|
49
104
|
}
|
|
50
105
|
|
|
51
106
|
kill(paneId: string): void {
|
package/src/utils/models.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
2
|
+
|
|
3
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
4
|
+
|
|
1
5
|
export interface Member {
|
|
2
6
|
agentId: string;
|
|
3
7
|
name: string;
|
|
@@ -10,7 +14,7 @@ export interface Member {
|
|
|
10
14
|
subscriptions: any[];
|
|
11
15
|
prompt?: string;
|
|
12
16
|
color?: string;
|
|
13
|
-
thinking?:
|
|
17
|
+
thinking?: ThinkingLevel;
|
|
14
18
|
planModeRequired?: boolean;
|
|
15
19
|
backendType?: string;
|
|
16
20
|
isActive?: boolean;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getAllPredefinedTeams,
|
|
12
12
|
getAgentDefinition,
|
|
13
13
|
getPredefinedTeam,
|
|
14
|
+
saveTeamTemplate,
|
|
14
15
|
} from "./predefined-teams";
|
|
15
16
|
|
|
16
17
|
describe("parseAgentFrontmatter", () => {
|
|
@@ -256,7 +257,8 @@ full:
|
|
|
256
257
|
|
|
257
258
|
describe("getAllAgentDefinitions and getAllPredefinedTeams", () => {
|
|
258
259
|
const globalDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
259
|
-
const globalTeamsDir = path.join(os.homedir(), ".pi"
|
|
260
|
+
const globalTeamsDir = path.join(os.homedir(), ".pi");
|
|
261
|
+
const legacyGlobalTeamsDir = path.join(os.homedir(), ".pi", "agent");
|
|
260
262
|
const projectDir = path.join(os.tmpdir(), "pi-teams-test-project-" + Date.now());
|
|
261
263
|
const projectAgentsDir = path.join(projectDir, ".pi", "agents");
|
|
262
264
|
const projectTeamsDir = path.join(projectDir, ".pi");
|
|
@@ -264,6 +266,7 @@ describe("getAllAgentDefinitions and getAllPredefinedTeams", () => {
|
|
|
264
266
|
// Store original files to restore later
|
|
265
267
|
let originalGlobalAgents: string[] = [];
|
|
266
268
|
let originalGlobalTeams: string | null = null;
|
|
269
|
+
let originalLegacyGlobalTeams: string | null = null;
|
|
267
270
|
|
|
268
271
|
beforeEach(() => {
|
|
269
272
|
// Create project directory
|
|
@@ -279,12 +282,31 @@ describe("getAllAgentDefinitions and getAllPredefinedTeams", () => {
|
|
|
279
282
|
if (fs.existsSync(path.join(globalTeamsDir, "teams.yaml"))) {
|
|
280
283
|
originalGlobalTeams = fs.readFileSync(path.join(globalTeamsDir, "teams.yaml"), "utf-8");
|
|
281
284
|
}
|
|
285
|
+
if (fs.existsSync(path.join(legacyGlobalTeamsDir, "teams.yaml"))) {
|
|
286
|
+
originalLegacyGlobalTeams = fs.readFileSync(path.join(legacyGlobalTeamsDir, "teams.yaml"), "utf-8");
|
|
287
|
+
}
|
|
282
288
|
});
|
|
283
289
|
|
|
284
290
|
afterEach(() => {
|
|
285
291
|
if (fs.existsSync(projectDir)) {
|
|
286
292
|
fs.rmSync(projectDir, { recursive: true });
|
|
287
293
|
}
|
|
294
|
+
|
|
295
|
+
const globalTeamsPath = path.join(globalTeamsDir, "teams.yaml");
|
|
296
|
+
if (originalGlobalTeams === null) {
|
|
297
|
+
if (fs.existsSync(globalTeamsPath)) fs.rmSync(globalTeamsPath);
|
|
298
|
+
} else {
|
|
299
|
+
fs.mkdirSync(globalTeamsDir, { recursive: true });
|
|
300
|
+
fs.writeFileSync(globalTeamsPath, originalGlobalTeams);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const legacyGlobalTeamsPath = path.join(legacyGlobalTeamsDir, "teams.yaml");
|
|
304
|
+
if (originalLegacyGlobalTeams === null) {
|
|
305
|
+
if (fs.existsSync(legacyGlobalTeamsPath)) fs.rmSync(legacyGlobalTeamsPath);
|
|
306
|
+
} else {
|
|
307
|
+
fs.mkdirSync(legacyGlobalTeamsDir, { recursive: true });
|
|
308
|
+
fs.writeFileSync(legacyGlobalTeamsPath, originalLegacyGlobalTeams);
|
|
309
|
+
}
|
|
288
310
|
});
|
|
289
311
|
|
|
290
312
|
it("combines global and project-local agents", () => {
|
|
@@ -330,6 +352,37 @@ custom:
|
|
|
330
352
|
expect(result.find(t => t.name === "custom")).toBeDefined();
|
|
331
353
|
expect(result.find(t => t.name === "custom")?.agents).toEqual(["agent1", "agent2"]);
|
|
332
354
|
});
|
|
355
|
+
|
|
356
|
+
it("reads global predefined teams from ~/.pi/teams.yaml", () => {
|
|
357
|
+
fs.mkdirSync(globalTeamsDir, { recursive: true });
|
|
358
|
+
fs.writeFileSync(path.join(globalTeamsDir, "teams.yaml"), `
|
|
359
|
+
root-global:
|
|
360
|
+
- scout
|
|
361
|
+
`);
|
|
362
|
+
|
|
363
|
+
const result = getAllPredefinedTeams();
|
|
364
|
+
|
|
365
|
+
expect(result.find(t => t.name === "root-global")).toBeDefined();
|
|
366
|
+
expect(result.find(t => t.name === "root-global")?.agents).toEqual(["scout"]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("falls back to legacy ~/.pi/agent/teams.yaml when needed", () => {
|
|
370
|
+
const globalTeamsPath = path.join(globalTeamsDir, "teams.yaml");
|
|
371
|
+
if (fs.existsSync(globalTeamsPath)) {
|
|
372
|
+
fs.rmSync(globalTeamsPath);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fs.mkdirSync(legacyGlobalTeamsDir, { recursive: true });
|
|
376
|
+
fs.writeFileSync(path.join(legacyGlobalTeamsDir, "teams.yaml"), `
|
|
377
|
+
legacy-global:
|
|
378
|
+
- scout
|
|
379
|
+
`);
|
|
380
|
+
|
|
381
|
+
const result = getAllPredefinedTeams();
|
|
382
|
+
|
|
383
|
+
expect(result.find(t => t.name === "legacy-global")).toBeDefined();
|
|
384
|
+
expect(result.find(t => t.name === "legacy-global")?.agents).toEqual(["scout"]);
|
|
385
|
+
});
|
|
333
386
|
});
|
|
334
387
|
|
|
335
388
|
describe("getAgentDefinition and getPredefinedTeam", () => {
|
|
@@ -386,4 +439,73 @@ test-team:
|
|
|
386
439
|
const result = getPredefinedTeam("non-existent", projectDir);
|
|
387
440
|
expect(result).toBeUndefined();
|
|
388
441
|
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe("saveTeamTemplate", () => {
|
|
445
|
+
const rootPiDir = path.join(os.homedir(), ".pi");
|
|
446
|
+
const globalAgentsDir = path.join(rootPiDir, "agent", "agents");
|
|
447
|
+
const globalTeamsPath = path.join(rootPiDir, "teams.yaml");
|
|
448
|
+
const projectDir = path.join(os.tmpdir(), "pi-teams-test-save-" + Date.now());
|
|
449
|
+
|
|
450
|
+
let originalGlobalTeams: string | null = null;
|
|
451
|
+
let originalAgentFiles = new Set<string>();
|
|
452
|
+
|
|
453
|
+
beforeEach(() => {
|
|
454
|
+
if (fs.existsSync(projectDir)) {
|
|
455
|
+
fs.rmSync(projectDir, { recursive: true });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (fs.existsSync(globalTeamsPath)) {
|
|
459
|
+
originalGlobalTeams = fs.readFileSync(globalTeamsPath, "utf-8");
|
|
460
|
+
}
|
|
461
|
+
if (fs.existsSync(globalAgentsDir)) {
|
|
462
|
+
originalAgentFiles = new Set(fs.readdirSync(globalAgentsDir));
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
afterEach(() => {
|
|
467
|
+
if (fs.existsSync(projectDir)) {
|
|
468
|
+
fs.rmSync(projectDir, { recursive: true });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (originalGlobalTeams === null) {
|
|
472
|
+
if (fs.existsSync(globalTeamsPath)) fs.rmSync(globalTeamsPath);
|
|
473
|
+
} else {
|
|
474
|
+
fs.mkdirSync(path.dirname(globalTeamsPath), { recursive: true });
|
|
475
|
+
fs.writeFileSync(globalTeamsPath, originalGlobalTeams);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (fs.existsSync(globalAgentsDir)) {
|
|
479
|
+
for (const file of fs.readdirSync(globalAgentsDir)) {
|
|
480
|
+
if (!originalAgentFiles.has(file)) {
|
|
481
|
+
fs.rmSync(path.join(globalAgentsDir, file));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("writes user-scoped teams to ~/.pi/teams.yaml and agents to ~/.pi/agent/agents", () => {
|
|
488
|
+
const result = saveTeamTemplate(
|
|
489
|
+
{
|
|
490
|
+
name: "audit-team",
|
|
491
|
+
members: [
|
|
492
|
+
{
|
|
493
|
+
name: "security-worker",
|
|
494
|
+
agentType: "teammate",
|
|
495
|
+
prompt: "Audit security issues",
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
templateName: "audit-team",
|
|
501
|
+
scope: "user",
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
expect(result.teamsYamlPath).toBe(globalTeamsPath);
|
|
506
|
+
expect(result.agentsDir).toBe(globalAgentsDir);
|
|
507
|
+
expect(fs.existsSync(globalTeamsPath)).toBe(true);
|
|
508
|
+
expect(fs.readFileSync(globalTeamsPath, "utf-8")).toContain("audit-team:");
|
|
509
|
+
expect(fs.existsSync(path.join(globalAgentsDir, "security-worker.md"))).toBe(true);
|
|
510
|
+
});
|
|
389
511
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { ThinkingLevel } from "./models";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Represents an agent definition from a .md file
|
|
@@ -10,7 +11,7 @@ export interface AgentDefinition {
|
|
|
10
11
|
description: string;
|
|
11
12
|
tools?: string[];
|
|
12
13
|
model?: string;
|
|
13
|
-
thinking?:
|
|
14
|
+
thinking?: ThinkingLevel;
|
|
14
15
|
prompt: string;
|
|
15
16
|
filePath: string;
|
|
16
17
|
}
|
|
@@ -215,12 +216,18 @@ export function getAllPredefinedTeams(projectDir?: string): PredefinedTeam[] {
|
|
|
215
216
|
const teams: PredefinedTeam[] = [];
|
|
216
217
|
const seenNames = new Set<string>();
|
|
217
218
|
|
|
218
|
-
// Global teams
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
// Global teams: prefer the documented ~/.pi/teams.yaml location,
|
|
220
|
+
// but still fall back to the legacy ~/.pi/agent/teams.yaml path.
|
|
221
|
+
const globalDirs = [
|
|
222
|
+
path.join(os.homedir(), ".pi"),
|
|
223
|
+
path.join(os.homedir(), ".pi", "agent"),
|
|
224
|
+
];
|
|
225
|
+
for (const globalDir of globalDirs) {
|
|
226
|
+
for (const team of discoverTeams(globalDir)) {
|
|
227
|
+
if (!seenNames.has(team.name)) {
|
|
228
|
+
seenNames.add(team.name);
|
|
229
|
+
teams.push(team);
|
|
230
|
+
}
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
233
|
|
|
@@ -293,7 +300,7 @@ export function generateAgentMarkdown(agent: {
|
|
|
293
300
|
description?: string;
|
|
294
301
|
tools?: string[];
|
|
295
302
|
model?: string;
|
|
296
|
-
thinking?:
|
|
303
|
+
thinking?: ThinkingLevel;
|
|
297
304
|
prompt?: string;
|
|
298
305
|
}): string {
|
|
299
306
|
const lines: string[] = ["---"];
|
|
@@ -387,7 +394,7 @@ export function saveTeamTemplate(
|
|
|
387
394
|
name: string;
|
|
388
395
|
agentType: string;
|
|
389
396
|
model?: string;
|
|
390
|
-
thinking?:
|
|
397
|
+
thinking?: ThinkingLevel;
|
|
391
398
|
prompt?: string;
|
|
392
399
|
}>;
|
|
393
400
|
defaultModel?: string;
|
|
@@ -395,12 +402,13 @@ export function saveTeamTemplate(
|
|
|
395
402
|
options: SaveTeamTemplateOptions
|
|
396
403
|
): SaveTeamTemplateResult {
|
|
397
404
|
// Determine output paths based on scope
|
|
398
|
-
const
|
|
399
|
-
? path.join(options.projectDir || process.cwd(), ".pi")
|
|
400
|
-
: path.join(os.homedir(), ".pi", "agent");
|
|
405
|
+
const agentsDir = options.scope === "project"
|
|
406
|
+
? path.join(options.projectDir || process.cwd(), ".pi", "agents")
|
|
407
|
+
: path.join(os.homedir(), ".pi", "agent", "agents");
|
|
401
408
|
|
|
402
|
-
const
|
|
403
|
-
|
|
409
|
+
const teamsYamlPath = options.scope === "project"
|
|
410
|
+
? path.join(options.projectDir || process.cwd(), ".pi", "teams.yaml")
|
|
411
|
+
: path.join(os.homedir(), ".pi", "teams.yaml");
|
|
404
412
|
|
|
405
413
|
// Ensure agents directory exists
|
|
406
414
|
if (!fs.existsSync(agentsDir)) {
|