@radaros/core 0.3.1 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as zod from 'zod';
1
2
  import { z } from 'zod';
2
3
 
3
4
  type MessageRole = "system" | "user" | "assistant" | "tool";
@@ -341,6 +342,7 @@ declare class Agent {
341
342
  get modelId(): string;
342
343
  get providerId(): string;
343
344
  get hasStructuredOutput(): boolean;
345
+ get structuredOutputSchema(): zod.ZodSchema | undefined;
344
346
  constructor(config: AgentConfig);
345
347
  run(input: MessageContent, opts?: RunOpts): Promise<RunOutput>;
346
348
  stream(input: MessageContent, opts?: RunOpts): AsyncGenerator<StreamChunk>;
@@ -1280,4 +1282,36 @@ declare class HackerNewsToolkit extends Toolkit {
1280
1282
  getTools(): ToolDef[];
1281
1283
  }
1282
1284
 
1283
- export { type A2AAgentCard, type A2AArtifact, type A2ADataPart, type A2AFilePart, type A2AJsonRpcRequest, type A2AJsonRpcResponse, type A2AMessage, type A2APart, A2ARemoteAgent, type A2ARemoteAgentConfig, type A2ASendParams, type A2ASkill, type A2ATask, type A2ATaskQueryParams, type A2ATaskState, type A2ATextPart, Agent, type AgentConfig, type AgentEventMap, type AgentHooks, type AgentStep, AnthropicProvider, type Artifact, type AudioPart, BaseVectorStore, type ChatMessage, type ConditionStep, type ContentPart, type EmbeddingProvider, EventBus, type FilePart, type FunctionStep, type GmailConfig, GmailToolkit, GoogleEmbedding, type GoogleEmbeddingConfig, GoogleProvider, type GuardrailResult, type HackerNewsConfig, HackerNewsToolkit, type ImagePart, InMemoryStorage, InMemoryVectorStore, type InputGuardrail, KnowledgeBase, type KnowledgeBaseConfig, type KnowledgeBaseToolConfig, LLMLoop, type LogLevel, Logger, type LoggerConfig, MCPToolProvider, type MCPToolProviderConfig, Memory, type MemoryConfig, type MemoryEntry, type MessageContent, type MessageRole, type ModelConfig, type ModelProvider, ModelRegistry, type ModelResponse, MongoDBStorage, type MongoDBVectorConfig, MongoDBVectorStore, OllamaProvider, OpenAIEmbedding, type OpenAIEmbeddingConfig, OpenAIProvider, type OutputGuardrail, type ParallelStep, type PgVectorConfig, PgVectorStore, PostgresStorage, type QdrantConfig, QdrantVectorStore, RunContext, type RunOpts, type RunOutput, type Session, SessionManager, SqliteStorage, type StepDef, type StepResult, type StorageDriver, type StreamChunk, Team, type TeamConfig, TeamMode, type TextPart, type TokenUsage, type ToolCall, type ToolCallResult, type ToolDef, type ToolDefinition, ToolExecutor, type ToolResult, Toolkit, type VectorDocument, type VectorSearchOptions, type VectorSearchResult, type VectorStore, type VertexAIConfig, VertexAIProvider, type WebSearchConfig, WebSearchToolkit, type WhatsAppConfig, WhatsAppToolkit, Workflow, type WorkflowConfig, type WorkflowResult, anthropic, defineTool, getTextContent, google, isMultiModal, ollama, openai, registry, vertex };
1285
+ interface DuckDuckGoConfig {
1286
+ /** Enable web search (default true). */
1287
+ enableSearch?: boolean;
1288
+ /** Enable news search (default true). */
1289
+ enableNews?: boolean;
1290
+ /** Fixed max results per query (default 5). */
1291
+ maxResults?: number;
1292
+ }
1293
+ /**
1294
+ * DuckDuckGo Toolkit — search the web and news without any API key.
1295
+ *
1296
+ * Uses the DuckDuckGo HTML API (no key required).
1297
+ *
1298
+ * @example
1299
+ * ```ts
1300
+ * const ddg = new DuckDuckGoToolkit();
1301
+ * const agent = new Agent({ tools: [...ddg.getTools()] });
1302
+ * ```
1303
+ */
1304
+ declare class DuckDuckGoToolkit extends Toolkit {
1305
+ readonly name = "duckduckgo";
1306
+ private config;
1307
+ constructor(config?: DuckDuckGoConfig);
1308
+ getTools(): ToolDef[];
1309
+ private buildSearchTool;
1310
+ private buildNewsTool;
1311
+ private search;
1312
+ private scrapeHtmlSearch;
1313
+ private decodeDdgUrl;
1314
+ private searchNews;
1315
+ }
1316
+
1317
+ export { type A2AAgentCard, type A2AArtifact, type A2ADataPart, type A2AFilePart, type A2AJsonRpcRequest, type A2AJsonRpcResponse, type A2AMessage, type A2APart, A2ARemoteAgent, type A2ARemoteAgentConfig, type A2ASendParams, type A2ASkill, type A2ATask, type A2ATaskQueryParams, type A2ATaskState, type A2ATextPart, Agent, type AgentConfig, type AgentEventMap, type AgentHooks, type AgentStep, AnthropicProvider, type Artifact, type AudioPart, BaseVectorStore, type ChatMessage, type ConditionStep, type ContentPart, type DuckDuckGoConfig, DuckDuckGoToolkit, type EmbeddingProvider, EventBus, type FilePart, type FunctionStep, type GmailConfig, GmailToolkit, GoogleEmbedding, type GoogleEmbeddingConfig, GoogleProvider, type GuardrailResult, type HackerNewsConfig, HackerNewsToolkit, type ImagePart, InMemoryStorage, InMemoryVectorStore, type InputGuardrail, KnowledgeBase, type KnowledgeBaseConfig, type KnowledgeBaseToolConfig, LLMLoop, type LogLevel, Logger, type LoggerConfig, MCPToolProvider, type MCPToolProviderConfig, Memory, type MemoryConfig, type MemoryEntry, type MessageContent, type MessageRole, type ModelConfig, type ModelProvider, ModelRegistry, type ModelResponse, MongoDBStorage, type MongoDBVectorConfig, MongoDBVectorStore, OllamaProvider, OpenAIEmbedding, type OpenAIEmbeddingConfig, OpenAIProvider, type OutputGuardrail, type ParallelStep, type PgVectorConfig, PgVectorStore, PostgresStorage, type QdrantConfig, QdrantVectorStore, RunContext, type RunOpts, type RunOutput, type Session, SessionManager, SqliteStorage, type StepDef, type StepResult, type StorageDriver, type StreamChunk, Team, type TeamConfig, TeamMode, type TextPart, type TokenUsage, type ToolCall, type ToolCallResult, type ToolDef, type ToolDefinition, ToolExecutor, type ToolResult, Toolkit, type VectorDocument, type VectorSearchOptions, type VectorSearchResult, type VectorStore, type VertexAIConfig, VertexAIProvider, type WebSearchConfig, WebSearchToolkit, type WhatsAppConfig, WhatsAppToolkit, Workflow, type WorkflowConfig, type WorkflowResult, anthropic, defineTool, getTextContent, google, isMultiModal, ollama, openai, registry, vertex };
package/dist/index.js CHANGED
@@ -639,6 +639,9 @@ var Agent = class {
639
639
  get hasStructuredOutput() {
640
640
  return !!this.config.structuredOutput;
641
641
  }
642
+ get structuredOutputSchema() {
643
+ return this.config.structuredOutput;
644
+ }
642
645
  constructor(config) {
643
646
  this.config = config;
644
647
  this.name = config.name;
@@ -3787,14 +3790,14 @@ var MCPToolProvider = class {
3787
3790
  await this.discoverTools();
3788
3791
  }
3789
3792
  async discoverTools() {
3790
- const { z: z7 } = await import("zod");
3793
+ const { z: z8 } = await import("zod");
3791
3794
  const result = await this.client.listTools();
3792
3795
  const mcpTools = result.tools ?? [];
3793
3796
  this.tools = mcpTools.map((mcpTool) => {
3794
3797
  const toolName = mcpTool.name;
3795
3798
  const description = mcpTool.description ?? "";
3796
3799
  const inputSchema = mcpTool.inputSchema ?? { type: "object", properties: {} };
3797
- const parameters = this.jsonSchemaToZod(inputSchema, z7);
3800
+ const parameters = this.jsonSchemaToZod(inputSchema, z8);
3798
3801
  const execute = async (args, _ctx) => {
3799
3802
  const callResult = await this.client.callTool({
3800
3803
  name: toolName,
@@ -3822,9 +3825,9 @@ var MCPToolProvider = class {
3822
3825
  };
3823
3826
  });
3824
3827
  }
3825
- jsonSchemaToZod(schema, z7) {
3828
+ jsonSchemaToZod(schema, z8) {
3826
3829
  if (!schema || !schema.properties) {
3827
- return z7.object({}).passthrough();
3830
+ return z8.object({}).passthrough();
3828
3831
  }
3829
3832
  const shape = {};
3830
3833
  const required = schema.required ?? [];
@@ -3832,24 +3835,24 @@ var MCPToolProvider = class {
3832
3835
  let field;
3833
3836
  switch (prop.type) {
3834
3837
  case "string":
3835
- field = z7.string();
3836
- if (prop.enum) field = z7.enum(prop.enum);
3838
+ field = z8.string();
3839
+ if (prop.enum) field = z8.enum(prop.enum);
3837
3840
  break;
3838
3841
  case "number":
3839
3842
  case "integer":
3840
- field = z7.number();
3843
+ field = z8.number();
3841
3844
  break;
3842
3845
  case "boolean":
3843
- field = z7.boolean();
3846
+ field = z8.boolean();
3844
3847
  break;
3845
3848
  case "array":
3846
- field = z7.array(z7.any());
3849
+ field = z8.array(z8.any());
3847
3850
  break;
3848
3851
  case "object":
3849
- field = z7.record(z7.any());
3852
+ field = z8.record(z8.any());
3850
3853
  break;
3851
3854
  default:
3852
- field = z7.any();
3855
+ field = z8.any();
3853
3856
  }
3854
3857
  if (prop.description) {
3855
3858
  field = field.describe(prop.description);
@@ -3859,7 +3862,7 @@ var MCPToolProvider = class {
3859
3862
  }
3860
3863
  shape[key] = field;
3861
3864
  }
3862
- return z7.object(shape);
3865
+ return z8.object(shape);
3863
3866
  }
3864
3867
  /**
3865
3868
  * Returns tools from this MCP server as RadarOS ToolDef[].
@@ -4591,11 +4594,191 @@ var HackerNewsToolkit = class extends Toolkit {
4591
4594
  return tools;
4592
4595
  }
4593
4596
  };
4597
+
4598
+ // src/toolkits/duckduckgo.ts
4599
+ import { z as z7 } from "zod";
4600
+ var DuckDuckGoToolkit = class extends Toolkit {
4601
+ name = "duckduckgo";
4602
+ config;
4603
+ constructor(config = {}) {
4604
+ super();
4605
+ this.config = {
4606
+ enableSearch: config.enableSearch ?? true,
4607
+ enableNews: config.enableNews ?? true,
4608
+ maxResults: config.maxResults ?? 5
4609
+ };
4610
+ }
4611
+ getTools() {
4612
+ const tools = [];
4613
+ if (this.config.enableSearch) {
4614
+ tools.push(this.buildSearchTool());
4615
+ }
4616
+ if (this.config.enableNews) {
4617
+ tools.push(this.buildNewsTool());
4618
+ }
4619
+ return tools;
4620
+ }
4621
+ buildSearchTool() {
4622
+ const self = this;
4623
+ return {
4624
+ name: "duckduckgo_search",
4625
+ description: "Search the web using DuckDuckGo. Returns titles, URLs, and snippets. No API key required.",
4626
+ parameters: z7.object({
4627
+ query: z7.string().describe("The search query"),
4628
+ maxResults: z7.number().optional().describe("Maximum number of results (default 5)")
4629
+ }),
4630
+ async execute(args, _ctx) {
4631
+ const query = args.query;
4632
+ const max = args.maxResults ?? self.config.maxResults ?? 5;
4633
+ return self.search(query, max);
4634
+ }
4635
+ };
4636
+ }
4637
+ buildNewsTool() {
4638
+ const self = this;
4639
+ return {
4640
+ name: "duckduckgo_news",
4641
+ description: "Get the latest news from DuckDuckGo. Returns headlines, sources, URLs, and dates.",
4642
+ parameters: z7.object({
4643
+ query: z7.string().describe("The news search query"),
4644
+ maxResults: z7.number().optional().describe("Maximum number of results (default 5)")
4645
+ }),
4646
+ async execute(args, _ctx) {
4647
+ const query = args.query;
4648
+ const max = args.maxResults ?? self.config.maxResults ?? 5;
4649
+ return self.searchNews(query, max);
4650
+ }
4651
+ };
4652
+ }
4653
+ async search(query, maxResults) {
4654
+ const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
4655
+ const res = await fetch(url, {
4656
+ headers: { "User-Agent": "RadarOS/1.0" }
4657
+ });
4658
+ if (!res.ok) {
4659
+ throw new Error(`DuckDuckGo search failed: ${res.status}`);
4660
+ }
4661
+ const data = await res.json();
4662
+ const results = [];
4663
+ if (data.Abstract) {
4664
+ results.push(
4665
+ `Answer: ${data.Abstract}
4666
+ Source: ${data.AbstractSource}
4667
+ URL: ${data.AbstractURL}`
4668
+ );
4669
+ }
4670
+ const topics = data.RelatedTopics ?? [];
4671
+ for (const topic of topics.slice(0, maxResults)) {
4672
+ if (topic.Text && topic.FirstURL) {
4673
+ results.push(`${topic.Text}
4674
+ URL: ${topic.FirstURL}`);
4675
+ }
4676
+ if (topic.Topics) {
4677
+ for (const sub of topic.Topics.slice(0, 2)) {
4678
+ if (sub.Text && sub.FirstURL) {
4679
+ results.push(`${sub.Text}
4680
+ URL: ${sub.FirstURL}`);
4681
+ }
4682
+ }
4683
+ }
4684
+ }
4685
+ if (results.length === 0 && data.Redirect) {
4686
+ return `Redirect: ${data.Redirect}`;
4687
+ }
4688
+ if (results.length === 0) {
4689
+ const htmlResults = await this.scrapeHtmlSearch(query, maxResults);
4690
+ if (htmlResults) return htmlResults;
4691
+ return "No results found.";
4692
+ }
4693
+ return results.join("\n\n---\n\n");
4694
+ }
4695
+ async scrapeHtmlSearch(query, maxResults) {
4696
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
4697
+ const res = await fetch(url, {
4698
+ headers: {
4699
+ "User-Agent": "Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)"
4700
+ }
4701
+ });
4702
+ if (!res.ok) return null;
4703
+ const html = await res.text();
4704
+ const results = [];
4705
+ const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
4706
+ const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
4707
+ const links = [];
4708
+ let match;
4709
+ while ((match = linkRegex.exec(html)) !== null) {
4710
+ const rawUrl = match[1];
4711
+ const title = match[2].replace(/<[^>]*>/g, "").trim();
4712
+ const decoded = this.decodeDdgUrl(rawUrl);
4713
+ if (title && decoded) {
4714
+ links.push({ url: decoded, title });
4715
+ }
4716
+ }
4717
+ const snippets = [];
4718
+ while ((match = snippetRegex.exec(html)) !== null) {
4719
+ snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
4720
+ }
4721
+ for (let i = 0; i < Math.min(links.length, maxResults); i++) {
4722
+ results.push(
4723
+ `Title: ${links[i].title}
4724
+ URL: ${links[i].url}
4725
+ Snippet: ${snippets[i] ?? ""}`
4726
+ );
4727
+ }
4728
+ return results.length > 0 ? results.join("\n\n---\n\n") : null;
4729
+ }
4730
+ decodeDdgUrl(url) {
4731
+ if (url.startsWith("//duckduckgo.com/l/?uddg=")) {
4732
+ const match = url.match(/uddg=([^&]*)/);
4733
+ if (match) return decodeURIComponent(match[1]);
4734
+ }
4735
+ if (url.startsWith("http")) return url;
4736
+ return null;
4737
+ }
4738
+ async searchNews(query, maxResults) {
4739
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query + " news")}&iar=news`;
4740
+ const res = await fetch(url, {
4741
+ headers: {
4742
+ "User-Agent": "Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)"
4743
+ }
4744
+ });
4745
+ if (!res.ok) {
4746
+ throw new Error(`DuckDuckGo news search failed: ${res.status}`);
4747
+ }
4748
+ const html = await res.text();
4749
+ const results = [];
4750
+ const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
4751
+ const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
4752
+ const links = [];
4753
+ let match;
4754
+ while ((match = linkRegex.exec(html)) !== null) {
4755
+ const rawUrl = match[1];
4756
+ const title = match[2].replace(/<[^>]*>/g, "").trim();
4757
+ const decoded = this.decodeDdgUrl(rawUrl);
4758
+ if (title && decoded) {
4759
+ links.push({ url: decoded, title });
4760
+ }
4761
+ }
4762
+ const snippets = [];
4763
+ while ((match = snippetRegex.exec(html)) !== null) {
4764
+ snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
4765
+ }
4766
+ for (let i = 0; i < Math.min(links.length, maxResults); i++) {
4767
+ results.push(
4768
+ `Title: ${links[i].title}
4769
+ URL: ${links[i].url}
4770
+ Snippet: ${snippets[i] ?? ""}`
4771
+ );
4772
+ }
4773
+ return results.length > 0 ? results.join("\n\n---\n\n") : "No news results found.";
4774
+ }
4775
+ };
4594
4776
  export {
4595
4777
  A2ARemoteAgent,
4596
4778
  Agent,
4597
4779
  AnthropicProvider,
4598
4780
  BaseVectorStore,
4781
+ DuckDuckGoToolkit,
4599
4782
  EventBus,
4600
4783
  GmailToolkit,
4601
4784
  GoogleEmbedding,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radaros/core",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -39,6 +39,10 @@ export class Agent {
39
39
  return !!this.config.structuredOutput;
40
40
  }
41
41
 
42
+ get structuredOutputSchema(): import("zod").ZodSchema | undefined {
43
+ return this.config.structuredOutput;
44
+ }
45
+
42
46
  constructor(config: AgentConfig) {
43
47
  this.config = config;
44
48
  this.name = config.name;
package/src/index.ts CHANGED
@@ -146,3 +146,5 @@ export { WhatsAppToolkit } from "./toolkits/whatsapp.js";
146
146
  export type { WhatsAppConfig } from "./toolkits/whatsapp.js";
147
147
  export { HackerNewsToolkit } from "./toolkits/hackernews.js";
148
148
  export type { HackerNewsConfig } from "./toolkits/hackernews.js";
149
+ export { DuckDuckGoToolkit } from "./toolkits/duckduckgo.js";
150
+ export type { DuckDuckGoConfig } from "./toolkits/duckduckgo.js";
@@ -0,0 +1,256 @@
1
+ import { z } from "zod";
2
+ import type { ToolDef } from "../tools/types.js";
3
+ import type { RunContext } from "../agent/run-context.js";
4
+ import { Toolkit } from "./base.js";
5
+
6
+ export interface DuckDuckGoConfig {
7
+ /** Enable web search (default true). */
8
+ enableSearch?: boolean;
9
+ /** Enable news search (default true). */
10
+ enableNews?: boolean;
11
+ /** Fixed max results per query (default 5). */
12
+ maxResults?: number;
13
+ }
14
+
15
+ /**
16
+ * DuckDuckGo Toolkit — search the web and news without any API key.
17
+ *
18
+ * Uses the DuckDuckGo HTML API (no key required).
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const ddg = new DuckDuckGoToolkit();
23
+ * const agent = new Agent({ tools: [...ddg.getTools()] });
24
+ * ```
25
+ */
26
+ export class DuckDuckGoToolkit extends Toolkit {
27
+ readonly name = "duckduckgo";
28
+ private config: DuckDuckGoConfig;
29
+
30
+ constructor(config: DuckDuckGoConfig = {}) {
31
+ super();
32
+ this.config = {
33
+ enableSearch: config.enableSearch ?? true,
34
+ enableNews: config.enableNews ?? true,
35
+ maxResults: config.maxResults ?? 5,
36
+ };
37
+ }
38
+
39
+ getTools(): ToolDef[] {
40
+ const tools: ToolDef[] = [];
41
+
42
+ if (this.config.enableSearch) {
43
+ tools.push(this.buildSearchTool());
44
+ }
45
+
46
+ if (this.config.enableNews) {
47
+ tools.push(this.buildNewsTool());
48
+ }
49
+
50
+ return tools;
51
+ }
52
+
53
+ private buildSearchTool(): ToolDef {
54
+ const self = this;
55
+ return {
56
+ name: "duckduckgo_search",
57
+ description:
58
+ "Search the web using DuckDuckGo. Returns titles, URLs, and snippets. No API key required.",
59
+ parameters: z.object({
60
+ query: z.string().describe("The search query"),
61
+ maxResults: z
62
+ .number()
63
+ .optional()
64
+ .describe("Maximum number of results (default 5)"),
65
+ }),
66
+ async execute(
67
+ args: Record<string, unknown>,
68
+ _ctx: RunContext
69
+ ): Promise<string> {
70
+ const query = args.query as string;
71
+ const max = (args.maxResults as number) ?? self.config.maxResults ?? 5;
72
+ return self.search(query, max);
73
+ },
74
+ };
75
+ }
76
+
77
+ private buildNewsTool(): ToolDef {
78
+ const self = this;
79
+ return {
80
+ name: "duckduckgo_news",
81
+ description:
82
+ "Get the latest news from DuckDuckGo. Returns headlines, sources, URLs, and dates.",
83
+ parameters: z.object({
84
+ query: z.string().describe("The news search query"),
85
+ maxResults: z
86
+ .number()
87
+ .optional()
88
+ .describe("Maximum number of results (default 5)"),
89
+ }),
90
+ async execute(
91
+ args: Record<string, unknown>,
92
+ _ctx: RunContext
93
+ ): Promise<string> {
94
+ const query = args.query as string;
95
+ const max = (args.maxResults as number) ?? self.config.maxResults ?? 5;
96
+ return self.searchNews(query, max);
97
+ },
98
+ };
99
+ }
100
+
101
+ private async search(query: string, maxResults: number): Promise<string> {
102
+ const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
103
+
104
+ const res = await fetch(url, {
105
+ headers: { "User-Agent": "RadarOS/1.0" },
106
+ });
107
+
108
+ if (!res.ok) {
109
+ throw new Error(`DuckDuckGo search failed: ${res.status}`);
110
+ }
111
+
112
+ const data = (await res.json()) as any;
113
+ const results: string[] = [];
114
+
115
+ if (data.Abstract) {
116
+ results.push(
117
+ `Answer: ${data.Abstract}\nSource: ${data.AbstractSource}\nURL: ${data.AbstractURL}`
118
+ );
119
+ }
120
+
121
+ const topics = data.RelatedTopics ?? [];
122
+ for (const topic of topics.slice(0, maxResults)) {
123
+ if (topic.Text && topic.FirstURL) {
124
+ results.push(`${topic.Text}\nURL: ${topic.FirstURL}`);
125
+ }
126
+ if (topic.Topics) {
127
+ for (const sub of topic.Topics.slice(0, 2)) {
128
+ if (sub.Text && sub.FirstURL) {
129
+ results.push(`${sub.Text}\nURL: ${sub.FirstURL}`);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (results.length === 0 && data.Redirect) {
136
+ return `Redirect: ${data.Redirect}`;
137
+ }
138
+
139
+ if (results.length === 0) {
140
+ const htmlResults = await this.scrapeHtmlSearch(query, maxResults);
141
+ if (htmlResults) return htmlResults;
142
+ return "No results found.";
143
+ }
144
+
145
+ return results.join("\n\n---\n\n");
146
+ }
147
+
148
+ private async scrapeHtmlSearch(
149
+ query: string,
150
+ maxResults: number
151
+ ): Promise<string | null> {
152
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
153
+
154
+ const res = await fetch(url, {
155
+ headers: {
156
+ "User-Agent":
157
+ "Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)",
158
+ },
159
+ });
160
+
161
+ if (!res.ok) return null;
162
+
163
+ const html = await res.text();
164
+
165
+ const results: string[] = [];
166
+ const linkRegex =
167
+ /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
168
+ const snippetRegex =
169
+ /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
170
+
171
+ const links: { url: string; title: string }[] = [];
172
+ let match;
173
+ while ((match = linkRegex.exec(html)) !== null) {
174
+ const rawUrl = match[1];
175
+ const title = match[2].replace(/<[^>]*>/g, "").trim();
176
+ const decoded = this.decodeDdgUrl(rawUrl);
177
+ if (title && decoded) {
178
+ links.push({ url: decoded, title });
179
+ }
180
+ }
181
+
182
+ const snippets: string[] = [];
183
+ while ((match = snippetRegex.exec(html)) !== null) {
184
+ snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
185
+ }
186
+
187
+ for (let i = 0; i < Math.min(links.length, maxResults); i++) {
188
+ results.push(
189
+ `Title: ${links[i].title}\nURL: ${links[i].url}\nSnippet: ${snippets[i] ?? ""}`
190
+ );
191
+ }
192
+
193
+ return results.length > 0 ? results.join("\n\n---\n\n") : null;
194
+ }
195
+
196
+ private decodeDdgUrl(url: string): string | null {
197
+ if (url.startsWith("//duckduckgo.com/l/?uddg=")) {
198
+ const match = url.match(/uddg=([^&]*)/);
199
+ if (match) return decodeURIComponent(match[1]);
200
+ }
201
+ if (url.startsWith("http")) return url;
202
+ return null;
203
+ }
204
+
205
+ private async searchNews(
206
+ query: string,
207
+ maxResults: number
208
+ ): Promise<string> {
209
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query + " news")}&iar=news`;
210
+
211
+ const res = await fetch(url, {
212
+ headers: {
213
+ "User-Agent":
214
+ "Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)",
215
+ },
216
+ });
217
+
218
+ if (!res.ok) {
219
+ throw new Error(`DuckDuckGo news search failed: ${res.status}`);
220
+ }
221
+
222
+ const html = await res.text();
223
+ const results: string[] = [];
224
+
225
+ const linkRegex =
226
+ /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
227
+ const snippetRegex =
228
+ /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
229
+
230
+ const links: { url: string; title: string }[] = [];
231
+ let match;
232
+ while ((match = linkRegex.exec(html)) !== null) {
233
+ const rawUrl = match[1];
234
+ const title = match[2].replace(/<[^>]*>/g, "").trim();
235
+ const decoded = this.decodeDdgUrl(rawUrl);
236
+ if (title && decoded) {
237
+ links.push({ url: decoded, title });
238
+ }
239
+ }
240
+
241
+ const snippets: string[] = [];
242
+ while ((match = snippetRegex.exec(html)) !== null) {
243
+ snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
244
+ }
245
+
246
+ for (let i = 0; i < Math.min(links.length, maxResults); i++) {
247
+ results.push(
248
+ `Title: ${links[i].title}\nURL: ${links[i].url}\nSnippet: ${snippets[i] ?? ""}`
249
+ );
250
+ }
251
+
252
+ return results.length > 0
253
+ ? results.join("\n\n---\n\n")
254
+ : "No news results found.";
255
+ }
256
+ }