hanzi-browse 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/domain-knowledge.d.ts +15 -0
- package/dist/agent/domain-knowledge.js +63 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +41 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +12 -2
- package/dist/cli/json-output.d.ts +21 -0
- package/dist/cli/json-output.js +30 -0
- package/dist/cli/setup.d.ts +51 -0
- package/dist/cli/setup.js +113 -41
- package/dist/cli.js +29 -8
- package/dist/index.js +1 -567
- package/dist/managed/api.d.ts +20 -1
- package/dist/managed/api.js +177 -514
- package/dist/managed/deploy.js +82 -0
- package/dist/managed/routes/api.d.ts +44 -0
- package/dist/managed/routes/api.js +220 -0
- package/dist/managed/routes/pages.d.ts +13 -0
- package/dist/managed/routes/pages.js +149 -0
- package/dist/managed/store-pg.d.ts +5 -1
- package/dist/managed/store-pg.js +12 -4
- package/dist/managed/store.d.ts +6 -1
- package/dist/managed/store.js +4 -2
- package/dist/managed/templates/pair-self.html +67 -0
- package/dist/managed/templates/pair.html +97 -0
- package/dist/mcp/tools.d.ts +20 -0
- package/dist/mcp/tools.js +263 -0
- package/dist/relay/api-proxy.d.ts +2 -0
- package/dist/relay/api-proxy.js +165 -0
- package/dist/relay/server.js +2 -112
- package/package.json +3 -3
- package/skills/data-extractor/SKILL.md +223 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-specific knowledge for the server-side agent loop.
|
|
3
|
+
* Matches the extension's domain-skills.js but only includes domains
|
|
4
|
+
* relevant to managed/API tasks.
|
|
5
|
+
*/
|
|
6
|
+
interface DomainEntry {
|
|
7
|
+
domain: string;
|
|
8
|
+
skill: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Look up domain knowledge for a URL.
|
|
12
|
+
* Returns the first matching entry, or null.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getDomainSkill(url: string): DomainEntry | null;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-specific knowledge for the server-side agent loop.
|
|
3
|
+
* Matches the extension's domain-skills.js but only includes domains
|
|
4
|
+
* relevant to managed/API tasks.
|
|
5
|
+
*/
|
|
6
|
+
const DOMAIN_KNOWLEDGE = [
|
|
7
|
+
{
|
|
8
|
+
domain: "x.com",
|
|
9
|
+
skill: `X/Twitter — verified patterns (updated 2026-03-30)
|
|
10
|
+
|
|
11
|
+
## Reading pages (CRITICAL)
|
|
12
|
+
- X loads content asynchronously — page looks empty for 3-5 seconds after navigation.
|
|
13
|
+
- read_page often returns ONLY "To view keyboard shortcuts" — tweets haven't loaded yet.
|
|
14
|
+
- DO NOT re-navigate to the same URL. That resets loading and makes it worse.
|
|
15
|
+
- Instead: wait 5 seconds, then use get_page_text — it reads visible text and is more reliable.
|
|
16
|
+
- If get_page_text returns nothing, scroll down once and try again.
|
|
17
|
+
|
|
18
|
+
## Search
|
|
19
|
+
- URL: x.com/search?q={encoded_query}&src=typed_query&f=live
|
|
20
|
+
- After navigating, wait 5 seconds, then get_page_text (NOT read_page).
|
|
21
|
+
- Scroll down once to load more tweets, then get_page_text again.
|
|
22
|
+
- Tweet URLs in page text follow pattern: /status/{id}
|
|
23
|
+
|
|
24
|
+
## Text input (CRITICAL — Draft.js)
|
|
25
|
+
- form_input DOES NOT WORK — Draft.js ignores programmatic input.
|
|
26
|
+
- computer type action GARBLES TEXT.
|
|
27
|
+
- ONLY RELIABLE METHOD — use javascript_tool:
|
|
28
|
+
document.querySelector('[data-testid="tweetTextarea_0"]').focus();
|
|
29
|
+
document.execCommand('insertText', false, 'your reply text here');
|
|
30
|
+
- Always verify text appeared by reading after insertion.
|
|
31
|
+
|
|
32
|
+
## Replying to a tweet
|
|
33
|
+
1. Navigate to tweet URL (x.com/{handle}/status/{id})
|
|
34
|
+
2. Wait 3 seconds, read the page
|
|
35
|
+
3. Click the reply/comment icon (speech bubble) in the action bar
|
|
36
|
+
4. Use javascript_tool to insert text (see above)
|
|
37
|
+
5. Verify text appeared, then click blue "Reply" button
|
|
38
|
+
6. Wait 2 seconds to confirm reply posted
|
|
39
|
+
|
|
40
|
+
## Known traps
|
|
41
|
+
- DO NOT scroll looking for "Post your reply" — reply box appears after clicking comment icon
|
|
42
|
+
- x.com/compose/post may open — that's fine, type and click Reply there
|
|
43
|
+
- "Leave site?" dialog — ALWAYS click Cancel, finish posting first
|
|
44
|
+
- Reply button is disabled until text is entered — verify first
|
|
45
|
+
- Space replies 15+ seconds apart (rate limiting)
|
|
46
|
+
- NEVER navigate to the same URL you're already on`,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Look up domain knowledge for a URL.
|
|
51
|
+
* Returns the first matching entry, or null.
|
|
52
|
+
*/
|
|
53
|
+
export function getDomainSkill(url) {
|
|
54
|
+
try {
|
|
55
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
56
|
+
return DOMAIN_KNOWLEDGE.find((d) => hostname === d.domain || hostname.endsWith("." + d.domain)) || null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// URL might not be a full URL — try matching as a bare domain
|
|
60
|
+
const lower = url.toLowerCase();
|
|
61
|
+
return DOMAIN_KNOWLEDGE.find((d) => lower.includes(d.domain)) || null;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -48,6 +48,16 @@ export interface StepUpdate {
|
|
|
48
48
|
toolInput?: Record<string, any>;
|
|
49
49
|
text?: string;
|
|
50
50
|
}
|
|
51
|
+
export interface TurnLog {
|
|
52
|
+
step: number;
|
|
53
|
+
tools: Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
input: Record<string, any>;
|
|
56
|
+
result: string;
|
|
57
|
+
durationMs: number;
|
|
58
|
+
}>;
|
|
59
|
+
ai_response: string | null;
|
|
60
|
+
}
|
|
51
61
|
export interface AgentLoopResult {
|
|
52
62
|
status: "complete" | "error" | "max_steps";
|
|
53
63
|
answer: string;
|
|
@@ -59,5 +69,7 @@ export interface AgentLoopResult {
|
|
|
59
69
|
};
|
|
60
70
|
/** The model used for the last LLM call (for billing attribution) */
|
|
61
71
|
model?: string;
|
|
72
|
+
/** Structured turn-by-turn log of the agent's actions */
|
|
73
|
+
turns?: TurnLog[];
|
|
62
74
|
}
|
|
63
75
|
export declare function runAgentLoop(params: AgentLoopParams): Promise<AgentLoopResult>;
|
package/dist/agent/loop.js
CHANGED
|
@@ -19,9 +19,12 @@ import { buildSystemPrompt } from "./system-prompt.js";
|
|
|
19
19
|
// --- Agent Loop ---
|
|
20
20
|
export async function runAgentLoop(params) {
|
|
21
21
|
const { task, url, context, executeTool, onStep, onText, maxSteps = 50, signal, } = params;
|
|
22
|
-
|
|
22
|
+
// Detect target URL for domain knowledge — from explicit url param or from task text
|
|
23
|
+
const targetUrl = url || task.match(/https?:\/\/[^\s"')]+/)?.[0];
|
|
24
|
+
const system = buildSystemPrompt(targetUrl);
|
|
23
25
|
const tools = AGENT_TOOLS;
|
|
24
26
|
const messages = [];
|
|
27
|
+
const turns = [];
|
|
25
28
|
let totalUsage = { inputTokens: 0, outputTokens: 0, apiCalls: 0 };
|
|
26
29
|
let lastModel;
|
|
27
30
|
// Build initial user message
|
|
@@ -41,6 +44,7 @@ export async function runAgentLoop(params) {
|
|
|
41
44
|
steps: step - 1,
|
|
42
45
|
usage: totalUsage,
|
|
43
46
|
model: lastModel,
|
|
47
|
+
turns,
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
onStep?.({ step, status: "thinking" });
|
|
@@ -63,6 +67,7 @@ export async function runAgentLoop(params) {
|
|
|
63
67
|
steps: step,
|
|
64
68
|
usage: totalUsage,
|
|
65
69
|
model: lastModel,
|
|
70
|
+
turns,
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
totalUsage.apiCalls++;
|
|
@@ -79,9 +84,17 @@ export async function runAgentLoop(params) {
|
|
|
79
84
|
// Extract text and tool calls
|
|
80
85
|
const textBlocks = response.content.filter((b) => b.type === "text");
|
|
81
86
|
const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
|
|
87
|
+
// Start building the turn log for this step
|
|
88
|
+
const currentTurn = {
|
|
89
|
+
step,
|
|
90
|
+
tools: [],
|
|
91
|
+
ai_response: textBlocks.map((b) => b.text).join("\n").trim() || null,
|
|
92
|
+
};
|
|
82
93
|
// If no tool calls, we're done
|
|
83
94
|
if (response.stop_reason === "end_turn" || toolUseBlocks.length === 0) {
|
|
84
95
|
const answer = textBlocks.map((b) => b.text).join("\n").trim();
|
|
96
|
+
turns.push(currentTurn);
|
|
97
|
+
console.error(`[AgentLoop] Complete at step ${step} (${totalUsage.apiCalls} API calls, ${totalUsage.inputTokens} input tokens)`);
|
|
85
98
|
onStep?.({ step, status: "complete", text: answer });
|
|
86
99
|
return {
|
|
87
100
|
status: "complete",
|
|
@@ -89,6 +102,7 @@ export async function runAgentLoop(params) {
|
|
|
89
102
|
steps: step,
|
|
90
103
|
usage: totalUsage,
|
|
91
104
|
model: lastModel,
|
|
105
|
+
turns,
|
|
92
106
|
};
|
|
93
107
|
}
|
|
94
108
|
// Execute each tool call
|
|
@@ -105,6 +119,12 @@ export async function runAgentLoop(params) {
|
|
|
105
119
|
});
|
|
106
120
|
continue;
|
|
107
121
|
}
|
|
122
|
+
// Log tool call
|
|
123
|
+
const inputSummary = toolUse.name === "navigate" ? toolUse.input.url
|
|
124
|
+
: toolUse.name === "computer" ? `${toolUse.input.action}${toolUse.input.ref ? ` ref=${toolUse.input.ref}` : ""}${toolUse.input.coordinate ? ` @${toolUse.input.coordinate}` : ""}`
|
|
125
|
+
: toolUse.name === "javascript_tool" ? toolUse.input.text?.slice(0, 80)
|
|
126
|
+
: JSON.stringify(toolUse.input).slice(0, 80);
|
|
127
|
+
console.error(`[AgentLoop] Step ${step}: ${toolUse.name}(${inputSummary})`);
|
|
108
128
|
onStep?.({
|
|
109
129
|
step,
|
|
110
130
|
status: "tool_use",
|
|
@@ -112,6 +132,7 @@ export async function runAgentLoop(params) {
|
|
|
112
132
|
toolInput: toolUse.input,
|
|
113
133
|
});
|
|
114
134
|
let result;
|
|
135
|
+
const toolStartMs = Date.now();
|
|
115
136
|
try {
|
|
116
137
|
result = await executeTool(toolUse.name, toolUse.input);
|
|
117
138
|
}
|
|
@@ -133,15 +154,32 @@ export async function runAgentLoop(params) {
|
|
|
133
154
|
result = { success: false, error: err.message };
|
|
134
155
|
}
|
|
135
156
|
}
|
|
157
|
+
// Log result summary
|
|
158
|
+
const toolDurationMs = Date.now() - toolStartMs;
|
|
159
|
+
const resultText = result.error ? `Error: ${result.error}`
|
|
160
|
+
: typeof result.output === "string" ? result.output
|
|
161
|
+
: JSON.stringify(result.output);
|
|
162
|
+
const resultSummary = resultText.length > 120 ? resultText.slice(0, 120) + "..." : resultText;
|
|
163
|
+
console.error(`[AgentLoop] Step ${step}: ${toolUse.name} → ${resultSummary}`);
|
|
164
|
+
// Add to structured turn log (truncate large results to keep log manageable)
|
|
165
|
+
currentTurn.tools.push({
|
|
166
|
+
name: toolUse.name,
|
|
167
|
+
input: toolUse.input,
|
|
168
|
+
result: (resultText.length > 5000 ? resultText.slice(0, 5000) + "... [truncated]" : resultText)
|
|
169
|
+
+ (result.screenshot ? " [+screenshot]" : ""),
|
|
170
|
+
durationMs: toolDurationMs,
|
|
171
|
+
});
|
|
136
172
|
onStep?.({ step, status: "tool_result", toolName: toolUse.name });
|
|
137
173
|
// Check abort after each tool — don't feed results back to LLM if cancelled
|
|
138
174
|
if (signal?.aborted) {
|
|
175
|
+
turns.push(currentTurn);
|
|
139
176
|
return {
|
|
140
177
|
status: "error",
|
|
141
178
|
answer: "Task was cancelled.",
|
|
142
179
|
steps: step,
|
|
143
180
|
usage: totalUsage,
|
|
144
181
|
model: lastModel,
|
|
182
|
+
turns,
|
|
145
183
|
};
|
|
146
184
|
}
|
|
147
185
|
// Build tool result content block
|
|
@@ -172,6 +210,7 @@ export async function runAgentLoop(params) {
|
|
|
172
210
|
}
|
|
173
211
|
// Add tool results as user message
|
|
174
212
|
messages.push({ role: "user", content: toolResults });
|
|
213
|
+
turns.push(currentTurn);
|
|
175
214
|
}
|
|
176
215
|
// Exceeded max steps
|
|
177
216
|
const lastText = messages
|
|
@@ -186,5 +225,6 @@ export async function runAgentLoop(params) {
|
|
|
186
225
|
steps: maxSteps,
|
|
187
226
|
usage: totalUsage,
|
|
188
227
|
model: lastModel,
|
|
228
|
+
turns,
|
|
189
229
|
};
|
|
190
230
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System prompt for server-side managed agent loop.
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
import { getDomainSkill } from "./domain-knowledge.js";
|
|
5
|
+
export function buildSystemPrompt(taskUrl) {
|
|
5
6
|
const now = new Date();
|
|
6
7
|
const dateStr = now.toLocaleDateString("en-US", {
|
|
7
8
|
month: "numeric",
|
|
@@ -9,7 +10,7 @@ export function buildSystemPrompt() {
|
|
|
9
10
|
year: "numeric",
|
|
10
11
|
});
|
|
11
12
|
const timeStr = now.toLocaleTimeString("en-US");
|
|
12
|
-
|
|
13
|
+
const blocks = [
|
|
13
14
|
{
|
|
14
15
|
type: "text",
|
|
15
16
|
text: `You are a web automation assistant with browser tools. Your priority is to complete the user's request efficiently and autonomously.
|
|
@@ -38,4 +39,13 @@ When a page shows only a loading spinner, use the computer tool with action "wai
|
|
|
38
39
|
</tool_usage_requirements>`,
|
|
39
40
|
},
|
|
40
41
|
];
|
|
42
|
+
// Inject domain-specific knowledge if the task targets a known site
|
|
43
|
+
const domainSkill = taskUrl ? getDomainSkill(taskUrl) : null;
|
|
44
|
+
if (domainSkill) {
|
|
45
|
+
blocks.push({
|
|
46
|
+
type: "text",
|
|
47
|
+
text: `<domain_knowledge domain="${domainSkill.domain}">\n${domainSkill.skill}\n</domain_knowledge>`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return blocks;
|
|
41
51
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SessionFileStatus } from './session-files.js';
|
|
2
|
+
export declare function buildTaskCompletePayload(sessionId: string, result: unknown): {
|
|
3
|
+
session_id: string;
|
|
4
|
+
status: string;
|
|
5
|
+
result: unknown;
|
|
6
|
+
};
|
|
7
|
+
export declare function buildTaskErrorPayload(sessionId: string, error: string): {
|
|
8
|
+
session_id: string;
|
|
9
|
+
status: string;
|
|
10
|
+
error: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildStatusPayload(status: SessionFileStatus | SessionFileStatus[]): SessionFileStatus | SessionFileStatus[];
|
|
13
|
+
export declare function buildStopPayload(sessionId: string, remove?: boolean): {
|
|
14
|
+
session_id: string;
|
|
15
|
+
status: string;
|
|
16
|
+
removed: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function buildScreenshotPayload(sessionId: string, screenshotPath: string): {
|
|
19
|
+
session_id: string;
|
|
20
|
+
screenshot_path: string;
|
|
21
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function buildTaskCompletePayload(sessionId, result) {
|
|
2
|
+
return {
|
|
3
|
+
session_id: sessionId,
|
|
4
|
+
status: 'completed',
|
|
5
|
+
result,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function buildTaskErrorPayload(sessionId, error) {
|
|
9
|
+
return {
|
|
10
|
+
session_id: sessionId,
|
|
11
|
+
status: 'error',
|
|
12
|
+
error,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function buildStatusPayload(status) {
|
|
16
|
+
return status;
|
|
17
|
+
}
|
|
18
|
+
export function buildStopPayload(sessionId, remove = false) {
|
|
19
|
+
return {
|
|
20
|
+
session_id: sessionId,
|
|
21
|
+
status: 'stopped',
|
|
22
|
+
removed: remove,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function buildScreenshotPayload(sessionId, screenshotPath) {
|
|
26
|
+
return {
|
|
27
|
+
session_id: sessionId,
|
|
28
|
+
screenshot_path: screenshotPath,
|
|
29
|
+
};
|
|
30
|
+
}
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -4,7 +4,58 @@
|
|
|
4
4
|
* Scans the machine for Claude Code, Cursor, Windsurf, and Claude Desktop,
|
|
5
5
|
* then merges the Hanzi MCP server entry into each agent's config file.
|
|
6
6
|
*/
|
|
7
|
+
interface AgentConfig {
|
|
8
|
+
name: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
method: 'json-merge' | 'cli-command';
|
|
11
|
+
detect: () => boolean;
|
|
12
|
+
configPath?: () => string;
|
|
13
|
+
cliCommand?: string;
|
|
14
|
+
skillsDir?: () => string;
|
|
15
|
+
}
|
|
16
|
+
interface SetupResult {
|
|
17
|
+
agent: string;
|
|
18
|
+
status: 'configured' | 'already-configured' | 'skipped' | 'error';
|
|
19
|
+
detail: string;
|
|
20
|
+
}
|
|
21
|
+
interface AgentRegistryDeps {
|
|
22
|
+
home?: string;
|
|
23
|
+
plat?: NodeJS.Platform;
|
|
24
|
+
appData?: string;
|
|
25
|
+
pathExists?: (path: string) => boolean;
|
|
26
|
+
runCommand?: (command: string, options?: any) => Buffer | string;
|
|
27
|
+
}
|
|
28
|
+
interface JsonConfigDeps {
|
|
29
|
+
pathExists?: (path: string) => boolean;
|
|
30
|
+
readTextFile?: (path: string, encoding: BufferEncoding) => string;
|
|
31
|
+
writeTextFile?: (path: string, contents: string) => void;
|
|
32
|
+
ensureDir?: (path: string, options: {
|
|
33
|
+
recursive: boolean;
|
|
34
|
+
}) => void;
|
|
35
|
+
copyFile?: (source: string, destination: string) => void;
|
|
36
|
+
}
|
|
37
|
+
interface BrowserDetectionDeps {
|
|
38
|
+
plat?: NodeJS.Platform;
|
|
39
|
+
pathExists?: (path: string) => boolean;
|
|
40
|
+
runCommand?: (command: string, options?: any) => Buffer | string;
|
|
41
|
+
}
|
|
42
|
+
export declare function getAgentRegistry(deps?: AgentRegistryDeps): AgentConfig[];
|
|
43
|
+
export declare function mergeJsonConfig(configPath: string, deps?: JsonConfigDeps): SetupResult;
|
|
44
|
+
interface BrowserInfo {
|
|
45
|
+
name: string;
|
|
46
|
+
slug: string;
|
|
47
|
+
macApp: string;
|
|
48
|
+
linuxBin: string;
|
|
49
|
+
winPaths: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function detectBrowsers(deps?: BrowserDetectionDeps): BrowserInfo[];
|
|
52
|
+
export declare function resolveInteractiveMode(options?: {
|
|
53
|
+
yes?: boolean;
|
|
54
|
+
}, stdinIsTTY?: boolean): boolean;
|
|
55
|
+
export declare function buildBrowserOpenCommand(browser: BrowserInfo, url: string, plat: NodeJS.Platform): string;
|
|
56
|
+
export declare function buildSystemOpenCommand(url: string, plat: NodeJS.Platform): string;
|
|
7
57
|
export declare function runSetup(options?: {
|
|
8
58
|
only?: string;
|
|
9
59
|
yes?: boolean;
|
|
10
60
|
}): Promise<void>;
|
|
61
|
+
export {};
|
package/dist/cli/setup.js
CHANGED
|
@@ -65,12 +65,15 @@ const MCP_ENTRY = {
|
|
|
65
65
|
args: ['-y', 'hanzi-browse'],
|
|
66
66
|
};
|
|
67
67
|
// ── Agent registry ─────────────────────────────────────────────────────
|
|
68
|
-
function getAgentRegistry() {
|
|
69
|
-
const home = homedir();
|
|
70
|
-
const plat = platform();
|
|
68
|
+
export function getAgentRegistry(deps = {}) {
|
|
69
|
+
const home = deps.home ?? homedir();
|
|
70
|
+
const plat = deps.plat ?? platform();
|
|
71
|
+
const appData = deps.appData ?? process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
|
|
72
|
+
const pathExists = deps.pathExists ?? existsSync;
|
|
73
|
+
const runCommand = deps.runCommand ?? execSync;
|
|
71
74
|
const hasCli = (bin) => {
|
|
72
75
|
try {
|
|
73
|
-
|
|
76
|
+
runCommand(`which ${bin}`, { stdio: 'ignore' });
|
|
74
77
|
return true;
|
|
75
78
|
}
|
|
76
79
|
catch {
|
|
@@ -94,7 +97,7 @@ function getAgentRegistry() {
|
|
|
94
97
|
method: 'json-merge',
|
|
95
98
|
configPath: () => join(home, '.cursor', 'mcp.json'),
|
|
96
99
|
skillsDir: () => join(home, '.cursor', 'skills'),
|
|
97
|
-
detect: () =>
|
|
100
|
+
detect: () => pathExists(join(home, '.cursor')),
|
|
98
101
|
},
|
|
99
102
|
{
|
|
100
103
|
name: 'Windsurf',
|
|
@@ -102,7 +105,7 @@ function getAgentRegistry() {
|
|
|
102
105
|
method: 'json-merge',
|
|
103
106
|
configPath: () => join(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
104
107
|
skillsDir: () => join(home, '.codeium', 'windsurf', 'skills'),
|
|
105
|
-
detect: () =>
|
|
108
|
+
detect: () => pathExists(join(home, '.codeium', 'windsurf')),
|
|
106
109
|
},
|
|
107
110
|
{
|
|
108
111
|
name: 'VS Code',
|
|
@@ -110,7 +113,7 @@ function getAgentRegistry() {
|
|
|
110
113
|
method: 'json-merge',
|
|
111
114
|
configPath: () => join(home, '.vscode', 'mcp.json'),
|
|
112
115
|
skillsDir: () => join(home, '.vscode', 'skills'),
|
|
113
|
-
detect: () =>
|
|
116
|
+
detect: () => pathExists(join(home, '.vscode')),
|
|
114
117
|
},
|
|
115
118
|
{
|
|
116
119
|
name: 'Codex',
|
|
@@ -118,7 +121,7 @@ function getAgentRegistry() {
|
|
|
118
121
|
method: 'json-merge',
|
|
119
122
|
configPath: () => join(home, '.codex', 'mcp.json'),
|
|
120
123
|
skillsDir: () => join(home, '.agents', 'skills'),
|
|
121
|
-
detect: () =>
|
|
124
|
+
detect: () => pathExists(join(home, '.codex')) || hasCli('codex'),
|
|
122
125
|
},
|
|
123
126
|
{
|
|
124
127
|
name: 'Claude Desktop',
|
|
@@ -128,15 +131,15 @@ function getAgentRegistry() {
|
|
|
128
131
|
if (plat === 'darwin')
|
|
129
132
|
return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
130
133
|
if (plat === 'win32')
|
|
131
|
-
return join(
|
|
134
|
+
return join(appData, 'Claude', 'claude_desktop_config.json');
|
|
132
135
|
return join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
133
136
|
},
|
|
134
137
|
detect: () => {
|
|
135
138
|
if (plat === 'darwin')
|
|
136
|
-
return
|
|
139
|
+
return pathExists(join(home, 'Library', 'Application Support', 'Claude'));
|
|
137
140
|
if (plat === 'win32')
|
|
138
|
-
return
|
|
139
|
-
return
|
|
141
|
+
return pathExists(join(appData, 'Claude'));
|
|
142
|
+
return pathExists(join(home, '.config', 'Claude'));
|
|
140
143
|
},
|
|
141
144
|
},
|
|
142
145
|
{
|
|
@@ -145,7 +148,7 @@ function getAgentRegistry() {
|
|
|
145
148
|
method: 'json-merge',
|
|
146
149
|
configPath: () => join(home, '.gemini', 'settings.json'),
|
|
147
150
|
skillsDir: () => join(home, '.gemini', 'skills'),
|
|
148
|
-
detect: () =>
|
|
151
|
+
detect: () => pathExists(join(home, '.gemini')) || hasCli('gemini'),
|
|
149
152
|
},
|
|
150
153
|
{
|
|
151
154
|
name: 'Amp',
|
|
@@ -153,21 +156,21 @@ function getAgentRegistry() {
|
|
|
153
156
|
method: 'json-merge',
|
|
154
157
|
configPath: () => join(home, '.amp', 'mcp.json'),
|
|
155
158
|
skillsDir: () => join(home, '.amp', 'skills'),
|
|
156
|
-
detect: () =>
|
|
159
|
+
detect: () => pathExists(join(home, '.amp')),
|
|
157
160
|
},
|
|
158
161
|
{
|
|
159
162
|
name: 'Cline',
|
|
160
163
|
slug: 'cline',
|
|
161
164
|
method: 'json-merge',
|
|
162
165
|
configPath: () => join(home, '.cline', 'mcp_settings.json'),
|
|
163
|
-
detect: () =>
|
|
166
|
+
detect: () => pathExists(join(home, '.cline')),
|
|
164
167
|
},
|
|
165
168
|
{
|
|
166
169
|
name: 'Roo Code',
|
|
167
170
|
slug: 'roo-code',
|
|
168
171
|
method: 'json-merge',
|
|
169
172
|
configPath: () => join(home, '.roo-code', 'mcp_settings.json'),
|
|
170
|
-
detect: () =>
|
|
173
|
+
detect: () => pathExists(join(home, '.roo-code')),
|
|
171
174
|
},
|
|
172
175
|
];
|
|
173
176
|
}
|
|
@@ -177,16 +180,21 @@ function stripJsonComments(text) {
|
|
|
177
180
|
.replace(/\/\/.*$/gm, '')
|
|
178
181
|
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
179
182
|
}
|
|
180
|
-
function mergeJsonConfig(configPath) {
|
|
183
|
+
export function mergeJsonConfig(configPath, deps = {}) {
|
|
181
184
|
const agentName = configPath;
|
|
185
|
+
const pathExists = deps.pathExists ?? existsSync;
|
|
186
|
+
const readTextFile = deps.readTextFile ?? readFileSync;
|
|
187
|
+
const writeTextFile = deps.writeTextFile ?? writeFileSync;
|
|
188
|
+
const ensureDir = deps.ensureDir ?? mkdirSync;
|
|
189
|
+
const copyFile = deps.copyFile ?? copyFileSync;
|
|
182
190
|
try {
|
|
183
|
-
if (!
|
|
184
|
-
|
|
191
|
+
if (!pathExists(configPath)) {
|
|
192
|
+
ensureDir(join(configPath, '..'), { recursive: true });
|
|
185
193
|
const config = { mcpServers: { "hanzi-browser": MCP_ENTRY } };
|
|
186
|
-
|
|
194
|
+
writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
187
195
|
return { agent: agentName, status: 'configured', detail: `created ${configPath}` };
|
|
188
196
|
}
|
|
189
|
-
const raw =
|
|
197
|
+
const raw = readTextFile(configPath, 'utf-8');
|
|
190
198
|
let config;
|
|
191
199
|
try {
|
|
192
200
|
config = JSON.parse(raw);
|
|
@@ -197,9 +205,9 @@ function mergeJsonConfig(configPath) {
|
|
|
197
205
|
}
|
|
198
206
|
catch {
|
|
199
207
|
const bakPath = configPath + '.bak';
|
|
200
|
-
|
|
208
|
+
copyFile(configPath, bakPath);
|
|
201
209
|
config = { mcpServers: { "hanzi-browser": MCP_ENTRY } };
|
|
202
|
-
|
|
210
|
+
writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
203
211
|
return { agent: agentName, status: 'configured', detail: `backed up malformed config to ${bakPath}` };
|
|
204
212
|
}
|
|
205
213
|
}
|
|
@@ -212,7 +220,7 @@ function mergeJsonConfig(configPath) {
|
|
|
212
220
|
if (!config.mcpServers)
|
|
213
221
|
config.mcpServers = {};
|
|
214
222
|
config.mcpServers["hanzi-browser"] = MCP_ENTRY;
|
|
215
|
-
|
|
223
|
+
writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
216
224
|
return { agent: agentName, status: 'configured', detail: `merged into ${configPath}` };
|
|
217
225
|
}
|
|
218
226
|
catch (err) {
|
|
@@ -245,20 +253,67 @@ function runClaudeCodeSetup() {
|
|
|
245
253
|
// ── Browser detection ──────────────────────────────────────────────────
|
|
246
254
|
const EXTENSION_URL = 'https://chromewebstore.google.com/detail/hanzi-browse/iklpkemlmbhemkiojndpbhoakgikpmcd';
|
|
247
255
|
const BROWSERS = [
|
|
248
|
-
{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
{
|
|
257
|
+
name: 'Google Chrome',
|
|
258
|
+
slug: 'chrome',
|
|
259
|
+
macApp: 'Google Chrome',
|
|
260
|
+
linuxBin: 'google-chrome',
|
|
261
|
+
winPaths: [
|
|
262
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
263
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'Brave',
|
|
268
|
+
slug: 'brave',
|
|
269
|
+
macApp: 'Brave Browser',
|
|
270
|
+
linuxBin: 'brave-browser',
|
|
271
|
+
winPaths: [
|
|
272
|
+
'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
273
|
+
'C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'Microsoft Edge',
|
|
278
|
+
slug: 'edge',
|
|
279
|
+
macApp: 'Microsoft Edge',
|
|
280
|
+
linuxBin: 'microsoft-edge',
|
|
281
|
+
winPaths: [
|
|
282
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
283
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'Arc',
|
|
288
|
+
slug: 'arc',
|
|
289
|
+
macApp: 'Arc',
|
|
290
|
+
linuxBin: 'arc',
|
|
291
|
+
winPaths: [],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'Chromium',
|
|
295
|
+
slug: 'chromium',
|
|
296
|
+
macApp: 'Chromium',
|
|
297
|
+
linuxBin: 'chromium-browser',
|
|
298
|
+
winPaths: [
|
|
299
|
+
'C:\\Program Files\\Chromium\\Application\\chrome.exe',
|
|
300
|
+
'C:\\Program Files (x86)\\Chromium\\Application\\chrome.exe',
|
|
301
|
+
],
|
|
302
|
+
},
|
|
253
303
|
];
|
|
254
|
-
function detectBrowsers() {
|
|
255
|
-
const plat = platform();
|
|
304
|
+
export function detectBrowsers(deps = {}) {
|
|
305
|
+
const plat = deps.plat ?? platform();
|
|
306
|
+
const pathExists = deps.pathExists ?? existsSync;
|
|
307
|
+
const runCommand = deps.runCommand ?? execSync;
|
|
256
308
|
return BROWSERS.filter(b => {
|
|
257
309
|
if (plat === 'darwin') {
|
|
258
|
-
return
|
|
310
|
+
return pathExists(`/Applications/${b.macApp}.app`);
|
|
311
|
+
}
|
|
312
|
+
if (plat === 'win32') {
|
|
313
|
+
return b.winPaths.some(path => pathExists(path));
|
|
259
314
|
}
|
|
260
315
|
try {
|
|
261
|
-
|
|
316
|
+
runCommand(`which ${b.linuxBin}`, { stdio: 'ignore' });
|
|
262
317
|
return true;
|
|
263
318
|
}
|
|
264
319
|
catch {
|
|
@@ -266,19 +321,36 @@ function detectBrowsers() {
|
|
|
266
321
|
}
|
|
267
322
|
});
|
|
268
323
|
}
|
|
324
|
+
export function resolveInteractiveMode(options = {}, stdinIsTTY = process.stdin.isTTY ?? false) {
|
|
325
|
+
return options.yes ? false : stdinIsTTY;
|
|
326
|
+
}
|
|
327
|
+
export function buildBrowserOpenCommand(browser, url, plat) {
|
|
328
|
+
if (plat === 'darwin') {
|
|
329
|
+
return `open -a "${browser.macApp}" "${url}"`;
|
|
330
|
+
}
|
|
331
|
+
if (plat === 'win32') {
|
|
332
|
+
const exePath = browser.winPaths.find(path => existsSync(path)) ?? browser.winPaths[0];
|
|
333
|
+
if (!exePath)
|
|
334
|
+
return `cmd /c start "" "${url}"`;
|
|
335
|
+
return `cmd /c start "" "${exePath}" "${url}"`;
|
|
336
|
+
}
|
|
337
|
+
return `${browser.linuxBin} "${url}" &`;
|
|
338
|
+
}
|
|
339
|
+
export function buildSystemOpenCommand(url, plat) {
|
|
340
|
+
if (plat === 'darwin')
|
|
341
|
+
return `open "${url}"`;
|
|
342
|
+
if (plat === 'win32')
|
|
343
|
+
return `cmd /c start "" "${url}"`;
|
|
344
|
+
return `xdg-open "${url}"`;
|
|
345
|
+
}
|
|
269
346
|
function openInBrowser(browser, url) {
|
|
270
347
|
const plat = platform();
|
|
271
348
|
try {
|
|
272
|
-
|
|
273
|
-
execSync(`open -a "${browser.macApp}" "${url}"`, { stdio: 'ignore' });
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
execSync(`${browser.linuxBin} "${url}" &`, { stdio: 'ignore' });
|
|
277
|
-
}
|
|
349
|
+
execSync(buildBrowserOpenCommand(browser, url, plat), { stdio: 'ignore' });
|
|
278
350
|
}
|
|
279
351
|
catch {
|
|
280
352
|
// Fallback: system default
|
|
281
|
-
execSync(
|
|
353
|
+
execSync(buildSystemOpenCommand(url, plat), { stdio: 'ignore' });
|
|
282
354
|
}
|
|
283
355
|
}
|
|
284
356
|
async function ensureExtension(isInteractive) {
|
|
@@ -700,7 +772,7 @@ export async function runSetup(options = {}) {
|
|
|
700
772
|
trackEvent("setup_started");
|
|
701
773
|
const registry = getAgentRegistry();
|
|
702
774
|
const only = options.only;
|
|
703
|
-
const interactive = options
|
|
775
|
+
const interactive = resolveInteractiveMode(options);
|
|
704
776
|
// ── Banner ──
|
|
705
777
|
if (interactive) {
|
|
706
778
|
console.log(BANNER);
|