add: AnySearch-Skill

This commit is contained in:
InfinityPacer
2026-06-21 08:17:32 +08:00
committed by jxxghp
parent e02cebe16c
commit 6b21abd547
18 changed files with 3023 additions and 116 deletions
+415
View File
@@ -0,0 +1,415 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const https = require("https");
process.stdout.setDefaultEncoding && process.stdout.setDefaultEncoding("utf-8");
const ENDPOINT = "https://api.anysearch.com/mcp";
// BEGIN GENERATED:CONSTANTS
const AVAILABLE_DOMAINS = [
"general","resource","social_media","finance","academic","legal",
"health","business","security","ip","code","energy",
"environment","agriculture","travel","film","gaming",
];
// END GENERATED:CONSTANTS
function loadEnv() {
const envPaths = [path.join(__dirname, ".env"), path.join(__dirname, "..", ".env")];
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
const lines = fs.readFileSync(envPath, "utf-8").split(/\r?\n/);
for (const raw of lines) {
const line = raw.replace(/#.*$/, "").trim();
if (!line || line.indexOf("=") === -1) continue;
const idx = line.indexOf("=");
const key = line.substring(0, idx).trim();
let val = line.substring(idx + 1).trim().replace(/^["']|["']$/g, "");
process.env[key] = val;
}
}
}
}
loadEnv();
function httpRequest(url, payload, apikey) {
const body = JSON.stringify(payload);
const urlObj = new URL(url);
const options = {
hostname: urlObj.hostname,
path: urlObj.pathname,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
},
};
if (apikey) {
options.headers["Authorization"] = `Bearer ${apikey}`;
}
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
const json = JSON.parse(data);
if (res.statusCode >= 400) {
reject(new Error(`HTTP ${res.statusCode}: ${JSON.stringify(json)}`));
return;
}
if (json.error) {
reject(new Error(json.error.message || JSON.stringify(json.error)));
return;
}
const content = json.result && json.result.content;
if (Array.isArray(content)) {
const textItem = content.find((c) => c.type === "text");
if (textItem) {
resolve(textItem.text);
return;
}
}
resolve(JSON.stringify(json.result || json, null, 2));
} catch (e) {
reject(new Error(`Invalid JSON response: ${data.slice(0, 500)}`));
}
});
});
req.setTimeout(30000, () => {
req.destroy();
reject(new Error("Timeout: The API request timed out."));
});
req.on("error", (e) => reject(new Error(`Connection Error: ${e.message}`)));
req.write(body);
req.end();
});
}
async function callApi(toolName, args, apikey) {
const payload = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: toolName, arguments: args },
};
try {
return await httpRequest(ENDPOINT, payload, apikey);
} catch (e) {
console.error(e.message);
process.exit(1);
}
}
function parseJsonList(value) {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (_) {
return value.split(",").map((s) => s.trim()).filter(Boolean);
}
}
async function cmdSearch(opts) {
const args = { query: opts.query };
if (opts.domain) {
args.domain = opts.domain;
if (opts.subDomain) args.sub_domain = opts.subDomain;
if (opts.subDomainParams) {
try {
args.sub_domain_params = JSON.parse(opts.subDomainParams);
} catch (_) {
console.error("Error: --sub_domain_params must be valid JSON");
process.exit(1);
}
}
}
if (opts.maxResults !== undefined) args.max_results = Math.min(opts.maxResults, 10);
const result = await callApi("search", args, opts.apiKey);
console.log(result);
}
async function cmdListDomains(opts) {
let args;
if (opts.domains) {
args = { domains: parseJsonList(opts.domains) };
} else if (opts.domain) {
args = { domain: opts.domain };
} else {
console.error("Error: provide --domain or --domains");
process.exit(1);
}
const result = await callApi("get_sub_domains", args, opts.apiKey);
console.log(result);
}
async function cmdExtract(opts) {
const url = opts.url;
if (!url) {
console.error("Error: url is required");
process.exit(1);
}
const result = await callApi("extract", { url }, opts.apiKey);
console.log(result);
}
function repairJson(raw) {
raw = raw.trim();
if (raw.startsWith("{") && !raw.startsWith("[")) raw = "[" + raw + "]";
if (raw.startsWith("[")) {
const content = raw.slice(1, -1).trim();
if (!content) return [];
const items = splitJsonItems(content);
return items.map((item) => {
item = item.trim().replace(/^,|,$/g, "");
if (!item) return null;
if (item.startsWith("{")) return repairJsonObject(item);
return { query: item.trim().replace(/^['"]|['"]$/g, "") };
}).filter(Boolean);
}
return [{ query: raw.trim().replace(/^['"]|['"]$/g, "") }];
}
function splitJsonItems(s) {
let depth = 0;
let current = "";
const items = [];
for (const ch of s) {
if (ch === "{") depth++;
else if (ch === "}") depth--;
if (ch === "," && depth === 0) {
items.push(current);
current = "";
} else {
current += ch;
}
}
if (current.trim()) items.push(current);
return items;
}
function repairJsonObject(s) {
const inner = s.trim().replace(/^{|}$/g, "").trim();
if (!inner) return {};
const pairs = splitJsonItems(inner);
const result = {};
for (const pair of pairs) {
const p = pair.trim().replace(/^,|,$/g, "");
if (!p || p.indexOf(":") === -1) continue;
const colon = p.indexOf(":");
const key = p.substring(0, colon).trim().replace(/^['"]|['"]$/g, "");
let val = p.substring(colon + 1).trim();
if (val.startsWith("{")) {
try { result[key] = JSON.parse(val); } catch (_) { result[key] = repairJsonObject(val); }
} else if (val.startsWith("[")) {
try { result[key] = JSON.parse(val); } catch (_) { result[key] = val.slice(1, -1).split(","); }
} else if (val === "true") {
result[key] = true;
} else if (val === "false") {
result[key] = false;
} else if (val === "null") {
result[key] = null;
} else {
try { result[key] = JSON.parse(val); } catch (_) { result[key] = val.replace(/^['"]|['"]$/g, ""); }
}
}
return result;
}
async function cmdBatchSearch(opts) {
let queries;
if (opts.queryItems && opts.queryItems.length > 0) {
if (opts.queryItems.length > 5) {
console.error("Error: batch_search supports a maximum of 5 queries");
process.exit(1);
}
queries = opts.queryItems.map((q) => ({ query: q }));
} else if (opts.queries) {
let raw = opts.queries;
if (raw.startsWith("@")) {
const fpath = raw.substring(1);
if (!fs.existsSync(fpath)) {
console.error(`Error: file not found: ${fpath}`);
process.exit(1);
}
raw = fs.readFileSync(fpath, "utf-8");
}
try {
const parsed = JSON.parse(raw);
queries = Array.isArray(parsed) ? parsed : [parsed];
} catch (_) {
queries = repairJson(raw);
}
} else {
console.error("Error: provide --queries or --query");
process.exit(1);
}
if (queries.length < 1) {
console.error("Error: queries must contain at least 1 item");
process.exit(1);
}
if (queries.length > 5) {
console.error("Error: batch_search supports a maximum of 5 queries");
process.exit(1);
}
const result = await callApi("batch_search", { queries }, opts.apiKey);
console.log(result);
}
// BEGIN GENERATED:DOC_SPEC
function renderDoc() {
const shared = path.join(__dirname, "shared");
let tpl = fs.readFileSync(path.join(shared, "doc_spec.md"), "utf-8");
const c = JSON.parse(fs.readFileSync(path.join(shared, "constants.json"), "utf-8"));
tpl = tpl.replace(/\{\{LANG_NAME\}\}/g, "Node.js");
tpl = tpl.replace(/\{\{LANG_CODEBLOCK\}\}/g, "");
tpl = tpl.replace(/\{\{LANG_INVOKE\}\}/g, "node scripts/anysearch_cli.js");
tpl = tpl.replace(/\{\{DOMAINS_SPACE\}\}/g, c.available_domains.join(" "));
return tpl;
}
// END GENERATED:DOC_SPEC
function cmdDoc() {
console.log(renderDoc());
}
function usage() {
cmdDoc();
}
function parseArgs(argv) {
const args = argv.slice(2);
const command = args[0] || "";
const rest = args.slice(1);
const opts = { apiKey: process.env.ANYSEARCH_API_KEY || "" };
function shiftVal() {
if (rest.length === 0) {
console.error(`Error: missing value for ${rest[0] || "option"}`);
process.exit(1);
}
return rest.shift();
}
function nextFlag() {
return rest.length > 0 && rest[0].startsWith("--");
}
switch (command) {
case "search": {
opts.query = "";
while (rest.length > 0 && !rest[0].startsWith("-")) {
opts.query += (opts.query ? " " : "") + rest.shift();
}
if (!opts.query && rest.length > 0 && !rest[0].startsWith("-")) {
opts.query = rest.shift();
}
while (rest.length > 0) {
const flag = rest.shift();
switch (flag) {
case "--domain": case "-d": opts.domain = shiftVal(); break;
case "--sub_domain": case "-s": opts.subDomain = shiftVal(); break;
case "--sub_domain_params": opts.subDomainParams = shiftVal(); break;
case "--max_results": case "-m": opts.maxResults = parseInt(shiftVal(), 10); break;
case "--api_key": opts.apiKey = shiftVal(); break;
default: console.error(`Unknown flag: ${flag}`); usage(); process.exit(1);
}
}
if (!opts.query) {
console.error("Error: query is required");
process.exit(1);
}
return { action: "search", opts };
}
case "get_sub_domains": {
while (rest.length > 0) {
const flag = rest.shift();
switch (flag) {
case "--domain": opts.domain = shiftVal(); break;
case "--domains": opts.domains = shiftVal(); break;
case "--api_key": opts.apiKey = shiftVal(); break;
default: console.error(`Unknown flag: ${flag}`); process.exit(1);
}
}
return { action: "listDomains", opts };
}
case "extract": {
opts.url = "";
while (rest.length > 0 && !rest[0].startsWith("-")) {
opts.url += (opts.url ? " " : "") + rest.shift();
}
while (rest.length > 0) {
const flag = rest.shift();
switch (flag) {
case "--url": case "-u": opts.url = shiftVal(); break;
case "--api_key": opts.apiKey = shiftVal(); break;
default: console.error(`Unknown flag: ${flag}`); process.exit(1);
}
}
return { action: "extract", opts };
}
case "batch_search": {
opts.queryItems = [];
opts.queries = undefined;
let positional = undefined;
while (rest.length > 0) {
const flag = rest.shift();
switch (flag) {
case "--queries": case "-q": opts.queries = shiftVal(); break;
case "--query": opts.queryItems.push(shiftVal()); break;
case "--api_key": opts.apiKey = shiftVal(); break;
default:
if (!positional) positional = flag;
else { console.error(`Unknown argument: ${flag}`); process.exit(1); }
}
}
if (positional) opts.queries = opts.queries || positional;
return { action: "batchSearch", opts };
}
case "doc":
return { action: "doc", opts };
case "-h": case "--help": case "help":
usage();
process.exit(0);
default:
if (!command) { usage(); process.exit(0); }
console.error(`Unknown command: ${command}`);
usage();
process.exit(1);
}
}
async function main() {
const { action, opts } = parseArgs(process.argv);
switch (action) {
case "search": await cmdSearch(opts); break;
case "listDomains": await cmdListDomains(opts); break;
case "extract": await cmdExtract(opts); break;
case "batchSearch": await cmdBatchSearch(opts); break;
case "doc": cmdDoc(); break;
}
}
main().catch((e) => {
console.error(e.message);
process.exit(1);
});
+491
View File
@@ -0,0 +1,491 @@
#!/usr/bin/env pwsh
#Requires -Version 5.1
Set-StrictMode -Version Latest
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
chcp 65001 | Out-Null
$ENDPOINT = "https://api.anysearch.com/mcp"
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition
function Load-Env {
$envPaths = @((Join-Path $SCRIPT_DIR ".env"), (Join-Path (Join-Path $SCRIPT_DIR "..") ".env"))
foreach ($envPath in $envPaths) {
if (Test-Path $envPath) {
Get-Content $envPath -Encoding UTF8 | ForEach-Object {
$line = $_.Split('#')[0].Trim()
if ($line -and $line -match '=') {
$idx = $line.IndexOf('=')
$key = $line.Substring(0, $idx).Trim()
$val = $line.Substring($idx + 1).Trim().Trim('"').Trim("'")
Set-Item -Path "env:$key" -Value $val
}
}
}
}
}
Load-Env
# BEGIN GENERATED:CONSTANTS
$AVAILABLE_DOMAINS = @(
"general", "resource", "social_media", "finance", "academic", "legal",
"health", "business", "security", "ip", "code", "energy",
"environment", "agriculture", "travel", "film", "gaming"
)
# END GENERATED:CONSTANTS
function Call-Api {
param(
[string]$ToolName,
[hashtable]$Arguments,
[string]$ApiKey
)
$payload = @{
jsonrpc = "2.0"
id = 1
method = "tools/call"
params = @{
name = $ToolName
arguments = $Arguments
}
} | ConvertTo-Json -Depth 10 -Compress
$headers = @{ "Content-Type" = "application/json; charset=utf-8" }
if ($ApiKey) {
$headers["Authorization"] = "Bearer $ApiKey"
}
try {
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
$webReq = [System.Net.HttpWebRequest]::Create($ENDPOINT)
$webReq.Method = "POST"
$webReq.ContentType = "application/json; charset=utf-8"
$webReq.Timeout = 30000
if ($ApiKey) {
$webReq.Headers.Add("Authorization", "Bearer $ApiKey")
}
$reqStream = $webReq.GetRequestStream()
$reqStream.Write($bodyBytes, 0, $bodyBytes.Length)
$reqStream.Close()
$webResp = $webReq.GetResponse()
$respStream = $webResp.GetResponseStream()
$respReader = New-Object System.IO.StreamReader($respStream, [System.Text.Encoding]::UTF8)
$rawJson = $respReader.ReadToEnd()
$respReader.Close()
$webResp.Close()
$resp = $rawJson | ConvertFrom-Json
} catch {
$err = $_.Exception.Message
Write-Error "Connection Error: Unable to reach the API endpoint. ($err)"
exit 1
}
$hasError = $false
try { $hasError = ($null -ne $resp.error) } catch { }
if ($hasError) {
$errMsg = ""
try { $errMsg = $resp.error.message } catch { $errMsg = $resp.error | ConvertTo-Json -Depth 5 }
Write-Error "API Error: $errMsg"
exit 1
}
$result = $null
try { $result = $resp.result } catch { $result = $resp }
if ($result -and $result.content) {
foreach ($item in $result.content) {
if ($item.type -eq "text") {
return $item.text
}
}
}
return ($result | ConvertTo-Json -Depth 10)
}
function Parse-JsonList {
param([string]$Value)
try {
$parsed = $Value | ConvertFrom-Json
if ($parsed -is [array]) { return @($parsed) }
return @($parsed)
} catch {
return @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
}
function Invoke-Search {
param([hashtable]$Opts)
$arguments = @{ query = $Opts.Query }
if ($Opts.Domain) {
$arguments["domain"] = $Opts.Domain
if ($Opts.SubDomain) { $arguments["sub_domain"] = $Opts.SubDomain }
if ($Opts.SubDomainParams) {
try {
$arguments["sub_domain_params"] = $Opts.SubDomainParams | ConvertFrom-Json -AsHashtable
} catch {
Write-Error "Error: --sub_domain_params must be valid JSON"
exit 1
}
}
}
if ($Opts.MaxResults -ne $null) {
$arguments["max_results"] = [Math]::Min($Opts.MaxResults, 10)
}
$result = Call-Api -ToolName "search" -Arguments $arguments -ApiKey $Opts.ApiKey
Write-Output $result
}
function Invoke-ListDomains {
param([hashtable]$Opts)
$arguments = @{}
if ($Opts.Domains) {
$arguments["domains"] = @(Parse-JsonList $Opts.Domains)
} elseif ($Opts.Domain) {
$arguments["domain"] = $Opts.Domain
} else {
Write-Error "Error: provide --domain or --domains"
exit 1
}
$result = Call-Api -ToolName "get_sub_domains" -Arguments $arguments -ApiKey $Opts.ApiKey
Write-Output $result
}
function Invoke-Extract {
param([hashtable]$Opts)
if (-not $Opts.Url) {
Write-Error "Error: url is required"
exit 1
}
$arguments = @{ url = $Opts.Url }
$result = Call-Api -ToolName "extract" -Arguments $arguments -ApiKey $Opts.ApiKey
Write-Output $result
}
function Repair-Json {
param([string]$Raw)
$Raw = $Raw.Trim()
if ($Raw.StartsWith('{') -and -not $Raw.StartsWith('[')) {
$Raw = "[$Raw]"
}
if ($Raw.StartsWith('[')) {
$inner = $Raw.Substring(1, $Raw.Length - 2).Trim()
if (-not $inner) { return @() }
$items = Split-JsonItems $inner
$queries = @()
foreach ($item in $items) {
$item = $item.Trim().Trim(',')
if (-not $item) { continue }
if ($item.StartsWith('{')) {
$queries += Repair-JsonObject $item
} else {
$queries += @{ query = $item.Trim().Trim("'").Trim('"') }
}
}
return $queries
}
return @(@{ query = $Raw.Trim().Trim("'").Trim('"') })
}
function Split-JsonItems {
param([string]$S)
$depth = 0
$current = ""
$items = @()
foreach ($ch in $S.ToCharArray()) {
if ($ch -eq '{') { $depth++ }
elseif ($ch -eq '}') { $depth-- }
if ($ch -eq ',' -and $depth -eq 0) {
$items += $current
$current = ""
} else {
$current += $ch
}
}
if ($current) {
$tail = $current.Trim()
if ($tail) { $items += $tail }
}
return ,$items
}
function Repair-JsonObject {
param([string]$S)
$inner = $S.Trim()
if ($inner.StartsWith('{')) { $inner = $inner.Substring(1) }
if ($inner.EndsWith('}')) { $inner = $inner.Substring(0, $inner.Length - 1) }
$inner = $inner.Trim()
if (-not $inner) { return @{} }
$pairs = Split-JsonItems $inner
$result = @{}
foreach ($pair in $pairs) {
$p = $pair.Trim().Trim(',')
if (-not $p -or $p -notmatch ':') { continue }
$colon = $p.IndexOf(':')
$key = $p.Substring(0, $colon).Trim().Trim('"').Trim("'")
$val = $p.Substring($colon + 1).Trim()
if ($val.StartsWith('{')) {
try { $result[$key] = $val | ConvertFrom-Json -AsHashtable }
catch { $result[$key] = Repair-JsonObject $val }
} elseif ($val.StartsWith('[')) {
try { $result[$key] = @($val | ConvertFrom-Json) }
catch { $result[$key] = @($val.Trim('[]') -split ',') }
} elseif ($val -eq 'true') {
$result[$key] = $true
} elseif ($val -eq 'false') {
$result[$key] = $false
} elseif ($val -eq 'null') {
$result[$key] = $null
} else {
try { $result[$key] = $val | ConvertFrom-Json }
catch { $result[$key] = $val.Trim('"').Trim("'") }
}
}
return $result
}
function Invoke-BatchSearch {
param([hashtable]$Opts)
$queries = $null
if ($Opts.QueryItems -and $Opts.QueryItems.Count -gt 0) {
if ($Opts.QueryItems.Count -gt 5) {
Write-Error "Error: batch_search supports a maximum of 5 queries"
exit 1
}
$queries = @($Opts.QueryItems | ForEach-Object { @{ query = $_ } })
} elseif ($Opts.Queries) {
$raw = $Opts.Queries
if ($raw.StartsWith('@')) {
$fpath = $raw.Substring(1)
if (-not (Test-Path $fpath)) {
Write-Error "Error: file not found: $fpath"
exit 1
}
$raw = Get-Content $fpath -Raw -Encoding UTF8
}
try {
$parsed = $raw | ConvertFrom-Json
if ($parsed -is [array]) {
$queries = @($parsed)
} else {
$queries = @($parsed)
}
} catch {
$queries = Repair-Json $raw
}
} else {
Write-Error "Error: provide --queries or --query"
exit 1
}
$qcount = 0
if ($queries) { $qcount = @($queries).Count }
if ($qcount -lt 1) {
Write-Error "Error: queries must contain at least 1 item"
exit 1
}
if ($qcount -gt 5) {
Write-Error "Error: batch_search supports a maximum of 5 queries"
exit 1
}
$arguments = @{ queries = @($queries) }
$result = Call-Api -ToolName "batch_search" -Arguments $arguments -ApiKey $Opts.ApiKey
Write-Output $result
}
# BEGIN GENERATED:DOC_SPEC
function Render-Doc {
$shared = Join-Path (Split-Path -Parent $MyInvocation.ScriptName) "shared"
$tpl = Get-Content (Join-Path $shared "doc_spec.md") -Raw -Encoding UTF8
$c = Get-Content (Join-Path $shared "constants.json") -Raw -Encoding UTF8 | ConvertFrom-Json
$tpl = $tpl.Replace("{{LANG_NAME}}", "PowerShell")
$tpl = $tpl.Replace("{{LANG_CODEBLOCK}}", "powershell")
$tpl = $tpl.Replace("{{LANG_INVOKE}}", "powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1")
$tpl = $tpl.Replace("{{DOMAINS_SPACE}}", ($c.available_domains -join " "))
return $tpl
}
# END GENERATED:DOC_SPEC
function Show-Doc {
Write-Output (Render-Doc)
}
function Show-Usage {
Show-Doc
}
$apiKey = if ($env:ANYSEARCH_API_KEY) { $env:ANYSEARCH_API_KEY } else { "" }
if ($args.Count -eq 0) {
Show-Usage
exit 0
}
$command = $args[0]
if ($args.Count -gt 1) {
$rest = [array]$args[1..($args.Count - 1)]
} else {
$rest = [array]@()
}
switch ($command) {
"-h" { Show-Usage; exit 0 }
"--help" { Show-Usage; exit 0 }
"help" { Show-Usage; exit 0 }
}
switch ($command) {
"search" {
$query = ""
$domain = ""
$subDomain = ""
$subDomainParams = ""
$maxResults = $null
$i = 0
$positional = @()
while ($i -lt $rest.Count) {
if ($rest[$i] -match '^-') { break }
$positional += $rest[$i]
$i++
}
$query = $positional -join ' '
while ($i -lt $rest.Count) {
switch ($rest[$i]) {
"--domain" { $domain = $rest[$i+1]; $i += 2 }
"-d" { $domain = $rest[$i+1]; $i += 2 }
"--sub_domain" { $subDomain = $rest[$i+1]; $i += 2 }
"-s" { $subDomain = $rest[$i+1]; $i += 2 }
"--sub_domain_params" { $subDomainParams = $rest[$i+1]; $i += 2 }
"--max_results" { $maxResults = [int]$rest[$i+1]; $i += 2 }
"-m" { $maxResults = [int]$rest[$i+1]; $i += 2 }
"--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
default { Write-Error "Unknown flag: $($rest[$i])"; exit 1 }
}
}
if (-not $query) {
Write-Error "Error: query is required"
exit 1
}
Invoke-Search @{
Query = $query
Domain = $domain
SubDomain = $subDomain
SubDomainParams = $subDomainParams
MaxResults = $maxResults
ApiKey = $apiKey
}
}
"get_sub_domains" {
$domain = ""
$domains = ""
$i = 0
while ($i -lt $rest.Count) {
switch ($rest[$i]) {
"--domain" { $domain = $rest[$i+1]; $i += 2 }
"--domains" { $domains = $rest[$i+1]; $i += 2 }
"--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
default { Write-Error "Unknown flag: $($rest[$i])"; exit 1 }
}
}
Invoke-ListDomains @{
Domain = $domain
Domains = $domains
ApiKey = $apiKey
}
}
"extract" {
$url = ""
$positional = @()
$i = 0
while ($i -lt $rest.Count) {
if ($rest[$i] -match '^-') { break }
$positional += $rest[$i]
$i++
}
$url = $positional -join ' '
while ($i -lt $rest.Count) {
switch ($rest[$i]) {
"--url" { $url = $rest[$i+1]; $i += 2 }
"-u" { $url = $rest[$i+1]; $i += 2 }
"--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
default { Write-Error "Unknown flag: $($rest[$i])"; exit 1 }
}
}
Invoke-Extract @{ Url = $url; ApiKey = $apiKey }
}
"batch_search" {
$queryItems = [System.Collections.Generic.List[string]]::new()
$queries = $null
$positional = $null
$i = 0
while ($i -lt $rest.Count) {
switch ($rest[$i]) {
"--queries" { $queries = $rest[$i+1]; $i += 2 }
"-q" { $queries = $rest[$i+1]; $i += 2 }
"--query" { $queryItems.Add($rest[$i+1]); $i += 2 }
"--api_key" { $apiKey = $rest[$i+1]; $i += 2 }
default {
if (-not $positional) { $positional = $rest[$i] }
else { Write-Error "Unknown argument: $($rest[$i])"; exit 1 }
$i++
}
}
}
if ($positional -and -not $queries) { $queries = $positional }
Invoke-BatchSearch @{
Queries = $queries
QueryItems = $queryItems
ApiKey = $apiKey
}
}
"doc" {
Show-Doc
}
default {
Write-Error "Unknown command: $command"
Show-Usage
exit 1
}
}
+468
View File
@@ -0,0 +1,468 @@
#!/usr/bin/env python3
"""AnySearch CLI - Unified search client for AnySearch API."""
import argparse
import io
import json
import os
import sys
import requests
if sys.stdout.encoding != "utf-8":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
if sys.stderr.encoding != "utf-8":
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
ENDPOINT = "https://api.anysearch.com/mcp"
def _load_env():
"""Load API keys from .env files near the skill.
The documented priority is:
--api_key > .env file > environment variable > anonymous.
Use utf-8-sig so .env files saved by Windows Notepad with a BOM are parsed
correctly. The .env value intentionally overrides an existing environment
variable to match the documented priority order.
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
for env_path in [os.path.join(script_dir, ".env"), os.path.join(script_dir, "..", ".env")]:
if os.path.isfile(env_path):
with open(env_path, "r", encoding="utf-8-sig") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip().lstrip(chr(0xFEFF))
value = value.strip().strip("\"'").strip()
if key and value:
os.environ[key] = value
_load_env()
# BEGIN GENERATED:CONSTANTS
AVAILABLE_DOMAINS = [
"general", "resource", "social_media", "finance", "academic", "legal",
"health", "business", "security", "ip", "code", "energy",
"environment", "agriculture", "travel", "film", "gaming",
]
# END GENERATED:CONSTANTS
def _build_headers(api_key: str) -> dict:
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
return headers
def _call_api(tool_name: str, arguments: dict, api_key: str) -> str:
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": tool_name, "arguments": arguments},
}
try:
resp = requests.post(ENDPOINT, json=payload, headers=_build_headers(api_key), timeout=30)
resp.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}", file=sys.stderr)
try:
detail = resp.json()
print(f"Response: {json.dumps(detail, ensure_ascii=False)}", file=sys.stderr)
except Exception:
print(f"Response body: {resp.text[:500]}", file=sys.stderr)
sys.exit(1)
except requests.exceptions.ConnectionError:
print("Connection Error: Unable to reach the API endpoint.", file=sys.stderr)
sys.exit(1)
except requests.exceptions.Timeout:
print("Timeout: The API request timed out.", file=sys.stderr)
sys.exit(1)
data = resp.json()
if "error" in data:
error_msg = data["error"].get("message", str(data["error"]))
print(f"API Error: {error_msg}", file=sys.stderr)
sys.exit(1)
result = data.get("result", {})
content = result.get("content", [])
for item in content:
if item.get("type") == "text":
return item.get("text", "")
return json.dumps(result, indent=2, ensure_ascii=False)
def _parse_json_list(value: str) -> list:
try:
parsed = json.loads(value)
if isinstance(parsed, list):
return parsed
return [parsed]
except json.JSONDecodeError:
return [s.strip() for s in value.split(",") if s.strip()]
def cmd_search(args):
"""Execute search (general or vertical)."""
arguments = {"query": args.query}
if args.domain:
arguments["domain"] = args.domain
if args.sub_domain:
arguments["sub_domain"] = args.sub_domain
if args.sub_domain_params:
try:
arguments["sub_domain_params"] = json.loads(args.sub_domain_params)
except json.JSONDecodeError:
print("Error: --sub_domain_params must be valid JSON", file=sys.stderr)
sys.exit(1)
if args.max_results is not None:
arguments["max_results"] = min(args.max_results, 10)
print(_call_api("search", arguments, args.api_key))
def cmd_get_sub_domains(args):
"""List available sub_domains for given domain(s)."""
arguments = {}
if args.domains:
arguments["domains"] = _parse_json_list(args.domains)
elif args.domain:
arguments["domain"] = args.domain
else:
print("Error: provide --domain or --domains", file=sys.stderr)
sys.exit(1)
print(_call_api("get_sub_domains", arguments, args.api_key))
def cmd_extract(args):
"""Fetch and extract full page content from a URL."""
url = args.url or getattr(args, "url_opt", None)
if not url:
print("Error: url is required", file=sys.stderr)
sys.exit(1)
arguments = {"url": url}
print(_call_api("extract", arguments, args.api_key))
def _repair_json(raw: str) -> list:
raw = raw.strip()
if raw.startswith("{") and not raw.startswith("["):
raw = "[" + raw + "]"
if raw.startswith("["):
content = raw.strip("[]")
if not content:
return []
items = _split_json_items(content)
queries = []
for item in items:
item = item.strip().strip(",")
if not item:
continue
if item.startswith("{"):
d = _repair_json_object(item)
queries.append(d)
else:
s = item.strip().strip("'\"")
queries.append({"query": s})
return queries
return [{"query": raw.strip().strip("'\"")}]
def _split_json_items(s: str) -> list:
depth = 0
current = []
items = []
for ch in s:
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if ch == "," and depth == 0:
items.append("".join(current))
current = []
else:
current.append(ch)
if current:
tail = "".join(current).strip()
if tail:
items.append(tail)
return items
def _repair_json_object(s: str) -> dict:
inner = s.strip().strip("{}").strip()
if not inner:
return {}
pairs = _split_json_items(inner)
result = {}
for pair in pairs:
pair = pair.strip().strip(",")
if not pair:
continue
if ":" not in pair:
continue
colon = pair.index(":")
key = pair[:colon].strip().strip("'\"")
val = pair[colon + 1:].strip()
if val.startswith("{"):
try:
result[key] = json.loads(val)
except json.JSONDecodeError:
result[key] = _repair_json_object(val)
elif val.startswith("["):
try:
result[key] = json.loads(val)
except json.JSONDecodeError:
result[key] = val.strip("[]").split(",")
elif val.lower() in ("true", "false"):
result[key] = val.lower() == "true"
elif val.lower() == "null":
result[key] = None
else:
try:
result[key] = json.loads(val)
except (json.JSONDecodeError, ValueError):
result[key] = val.strip("'\"")
return result
def cmd_batch_search(args):
"""Execute multiple search queries in parallel (2-5 queries)."""
query_items = getattr(args, "query_items", None) or []
raw = args.queries or getattr(args, "queries_opt", None)
if query_items:
queries = [{"query": q} for q in query_items]
if len(queries) > 5:
print("Error: batch_search supports a maximum of 5 queries", file=sys.stderr)
sys.exit(1)
elif raw:
if raw.startswith("@"):
file_path = raw[1:]
try:
with open(file_path, "r", encoding="utf-8") as f:
raw = f.read()
except FileNotFoundError:
print(f"Error: file not found: {file_path}", file=sys.stderr)
sys.exit(1)
try:
queries = json.loads(raw)
if not isinstance(queries, list):
queries = [queries]
except json.JSONDecodeError:
queries = _repair_json(raw)
if len(queries) < 1:
print("Error: queries must contain at least 1 item", file=sys.stderr)
sys.exit(1)
if len(queries) > 5:
print("Error: batch_search supports a maximum of 5 queries", file=sys.stderr)
sys.exit(1)
else:
print("Error: provide --queries or --query", file=sys.stderr)
sys.exit(1)
arguments = {"queries": queries}
print(_call_api("batch_search", arguments, args.api_key))
# BEGIN GENERATED:DOC_SPEC
def _render_doc():
import json as _json
_dir = os.path.dirname(os.path.abspath(__file__))
_shared = os.path.join(_dir, "shared")
with open(os.path.join(_shared, "doc_spec.md"), "r", encoding="utf-8") as _f:
_tpl = _f.read()
with open(os.path.join(_shared, "constants.json"), "r", encoding="utf-8") as _f:
_c = _json.load(_f)
_tpl = _tpl.replace("{{LANG_NAME}}", "Python")
_tpl = _tpl.replace("{{LANG_CODEBLOCK}}", "")
_tpl = _tpl.replace("{{LANG_INVOKE}}", "python scripts/anysearch_cli.py")
_tpl = _tpl.replace("{{DOMAINS_SPACE}}", " ".join(_c["available_domains"]))
return _tpl
# END GENERATED:DOC_SPEC
def cmd_doc(args):
print(_render_doc())
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="anysearch",
description=(
"AnySearch CLI - Unified real-time search client.\n\n"
"Supports general search, vertical domain search, batch search,\n"
"domain directory lookup, and URL content extraction via the\n"
"AnySearch JSON-RPC API."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"examples:\n"
" anysearch search \"quantum computing\"\n"
" anysearch search \"AAPL\" --domain finance --sub_domain finance.us_stock\n"
" anysearch get_sub_domains --domain finance\n"
" anysearch extract --url https://example.com\n"
" anysearch batch_search --queries '[{\"query\":\"AAPL\"},{\"query\":\"GOOG\"}]'\n"
),
)
parser.add_argument(
"--api_key",
default=os.environ.get("ANYSEARCH_API_KEY", ""),
help="API key for authentication. Read from: --api_key > .env ANYSEARCH_API_KEY > env ANYSEARCH_API_KEY. "
"Without a key, anonymous access is used with lower rate limits.",
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
search_p = subparsers.add_parser(
"search",
help="Search the web (general or vertical domain search)",
description=(
"Execute a search query.\n\n"
"Two modes:\n"
" General search: omit --domain (open-ended natural language queries)\n"
" Vertical search: specify --domain and --sub_domain for structured queries\n\n"
"For vertical search, run 'get_sub_domains' first to discover available\n"
"sub_domains and their required query formats."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
search_p.add_argument("query", help="Search query string. For vertical search, follow the format returned by get_sub_domains.")
search_p.add_argument(
"--domain", "-d",
choices=AVAILABLE_DOMAINS,
help=(
"Vertical domain for structured search. "
f"Available: {', '.join(AVAILABLE_DOMAINS)}"
),
)
search_p.add_argument(
"--sub_domain", "-s",
help="Sub-domain routing key (e.g. finance.us_stock). Required for vertical search; obtain via get_sub_domains.",
)
search_p.add_argument(
"--sub_domain_params",
help="Additional sub_domain parameters as JSON string. Schema depends on the sub_domain (see get_sub_domains output).",
)
search_p.add_argument(
"--max_results", "-m",
type=int,
help="Maximum number of results to return (1-10, default 10).",
)
search_p.set_defaults(func=cmd_search)
ld_p = subparsers.add_parser(
"get_sub_domains",
help="Query domain directory for available sub_domains",
description=(
"List available sub_domains, query formats, and parameter schemas\n"
"for one or more vertical domains.\n\n"
"MUST be called before performing vertical search to obtain\n"
"the correct sub_domain value and query_format.\n\n"
"Results are returned as a Markdown table with columns:\n"
"domain, sub_domain, description, query_format, params_schema, zone."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
ld_p.add_argument(
"--domain",
choices=AVAILABLE_DOMAINS,
help="Single domain to query.",
)
ld_p.add_argument(
"--domains",
help=(
"Batch query up to 5 domains. Comma-separated or JSON array.\n"
f"Available: {', '.join(AVAILABLE_DOMAINS)}\n"
"Takes precedence over --domain."
),
)
ld_p.set_defaults(func=cmd_get_sub_domains)
ext_p = subparsers.add_parser(
"extract",
help="Fetch full page content from a URL",
description=(
"Extract the full content of a web page and return it as Markdown.\n\n"
"Use this when search snippets are insufficient, you need to verify\n"
"data, or want to extract structured content (tables, code, etc.).\n\n"
"Note: Output is truncated at 50,000 characters. Only HTML pages\n"
"are supported (not PDFs, images, etc.)."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
ext_p.add_argument("url", nargs="?", help="Target URL to extract content from (http(s)://).")
ext_p.add_argument("--url", "-u", dest="url_opt", help="Target URL to extract content from (alternative to positional arg).")
ext_p.set_defaults(func=cmd_extract)
batch_p = subparsers.add_parser(
"batch_search",
help="Execute 2-5 search queries in parallel",
description=(
"Run multiple independent search queries in a single API call.\n"
"Each query follows the same parameter structure as the 'search' command.\n"
"A single query failure does not block others; results are merged.\n\n"
"Queries are provided as a JSON array of objects. Each object supports\n"
"the same fields as 'search': query, domain, sub_domain, max_results."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"examples:\n"
' anysearch batch_search --query AAPL --query GOOG\n'
' anysearch batch_search --queries \'[{\"query\":\"AAPL\"},{\"query\":\"GOOG\"}]\'\n'
' anysearch batch_search \'[{\"query\":\"AAPL\"},{\"query\":\"GOOG\"}]\'\n'
' anysearch batch_search --queries @queries.json\n'
),
)
batch_p.add_argument(
"queries",
nargs="?",
help=(
'JSON array of search query objects (1-5 items). '
'Tolerates PowerShell quote-stripping automatically.\n'
'Each object supports: query (required), domain, sub_domain, sub_domain_params, max_results.\n'
'Example: \'[{"query":"AAPL"},{"query":"GOOG"}]\''
),
)
batch_p.add_argument(
"--queries", "-q", dest="queries_opt",
help="JSON array of search query objects (alternative to positional arg). Prefix @ to read from file.",
)
batch_p.add_argument(
"--query",
action="append",
dest="query_items",
help="Shorthand: repeatable single-query string. Easier for PowerShell. Up to 5.",
)
batch_p.set_defaults(func=cmd_batch_search)
doc_p = subparsers.add_parser(
"doc",
help="Print AI-facing interface specification",
)
doc_p.set_defaults(func=cmd_doc)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
if args.command is None:
print(_render_doc())
sys.exit(0)
args.func(args)
if __name__ == "__main__":
main()
+279
View File
@@ -0,0 +1,279 @@
#!/usr/bin/env bash
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
ENDPOINT="https://api.anysearch.com/mcp"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v jq &>/dev/null; then
echo "Error: jq is required but not found. Install it: https://jqlang.github.io/jq/download/" >&2
exit 1
fi
_load_env() {
for env_path in "$SCRIPT_DIR/.env" "$SCRIPT_DIR/../.env"; do
if [[ -f "$env_path" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
line="$(echo "$line" | xargs 2>/dev/null || true)"
[[ -z "$line" || "$line" != *=* ]] && continue
local key="${line%%=*}"
local val="${line#*=}"
val="$(echo "$val" | sed 's/^["\x27]\|["\x27]$//g')"
export "$key=$val"
done < "$env_path"
fi
done
}
_load_env
API_KEY="${ANYSEARCH_API_KEY:-}"
# BEGIN GENERATED:CONSTANTS
AVAILABLE_DOMAINS=("general" "resource" "social_media" "finance" "academic" "legal" "health" "business" "security" "ip" "code" "energy" "environment" "agriculture" "travel" "film" "gaming")
# END GENERATED:CONSTANTS
_call_api() {
local tool_name="$1"
local arguments="$2"
local auth_args=()
if [[ -n "$API_KEY" ]]; then
auth_args+=(-H "Authorization: Bearer $API_KEY")
fi
local payload
payload=$(jq -n --arg name "$tool_name" --argjson args "$arguments" \
'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":$name,"arguments":$args}}')
local response
response=$(curl -s -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
"${auth_args[@]}" \
-d "$payload" \
--max-time 30 2>/dev/null)
if [[ -z "$response" ]]; then
echo "Error: No response from API" >&2
exit 1
fi
local error_msg
error_msg=$(printf '%s' "$response" | jq -r '.error.message // empty' 2>/dev/null)
if [[ -n "$error_msg" ]]; then
echo "API Error: $error_msg" >&2
exit 1
fi
local text_block
text_block=$(printf '%s' "$response" | jq -r '.result.content[0].text // empty' 2>/dev/null)
if [[ -n "$text_block" ]]; then
printf '%s\n' "$text_block"
else
printf '%s\n' "$response"
fi
}
_cmd_search() {
local query=""
local domain=""
local sub_domain=""
local sub_domain_params=""
local max_results=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain|-d) domain="$2"; shift 2 ;;
--sub_domain|-s) sub_domain="$2"; shift 2 ;;
--sub_domain_params) sub_domain_params="$2"; shift 2 ;;
--max_results|-m) max_results="$2"; shift 2 ;;
--api_key) API_KEY="$2"; shift 2 ;;
-*) echo "Unknown flag: $1" >&2; _usage; exit 1 ;;
*) query="$1"; shift ;;
esac
done
if [[ -z "$query" ]]; then
echo "Error: query is required" >&2
exit 1
fi
local args
args=$(jq -n --arg q "$query" '{"query":$q}')
if [[ -n "$domain" ]]; then
args=$(printf '%s' "$args" | jq --arg d "$domain" '. + {"domain":$d}')
if [[ -n "$sub_domain" ]]; then
args=$(printf '%s' "$args" | jq --arg s "$sub_domain" '. + {"sub_domain":$s}')
fi
if [[ -n "$sub_domain_params" ]]; then
args=$(printf '%s' "$args" | jq --argjson p "$sub_domain_params" '. + {"sub_domain_params":$p}')
fi
fi
if [[ -n "$max_results" ]]; then
if [[ "$max_results" -gt 10 ]]; then
max_results=10
fi
args=$(printf '%s' "$args" | jq --argjson m "$max_results" '. + {"max_results":$m}')
fi
_call_api "search" "$args"
}
_cmd_get_sub_domains() {
local domain=""
local domains=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domains) domains="$2"; shift 2 ;;
--domain) domain="$2"; shift 2 ;;
--api_key) API_KEY="$2"; shift 2 ;;
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
*) domain="$1"; shift ;;
esac
done
local args
if [[ -n "$domains" ]]; then
local d_json
if [[ "$domains" == \[* ]]; then
d_json="$domains"
else
d_json=$(printf '%s' "$domains" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0))')
fi
args=$(jq -n --argjson d "$d_json" '{"domains":$d}')
elif [[ -n "$domain" ]]; then
args=$(jq -n --arg d "$domain" '{"domain":$d}')
else
echo "Error: provide --domain or --domains" >&2
exit 1
fi
_call_api "get_sub_domains" "$args"
}
_cmd_extract() {
local url=""
while [[ $# -gt 0 ]]; do
case "$1" in
--url|-u) url="$2"; shift 2 ;;
--api_key) API_KEY="$2"; shift 2 ;;
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
*) url="$1"; shift ;;
esac
done
if [[ -z "$url" ]]; then
echo "Error: url is required" >&2
exit 1
fi
local args
args=$(jq -n --arg u "$url" '{"url":$u}')
_call_api "extract" "$args"
}
_cmd_batch_search() {
local queries=""
local query_items=()
while [[ $# -gt 0 ]]; do
case "$1" in
--queries|-q) queries="$2"; shift 2 ;;
--query) query_items+=("$2"); shift 2 ;;
--api_key) API_KEY="$2"; shift 2 ;;
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
*) queries="$1"; shift ;;
esac
done
local args
if [[ ${#query_items[@]} -gt 0 ]]; then
if [[ ${#query_items[@]} -gt 5 ]]; then
echo "Error: batch_search supports a maximum of 5 queries" >&2
exit 1
fi
local items_json="[]"
for q in "${query_items[@]}"; do
items_json=$(printf '%s' "$items_json" | jq --arg q "$q" '. + [{"query":$q}]')
done
args=$(jq -n --argjson q "$items_json" '{"queries":$q}')
elif [[ -n "$queries" ]]; then
local raw="$queries"
if [[ "$raw" == @* ]]; then
local fpath="${raw:1}"
if [[ ! -f "$fpath" ]]; then
echo "Error: file not found: $fpath" >&2
exit 1
fi
raw=$(cat "$fpath")
fi
if [[ "$raw" == \[* || "$raw" == \{* ]]; then
if [[ "$raw" == \[* ]]; then
args=$(jq -n --argjson q "$raw" '{"queries":$q}')
else
args=$(jq -n --argjson q "[$raw]" '{"queries":$q}')
fi
else
local items_json
items_json=$(printf '%s' "$raw" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0)) | map({"query":.})')
args=$(jq -n --argjson q "$items_json" '{"queries":$q}')
fi
else
echo "Error: provide --queries or --query" >&2
exit 1
fi
local count
count=$(printf '%s' "$args" | jq '.queries | length')
if [[ "$count" -lt 1 ]]; then
echo "Error: queries must contain at least 1 item" >&2
exit 1
fi
if [[ "$count" -gt 5 ]]; then
echo "Error: batch_search supports a maximum of 5 queries" >&2
exit 1
fi
_call_api "batch_search" "$args"
}
# BEGIN GENERATED:DOC_SPEC
_cmd_doc() {
local shared="$SCRIPT_DIR/shared"
local tpl
tpl=$(cat "$shared/doc_spec.md")
local domains
domains=$(jq -r '.available_domains | join(" ")' "$shared/constants.json")
tpl="${tpl//\{\{LANG_NAME\}\}/Bash}"
tpl="${tpl//\{\{LANG_CODEBLOCK\}\}/bash}"
tpl="${tpl//\{\{LANG_INVOKE\}\}\}/bash scripts/anysearch_cli.sh}"
tpl="${tpl//\{\{DOMAINS_SPACE\}\}/$domains}"
printf '%s\n' "$tpl"
}
# END GENERATED:DOC_SPEC
_usage() {
_cmd_doc
}
main() {
local command="${1:-}"
shift || true
case "$command" in
search) _cmd_search "$@" ;;
get_sub_domains) _cmd_get_sub_domains "$@" ;;
extract) _cmd_extract "$@" ;;
batch_search) _cmd_batch_search "$@" ;;
doc) _cmd_doc ;;
-h|--help|help) _usage ;;
"") _usage ;;
*) echo "Unknown command: $command" >&2; _usage; exit 1 ;;
esac
}
main "$@"
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Code generator for AnySearch CLI scripts.
Reads constants.json from scripts/shared/ and injects the domain list
and doc command implementation into each CLI script. Eliminates duplication
across all 4 language implementations.
Usage:
python scripts/generate.py # Generate all scripts
python scripts/generate.py --check # Verify scripts are up-to-date (for CI)
"""
import json
import os
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SHARED_DIR = os.path.join(SCRIPT_DIR, "shared")
# --- Marker format per language ---
# Each script uses paired comments to delimit generated sections:
# BEGIN GENERATED:<section_name>
# ... generated content ...
# END GENERATED:<section_name>
MARKERS = {
".py": ("# BEGIN GENERATED:{name}", "# END GENERATED:{name}"),
".js": ("// BEGIN GENERATED:{name}", "// END GENERATED:{name}"),
".ps1": ("# BEGIN GENERATED:{name}", "# END GENERATED:{name}"),
".sh": ("# BEGIN GENERATED:{name}", "# END GENERATED:{name}"),
}
def load_constants():
with open(os.path.join(SHARED_DIR, "constants.json"), "r", encoding="utf-8") as f:
return json.load(f)
def render_constants(ext, constants):
"""Render constants block in the target language syntax."""
domains = constants["available_domains"]
if ext == ".py":
lines = []
lines.append("AVAILABLE_DOMAINS = [")
for i in range(0, len(domains), 6):
chunk = domains[i:i+6]
lines.append(" " + ", ".join(f'"{d}"' for d in chunk) + ",")
lines.append("]")
return "\n".join(lines)
elif ext == ".js":
lines = []
lines.append("const AVAILABLE_DOMAINS = [")
for i in range(0, len(domains), 6):
chunk = domains[i:i+6]
lines.append(" " + ",".join(f'"{d}"' for d in chunk) + ",")
lines.append("];")
return "\n".join(lines)
elif ext == ".ps1":
lines = []
lines.append("$AVAILABLE_DOMAINS = @(")
chunks = [domains[i:i+6] for i in range(0, len(domains), 6)]
for idx, chunk in enumerate(chunks):
suffix = "," if idx < len(chunks) - 1 else ""
lines.append(" " + ", ".join(f'"{d}"' for d in chunk) + suffix)
lines.append(")")
return "\n".join(lines)
elif ext == ".sh":
lines = []
lines.append("AVAILABLE_DOMAINS=(" + " ".join(f'"{d}"' for d in domains) + ")")
return "\n".join(lines)
raise ValueError(f"Unsupported extension: {ext}")
def render_doc_block(ext, constants):
"""Generate code that reads and renders doc_spec.md at runtime."""
if ext == ".py":
return '''def _render_doc():
import json as _json
_dir = os.path.dirname(os.path.abspath(__file__))
_shared = os.path.join(_dir, "shared")
with open(os.path.join(_shared, "doc_spec.md"), "r", encoding="utf-8") as _f:
_tpl = _f.read()
with open(os.path.join(_shared, "constants.json"), "r", encoding="utf-8") as _f:
_c = _json.load(_f)
_tpl = _tpl.replace("{{LANG_NAME}}", "Python")
_tpl = _tpl.replace("{{LANG_CODEBLOCK}}", "")
_tpl = _tpl.replace("{{LANG_INVOKE}}", "python scripts/anysearch_cli.py")
_tpl = _tpl.replace("{{DOMAINS_SPACE}}", " ".join(_c["available_domains"]))
return _tpl'''
elif ext == ".js":
return '''function renderDoc() {
const shared = path.join(__dirname, "shared");
let tpl = fs.readFileSync(path.join(shared, "doc_spec.md"), "utf-8");
const c = JSON.parse(fs.readFileSync(path.join(shared, "constants.json"), "utf-8"));
tpl = tpl.replace(/\\{\\{LANG_NAME\\}\\}/g, "Node.js");
tpl = tpl.replace(/\\{\\{LANG_CODEBLOCK\\}\\}/g, "");
tpl = tpl.replace(/\\{\\{LANG_INVOKE\\}\\}/g, "node scripts/anysearch_cli.js");
tpl = tpl.replace(/\\{\\{DOMAINS_SPACE\\}\\}/g, c.available_domains.join(" "));
return tpl;
}'''
elif ext == ".ps1":
return '''function Render-Doc {
$shared = Join-Path (Split-Path -Parent $MyInvocation.ScriptName) "shared"
$tpl = Get-Content (Join-Path $shared "doc_spec.md") -Raw -Encoding UTF8
$c = Get-Content (Join-Path $shared "constants.json") -Raw -Encoding UTF8 | ConvertFrom-Json
$tpl = $tpl.Replace("{{LANG_NAME}}", "PowerShell")
$tpl = $tpl.Replace("{{LANG_CODEBLOCK}}", "powershell")
$tpl = $tpl.Replace("{{LANG_INVOKE}}", "powershell -ExecutionPolicy Bypass -File scripts/anysearch_cli.ps1")
$tpl = $tpl.Replace("{{DOMAINS_SPACE}}", ($c.available_domains -join " "))
return $tpl
}'''
elif ext == ".sh":
return r'''_cmd_doc() {
local shared="$SCRIPT_DIR/shared"
local tpl
tpl=$(cat "$shared/doc_spec.md")
local domains
domains=$(jq -r '.available_domains | join(" ")' "$shared/constants.json")
tpl="${tpl//\{\{LANG_NAME\}\}/Bash}"
tpl="${tpl//\{\{LANG_CODEBLOCK\}\}/bash}"
tpl="${tpl//\{\{LANG_INVOKE\}\}\}/bash scripts/anysearch_cli.sh}"
tpl="${tpl//\{\{DOMAINS_SPACE\}\}/$domains}"
printf '%s\n' "$tpl"
}'''
raise ValueError(f"Unsupported extension: {ext}")
def replace_marker_section(content, ext, section_name, new_text):
"""Replace everything between marker comments for section_name with new_text."""
begin_tag, end_tag = MARKERS[ext]
begin = begin_tag.format(name=section_name)
end = end_tag.format(name=section_name)
if begin not in content:
raise ValueError(f"BEGIN marker '{begin_tag.format(name=section_name)}' not found")
if end not in content:
raise ValueError(f"END marker '{end_tag.format(name=section_name)}' not found")
before, rest = content.split(begin, 1)
_, after = rest.split(end, 1)
return before + begin + "\n" + new_text + "\n" + end + after
def generate_script(script_path, constants):
"""Regenerate the constants and doc blocks in a CLI script."""
ext = os.path.splitext(script_path)[1]
if ext not in MARKERS:
raise ValueError(f"Unsupported extension: {ext}")
with open(script_path, "r", encoding="utf-8") as f:
content = f.read()
constants_text = render_constants(ext, constants)
content = replace_marker_section(content, ext, "CONSTANTS", constants_text)
doc_block = render_doc_block(ext, constants)
content = replace_marker_section(content, ext, "DOC_SPEC", doc_block)
return content
def main():
import argparse
parser = argparse.ArgumentParser(description="Generate AnySearch CLI scripts from shared data")
parser.add_argument("--check", action="store_true", help="Verify scripts are up-to-date (for CI)")
args = parser.parse_args()
constants = load_constants()
scripts_changed = False
for ext in [".py", ".js", ".ps1", ".sh"]:
script_name = f"anysearch_cli{ext}"
script_path = os.path.join(SCRIPT_DIR, script_name)
try:
new_content = generate_script(script_path, constants)
with open(script_path, "r", encoding="utf-8") as f:
old_content = f.read()
if new_content != old_content:
scripts_changed = True
if not args.check:
with open(script_path, "w", encoding="utf-8") as f:
f.write(new_content)
print(f"Generated: {script_name}")
else:
print(f"CHANGED: {script_name} (run generate.py to update)")
else:
print(f"OK: {script_name}")
except Exception as e:
print(f"ERROR in {script_name}: {e}", file=sys.stderr)
sys.exit(1)
if args.check and scripts_changed:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,8 @@
{
"endpoint": "https://api.anysearch.com/mcp",
"available_domains": [
"general", "resource", "social_media", "finance", "academic",
"legal", "health", "business", "security", "ip", "code",
"energy", "environment", "agriculture", "travel", "film", "gaming"
]
}
+217
View File
@@ -0,0 +1,217 @@
# AnySearch Interface Specification (for AI Agent)
## Protocol
- Endpoint: POST https://api.anysearch.com/mcp
- Format: JSON-RPC 2.0, method = "tools/call"
- Auth: Header "Authorization: Bearer <API_KEY>" (optional, anonymous has lower rate limits)
## CLI Invocation ({{LANG_NAME}})
```{{LANG_CODEBLOCK}}
{{LANG_INVOKE}} <command> [options]
```
## Available Commands
### 1. search — Single query search
Two modes: general (omit --domain) and vertical (requires --domain + --sub_domain).
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| query | string | YES | Search query (positional) |
| --domain, -d | string | no | Vertical domain: {{DOMAINS_SPACE}} |
| --sub_domain, -s | string | no | Sub-domain routing key (e.g. finance.us_stock). REQUIRED for vertical search |
| --sub_domain_params | JSON | conditional | Extra params per sub_domain schema from get_sub_domains. ALL params marked (required) MUST be included, use "" for inapplicable ones. Omit entirely if no params are listed. |
| --max_results, -m | int | no | 1-10, default 10 |
### 2. get_sub_domains — Query vertical domain directory
MUST be called before vertical search to discover available sub_domains and their required parameters.
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| --domain | string | choose one | Single domain to query |
| --domains | string | choose one | Batch up to 5 domains (comma-separated). Takes precedence over --domain |
Returns a Markdown table grouped by domain. Each sub_domain entry shows: sub_domain, description, and parameters (name, description, whether required).
IMPORTANT: Cache get_sub_domains results per domain within a session. Do NOT call repeatedly.
### 3. batch_search — Execute 2-5 search queries in parallel
Single failure does not block others; results are merged.
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| --query | string | YES (x1-5) | Repeatable single-query shorthand (CLI-only). Each value becomes `{"query":"..."}` — equivalent to the `queries` array with plain query objects |
| --queries, -q | JSON | YES | JSON array of query objects, or @file.json to read from file |
Each query object supports: query (required), domain, sub_domain, sub_domain_params, max_results.
### 4. extract — Fetch full page content as Markdown
Truncated at 50,000 chars. HTML pages only.
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| url | string | YES | Target URL (positional or via --url / -u) |
---
## Decision Flow
Search has two paths. Path 1 is a narrow exception for pure encyclopedia only. Path 2 (the DEFAULT) requires `get_sub_domains` before search.
### Path 1 — General query (RARE EXCEPTION)
ONLY for pure encyclopedia / common knowledge with ZERO domain overlap.
"How high is Mount Everest?", "Who wrote Hamlet?", "What is gravity?"
→ {{LANG_INVOKE}} search "query" --max_results 10
### Path 2 — Vertical query (THE DEFAULT)
EVERYTHING that is NOT pure encyclopedia. Structured data, domain-specific topics,
specialized info, real-time data, locations, or ANY ambiguity.
Step 1: {{LANG_INVOKE}} get_sub_domains --domains domain1,domain2,...
Step 2: {{LANG_INVOKE}} search "query" --domain X --sub_domain Y [--sub_domain_params '{}']
Step 3 (optional): {{LANG_INVOKE}} extract "url"
**CRITICAL: When UNSURE, use hybrid via batch_search:**
{{LANG_INVOKE}} batch_search --queries '[{"query":"..."}, {"query":"...","domain":"X","sub_domain":"Y"}]'
This fires 1 general query + N vertical queries in parallel. Coverage beats guessing.
**Multi-domain intersection:** When a SINGLE topic crosses multiple domains,
`get_sub_domains` with ALL intersecting domains, then `batch_search` —
rephrase the SAME core question per domain perspective.
```
User query
|
+-- PURE encyclopedia / common knowledge with ZERO domain overlap?
| YES → Path 1: search "query" (no domain)
|
+-- UNSURE / could benefit from domain sources?
| YES → HYBRID: batch_search (1 general + N vertical)
|
+-- Clearly domain-specific / has structured identifiers?
YES → Path 2: get_sub_domains → search (or batch_search for multi-domain)
```
---
## Vertical Search Semantic Constraints
Before performing vertical search, you MUST call get_sub_domains for the target domain
and strictly obey the returned semantic constraints:
1. **params**: Parameters for the sub_domain. get_sub_domains output marks each param
as `(required)` or not. You MUST pass ALL required params via `--sub_domain_params`,
even if they have no meaningful value — use the key with an empty string:
`--sub_domain_params '{"param1":"value","param2":""}'`.
Optional params can be omitted if not needed.
2. **sub_domain selection**: Match the user's intent to the best sub_domain description.
Example: for "AAPL earnings report", prefer finance.us_stock over finance.forex.
---
## Scenario Examples (all runnable CLI commands)
### Scenario 1: General web search — look up a factual question
```bash
{{LANG_INVOKE}} search "What is the capital of France"
```
```bash
{{LANG_INVOKE}} search "quantum computing breakthroughs 2025" --max_results 5
```
### Scenario 2: Vertical search — stock market data (structured identifier)
Step 1: Discover available sub_domains for finance:
```bash
{{LANG_INVOKE}} get_sub_domains --domain finance
```
Step 2: Search with the correct sub_domain and required params (use "" for inapplicable ones):
```bash
{{LANG_INVOKE}} search "AAPL" --domain finance --sub_domain finance.us_stock --sub_domain_params '{"ticker":"AAPL"}' --max_results 5
```
If a param is marked `(required)` but has no meaningful value, pass it as empty string:
```bash
{{LANG_INVOKE}} search "latest market trends" --domain finance --sub_domain finance.market --sub_domain_params '{"region":"","timeframe":""}' --max_results 5
```
### Scenario 3: Vertical search — academic paper lookup
Step 1: Discover sub_domains for academic:
```bash
{{LANG_INVOKE}} get_sub_domains --domain academic
```
Step 2: Search with the correct sub_domain:
```bash
{{LANG_INVOKE}} search "transformer attention mechanism" --domain academic --sub_domain academic.search --max_results 3
```
### Scenario 4: Vertical search — legal document or case
```bash
{{LANG_INVOKE}} get_sub_domains --domain legal
```
```bash
{{LANG_INVOKE}} search "contract dispute damages" --domain legal --sub_domain legal.case --max_results 5
```
### Scenario 5: Vertical search — code documentation
```bash
{{LANG_INVOKE}} search "react:hooks" --domain code --sub_domain code.doc --max_results 5
```
### Scenario 6: Batch search — multiple independent queries in one call
CLI shorthand (`--query`, repeatable for simple queries):
```bash
{{LANG_INVOKE}} batch_search --query "AAPL stock price" --query "TSLA earnings 2025" --query "GOOG market cap"
```
With full query objects (vertical domain + parameters):
```bash
{{LANG_INVOKE}} batch_search --queries '[{"query":"AAPL","domain":"finance","sub_domain":"finance.us_stock"},{"query":"react:hooks","domain":"code","sub_domain":"code.doc"}]'
```
From a JSON file:
```bash
{{LANG_INVOKE}} batch_search --queries @queries.json
```
### Scenario 7: Extract full page content — read beyond search snippets
```bash
{{LANG_INVOKE}} extract "https://en.wikipedia.org/wiki/Quantum_computing"
```
```bash
{{LANG_INVOKE}} extract --url "https://example.com/news/article-12345"
```
### Scenario 8: Search with API key
```bash
{{LANG_INVOKE}} search "climate change policy 2025" --api_key <your_api_key> --max_results 3
```
---
## Rate Limit Handling
- On rate limit error with auto_registered api_key in response: present key to user for approval, then save to .env and retry
- On anonymous quota exhausted: inform user that a key provides higher limits; suggest configuring one via .env or environment variable