mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
add: AnySearch-Skill
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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 "$@"
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user