@phi-code-admin/phi-code 0.86.0 → 0.88.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/CHANGELOG.md +104 -0
- package/docs/adr/0001-phase-contract.md +57 -0
- package/docs/adr/0002-independent-review.md +47 -0
- package/docs/fork-policy.md +18 -0
- package/extensions/phi/agents.ts +7 -4
- package/extensions/phi/benchmark.ts +129 -49
- package/extensions/phi/browser.ts +10 -37
- package/extensions/phi/btw/btw.ts +1 -7
- package/extensions/phi/chrome/index.ts +1283 -741
- package/extensions/phi/commit.ts +9 -14
- package/extensions/phi/goal/index.ts +10 -33
- package/extensions/phi/init.ts +37 -47
- package/extensions/phi/keys.ts +2 -7
- package/extensions/phi/mcp/callback-server.ts +162 -165
- package/extensions/phi/mcp/config.ts +122 -136
- package/extensions/phi/mcp/errors.ts +18 -23
- package/extensions/phi/mcp/index.ts +322 -355
- package/extensions/phi/mcp/oauth-provider.ts +289 -289
- package/extensions/phi/mcp/server-manager.ts +390 -413
- package/extensions/phi/mcp/tool-bridge.ts +381 -415
- package/extensions/phi/memory.ts +2 -2
- package/extensions/phi/models.ts +27 -26
- package/extensions/phi/orchestrator.ts +451 -237
- package/extensions/phi/productivity.ts +4 -2
- package/extensions/phi/providers/agent-def.ts +1 -1
- package/extensions/phi/providers/alibaba.ts +56 -7
- package/extensions/phi/providers/context-window.ts +4 -1
- package/extensions/phi/providers/live-models.ts +5 -20
- package/extensions/phi/providers/orchestrator-helpers.ts +31 -5
- package/extensions/phi/providers/phase-machine.ts +283 -0
- package/extensions/phi/setup.ts +196 -169
- package/extensions/phi/skill-loader.ts +14 -17
- package/extensions/phi/smart-router.ts +6 -6
- package/extensions/phi/web-search.ts +90 -50
- package/package.json +1 -1
|
@@ -48,8 +48,12 @@ function randomUA(): string {
|
|
|
48
48
|
|
|
49
49
|
function decodeEntities(text: string): string {
|
|
50
50
|
return text
|
|
51
|
-
.replace(/&/g, "&")
|
|
52
|
-
.replace(/&
|
|
51
|
+
.replace(/&/g, "&")
|
|
52
|
+
.replace(/</g, "<")
|
|
53
|
+
.replace(/>/g, ">")
|
|
54
|
+
.replace(/"/g, '"')
|
|
55
|
+
.replace(/'/g, "'")
|
|
56
|
+
.replace(/'/g, "'")
|
|
53
57
|
.replace(/&#(\d+);/g, (m, n) => {
|
|
54
58
|
try {
|
|
55
59
|
return String.fromCodePoint(parseInt(n, 10));
|
|
@@ -70,6 +74,15 @@ function stripTags(html: string): string {
|
|
|
70
74
|
return decodeEntities(html.replace(/<[^>]*>/g, "")).trim();
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
/** Iterate every match of a global regex without assign-in-while. */
|
|
78
|
+
function* execAll(regex: RegExp, input: string): Generator<RegExpExecArray> {
|
|
79
|
+
let match = regex.exec(input);
|
|
80
|
+
while (match !== null) {
|
|
81
|
+
yield match;
|
|
82
|
+
match = regex.exec(input);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
// ─── Trust boundary ───
|
|
74
87
|
// Wrap untrusted remote content (scraped/fetched web text) in an explicit
|
|
75
88
|
// trust-boundary marker so the model treats it as data, not instructions.
|
|
@@ -268,9 +281,9 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
268
281
|
const response = await fetch(`https://www.google.com/search?${params}`, {
|
|
269
282
|
headers: {
|
|
270
283
|
"User-Agent": randomUA(),
|
|
271
|
-
|
|
284
|
+
Accept: "text/html,application/xhtml+xml",
|
|
272
285
|
"Accept-Language": "en-US,en;q=0.9",
|
|
273
|
-
|
|
286
|
+
Cookie: "CONSENT=PENDING+987",
|
|
274
287
|
},
|
|
275
288
|
signal: AbortSignal.timeout(HTTP_TIMEOUT),
|
|
276
289
|
});
|
|
@@ -287,13 +300,14 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
287
300
|
|
|
288
301
|
// Strategy 1: <div class="g"> blocks with <h3> and <a href>
|
|
289
302
|
const gBlockRegex = /<div class="g"[^>]*>(.*?)<\/div>\s*<\/div>\s*<\/div>/gs;
|
|
290
|
-
|
|
291
|
-
|
|
303
|
+
for (const gMatch of execAll(gBlockRegex, html)) {
|
|
304
|
+
if (results.length >= count) break;
|
|
292
305
|
const block = gMatch[1];
|
|
293
306
|
const linkMatch = block.match(/<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>/);
|
|
294
307
|
const titleMatch = block.match(/<h3[^>]*>(.*?)<\/h3>/s);
|
|
295
|
-
const snippetMatch =
|
|
296
|
-
|
|
308
|
+
const snippetMatch =
|
|
309
|
+
block.match(/<div[^>]*class="[^"]*VwiC3b[^"]*"[^>]*>(.*?)<\/div>/s) ||
|
|
310
|
+
block.match(/<span[^>]*class="[^"]*st[^"]*"[^>]*>(.*?)<\/span>/s);
|
|
297
311
|
|
|
298
312
|
if (linkMatch && titleMatch) {
|
|
299
313
|
const url = linkMatch[1];
|
|
@@ -311,8 +325,8 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
311
325
|
// Strategy 2: find <h3> + nearest <a href>
|
|
312
326
|
if (results.length === 0) {
|
|
313
327
|
const h3Regex = /<h3[^>]*>(.*?)<\/h3>/gs;
|
|
314
|
-
|
|
315
|
-
|
|
328
|
+
for (const h3Match of execAll(h3Regex, html)) {
|
|
329
|
+
if (results.length >= count) break;
|
|
316
330
|
const pos = h3Match.index;
|
|
317
331
|
const surrounding = html.substring(Math.max(0, pos - 500), pos + h3Match[0].length + 200);
|
|
318
332
|
const linkMatch = surrounding.match(/<a[^>]*href="(https?:\/\/(?!www\.google)[^"]+)"[^>]*>/);
|
|
@@ -332,10 +346,11 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
332
346
|
|
|
333
347
|
// Strategy 3: extract any external links
|
|
334
348
|
if (results.length === 0) {
|
|
335
|
-
const extRegex =
|
|
349
|
+
const extRegex =
|
|
350
|
+
/href="(https?:\/\/(?!www\.google|accounts\.google|support\.google|maps\.google|policies\.google)[^"]+)"/g;
|
|
336
351
|
const seen = new Set<string>();
|
|
337
|
-
|
|
338
|
-
|
|
352
|
+
for (const extMatch of execAll(extRegex, html)) {
|
|
353
|
+
if (seen.size >= count) break;
|
|
339
354
|
if (!seen.has(extMatch[1])) {
|
|
340
355
|
seen.add(extMatch[1]);
|
|
341
356
|
results.push({
|
|
@@ -365,7 +380,7 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
365
380
|
const response = await fetch(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
|
|
366
381
|
headers: {
|
|
367
382
|
"User-Agent": randomUA(),
|
|
368
|
-
|
|
383
|
+
Accept: "text/html",
|
|
369
384
|
"Accept-Language": "en-US,en;q=0.5",
|
|
370
385
|
},
|
|
371
386
|
signal: AbortSignal.timeout(HTTP_TIMEOUT),
|
|
@@ -386,14 +401,18 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
386
401
|
const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>(.*?)<\/a>/gs;
|
|
387
402
|
|
|
388
403
|
const links: Array<{ url: string; title: string }> = [];
|
|
389
|
-
|
|
390
|
-
|
|
404
|
+
for (const m of execAll(linkRegex, html)) {
|
|
405
|
+
if (links.length >= count) break;
|
|
391
406
|
let url = m[1];
|
|
392
407
|
const title = stripTags(m[2]);
|
|
393
408
|
|
|
394
409
|
// DDG wraps URLs through redirect
|
|
395
410
|
if (url.includes("uddg=")) {
|
|
396
|
-
try {
|
|
411
|
+
try {
|
|
412
|
+
url = decodeURIComponent(url.split("uddg=")[1].split("&")[0]);
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
397
416
|
}
|
|
398
417
|
|
|
399
418
|
if (url.startsWith("http") && title) {
|
|
@@ -402,8 +421,8 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
402
421
|
}
|
|
403
422
|
|
|
404
423
|
const snippets: string[] = [];
|
|
405
|
-
|
|
406
|
-
snippets.push(stripTags(
|
|
424
|
+
for (const sm of execAll(snippetRegex, html)) {
|
|
425
|
+
snippets.push(stripTags(sm[1]));
|
|
407
426
|
}
|
|
408
427
|
|
|
409
428
|
for (let i = 0; i < links.length; i++) {
|
|
@@ -440,7 +459,7 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
440
459
|
|
|
441
460
|
const response = await fetch(`${BRAVE_API_URL}?${params}`, {
|
|
442
461
|
headers: {
|
|
443
|
-
|
|
462
|
+
Accept: "application/json",
|
|
444
463
|
"Accept-Encoding": "gzip",
|
|
445
464
|
"X-Subscription-Token": BRAVE_API_KEY,
|
|
446
465
|
},
|
|
@@ -449,15 +468,17 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
449
468
|
|
|
450
469
|
if (!response.ok) throw new Error(`Brave API HTTP ${response.status}`);
|
|
451
470
|
|
|
452
|
-
const data = await response.json() as any;
|
|
471
|
+
const data = (await response.json()) as any;
|
|
453
472
|
if (!data.web?.results) return [];
|
|
454
473
|
|
|
455
|
-
return data.web.results.map(
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
474
|
+
return data.web.results.map(
|
|
475
|
+
(r: any): SearchResult => ({
|
|
476
|
+
title: r.title || "No title",
|
|
477
|
+
url: r.url || "",
|
|
478
|
+
description: r.description || "",
|
|
479
|
+
source: "brave",
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
461
482
|
}
|
|
462
483
|
|
|
463
484
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -556,7 +577,12 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
556
577
|
throw new Error(`Schema d'URL non autorise (${u.protocol}); seuls http et https sont permis`);
|
|
557
578
|
}
|
|
558
579
|
const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
559
|
-
if (
|
|
580
|
+
if (
|
|
581
|
+
host === "localhost" ||
|
|
582
|
+
host.endsWith(".localhost") ||
|
|
583
|
+
host.endsWith(".internal") ||
|
|
584
|
+
host.endsWith(".local")
|
|
585
|
+
) {
|
|
560
586
|
throw new Error(`Hote interne bloque (SSRF): ${host}`);
|
|
561
587
|
}
|
|
562
588
|
if (isIP(host)) {
|
|
@@ -584,7 +610,7 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
584
610
|
response = await fetch(currentUrl, {
|
|
585
611
|
headers: {
|
|
586
612
|
"User-Agent": randomUA(),
|
|
587
|
-
|
|
613
|
+
Accept: "text/html,application/xhtml+xml,text/plain,application/json",
|
|
588
614
|
},
|
|
589
615
|
redirect: "manual",
|
|
590
616
|
signal: AbortSignal.timeout(HTTP_TIMEOUT),
|
|
@@ -621,7 +647,7 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
621
647
|
}
|
|
622
648
|
|
|
623
649
|
// Basic HTML → text extraction (no dependencies)
|
|
624
|
-
|
|
650
|
+
const readable = text
|
|
625
651
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
626
652
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
627
653
|
.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "")
|
|
@@ -646,14 +672,17 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
646
672
|
pi.registerTool({
|
|
647
673
|
name: "web_search",
|
|
648
674
|
label: "Web Search",
|
|
649
|
-
description:
|
|
675
|
+
description:
|
|
676
|
+
"Search the web. Uses Google (primary), DuckDuckGo (fallback), Brave (if API key set). No API keys required.",
|
|
650
677
|
parameters: Type.Object({
|
|
651
678
|
query: Type.String({ description: "Search query" }),
|
|
652
|
-
count: Type.Optional(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
679
|
+
count: Type.Optional(
|
|
680
|
+
Type.Number({
|
|
681
|
+
description: "Number of results (1-10, default: 5)",
|
|
682
|
+
minimum: 1,
|
|
683
|
+
maximum: 10,
|
|
684
|
+
}),
|
|
685
|
+
),
|
|
657
686
|
}),
|
|
658
687
|
|
|
659
688
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
@@ -664,10 +693,12 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
664
693
|
|
|
665
694
|
if (response.results.length === 0) {
|
|
666
695
|
return {
|
|
667
|
-
content: [
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
696
|
+
content: [
|
|
697
|
+
{
|
|
698
|
+
type: "text",
|
|
699
|
+
text: `No search results found for "${query}". Try rephrasing your search.\n\nProviders tried: ${response.triedProviders.join(", ")}`,
|
|
700
|
+
},
|
|
701
|
+
],
|
|
671
702
|
details: { found: false, query, triedProviders: response.triedProviders },
|
|
672
703
|
};
|
|
673
704
|
}
|
|
@@ -681,13 +712,14 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
681
712
|
});
|
|
682
713
|
resultText += `\n*Results provided by ${response.provider}*`;
|
|
683
714
|
if (response.fallbackUsed) {
|
|
684
|
-
resultText += ` *(fallback from: ${response.triedProviders.filter(p => p !== response.provider).join(", ")})*`;
|
|
715
|
+
resultText += ` *(fallback from: ${response.triedProviders.filter((p) => p !== response.provider).join(", ")})*`;
|
|
685
716
|
}
|
|
686
717
|
|
|
687
718
|
return {
|
|
688
719
|
content: [{ type: "text", text: wrapUntrusted(resultText, "web") }],
|
|
689
720
|
details: {
|
|
690
|
-
found: true,
|
|
721
|
+
found: true,
|
|
722
|
+
query,
|
|
691
723
|
resultCount: response.results.length,
|
|
692
724
|
provider: response.provider,
|
|
693
725
|
fallbackUsed: response.fallbackUsed,
|
|
@@ -711,14 +743,17 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
711
743
|
pi.registerTool({
|
|
712
744
|
name: "fetch_url",
|
|
713
745
|
label: "Fetch URL",
|
|
714
|
-
description:
|
|
746
|
+
description:
|
|
747
|
+
"Fetch a URL and extract readable text content. Uses @mozilla/readability + jsdom if installed, otherwise basic HTML extraction.",
|
|
715
748
|
parameters: Type.Object({
|
|
716
749
|
url: Type.String({ description: "URL to fetch" }),
|
|
717
|
-
max_length: Type.Optional(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
750
|
+
max_length: Type.Optional(
|
|
751
|
+
Type.Number({
|
|
752
|
+
description: "Max characters to return (default: 8000)",
|
|
753
|
+
minimum: 500,
|
|
754
|
+
maximum: 50000,
|
|
755
|
+
}),
|
|
756
|
+
),
|
|
722
757
|
}),
|
|
723
758
|
|
|
724
759
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
@@ -729,7 +764,9 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
729
764
|
|
|
730
765
|
if (!content || content.length < 10) {
|
|
731
766
|
return {
|
|
732
|
-
content: [
|
|
767
|
+
content: [
|
|
768
|
+
{ type: "text", text: `Could not extract content from ${url}. The page may require JavaScript.` },
|
|
769
|
+
],
|
|
733
770
|
details: { success: false, url },
|
|
734
771
|
};
|
|
735
772
|
}
|
|
@@ -770,7 +807,10 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
770
807
|
description: "Quick web search (usage: /search <query>)",
|
|
771
808
|
handler: async (args, ctx) => {
|
|
772
809
|
const query = args.trim();
|
|
773
|
-
if (!query) {
|
|
810
|
+
if (!query) {
|
|
811
|
+
ctx.ui.notify("Usage: /search <query>", "warning");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
774
814
|
|
|
775
815
|
try {
|
|
776
816
|
ctx.ui.notify(`🔍 Searching: "${query}"...`, "info");
|