pi-teams 0.8.5 → 0.8.6
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 +11 -0
- package/extensions/index.ts +154 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,17 @@ pi install npm:pi-teams
|
|
|
83
83
|
**Customize model and thinking level:**
|
|
84
84
|
> **You:** "Spawn a teammate named 'architect-bot' using 'gpt-4o' with 'high' thinking level for deep reasoning."
|
|
85
85
|
|
|
86
|
+
**Smart Model Resolution:**
|
|
87
|
+
When you specify a model name without a provider (e.g., `gemini-2.5-flash`), pi-teams automatically:
|
|
88
|
+
- Queries available models from `pi --list-models`
|
|
89
|
+
- Prioritizes **OAuth/subscription providers** (cheaper/free) over API-key providers:
|
|
90
|
+
- `google-gemini-cli` (OAuth) is preferred over `google` (API key)
|
|
91
|
+
- `github-copilot`, `kimi-sub` are preferred over their API-key equivalents
|
|
92
|
+
- Falls back to API-key providers if OAuth providers aren't available
|
|
93
|
+
- Constructs the correct `--model provider/model:thinking` command
|
|
94
|
+
|
|
95
|
+
> **Example:** Specifying `gemini-2.5-flash` will automatically use `google-gemini-cli/gemini-2.5-flash` if available, saving API costs.
|
|
96
|
+
|
|
86
97
|
### 3. Assign Task & Get Approval
|
|
87
98
|
> **You:** "Create a task for security-bot: 'Check the .env.example file for sensitive defaults' and set it to in_progress."
|
|
88
99
|
|
package/extensions/index.ts
CHANGED
|
@@ -10,6 +10,136 @@ import { getTerminalAdapter } from "../src/adapters/terminal-registry";
|
|
|
10
10
|
import { Iterm2Adapter } from "../src/adapters/iterm2-adapter";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
// Cache for available models
|
|
16
|
+
let availableModelsCache: Array<{ provider: string; model: string }> | null = null;
|
|
17
|
+
let modelsCacheTime = 0;
|
|
18
|
+
const MODELS_CACHE_TTL = 60000; // 1 minute
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Query available models from pi --list-models
|
|
22
|
+
*/
|
|
23
|
+
function getAvailableModels(): Array<{ provider: string; model: string }> {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
if (availableModelsCache && now - modelsCacheTime < MODELS_CACHE_TTL) {
|
|
26
|
+
return availableModelsCache;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnSync("pi", ["--list-models"], {
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
timeout: 10000,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (result.status !== 0 || !result.stdout) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const models: Array<{ provider: string; model: string }> = [];
|
|
40
|
+
const lines = result.stdout.split("\n");
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
// Skip header line and empty lines
|
|
44
|
+
if (!line.trim() || line.startsWith("provider")) continue;
|
|
45
|
+
|
|
46
|
+
// Parse: provider model context max-out thinking images
|
|
47
|
+
const parts = line.trim().split(/\s+/);
|
|
48
|
+
if (parts.length >= 2) {
|
|
49
|
+
const provider = parts[0];
|
|
50
|
+
const model = parts[1];
|
|
51
|
+
if (provider && model) {
|
|
52
|
+
models.push({ provider, model });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
availableModelsCache = models;
|
|
58
|
+
modelsCacheTime = now;
|
|
59
|
+
return models;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provider priority list - OAuth/subscription providers first (cheaper), then API-key providers
|
|
67
|
+
*/
|
|
68
|
+
const PROVIDER_PRIORITY = [
|
|
69
|
+
// OAuth / Subscription providers (typically free/cheaper)
|
|
70
|
+
"google-gemini-cli", // Google Gemini CLI - OAuth, free tier
|
|
71
|
+
"github-copilot", // GitHub Copilot - subscription
|
|
72
|
+
"kimi-sub", // Kimi subscription
|
|
73
|
+
// API key providers
|
|
74
|
+
"anthropic",
|
|
75
|
+
"openai",
|
|
76
|
+
"google",
|
|
77
|
+
"zai",
|
|
78
|
+
"openrouter",
|
|
79
|
+
"azure-openai",
|
|
80
|
+
"amazon-bedrock",
|
|
81
|
+
"mistral",
|
|
82
|
+
"groq",
|
|
83
|
+
"cerebras",
|
|
84
|
+
"xai",
|
|
85
|
+
"vercel-ai-gateway",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find the best matching provider for a given model name.
|
|
90
|
+
* Returns the full provider/model string or null if not found.
|
|
91
|
+
*/
|
|
92
|
+
function resolveModelWithProvider(modelName: string): string | null {
|
|
93
|
+
// If already has provider prefix, return as-is
|
|
94
|
+
if (modelName.includes("/")) {
|
|
95
|
+
return modelName;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const availableModels = getAvailableModels();
|
|
99
|
+
if (availableModels.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lowerModelName = modelName.toLowerCase();
|
|
104
|
+
|
|
105
|
+
// Find all exact matches (case-insensitive) and sort by provider priority
|
|
106
|
+
const exactMatches = availableModels.filter(
|
|
107
|
+
(m) => m.model.toLowerCase() === lowerModelName
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (exactMatches.length > 0) {
|
|
111
|
+
// Sort by provider priority (lower index = higher priority)
|
|
112
|
+
exactMatches.sort((a, b) => {
|
|
113
|
+
const aIndex = PROVIDER_PRIORITY.indexOf(a.provider);
|
|
114
|
+
const bIndex = PROVIDER_PRIORITY.indexOf(b.provider);
|
|
115
|
+
// If provider not in priority list, put it at the end
|
|
116
|
+
const aPriority = aIndex === -1 ? 999 : aIndex;
|
|
117
|
+
const bPriority = bIndex === -1 ? 999 : bIndex;
|
|
118
|
+
return aPriority - bPriority;
|
|
119
|
+
});
|
|
120
|
+
return `${exactMatches[0].provider}/${exactMatches[0].model}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Try partial match (model name contains the search term)
|
|
124
|
+
const partialMatches = availableModels.filter((m) =>
|
|
125
|
+
m.model.toLowerCase().includes(lowerModelName)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (partialMatches.length > 0) {
|
|
129
|
+
for (const preferredProvider of PROVIDER_PRIORITY) {
|
|
130
|
+
const match = partialMatches.find(
|
|
131
|
+
(m) => m.provider === preferredProvider
|
|
132
|
+
);
|
|
133
|
+
if (match) {
|
|
134
|
+
return `${match.provider}/${match.model}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Return first match if no preferred provider found
|
|
138
|
+
return `${partialMatches[0].provider}/${partialMatches[0].model}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
13
143
|
|
|
14
144
|
export default function (pi: ExtensionAPI) {
|
|
15
145
|
const isTeammate = !!process.env.PI_AGENT_NAME;
|
|
@@ -165,15 +295,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
165
295
|
const teamConfig = await teams.readConfig(safeTeamName);
|
|
166
296
|
let chosenModel = params.model || teamConfig.defaultModel;
|
|
167
297
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
chosenModel =
|
|
175
|
-
} else if (
|
|
176
|
-
|
|
298
|
+
// Resolve model to provider/model format
|
|
299
|
+
if (chosenModel) {
|
|
300
|
+
if (!chosenModel.includes('/')) {
|
|
301
|
+
// Try to resolve using available models from pi --list-models
|
|
302
|
+
const resolved = resolveModelWithProvider(chosenModel);
|
|
303
|
+
if (resolved) {
|
|
304
|
+
chosenModel = resolved;
|
|
305
|
+
} else if (teamConfig.defaultModel && teamConfig.defaultModel.includes('/')) {
|
|
306
|
+
// Fall back to team default provider
|
|
307
|
+
const [provider] = teamConfig.defaultModel.split('/');
|
|
308
|
+
chosenModel = `${provider}/${chosenModel}`;
|
|
177
309
|
}
|
|
178
310
|
}
|
|
179
311
|
}
|
|
@@ -205,12 +337,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
337
|
let piCmd = piBinary;
|
|
206
338
|
|
|
207
339
|
if (chosenModel) {
|
|
208
|
-
|
|
209
|
-
const modelName = modelParts.join('/');
|
|
340
|
+
// Use the combined --model provider/model:thinking format
|
|
210
341
|
if (params.thinking) {
|
|
211
|
-
piCmd = `${piBinary} --
|
|
342
|
+
piCmd = `${piBinary} --model ${chosenModel}:${params.thinking}`;
|
|
212
343
|
} else {
|
|
213
|
-
piCmd = `${piBinary} --
|
|
344
|
+
piCmd = `${piBinary} --model ${chosenModel}`;
|
|
214
345
|
}
|
|
215
346
|
} else if (params.thinking) {
|
|
216
347
|
piCmd = `${piBinary} --thinking ${params.thinking}`;
|
|
@@ -284,8 +415,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
284
415
|
const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi";
|
|
285
416
|
let piCmd = piBinary;
|
|
286
417
|
if (teamConfig.defaultModel) {
|
|
287
|
-
|
|
288
|
-
piCmd = `${piBinary} --
|
|
418
|
+
// Use the combined --model provider/model format
|
|
419
|
+
piCmd = `${piBinary} --model ${teamConfig.defaultModel}`;
|
|
289
420
|
}
|
|
290
421
|
|
|
291
422
|
const env = { ...process.env, PI_TEAM_NAME: safeTeamName, PI_AGENT_NAME: "team-lead" };
|
|
@@ -530,11 +661,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
530
661
|
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
531
662
|
const config = await teams.readConfig(params.team_name);
|
|
532
663
|
const member = config.members.find(m => m.name === params.agent_name);
|
|
533
|
-
if (member) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
return {
|
|
664
|
+
if (!member) throw new Error(`Teammate ${params.agent_name} not found`);
|
|
665
|
+
|
|
666
|
+
await killTeammate(params.team_name, member);
|
|
667
|
+
await teams.removeMember(params.team_name, params.agent_name);
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: "text", text: `Teammate ${params.agent_name} has been shut down.` }],
|
|
670
|
+
details: {},
|
|
671
|
+
};
|
|
538
672
|
},
|
|
539
673
|
});
|
|
540
674
|
}
|