9router 0.2.49 → 0.2.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/app/.next/BUILD_ID +1 -1
- package/app/.next/app-build-manifest.json +78 -78
- package/app/.next/app-path-routes-manifest.json +18 -18
- package/app/.next/build-manifest.json +2 -2
- package/app/.next/prerender-manifest.json +36 -36
- package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/_not-found.html +1 -1
- package/app/.next/server/app/_not-found.rsc +1 -1
- package/app/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cli-tools/claude-settings/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cli-tools/codex-settings/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/auth/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/credentials/update/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/model/resolve/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/models/alias/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/combos/[id]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/combos/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/keys/[id]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/keys/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/models/alias/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/[provider]/[action]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/kiro/import/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/kiro/social-authorize/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/kiro/social-exchange/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/[id]/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/[id]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/[id]/test/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/client/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/validate/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/sync/cloud/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/sync/initialize/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/tags/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/load/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/save/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/send/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/translate/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/usage/[connectionId]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/usage/history/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/api/chat/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/chat/completions/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/messages/count_tokens/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/messages/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/responses/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1beta/models/[...path]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1beta/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/callback.html +1 -1
- package/app/.next/server/app/callback.rsc +1 -1
- package/app/.next/server/app/dashboard/cli-tools.html +1 -1
- package/app/.next/server/app/dashboard/cli-tools.rsc +1 -1
- package/app/.next/server/app/dashboard/combos.html +1 -1
- package/app/.next/server/app/dashboard/combos.rsc +1 -1
- package/app/.next/server/app/dashboard/endpoint.html +1 -1
- package/app/.next/server/app/dashboard/endpoint.rsc +1 -1
- package/app/.next/server/app/dashboard/profile.html +1 -1
- package/app/.next/server/app/dashboard/profile.rsc +1 -1
- package/app/.next/server/app/dashboard/providers/new.html +1 -1
- package/app/.next/server/app/dashboard/providers/new.rsc +1 -1
- package/app/.next/server/app/dashboard/providers.html +1 -1
- package/app/.next/server/app/dashboard/providers.rsc +1 -1
- package/app/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/dashboard/settings/pricing.html +1 -1
- package/app/.next/server/app/dashboard/settings/pricing.rsc +1 -1
- package/app/.next/server/app/dashboard/translator.html +1 -1
- package/app/.next/server/app/dashboard/translator.rsc +1 -1
- package/app/.next/server/app/dashboard/usage.html +1 -1
- package/app/.next/server/app/dashboard/usage.rsc +1 -1
- package/app/.next/server/app/dashboard.html +1 -1
- package/app/.next/server/app/dashboard.rsc +1 -1
- package/app/.next/server/app/index.html +1 -1
- package/app/.next/server/app/index.rsc +1 -1
- package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/landing.html +1 -1
- package/app/.next/server/app/landing.rsc +1 -1
- package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/login.html +1 -1
- package/app/.next/server/app/login.rsc +1 -1
- package/app/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app-paths-manifest.json +18 -18
- package/app/.next/server/middleware-manifest.json +1 -1
- package/app/.next/server/pages/404.html +1 -1
- package/app/.next/server/pages/500.html +1 -1
- package/cli.js +49 -14
- package/package.json +2 -1
- package/src/cli/api/client.js +375 -0
- package/src/cli/menus/apiKeys.js +259 -0
- package/src/cli/menus/cliTools.js +221 -0
- package/src/cli/menus/combos.js +477 -0
- package/src/cli/menus/providers.js +448 -0
- package/src/cli/menus/settings.js +86 -0
- package/src/cli/terminalUI.js +86 -0
- package/src/cli/utils/display.js +156 -0
- package/src/cli/utils/format.js +125 -0
- package/src/cli/utils/input.js +211 -0
- package/src/cli/utils/menuHelper.js +155 -0
- package/src/cli/utils/modelSelector.js +133 -0
- package/hooks/model-websearch.cjs +0 -319
- /package/app/.next/static/{z-NGTtnYbvnsa63SSl6HV → vEruHXnDBNEM-jV9xL72D}/_buildManifest.js +0 -0
- /package/app/.next/static/{z-NGTtnYbvnsa63SSl6HV → vEruHXnDBNEM-jV9xL72D}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const api = require("../api/client");
|
|
2
|
+
const { prompt } = require("./input");
|
|
3
|
+
const { clearScreen } = require("./display");
|
|
4
|
+
|
|
5
|
+
// Provider alias order: OAuth first, then API Key (matches ModelSelectModal)
|
|
6
|
+
const PROVIDER_ALIAS_ORDER = [
|
|
7
|
+
"cc", "ag", "cx", "if", "qw", "gc", "gh", "kr",
|
|
8
|
+
"openrouter", "glm", "kimi", "minimax", "openai", "anthropic", "gemini"
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// Alias to display name mapping
|
|
12
|
+
const PROVIDER_ALIAS_NAMES = {
|
|
13
|
+
cc: "Claude Code",
|
|
14
|
+
ag: "Antigravity",
|
|
15
|
+
cx: "OpenAI Codex",
|
|
16
|
+
if: "iFlow AI",
|
|
17
|
+
qw: "Qwen Code",
|
|
18
|
+
gc: "Gemini CLI",
|
|
19
|
+
gh: "GitHub Copilot",
|
|
20
|
+
kr: "Kiro AI",
|
|
21
|
+
openrouter: "OpenRouter",
|
|
22
|
+
glm: "GLM Coding",
|
|
23
|
+
kimi: "Kimi Coding",
|
|
24
|
+
minimax: "Minimax Coding",
|
|
25
|
+
openai: "OpenAI",
|
|
26
|
+
anthropic: "Anthropic",
|
|
27
|
+
gemini: "Gemini"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all available models grouped by provider + combos
|
|
32
|
+
* @returns {Promise<{combos: Array, groups: Object}>}
|
|
33
|
+
*/
|
|
34
|
+
async function getAvailableModelsGrouped() {
|
|
35
|
+
const result = await api.getAvailableModels();
|
|
36
|
+
if (!result.success) return { combos: [], groups: {} };
|
|
37
|
+
|
|
38
|
+
const models = result.data?.data || [];
|
|
39
|
+
const combos = [];
|
|
40
|
+
const groups = {};
|
|
41
|
+
|
|
42
|
+
models.forEach(m => {
|
|
43
|
+
if (m.owned_by === "combo") {
|
|
44
|
+
combos.push(m.id);
|
|
45
|
+
} else {
|
|
46
|
+
const provider = m.owned_by;
|
|
47
|
+
if (!groups[provider]) {
|
|
48
|
+
groups[provider] = [];
|
|
49
|
+
}
|
|
50
|
+
groups[provider].push(m.id);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return { combos, groups };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Display model list and prompt for selection
|
|
59
|
+
* @param {string} title - Title to display
|
|
60
|
+
* @param {string} currentValue - Current selected value (optional)
|
|
61
|
+
* @returns {Promise<string|null>} Selected model ID or null if cancelled
|
|
62
|
+
*/
|
|
63
|
+
async function selectModelFromList(title, currentValue = "") {
|
|
64
|
+
const { combos, groups } = await getAvailableModelsGrouped();
|
|
65
|
+
|
|
66
|
+
const totalModels = combos.length + Object.values(groups).flat().length;
|
|
67
|
+
if (totalModels === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build flat list for selection
|
|
72
|
+
const allModels = [];
|
|
73
|
+
|
|
74
|
+
// Display
|
|
75
|
+
clearScreen();
|
|
76
|
+
console.log(`\n🎯 ${title}`);
|
|
77
|
+
console.log("=".repeat(50));
|
|
78
|
+
if (currentValue) {
|
|
79
|
+
console.log(`Current: ${currentValue}\n`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let idx = 1;
|
|
85
|
+
|
|
86
|
+
// Combos first
|
|
87
|
+
if (combos.length > 0) {
|
|
88
|
+
console.log("[Combos]");
|
|
89
|
+
combos.forEach(combo => {
|
|
90
|
+
console.log(` ${idx}. ${combo}`);
|
|
91
|
+
allModels.push(combo);
|
|
92
|
+
idx++;
|
|
93
|
+
});
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Provider groups in order (by alias)
|
|
98
|
+
const sortedProviders = Object.keys(groups).sort((a, b) => {
|
|
99
|
+
const idxA = PROVIDER_ALIAS_ORDER.indexOf(a);
|
|
100
|
+
const idxB = PROVIDER_ALIAS_ORDER.indexOf(b);
|
|
101
|
+
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
sortedProviders.forEach(provider => {
|
|
105
|
+
const providerName = PROVIDER_ALIAS_NAMES[provider] || provider;
|
|
106
|
+
console.log(`[${providerName}]`);
|
|
107
|
+
groups[provider].forEach(model => {
|
|
108
|
+
console.log(` ${idx}. ${model}`);
|
|
109
|
+
allModels.push(model);
|
|
110
|
+
idx++;
|
|
111
|
+
});
|
|
112
|
+
console.log();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log(" 0. Cancel\n");
|
|
116
|
+
|
|
117
|
+
// Prompt for number input
|
|
118
|
+
const input = await prompt("Enter number: ");
|
|
119
|
+
const num = parseInt(input, 10);
|
|
120
|
+
|
|
121
|
+
if (isNaN(num) || num === 0 || num < 0 || num > allModels.length) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return allModels[num - 1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
selectModelFromList,
|
|
130
|
+
getAvailableModelsGrouped,
|
|
131
|
+
PROVIDER_ALIAS_ORDER,
|
|
132
|
+
PROVIDER_ALIAS_NAMES
|
|
133
|
+
};
|
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* 9ROUTER WebSearch Hook - Model-based WebSearch via API
|
|
4
|
-
*
|
|
5
|
-
* Intercepts Claude's WebSearch tool and executes search via 9ROUTER API.
|
|
6
|
-
* Reads configuration from ~/.claude/settings.json (set by dashboard).
|
|
7
|
-
*
|
|
8
|
-
* Environment Variables (from settings.json):
|
|
9
|
-
* ANTHROPIC_BASE_URL - API endpoint URL
|
|
10
|
-
* ANTHROPIC_AUTH_TOKEN - API authentication token
|
|
11
|
-
* 9ROUTER_WEBSEARCH_MODEL - Model to use for search (e.g., gemini/gemini-2.5-flash)
|
|
12
|
-
*
|
|
13
|
-
* Exit codes:
|
|
14
|
-
* 0 - Allow tool (pass-through to native WebSearch)
|
|
15
|
-
* 2 - Block tool (deny with results/message)
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const https = require("https");
|
|
19
|
-
const http = require("http");
|
|
20
|
-
const fs = require("fs");
|
|
21
|
-
const path = require("path");
|
|
22
|
-
const os = require("os");
|
|
23
|
-
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// CONFIGURATION
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
const SEARCH_PROMPT = `You are a helpful assistant. Answer the following question directly using your knowledge. Do NOT use any tools, do NOT output tool calls, do NOT output XML tags like <tool_code> or <invoke>. Just provide a direct, comprehensive text answer.
|
|
29
|
-
|
|
30
|
-
Instructions:
|
|
31
|
-
1. Provide current, accurate information based on your knowledge
|
|
32
|
-
2. Include relevant URLs/sources when available
|
|
33
|
-
3. Be concise but thorough - prioritize key facts
|
|
34
|
-
4. Focus on factual information from reliable sources
|
|
35
|
-
5. If information may be outdated, note the discrepancy
|
|
36
|
-
6. Format output clearly with sections if the topic is complex
|
|
37
|
-
7. IMPORTANT: Output plain text only, no tool calls
|
|
38
|
-
|
|
39
|
-
Question: `;
|
|
40
|
-
|
|
41
|
-
const MIN_VALID_RESPONSE_LENGTH = 20;
|
|
42
|
-
const DEFAULT_TIMEOUT_MS = 60000;
|
|
43
|
-
|
|
44
|
-
// ============================================================================
|
|
45
|
-
// SETTINGS READER
|
|
46
|
-
// ============================================================================
|
|
47
|
-
|
|
48
|
-
function getClaudeSettingsPath() {
|
|
49
|
-
return path.join(os.homedir(), ".claude", "settings.json");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function readSettings() {
|
|
53
|
-
try {
|
|
54
|
-
const settingsPath = getClaudeSettingsPath();
|
|
55
|
-
const content = fs.readFileSync(settingsPath, "utf-8");
|
|
56
|
-
return JSON.parse(content);
|
|
57
|
-
} catch {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function getConfig() {
|
|
63
|
-
const settings = readSettings();
|
|
64
|
-
const env = settings?.env || {};
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
baseUrl: env.ANTHROPIC_BASE_URL || process.env.ANTHROPIC_BASE_URL,
|
|
68
|
-
authToken: env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN,
|
|
69
|
-
model: env.9ROUTER_WEBSEARCH_MODEL || process.env.9ROUTER_WEBSEARCH_MODEL || "gemini/gemini-2.5-flash",
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// API CLIENT
|
|
75
|
-
// ============================================================================
|
|
76
|
-
|
|
77
|
-
function makeRequest(url, options, body) {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
const urlObj = new URL(url);
|
|
80
|
-
const isHttps = urlObj.protocol === "https:";
|
|
81
|
-
const client = isHttps ? https : http;
|
|
82
|
-
|
|
83
|
-
const req = client.request(url, options, (res) => {
|
|
84
|
-
let data = "";
|
|
85
|
-
res.on("data", (chunk) => (data += chunk));
|
|
86
|
-
res.on("end", () => {
|
|
87
|
-
resolve({ status: res.statusCode, data });
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
req.on("error", reject);
|
|
92
|
-
req.on("timeout", () => {
|
|
93
|
-
req.destroy();
|
|
94
|
-
reject(new Error("Request timeout"));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
if (body) {
|
|
98
|
-
req.write(body);
|
|
99
|
-
}
|
|
100
|
-
req.end();
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function searchWithModel(query, config) {
|
|
105
|
-
const { baseUrl, authToken, model } = config;
|
|
106
|
-
|
|
107
|
-
if (!baseUrl) {
|
|
108
|
-
return { success: false, error: "ANTHROPIC_BASE_URL not configured" };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const apiUrl = `${baseUrl}/api/v1/chat/completions`;
|
|
112
|
-
const prompt = SEARCH_PROMPT + query;
|
|
113
|
-
|
|
114
|
-
const requestBody = JSON.stringify({
|
|
115
|
-
model: model,
|
|
116
|
-
messages: [{ role: "user", content: prompt }],
|
|
117
|
-
stream: false,
|
|
118
|
-
max_tokens: 4096,
|
|
119
|
-
tool_choice: "none", // Disable tool use, return text directly
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const headers = {
|
|
123
|
-
"Content-Type": "application/json",
|
|
124
|
-
"Content-Length": Buffer.byteLength(requestBody),
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
if (authToken) {
|
|
128
|
-
headers["Authorization"] = `Bearer ${authToken}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const response = await makeRequest(
|
|
133
|
-
apiUrl,
|
|
134
|
-
{
|
|
135
|
-
method: "POST",
|
|
136
|
-
headers,
|
|
137
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
138
|
-
},
|
|
139
|
-
requestBody
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
if (response.status !== 200) {
|
|
143
|
-
return {
|
|
144
|
-
success: false,
|
|
145
|
-
error: `API returned ${response.status}: ${response.data.slice(0, 200)}`,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const result = JSON.parse(response.data);
|
|
150
|
-
|
|
151
|
-
// Support both OpenAI and Claude response formats
|
|
152
|
-
let content = "";
|
|
153
|
-
|
|
154
|
-
// OpenAI format: choices[0].message.content
|
|
155
|
-
if (result.choices?.[0]?.message?.content) {
|
|
156
|
-
content = result.choices[0].message.content;
|
|
157
|
-
}
|
|
158
|
-
// Claude format: content array with type "text"
|
|
159
|
-
else if (Array.isArray(result.content)) {
|
|
160
|
-
const textBlock = result.content.find(c => c.type === "text");
|
|
161
|
-
content = textBlock?.text || "";
|
|
162
|
-
}
|
|
163
|
-
// Direct content string
|
|
164
|
-
else if (typeof result.content === "string") {
|
|
165
|
-
content = result.content;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!content || content.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
169
|
-
return { success: false, error: "Empty or too short response" };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return { success: true, content, model };
|
|
173
|
-
} catch (err) {
|
|
174
|
-
return { success: false, error: err.message || "Unknown error" };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ============================================================================
|
|
179
|
-
// OUTPUT FORMATTERS
|
|
180
|
-
// ============================================================================
|
|
181
|
-
|
|
182
|
-
function formatSearchResults(query, content, model) {
|
|
183
|
-
// Keep it simple - just the content, Claude will see the context
|
|
184
|
-
return content;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function outputSuccess(query, content, model) {
|
|
188
|
-
const formattedResults = `[WebSearch via ${model}]\nQuery: "${query}"\n\n${content}`;
|
|
189
|
-
|
|
190
|
-
// Block tool with results, suppress duplicate output
|
|
191
|
-
const output = {
|
|
192
|
-
decision: "block",
|
|
193
|
-
reason: formattedResults,
|
|
194
|
-
suppressOutput: true,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
console.log(JSON.stringify(output));
|
|
198
|
-
process.exit(0);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function outputError(query, error, model) {
|
|
202
|
-
const message = [
|
|
203
|
-
`[WebSearch - ${model} Error]`,
|
|
204
|
-
"",
|
|
205
|
-
`Error: ${error}`,
|
|
206
|
-
"",
|
|
207
|
-
`Query: "${query}"`,
|
|
208
|
-
"",
|
|
209
|
-
"Troubleshooting:",
|
|
210
|
-
" - Check ANTHROPIC_BASE_URL in ~/.claude/settings.json",
|
|
211
|
-
" - Verify the model is available in your providers",
|
|
212
|
-
" - Run 9router dashboard to configure settings",
|
|
213
|
-
].join("\n");
|
|
214
|
-
|
|
215
|
-
const output = {
|
|
216
|
-
decision: "block",
|
|
217
|
-
reason: `WebSearch failed: ${error}`,
|
|
218
|
-
hookSpecificOutput: {
|
|
219
|
-
hookEventName: "PreToolUse",
|
|
220
|
-
permissionDecision: "deny",
|
|
221
|
-
permissionDecisionReason: message,
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
console.log(JSON.stringify(output));
|
|
226
|
-
process.exit(2);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function outputNotConfigured(query) {
|
|
230
|
-
const message = [
|
|
231
|
-
"[WebSearch - Not Configured]",
|
|
232
|
-
"",
|
|
233
|
-
"WebSearch model is not configured.",
|
|
234
|
-
"",
|
|
235
|
-
"To configure:",
|
|
236
|
-
" 1. Run 9router (or npm run dev)",
|
|
237
|
-
" 2. Go to Dashboard > CLI Tools",
|
|
238
|
-
" 3. Set WebSearch Model (e.g., gemini/gemini-2.5-flash)",
|
|
239
|
-
" 4. Click Apply",
|
|
240
|
-
"",
|
|
241
|
-
`Query: "${query}"`,
|
|
242
|
-
].join("\n");
|
|
243
|
-
|
|
244
|
-
const output = {
|
|
245
|
-
decision: "block",
|
|
246
|
-
reason: "WebSearch not configured",
|
|
247
|
-
hookSpecificOutput: {
|
|
248
|
-
hookEventName: "PreToolUse",
|
|
249
|
-
permissionDecision: "deny",
|
|
250
|
-
permissionDecisionReason: message,
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
console.log(JSON.stringify(output));
|
|
255
|
-
process.exit(2);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ============================================================================
|
|
259
|
-
// MAIN HOOK LOGIC
|
|
260
|
-
// ============================================================================
|
|
261
|
-
|
|
262
|
-
let input = "";
|
|
263
|
-
process.stdin.setEncoding("utf8");
|
|
264
|
-
process.stdin.on("data", (chunk) => {
|
|
265
|
-
input += chunk;
|
|
266
|
-
});
|
|
267
|
-
process.stdin.on("end", () => {
|
|
268
|
-
processHook();
|
|
269
|
-
});
|
|
270
|
-
process.stdin.on("error", () => {
|
|
271
|
-
process.exit(0);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
async function processHook() {
|
|
275
|
-
try {
|
|
276
|
-
const data = JSON.parse(input);
|
|
277
|
-
|
|
278
|
-
// Only handle WebSearch tool
|
|
279
|
-
if (data.tool_name !== "WebSearch") {
|
|
280
|
-
process.exit(0);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const query = data.tool_input?.query || "";
|
|
284
|
-
if (!query) {
|
|
285
|
-
process.exit(0);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const config = getConfig();
|
|
289
|
-
|
|
290
|
-
if (!config.baseUrl || !config.model) {
|
|
291
|
-
outputNotConfigured(query);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (process.env.9ROUTER_DEBUG) {
|
|
296
|
-
console.error(`[9ROUTER Hook] Searching with model: ${config.model}`);
|
|
297
|
-
console.error(`[9ROUTER Hook] API URL: ${config.baseUrl}`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const result = await searchWithModel(query, config);
|
|
301
|
-
|
|
302
|
-
if (result.success) {
|
|
303
|
-
outputSuccess(query, result.content, result.model);
|
|
304
|
-
} else {
|
|
305
|
-
// Fallback to native WebSearch when API fails
|
|
306
|
-
if (process.env.9ROUTER_DEBUG) {
|
|
307
|
-
console.error(`[9ROUTER Hook] API failed: ${result.error}, falling back to native WebSearch`);
|
|
308
|
-
}
|
|
309
|
-
process.exit(0);
|
|
310
|
-
}
|
|
311
|
-
} catch (err) {
|
|
312
|
-
if (process.env.9ROUTER_DEBUG) {
|
|
313
|
-
console.error("[9ROUTER Hook] Parse error:", err.message);
|
|
314
|
-
}
|
|
315
|
-
// Fallback to native WebSearch on any error
|
|
316
|
-
process.exit(0);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
File without changes
|
|
File without changes
|