oh-pi 0.1.30 → 0.1.32
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 +1 -1
- package/pi-package/extensions/ant-colony/queen.ts +8 -43
- package/pi-package/extensions/ant-colony/spawner.ts +15 -2
- package/pi-package/skills/context7/SKILL.md +34 -0
- package/pi-package/skills/context7/docs.js +13 -0
- package/pi-package/skills/context7/search.js +13 -0
- package/pi-package/skills/web-fetch/SKILL.md +24 -0
- package/pi-package/skills/web-fetch/fetch.js +28 -0
- package/pi-package/skills/web-search/SKILL.md +18 -0
- package/pi-package/skills/web-search/search.js +29 -0
package/package.json
CHANGED
|
@@ -140,14 +140,13 @@ interface WaveOptions {
|
|
|
140
140
|
currentModel: string;
|
|
141
141
|
signal?: AbortSignal;
|
|
142
142
|
callbacks: QueenCallbacks;
|
|
143
|
-
maxCost?: number;
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
/**
|
|
147
146
|
* 并发执行一批蚂蚁,自适应调节并发度
|
|
148
147
|
*/
|
|
149
|
-
async function runAntWave(opts: WaveOptions): Promise<"ok"
|
|
150
|
-
const { nest, cwd, caste, signal, callbacks,
|
|
148
|
+
async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
149
|
+
const { nest, cwd, caste, signal, callbacks, currentModel } = opts;
|
|
151
150
|
const config = { ...DEFAULT_ANT_CONFIGS[caste], model: currentModel };
|
|
152
151
|
|
|
153
152
|
let backoffMs = 0; // 429 退避时间
|
|
@@ -208,26 +207,6 @@ async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget_exceeded">
|
|
|
208
207
|
// 调度循环:持续派蚂蚁直到没有待处理任务
|
|
209
208
|
let lastSampleTime = 0;
|
|
210
209
|
while (!signal?.aborted) {
|
|
211
|
-
// 预算检查:派蚂蚁前预估,基于已完成蚂蚁的平均 cost
|
|
212
|
-
if (maxCost != null) {
|
|
213
|
-
const ants = nest.getState().ants;
|
|
214
|
-
const currentCost = ants.reduce((s, a) => s + a.usage.cost, 0);
|
|
215
|
-
if (currentCost >= maxCost) {
|
|
216
|
-
callbacks.onPhase("working", `Budget exceeded ($${currentCost.toFixed(3)} >= $${maxCost.toFixed(2)}). Stopping.`);
|
|
217
|
-
return "budget_exceeded";
|
|
218
|
-
}
|
|
219
|
-
// 预估:如果剩余预算不够一只蚂蚁的平均花费,也停止
|
|
220
|
-
const doneAnts = ants.filter(a => a.status === "done" && a.usage.cost > 0);
|
|
221
|
-
if (doneAnts.length > 0) {
|
|
222
|
-
const avgCost = doneAnts.reduce((s, a) => s + a.usage.cost, 0) / doneAnts.length;
|
|
223
|
-
const remaining = maxCost - currentCost;
|
|
224
|
-
if (remaining < avgCost * 0.5) {
|
|
225
|
-
callbacks.onPhase("working", `Budget nearly exhausted ($${currentCost.toFixed(3)}, avg/ant: $${avgCost.toFixed(3)}). Stopping.`);
|
|
226
|
-
return "budget_exceeded";
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
210
|
const state = nest.getState();
|
|
232
211
|
const pending = state.tasks.filter(t => t.status === "pending" && t.caste === caste);
|
|
233
212
|
if (pending.length === 0) break;
|
|
@@ -331,7 +310,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
331
310
|
const { signal, callbacks } = opts;
|
|
332
311
|
const waveBase: Omit<WaveOptions, "caste"> = {
|
|
333
312
|
nest, cwd: opts.cwd, signal, callbacks,
|
|
334
|
-
|
|
313
|
+
currentModel: opts.currentModel,
|
|
335
314
|
};
|
|
336
315
|
|
|
337
316
|
const cleanup = () => {
|
|
@@ -343,15 +322,6 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
343
322
|
} catch { /* ignore */ }
|
|
344
323
|
};
|
|
345
324
|
|
|
346
|
-
const budgetStop = (phase: string) => {
|
|
347
|
-
nest.updateState({ status: "budget_exceeded", finishedAt: Date.now() });
|
|
348
|
-
callbacks.onPhase("budget_exceeded" as any, phase);
|
|
349
|
-
const s = nest.getState();
|
|
350
|
-
callbacks.onComplete(s);
|
|
351
|
-
cleanup();
|
|
352
|
-
return s;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
325
|
try {
|
|
356
326
|
// ═══ Phase 1: 侦察(最多重试2次) ═══
|
|
357
327
|
let scoutAttempt = 0;
|
|
@@ -363,8 +333,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
363
333
|
? "Dispatching scout ants to explore codebase..."
|
|
364
334
|
: `Scout retry ${scoutAttempt}/${MAX_SCOUT_RETRIES}...`);
|
|
365
335
|
|
|
366
|
-
|
|
367
|
-
return budgetStop("Budget exceeded during scouting");
|
|
336
|
+
await runAntWave({ ...waveBase, caste: "scout" });
|
|
368
337
|
|
|
369
338
|
workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
370
339
|
if (workerTasks.length > 0) break;
|
|
@@ -389,8 +358,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
389
358
|
// ═══ Phase 2: 工作 ═══
|
|
390
359
|
nest.updateState({ status: "working" });
|
|
391
360
|
callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
|
|
392
|
-
|
|
393
|
-
return budgetStop("Budget exceeded during working");
|
|
361
|
+
await runAntWave({ ...waveBase, caste: "worker" });
|
|
394
362
|
|
|
395
363
|
// 处理工蚁产生的子任务(可能有多轮)
|
|
396
364
|
let rounds = 0;
|
|
@@ -401,8 +369,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
401
369
|
if (remaining.length === 0) break;
|
|
402
370
|
rounds++;
|
|
403
371
|
callbacks.onPhase("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
|
|
404
|
-
|
|
405
|
-
return budgetStop("Budget exceeded during sub-tasks");
|
|
372
|
+
await runAntWave({ ...waveBase, caste: "worker" });
|
|
406
373
|
}
|
|
407
374
|
|
|
408
375
|
// ═══ Phase 3: 审查 ═══
|
|
@@ -412,8 +379,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
412
379
|
callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
|
|
413
380
|
const reviewTask = makeReviewTask(completedWorkerTasks);
|
|
414
381
|
nest.writeTask(reviewTask);
|
|
415
|
-
|
|
416
|
-
return budgetStop("Budget exceeded during review");
|
|
382
|
+
await runAntWave({ ...waveBase, caste: "soldier" });
|
|
417
383
|
|
|
418
384
|
// 兵蚁产生的修复任务
|
|
419
385
|
const fixTasks = nest.getAllTasks().filter(t =>
|
|
@@ -422,8 +388,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
422
388
|
if (fixTasks.length > 0) {
|
|
423
389
|
nest.updateState({ status: "working" });
|
|
424
390
|
callbacks.onPhase("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
|
|
425
|
-
|
|
426
|
-
return budgetStop("Budget exceeded during fixes");
|
|
391
|
+
await runAntWave({ ...waveBase, caste: "worker" });
|
|
427
392
|
}
|
|
428
393
|
}
|
|
429
394
|
|
|
@@ -114,8 +114,11 @@ Output format (MUST follow exactly):
|
|
|
114
114
|
PASS or FAIL with summary.`,
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
-
function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string): string {
|
|
117
|
+
function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string, maxTurns?: number): string {
|
|
118
118
|
let prompt = castePrompt + "\n\n";
|
|
119
|
+
if (maxTurns) {
|
|
120
|
+
prompt += `## ⚠️ Turn Limit\nYou have a MAXIMUM of ${maxTurns} turns. Plan accordingly — reserve your LAST turn to output the structured result format above. Do NOT waste turns on unnecessary exploration.\n\n`;
|
|
121
|
+
}
|
|
119
122
|
if (pheromoneContext) {
|
|
120
123
|
prompt += `## Colony Pheromone Trail (intelligence from other ants)\n${pheromoneContext}\n\n`;
|
|
121
124
|
}
|
|
@@ -223,7 +226,7 @@ export async function spawnAnt(
|
|
|
223
226
|
// 构建 prompt
|
|
224
227
|
const pheromoneCtx = nest.getPheromoneContext(task.files);
|
|
225
228
|
const castePrompt = CASTE_PROMPTS[antConfig.caste];
|
|
226
|
-
const fullPrompt = buildPrompt(task, pheromoneCtx, castePrompt);
|
|
229
|
+
const fullPrompt = buildPrompt(task, pheromoneCtx, castePrompt, antConfig.maxTurns);
|
|
227
230
|
const tmpFile = writePromptFile(nest.dir, antId, fullPrompt);
|
|
228
231
|
|
|
229
232
|
const args = [
|
|
@@ -246,6 +249,7 @@ export async function spawnAnt(
|
|
|
246
249
|
nest.updateAnt(ant);
|
|
247
250
|
|
|
248
251
|
let buffer = "";
|
|
252
|
+
let turnCount = 0;
|
|
249
253
|
|
|
250
254
|
proc.stdout.on("data", (data) => {
|
|
251
255
|
buffer += data.toString();
|
|
@@ -255,6 +259,15 @@ export async function spawnAnt(
|
|
|
255
259
|
if (!line.trim()) continue;
|
|
256
260
|
try {
|
|
257
261
|
const event = JSON.parse(line);
|
|
262
|
+
if (event.type === "turn_end") {
|
|
263
|
+
turnCount++;
|
|
264
|
+
if (antConfig.maxTurns && turnCount === antConfig.maxTurns) {
|
|
265
|
+
stderr += `[ant-colony] Warning: ant reached maxTurns (${antConfig.maxTurns}), 1 grace turn remaining\n`;
|
|
266
|
+
} else if (antConfig.maxTurns && turnCount > antConfig.maxTurns) {
|
|
267
|
+
proc.kill("SIGTERM");
|
|
268
|
+
setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
258
271
|
if (event.type === "message_end" && event.message) {
|
|
259
272
|
messages.push(event.message);
|
|
260
273
|
if (event.message.role === "assistant") {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: context7
|
|
3
|
+
description: Search and query up-to-date documentation for any programming library via Context7 API. Use when you need current docs, code examples, or API references for libraries and frameworks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Context7
|
|
7
|
+
|
|
8
|
+
Search for libraries and query their documentation via the Context7 API.
|
|
9
|
+
|
|
10
|
+
## Search Libraries
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
{baseDir}/search.js "library name" "what you need help with"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
```bash
|
|
18
|
+
{baseDir}/search.js "react" "hooks for state management"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Returns matching libraries with Context7-compatible IDs for use with the docs tool.
|
|
22
|
+
|
|
23
|
+
## Query Documentation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
{baseDir}/docs.js "/org/project" "your question"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```bash
|
|
31
|
+
{baseDir}/docs.js "/websites/react_dev" "useEffect cleanup"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use the library ID from search results. Returns relevant documentation and code examples.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const [libraryId, query] = process.argv.slice(2);
|
|
4
|
+
if (!libraryId || !query) { console.error("Usage: docs.js <libraryId> <query>"); process.exit(1); }
|
|
5
|
+
|
|
6
|
+
const res = await fetch("https://mcp.context7.com/mcp", {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
|
|
9
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "query-docs", arguments: { libraryId, query } } })
|
|
10
|
+
});
|
|
11
|
+
const data = await res.json();
|
|
12
|
+
if (data.error) { console.error("Error:", data.error.message); process.exit(1); }
|
|
13
|
+
console.log(data.result.content[0].text);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const [libraryName, query = libraryName] = process.argv.slice(2);
|
|
4
|
+
if (!libraryName) { console.error("Usage: search.js <libraryName> [query]"); process.exit(1); }
|
|
5
|
+
|
|
6
|
+
const res = await fetch("https://mcp.context7.com/mcp", {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
|
|
9
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "resolve-library-id", arguments: { query, libraryName } } })
|
|
10
|
+
});
|
|
11
|
+
const data = await res.json();
|
|
12
|
+
if (data.error) { console.error("Error:", data.error.message); process.exit(1); }
|
|
13
|
+
console.log(data.result.content[0].text);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-fetch
|
|
3
|
+
description: Fetch a web page and extract readable text content. Use when user needs to retrieve or read a web page.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# web-fetch
|
|
7
|
+
|
|
8
|
+
Fetch a web page and extract readable text content.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
{baseDir}/fetch.js <url> [--raw]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `<url>` — URL to fetch
|
|
17
|
+
- `--raw` — Output raw HTML instead of extracted text
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
{baseDir}/fetch.js https://example.com
|
|
23
|
+
{baseDir}/fetch.js https://example.com --raw
|
|
24
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const raw = args.includes('--raw');
|
|
7
|
+
const url = args.find(a => !a.startsWith('--'));
|
|
8
|
+
|
|
9
|
+
if (!url) { console.error('Usage: fetch.js <url> [--raw]'); process.exit(1); }
|
|
10
|
+
|
|
11
|
+
const res = await fetch(url);
|
|
12
|
+
const html = await res.text();
|
|
13
|
+
|
|
14
|
+
if (raw) { console.log(html); } else {
|
|
15
|
+
const text = html
|
|
16
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
17
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
18
|
+
.replace(/<[^>]+>/g, ' ')
|
|
19
|
+
.replace(/ /g, ' ')
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(n))
|
|
24
|
+
.replace(/[ \t]+/g, ' ')
|
|
25
|
+
.replace(/\n\s*\n/g, '\n')
|
|
26
|
+
.trim();
|
|
27
|
+
console.log(text);
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-search
|
|
3
|
+
description: Web search via DuckDuckGo. Use when the user needs to look up current information online.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# web-search
|
|
7
|
+
|
|
8
|
+
Web search via DuckDuckGo. Use when the user needs to look up current information online.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
{baseDir}/search.js "query terms"
|
|
14
|
+
{baseDir}/search.js -n 10 "query terms"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `-n <count>` — number of results to return (default: 5)
|
|
18
|
+
- Returns title, URL, and snippet for each result.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
let n = 5, query;
|
|
5
|
+
|
|
6
|
+
for (let i = 0; i < args.length; i++) {
|
|
7
|
+
if (args[i] === '-n' && args[i + 1]) { n = parseInt(args[++i], 10); }
|
|
8
|
+
else { query = args[i]; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!query) { console.error('Usage: search.js [-n count] "query"'); process.exit(1); }
|
|
12
|
+
|
|
13
|
+
const res = await fetch(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
|
|
14
|
+
headers: { 'User-Agent': 'Mozilla/5.0' }
|
|
15
|
+
});
|
|
16
|
+
const html = await res.text();
|
|
17
|
+
|
|
18
|
+
const results = [];
|
|
19
|
+
const blockRe = /<a rel="nofollow" class="result__a" href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
20
|
+
let m;
|
|
21
|
+
while ((m = blockRe.exec(html)) && results.length < n) {
|
|
22
|
+
const url = decodeURIComponent(m[1].replace(/^\/\/duckduckgo\.com\/l\/\?uddg=/, '').replace(/&rut=.*$/, '').replace(/&rut=.*$/, ''));
|
|
23
|
+
const title = m[2].replace(/<[^>]*>/g, '').trim();
|
|
24
|
+
const snippet = m[3].replace(/<[^>]*>/g, '').trim();
|
|
25
|
+
results.push({ title, url, snippet });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!results.length) { console.log('No results found.'); }
|
|
29
|
+
else { results.forEach((r, i) => console.log(`${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}\n`)); }
|