hanzi-browse 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +63 -79
  2. package/dist/agent/domain-knowledge.d.ts +20 -0
  3. package/dist/agent/domain-knowledge.js +33 -0
  4. package/dist/agent/domain-skills.json +66 -0
  5. package/dist/agent/loop.d.ts +12 -0
  6. package/dist/agent/loop.js +41 -1
  7. package/dist/agent/system-prompt.d.ts +1 -1
  8. package/dist/agent/system-prompt.js +12 -2
  9. package/dist/cli/json-output.d.ts +21 -0
  10. package/dist/cli/json-output.js +30 -0
  11. package/dist/cli/setup.d.ts +51 -0
  12. package/dist/cli/setup.js +113 -41
  13. package/dist/cli.js +29 -8
  14. package/dist/index.js +1 -567
  15. package/dist/managed/api.d.ts +20 -1
  16. package/dist/managed/api.js +181 -521
  17. package/dist/managed/auth.js +0 -5
  18. package/dist/managed/deploy.js +82 -0
  19. package/dist/managed/routes/api.d.ts +44 -0
  20. package/dist/managed/routes/api.js +220 -0
  21. package/dist/managed/routes/pages.d.ts +13 -0
  22. package/dist/managed/routes/pages.js +149 -0
  23. package/dist/managed/store-pg.d.ts +5 -1
  24. package/dist/managed/store-pg.js +12 -4
  25. package/dist/managed/store.d.ts +6 -1
  26. package/dist/managed/store.js +4 -2
  27. package/dist/managed/templates/pair-self.html +67 -0
  28. package/dist/managed/templates/pair.html +97 -0
  29. package/dist/mcp/tools.d.ts +20 -0
  30. package/dist/mcp/tools.js +263 -0
  31. package/dist/relay/api-proxy.d.ts +2 -0
  32. package/dist/relay/api-proxy.js +165 -0
  33. package/dist/relay/server.js +2 -112
  34. package/package.json +3 -3
  35. package/skills/competitor-monitor/SKILL.md +290 -0
  36. package/skills/data-extractor/SKILL.md +223 -0
  37. package/skills/job-applier/SKILL.md +260 -0
  38. package/skills/seo-checker/SKILL.md +146 -0
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
- execSync(`which ${bin}`, { stdio: 'ignore' });
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: () => existsSync(join(home, '.cursor')),
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: () => existsSync(join(home, '.codeium', 'windsurf')),
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: () => existsSync(join(home, '.vscode')),
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: () => existsSync(join(home, '.codex')) || hasCli('codex'),
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(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
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 existsSync(join(home, 'Library', 'Application Support', 'Claude'));
139
+ return pathExists(join(home, 'Library', 'Application Support', 'Claude'));
137
140
  if (plat === 'win32')
138
- return existsSync(join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'Claude'));
139
- return existsSync(join(home, '.config', 'Claude'));
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: () => existsSync(join(home, '.gemini')) || hasCli('gemini'),
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: () => existsSync(join(home, '.amp')),
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: () => existsSync(join(home, '.cline')),
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: () => existsSync(join(home, '.roo-code')),
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 (!existsSync(configPath)) {
184
- mkdirSync(join(configPath, '..'), { recursive: true });
191
+ if (!pathExists(configPath)) {
192
+ ensureDir(join(configPath, '..'), { recursive: true });
185
193
  const config = { mcpServers: { "hanzi-browser": MCP_ENTRY } };
186
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
194
+ writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
187
195
  return { agent: agentName, status: 'configured', detail: `created ${configPath}` };
188
196
  }
189
- const raw = readFileSync(configPath, 'utf-8');
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
- copyFileSync(configPath, bakPath);
208
+ copyFile(configPath, bakPath);
201
209
  config = { mcpServers: { "hanzi-browser": MCP_ENTRY } };
202
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
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
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
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
- { name: 'Google Chrome', slug: 'chrome', macApp: 'Google Chrome', linuxBin: 'google-chrome' },
249
- { name: 'Brave', slug: 'brave', macApp: 'Brave Browser', linuxBin: 'brave-browser' },
250
- { name: 'Microsoft Edge', slug: 'edge', macApp: 'Microsoft Edge', linuxBin: 'microsoft-edge' },
251
- { name: 'Arc', slug: 'arc', macApp: 'Arc', linuxBin: 'arc' },
252
- { name: 'Chromium', slug: 'chromium', macApp: 'Chromium', linuxBin: 'chromium-browser' },
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 existsSync(`/Applications/${b.macApp}.app`);
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
- execSync(`which ${b.linuxBin}`, { stdio: 'ignore' });
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
- if (plat === 'darwin') {
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(`open "${url}" 2>/dev/null || xdg-open "${url}" 2>/dev/null`, { stdio: 'ignore' });
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.yes ? false : (process.stdin.isTTY ?? false);
775
+ const interactive = resolveInteractiveMode(options);
704
776
  // ── Banner ──
705
777
  if (interactive) {
706
778
  console.log(BANNER);
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'url';
20
20
  import { dirname } from 'path';
21
21
  import { WebSocketClient } from './ipc/websocket-client.js';
22
22
  import { writeSessionStatus, readSessionStatus, appendSessionLog, listSessions, deleteSessionFiles, getSessionLogPath, getSessionScreenshotPath, } from './cli/session-files.js';
23
+ import { buildScreenshotPayload, buildStatusPayload, buildStopPayload, buildTaskCompletePayload, buildTaskErrorPayload, } from './cli/json-output.js';
23
24
  // Parse command line arguments
24
25
  const args = process.argv.slice(2);
25
26
  const command = args[0];
@@ -67,7 +68,7 @@ function handleMessage(message) {
67
68
  appendSessionLog(sessionId, `[COMPLETE] ${answer}`);
68
69
  writeSessionStatus(sessionId, { status: 'complete', result: answer });
69
70
  if (jsonOutput) {
70
- console.log(JSON.stringify({ session_id: sessionId, status: 'completed', result }));
71
+ console.log(JSON.stringify(buildTaskCompletePayload(sessionId, result)));
71
72
  }
72
73
  else {
73
74
  console.log(`\n[CLI] Task completed: ${sessionId}`);
@@ -80,7 +81,7 @@ function handleMessage(message) {
80
81
  appendSessionLog(sessionId, `[ERROR] ${data.error}`);
81
82
  writeSessionStatus(sessionId, { status: 'error', error: data.error });
82
83
  if (jsonOutput) {
83
- console.log(JSON.stringify({ session_id: sessionId, status: 'error', error: data.error }));
84
+ console.log(JSON.stringify(buildTaskErrorPayload(sessionId, data.error)));
84
85
  }
85
86
  else {
86
87
  console.error(`\n[CLI] Task error: ${data.error}`);
@@ -193,12 +194,12 @@ function cmdStatus() {
193
194
  console.error(`Session not found: ${sessionId}`);
194
195
  process.exit(1);
195
196
  }
196
- console.log(JSON.stringify(status, jsonOutput ? undefined : null, jsonOutput ? undefined : 2));
197
+ console.log(JSON.stringify(buildStatusPayload(status), jsonOutput ? undefined : null, jsonOutput ? undefined : 2));
197
198
  }
198
199
  else {
199
200
  const allSessions = listSessions();
200
201
  if (jsonOutput) {
201
- console.log(JSON.stringify(allSessions));
202
+ console.log(JSON.stringify(buildStatusPayload(allSessions)));
202
203
  }
203
204
  else if (allSessions.length === 0) {
204
205
  console.log('No sessions found.');
@@ -267,11 +268,21 @@ async function cmdStop() {
267
268
  await connection.send({ type: 'mcp_stop_task', sessionId, remove });
268
269
  if (remove) {
269
270
  deleteSessionFiles(sessionId);
270
- console.log(`Session ${sessionId} stopped and removed.`);
271
+ if (jsonOutput) {
272
+ console.log(JSON.stringify(buildStopPayload(sessionId, true)));
273
+ }
274
+ else {
275
+ console.log(`Session ${sessionId} stopped and removed.`);
276
+ }
271
277
  }
272
278
  else {
273
279
  writeSessionStatus(sessionId, { status: 'stopped' });
274
- console.log(`Session ${sessionId} stopped.`);
280
+ if (jsonOutput) {
281
+ console.log(JSON.stringify(buildStopPayload(sessionId, false)));
282
+ }
283
+ else {
284
+ console.log(`Session ${sessionId} stopped.`);
285
+ }
275
286
  }
276
287
  disconnectAndExit(0);
277
288
  }
@@ -281,7 +292,9 @@ async function cmdScreenshot() {
281
292
  activeSessionId = requestId;
282
293
  await initConnection();
283
294
  await connection.send({ type: 'mcp_screenshot', sessionId: requestId });
284
- console.log(`Screenshot requested for ${requestId}. Waiting for image...\n`);
295
+ if (!jsonOutput) {
296
+ console.log(`Screenshot requested for ${requestId}. Waiting for image...\n`);
297
+ }
285
298
  const data = await new Promise((resolve) => {
286
299
  pendingScreenshotResolve = resolve;
287
300
  setTimeout(() => {
@@ -296,7 +309,12 @@ async function cmdScreenshot() {
296
309
  }
297
310
  const screenshotPath = getSessionScreenshotPath(requestId);
298
311
  writeFileSync(screenshotPath, Buffer.from(data, 'base64'));
299
- console.log(`[CLI] Screenshot saved: ${screenshotPath}`);
312
+ if (jsonOutput) {
313
+ console.log(JSON.stringify(buildScreenshotPayload(requestId, screenshotPath)));
314
+ }
315
+ else {
316
+ console.log(`[CLI] Screenshot saved: ${screenshotPath}`);
317
+ }
300
318
  disconnectAndExit(0);
301
319
  }
302
320
  // --- Skills ---
@@ -408,6 +426,7 @@ Commands:
408
426
  Each session gets its own browser window.
409
427
 
410
428
  status [session_id] Show status of session(s)
429
+ --json Output machine-readable JSON
411
430
 
412
431
  message <session_id> <msg> Send follow-up instructions to a session
413
432
  Reuses the same browser window and page state.
@@ -417,8 +436,10 @@ Commands:
417
436
 
418
437
  stop <session_id> Stop a session
419
438
  --remove, -r Also delete session files
439
+ --json Output machine-readable JSON
420
440
 
421
441
  screenshot [session_id] Take a screenshot
442
+ --json Output machine-readable JSON
422
443
 
423
444
  setup Auto-detect AI agents and configure MCP
424
445
  --only <agent> Only configure one agent (claude-code, cursor, windsurf, claude-desktop)