@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/adr/0001-phase-contract.md +57 -0
  3. package/docs/adr/0002-independent-review.md +47 -0
  4. package/docs/fork-policy.md +18 -0
  5. package/extensions/phi/agents.ts +7 -4
  6. package/extensions/phi/benchmark.ts +129 -49
  7. package/extensions/phi/browser.ts +10 -37
  8. package/extensions/phi/btw/btw.ts +1 -7
  9. package/extensions/phi/chrome/index.ts +1283 -741
  10. package/extensions/phi/commit.ts +9 -14
  11. package/extensions/phi/goal/index.ts +10 -33
  12. package/extensions/phi/init.ts +37 -47
  13. package/extensions/phi/keys.ts +2 -7
  14. package/extensions/phi/mcp/callback-server.ts +162 -165
  15. package/extensions/phi/mcp/config.ts +122 -136
  16. package/extensions/phi/mcp/errors.ts +18 -23
  17. package/extensions/phi/mcp/index.ts +322 -355
  18. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  19. package/extensions/phi/mcp/server-manager.ts +390 -413
  20. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  21. package/extensions/phi/memory.ts +2 -2
  22. package/extensions/phi/models.ts +27 -26
  23. package/extensions/phi/orchestrator.ts +451 -237
  24. package/extensions/phi/productivity.ts +4 -2
  25. package/extensions/phi/providers/agent-def.ts +1 -1
  26. package/extensions/phi/providers/alibaba.ts +56 -7
  27. package/extensions/phi/providers/context-window.ts +4 -1
  28. package/extensions/phi/providers/live-models.ts +5 -20
  29. package/extensions/phi/providers/orchestrator-helpers.ts +31 -5
  30. package/extensions/phi/providers/phase-machine.ts +283 -0
  31. package/extensions/phi/setup.ts +196 -169
  32. package/extensions/phi/skill-loader.ts +14 -17
  33. package/extensions/phi/smart-router.ts +6 -6
  34. package/extensions/phi/web-search.ts +90 -50
  35. 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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
52
- .replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x27;/g, "'")
51
+ .replace(/&amp;/g, "&")
52
+ .replace(/&lt;/g, "<")
53
+ .replace(/&gt;/g, ">")
54
+ .replace(/&quot;/g, '"')
55
+ .replace(/&#39;/g, "'")
56
+ .replace(/&#x27;/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
- "Accept": "text/html,application/xhtml+xml",
284
+ Accept: "text/html,application/xhtml+xml",
272
285
  "Accept-Language": "en-US,en;q=0.9",
273
- "Cookie": "CONSENT=PENDING+987",
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
- let gMatch;
291
- while ((gMatch = gBlockRegex.exec(html)) !== null && results.length < count) {
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 = block.match(/<div[^>]*class="[^"]*VwiC3b[^"]*"[^>]*>(.*?)<\/div>/s)
296
- || block.match(/<span[^>]*class="[^"]*st[^"]*"[^>]*>(.*?)<\/span>/s);
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
- let h3Match;
315
- while ((h3Match = h3Regex.exec(html)) !== null && results.length < count) {
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 = /href="(https?:\/\/(?!www\.google|accounts\.google|support\.google|maps\.google|policies\.google)[^"]+)"/g;
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
- let extMatch;
338
- while ((extMatch = extRegex.exec(html)) !== null && seen.size < count) {
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
- "Accept": "text/html",
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
- let m;
390
- while ((m = linkRegex.exec(html)) !== null && links.length < count) {
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 { url = decodeURIComponent(url.split("uddg=")[1].split("&")[0]); } catch { continue; }
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
- while ((m = snippetRegex.exec(html)) !== null) {
406
- snippets.push(stripTags(m[1]));
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
- "Accept": "application/json",
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((r: any): SearchResult => ({
456
- title: r.title || "No title",
457
- url: r.url || "",
458
- description: r.description || "",
459
- source: "brave",
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 (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
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
- "Accept": "text/html,application/xhtml+xml,text/plain,application/json",
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
- let readable = text
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: "Search the web. Uses Google (primary), DuckDuckGo (fallback), Brave (if API key set). No API keys required.",
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(Type.Number({
653
- description: "Number of results (1-10, default: 5)",
654
- minimum: 1,
655
- maximum: 10,
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
- type: "text",
669
- text: `No search results found for "${query}". Try rephrasing your search.\n\nProviders tried: ${response.triedProviders.join(", ")}`,
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, query,
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: "Fetch a URL and extract readable text content. Uses @mozilla/readability + jsdom if installed, otherwise basic HTML extraction.",
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(Type.Number({
718
- description: "Max characters to return (default: 8000)",
719
- minimum: 500,
720
- maximum: 50000,
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: [{ type: "text", text: `Could not extract content from ${url}. The page may require JavaScript.` }],
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) { ctx.ui.notify("Usage: /search <query>", "warning"); return; }
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.86.0",
3
+ "version": "0.88.0",
4
4
  "description": "Coding agent CLI with persistent memory, sub-agents, intelligent routing, and orchestration",
5
5
  "type": "module",
6
6
  "piConfig": {