pi-web-search 1.0.0 → 1.0.2

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 CHANGED
@@ -5,8 +5,7 @@ A pi extension that provides web search and content analysis capabilities powere
5
5
  ## Features
6
6
 
7
7
  - **🔍 Web Search** - Google search with citations
8
- - **📄 URL Analysis** - Analyze web pages, PDFs, and images (up to 20 URLs)
9
- - **🎥 YouTube** - Video summaries and Q&A
8
+ - **📄 URL Analysis** - Analyze web pages, PDFs, images, and YouTube videos (up to 20 URLs)
10
9
 
11
10
  ## Installation
12
11
 
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { webSearch, WebSearchSchema } from "./web_search.js";
2
2
  import { urlContext, UrlContextSchema } from "./url_context.js";
3
- import { youtubeVideo, YoutubeVideoSchema } from "./youtube_video.js";
4
3
  export default function (pi) {
5
4
  pi.registerTool({
6
5
  name: "web_search",
@@ -12,15 +11,8 @@ export default function (pi) {
12
11
  pi.registerTool({
13
12
  name: "url_context",
14
13
  label: "URL Context",
15
- description: "Analyze web pages and documents. Extract data, compare documents, synthesize content. Supports text/html, PDF, images, JSON, CSV. Up to 20 URLs.",
14
+ description: "Analyze the content of up to 20 public URLs. Supports web pages, documents, images, and YouTube videos.",
16
15
  parameters: UrlContextSchema,
17
16
  execute: urlContext
18
17
  });
19
- pi.registerTool({
20
- name: "youtube_video",
21
- label: "YouTube Video",
22
- description: "Analyze YouTube videos. Summarize, answer questions, find timestamps. Supports video clipping (start/end offsets). Preview feature.",
23
- parameters: YoutubeVideoSchema,
24
- execute: youtubeVideo
25
- });
26
18
  }
@@ -4,11 +4,12 @@ import { getModel, missingConfigResult, errorResult, formatResult } from "./util
4
4
  export const UrlContextSchema = Type.Object({
5
5
  query: Type.String({ description: "Question or task to perform on the URLs" }),
6
6
  urls: Type.Array(Type.String(), {
7
- description: "URLs to analyze (up to 20). Supports text/html, PDF, images, JSON, CSV, etc.",
7
+ description: "Public URLs to analyze (web pages, documents, images, YouTube videos, etc).",
8
8
  minItems: 1,
9
9
  maxItems: 20
10
10
  }),
11
11
  });
12
+ const YOUTUBE_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
12
13
  export async function urlContext(id, params, signal, onUpdate, ctx) {
13
14
  const model = await getModel(ctx);
14
15
  if (!model)
@@ -17,10 +18,56 @@ export async function urlContext(id, params, signal, onUpdate, ctx) {
17
18
  onUpdate?.({ content: [{ type: "text", text: `Analyzing ${count} URL${count > 1 ? 's' : ''}...` }], details: {} });
18
19
  try {
19
20
  const config = getConfig(model);
20
- const combinedPrompt = `${params.query}\n\nURLs:\n${params.urls.join("\n")}`;
21
+ let contents = [];
22
+ let tools = [{ [config.urlContextTool]: {} }];
23
+ // Special handling for YouTube videos on Gemini
24
+ if (model.api === "google-generative-ai") {
25
+ const youtubeUrls = [];
26
+ const otherUrls = [];
27
+ for (const url of params.urls) {
28
+ if (YOUTUBE_REGEX.test(url)) {
29
+ youtubeUrls.push(url);
30
+ }
31
+ else {
32
+ otherUrls.push(url);
33
+ }
34
+ }
35
+ // If we have YouTube URLs, construct file_data parts
36
+ if (youtubeUrls.length > 0) {
37
+ const parts = [];
38
+ for (const url of youtubeUrls) {
39
+ parts.push({
40
+ file_data: { file_uri: url, mime_type: "video/mp4" }
41
+ });
42
+ }
43
+ let prompt = params.query;
44
+ if (otherUrls.length > 0) {
45
+ prompt += `\n\nURLs:\n${otherUrls.join("\n")}`;
46
+ }
47
+ else {
48
+ // If no other URLs, we might not need the tool, but keep it just in case
49
+ // or maybe the tool is required for grounding even with video?
50
+ // "google_search_retrieval" tool might confuse if there are no URLs to retrieve.
51
+ // But if we remove the tool, we might lose grounding capabilities (like search).
52
+ // Let's keep the tool enabled.
53
+ }
54
+ parts.push({ text: prompt });
55
+ contents = [{ role: "user", parts }];
56
+ }
57
+ else {
58
+ // No YouTube URLs, standard behavior
59
+ const combinedPrompt = `${params.query}\n\nURLs:\n${params.urls.join("\n")}`;
60
+ contents = [{ role: "user", parts: [{ text: combinedPrompt }] }];
61
+ }
62
+ }
63
+ else {
64
+ // Not Gemini, standard behavior
65
+ const combinedPrompt = `${params.query}\n\nURLs:\n${params.urls.join("\n")}`;
66
+ contents = [{ role: "user", parts: [{ text: combinedPrompt }] }];
67
+ }
21
68
  const result = await callApiStream(ctx, model, {
22
- contents: [{ role: "user", parts: [{ text: combinedPrompt }] }],
23
- tools: [{ [config.urlContextTool]: {} }]
69
+ contents,
70
+ tools
24
71
  }, onUpdate);
25
72
  const { text, sources } = applyCitations(result.text, result.groundingMetadata);
26
73
  // Handle both camelCase and snake_case metadata
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-search",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Web search and content analysis extension for pi, powered by Gemini API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,15 +24,9 @@
24
24
  "url": "https://github.com/ttttmr/pi-web-search/issues"
25
25
  },
26
26
  "homepage": "https://github.com/ttttmr/pi-web-search#readme",
27
- "scripts": {
28
- "build": "tsc",
29
- "prepublishOnly": "npm run build"
30
- },
31
- "dependencies": {
32
- "@mariozechner/pi-coding-agent": "^0.51.5",
33
- "@sinclair/typebox": "^0.32.15"
34
- },
35
27
  "devDependencies": {
28
+ "@mariozechner/pi-coding-agent": "^0.51.5",
29
+ "@sinclair/typebox": "^0.32.15",
36
30
  "typescript": "^5.0.0",
37
31
  "@types/node": "^20.0.0"
38
32
  },
package/src/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { webSearch, WebSearchSchema } from "./web_search.js";
3
3
  import { urlContext, UrlContextSchema } from "./url_context.js";
4
- import { youtubeVideo, YoutubeVideoSchema } from "./youtube_video.js";
5
4
 
6
5
  export default function (pi: ExtensionAPI) {
7
6
  pi.registerTool({
@@ -15,16 +14,8 @@ export default function (pi: ExtensionAPI) {
15
14
  pi.registerTool({
16
15
  name: "url_context",
17
16
  label: "URL Context",
18
- description: "Analyze web pages and documents. Extract data, compare documents, synthesize content. Supports text/html, PDF, images, JSON, CSV. Up to 20 URLs.",
17
+ description: "Analyze the content of up to 20 public URLs. Supports web pages, documents, images, and YouTube videos.",
19
18
  parameters: UrlContextSchema,
20
19
  execute: urlContext
21
20
  });
22
-
23
- pi.registerTool({
24
- name: "youtube_video",
25
- label: "YouTube Video",
26
- description: "Analyze YouTube videos. Summarize, answer questions, find timestamps. Supports video clipping (start/end offsets). Preview feature.",
27
- parameters: YoutubeVideoSchema,
28
- execute: youtubeVideo
29
- });
30
21
  }
@@ -6,13 +6,15 @@ import { getModel, missingConfigResult, errorResult, formatResult } from "./util
6
6
  export const UrlContextSchema = Type.Object({
7
7
  query: Type.String({ description: "Question or task to perform on the URLs" }),
8
8
  urls: Type.Array(Type.String(), {
9
- description: "URLs to analyze (up to 20). Supports text/html, PDF, images, JSON, CSV, etc.",
9
+ description: "Public URLs to analyze (web pages, documents, images, YouTube videos, etc).",
10
10
  minItems: 1,
11
11
  maxItems: 20
12
12
  }),
13
13
  });
14
14
  export type UrlContextInput = Static<typeof UrlContextSchema>;
15
15
 
16
+ const YOUTUBE_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
17
+
16
18
  export async function urlContext(
17
19
  id: string,
18
20
  params: UrlContextInput,
@@ -28,11 +30,60 @@ export async function urlContext(
28
30
 
29
31
  try {
30
32
  const config = getConfig(model);
31
- const combinedPrompt = `${params.query}\n\nURLs:\n${params.urls.join("\n")}`;
33
+
34
+ let contents: any[] = [];
35
+ let tools: any[] | undefined = [{ [config.urlContextTool]: {} }];
36
+
37
+ // Special handling for YouTube videos on Gemini
38
+ if (model.api === "google-generative-ai") {
39
+ const youtubeUrls: string[] = [];
40
+ const otherUrls: string[] = [];
41
+
42
+ for (const url of params.urls) {
43
+ if (YOUTUBE_REGEX.test(url)) {
44
+ youtubeUrls.push(url);
45
+ } else {
46
+ otherUrls.push(url);
47
+ }
48
+ }
49
+
50
+ // If we have YouTube URLs, construct file_data parts
51
+ if (youtubeUrls.length > 0) {
52
+ const parts: any[] = [];
53
+
54
+ for (const url of youtubeUrls) {
55
+ parts.push({
56
+ file_data: { file_uri: url, mime_type: "video/mp4" }
57
+ });
58
+ }
59
+
60
+ let prompt = params.query;
61
+ if (otherUrls.length > 0) {
62
+ prompt += `\n\nURLs:\n${otherUrls.join("\n")}`;
63
+ } else {
64
+ // If no other URLs, we might not need the tool, but keep it just in case
65
+ // or maybe the tool is required for grounding even with video?
66
+ // "google_search_retrieval" tool might confuse if there are no URLs to retrieve.
67
+ // But if we remove the tool, we might lose grounding capabilities (like search).
68
+ // Let's keep the tool enabled.
69
+ }
70
+
71
+ parts.push({ text: prompt });
72
+ contents = [{ role: "user", parts }];
73
+ } else {
74
+ // No YouTube URLs, standard behavior
75
+ const combinedPrompt = `${params.query}\n\nURLs:\n${params.urls.join("\n")}`;
76
+ contents = [{ role: "user", parts: [{ text: combinedPrompt }] }];
77
+ }
78
+ } else {
79
+ // Not Gemini, standard behavior
80
+ const combinedPrompt = `${params.query}\n\nURLs:\n${params.urls.join("\n")}`;
81
+ contents = [{ role: "user", parts: [{ text: combinedPrompt }] }];
82
+ }
32
83
 
33
84
  const result = await callApiStream(ctx, model, {
34
- contents: [{ role: "user", parts: [{ text: combinedPrompt }] }],
35
- tools: [{ [config.urlContextTool]: {} }]
85
+ contents,
86
+ tools
36
87
  }, onUpdate);
37
88
 
38
89
  const { text, sources } = applyCitations(result.text, result.groundingMetadata);
@@ -1,8 +0,0 @@
1
- import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
2
- import { type Static } from "@sinclair/typebox";
3
- export declare const AdvancedSearchSchema: import("@sinclair/typebox").TObject<{
4
- query: import("@sinclair/typebox").TString;
5
- urls: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
6
- }>;
7
- export type AdvancedSearchInput = Static<typeof AdvancedSearchSchema>;
8
- export declare function advancedSearch(id: string, params: AdvancedSearchInput, signal: AbortSignal, onUpdate: AgentToolUpdateCallback | undefined, ctx: ExtensionContext): Promise<import("@mariozechner/pi-coding-agent").AgentToolResult<any>>;
@@ -1,76 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import { callApiStream, getConfig, applyCitations } from "./api.js";
3
- import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.js";
4
- export const AdvancedSearchSchema = Type.Object({
5
- query: Type.String({ description: "The search query or question to answer" }),
6
- urls: Type.Optional(Type.Array(Type.String(), {
7
- description: "Additional URLs to include in context (up to 20)",
8
- maxItems: 20
9
- })),
10
- });
11
- export async function advancedSearch(id, params, signal, onUpdate, ctx) {
12
- const model = await getModel(ctx);
13
- if (!model)
14
- return missingConfigResult(ctx);
15
- const hasUrls = params.urls && params.urls.length > 0;
16
- const urlCount = hasUrls ? params.urls.length : 0;
17
- onUpdate?.({
18
- content: [{
19
- type: "text",
20
- text: hasUrls
21
- ? `Searching and analyzing ${urlCount} URL(s)...`
22
- : `Searching for "${params.query}"...`
23
- }],
24
- details: {}
25
- });
26
- try {
27
- const config = getConfig(model);
28
- // Build prompt: include URLs in the query if provided
29
- const prompt = hasUrls
30
- ? `${params.query}\n\nAlso analyze these URLs:\n${params.urls.join("\n")}`
31
- : params.query;
32
- // Enable both tools: google_search + url_context
33
- // Gemini will decide when to use each
34
- const tools = hasUrls
35
- ? [{ [config.searchTool]: {} }, { [config.urlContextTool]: {} }]
36
- : [{ [config.searchTool]: {} }];
37
- const result = await callApiStream(ctx, model, {
38
- contents: [{ role: "user", parts: [{ text: prompt }] }],
39
- tools
40
- }, onUpdate);
41
- const { text, sources } = applyCitations(result.text, result.groundingMetadata);
42
- // Handle URL context metadata
43
- const urlMeta = result.urlContextMetadata?.urlMetadata
44
- || result.urlContextMetadata?.url_metadata || [];
45
- const retrieved = urlMeta
46
- .filter((m) => (m.urlRetrievalStatus || m.url_retrieval_status) === "URL_RETRIEVAL_STATUS_SUCCESS")
47
- .map((m) => m.retrievedUrl || m.retrieved_url || m.url);
48
- const failed = urlMeta
49
- .filter((m) => (m.urlRetrievalStatus || m.url_retrieval_status) !== "URL_RETRIEVAL_STATUS_SUCCESS")
50
- .map((m) => ({
51
- url: m.retrievedUrl || m.retrieved_url || m.url,
52
- status: m.urlRetrievalStatus || m.url_retrieval_status
53
- }));
54
- let summary = text;
55
- // Add URL status if there were failures
56
- if (failed.length > 0) {
57
- summary += `\n\n## URL Status\n✅ Retrieved: ${retrieved.length}\n❌ Failed: ${failed.length}`;
58
- failed.forEach((f) => { summary += `\n- ${f.url}: ${f.status}`; });
59
- }
60
- // Add sources
61
- if (sources.length > 0) {
62
- summary += `\n\n## Sources\n${sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")}`;
63
- }
64
- return formatResult(summary, {
65
- sources,
66
- searchQueries: result.groundingMetadata?.webSearchQueries,
67
- retrieved: retrieved.length > 0 ? retrieved : undefined,
68
- failed: failed.length > 0 ? failed : undefined,
69
- model: model.id,
70
- grounded: sources.length > 0
71
- });
72
- }
73
- catch (e) {
74
- return errorResult(e);
75
- }
76
- }
@@ -1,10 +0,0 @@
1
- import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
2
- import { type Static } from "@sinclair/typebox";
3
- export declare const YoutubeVideoSchema: import("@sinclair/typebox").TObject<{
4
- video_url: import("@sinclair/typebox").TString;
5
- query: import("@sinclair/typebox").TString;
6
- start_offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
7
- end_offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
8
- }>;
9
- export type YoutubeVideoInput = Static<typeof YoutubeVideoSchema>;
10
- export declare function youtubeVideo(id: string, params: YoutubeVideoInput, signal: AbortSignal, onUpdate: AgentToolUpdateCallback | undefined, ctx: ExtensionContext): Promise<import("@mariozechner/pi-coding-agent").AgentToolResult<any>>;
@@ -1,54 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import { callApiStream } from "./api.js";
3
- import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.js";
4
- import { urlContext } from "./url_context.js";
5
- export const YoutubeVideoSchema = Type.Object({
6
- video_url: Type.String({ description: "YouTube video URL" }),
7
- query: Type.String({ description: "Question or task about the video" }),
8
- start_offset: Type.Optional(Type.String({ description: "Start time (e.g., '120s' or '2:00')" })),
9
- end_offset: Type.Optional(Type.String({ description: "End time (e.g., '300s' or '5:00')" })),
10
- });
11
- export async function youtubeVideo(id, params, signal, onUpdate, ctx) {
12
- const model = await getModel(ctx);
13
- if (!model)
14
- return missingConfigResult(ctx);
15
- // Only google-generative-ai supports native video; others use URL context
16
- if (model.api !== "google-generative-ai") {
17
- let query = params.query;
18
- if (params.start_offset || params.end_offset) {
19
- query += "\n\nFocus on the video section";
20
- if (params.start_offset)
21
- query += ` from ${params.start_offset}`;
22
- if (params.end_offset)
23
- query += ` to ${params.end_offset}`;
24
- query += ".";
25
- }
26
- return urlContext(id, { query, urls: [params.video_url] }, signal, onUpdate, ctx);
27
- }
28
- onUpdate?.({ content: [{ type: "text", text: `Analyzing YouTube video...` }], details: {} });
29
- try {
30
- const videoPart = {
31
- file_data: { file_uri: params.video_url, mime_type: "video/mp4" }
32
- };
33
- if (params.start_offset || params.end_offset) {
34
- videoPart.video_metadata = {};
35
- if (params.start_offset)
36
- videoPart.video_metadata.start_offset = params.start_offset;
37
- if (params.end_offset)
38
- videoPart.video_metadata.end_offset = params.end_offset;
39
- }
40
- const result = await callApiStream(ctx, model, {
41
- contents: [{ role: "user", parts: [videoPart, { text: params.query }] }]
42
- }, onUpdate);
43
- return formatResult(result.text, {
44
- videoUrl: params.video_url,
45
- clipping: (params.start_offset || params.end_offset)
46
- ? { start: params.start_offset, end: params.end_offset }
47
- : undefined,
48
- model: model.id
49
- });
50
- }
51
- catch (e) {
52
- return errorResult(e);
53
- }
54
- }
package/run.sh DELETED
@@ -1,9 +0,0 @@
1
- pi --no-skills --no-extensions -e ./src/index.ts --models "google-antigravity/*" '搜索今天btc价格'
2
- pi --no-skills --no-extensions -e ./src/index.ts --models "google-gemini-cli/*" '搜索今天btc价格'
3
- pi --no-skills --no-extensions -e ./src/index.ts --models "google/*" '搜索今天btc价格'
4
-
5
- pi --no-skills --no-extensions -e ./src/index.ts --provider google-antigravity --model claude-sonnet-4-5 '搜索今天btc价格'
6
- # ok
7
- pi --no-skills --no-extensions -e ./src/index.ts --provider google-gemini-cli --model gemini-3-flash-preview '搜索今天btc价格'
8
- # ok
9
- pi --no-skills --no-extensions -e ./src/index.ts --provider google --model gemini-2.5-flash '搜索今天btc价格'
@@ -1,64 +0,0 @@
1
- import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
2
- import { Type, type Static } from "@sinclair/typebox";
3
- import { callApiStream } from "./api.js";
4
- import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.js";
5
- import { urlContext } from "./url_context.js";
6
-
7
- export const YoutubeVideoSchema = Type.Object({
8
- video_url: Type.String({ description: "YouTube video URL" }),
9
- query: Type.String({ description: "Question or task about the video" }),
10
- start_offset: Type.Optional(Type.String({ description: "Start time (e.g., '120s' or '2:00')" })),
11
- end_offset: Type.Optional(Type.String({ description: "End time (e.g., '300s' or '5:00')" })),
12
- });
13
- export type YoutubeVideoInput = Static<typeof YoutubeVideoSchema>;
14
-
15
- export async function youtubeVideo(
16
- id: string,
17
- params: YoutubeVideoInput,
18
- signal: AbortSignal,
19
- onUpdate: AgentToolUpdateCallback | undefined,
20
- ctx: ExtensionContext
21
- ) {
22
- const model = await getModel(ctx);
23
- if (!model) return missingConfigResult(ctx);
24
-
25
- // Only google-generative-ai supports native video; others use URL context
26
- if (model.api !== "google-generative-ai") {
27
- let query = params.query;
28
- if (params.start_offset || params.end_offset) {
29
- query += "\n\nFocus on the video section";
30
- if (params.start_offset) query += ` from ${params.start_offset}`;
31
- if (params.end_offset) query += ` to ${params.end_offset}`;
32
- query += ".";
33
- }
34
- return urlContext(id, { query, urls: [params.video_url] }, signal, onUpdate, ctx);
35
- }
36
-
37
- onUpdate?.({ content: [{ type: "text", text: `Analyzing YouTube video...` }], details: {} });
38
-
39
- try {
40
- const videoPart: any = {
41
- file_data: { file_uri: params.video_url, mime_type: "video/mp4" }
42
- };
43
-
44
- if (params.start_offset || params.end_offset) {
45
- videoPart.video_metadata = {};
46
- if (params.start_offset) videoPart.video_metadata.start_offset = params.start_offset;
47
- if (params.end_offset) videoPart.video_metadata.end_offset = params.end_offset;
48
- }
49
-
50
- const result = await callApiStream(ctx, model, {
51
- contents: [{ role: "user", parts: [videoPart, { text: params.query }] }]
52
- }, onUpdate);
53
-
54
- return formatResult(result.text, {
55
- videoUrl: params.video_url,
56
- clipping: (params.start_offset || params.end_offset)
57
- ? { start: params.start_offset, end: params.end_offset }
58
- : undefined,
59
- model: model.id
60
- });
61
- } catch (e: any) {
62
- return errorResult(e);
63
- }
64
- }