hanzi-browse 2.3.2 → 2.3.3
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 +4 -2
- package/dist/agent/domain-skills.json +27 -1
- package/dist/agent/system-prompt.js +6 -2
- package/dist/cli/setup.d.ts +6 -0
- package/dist/cli/setup.js +211 -29
- package/dist/cli.js +17 -3
- package/dist/dashboard/assets/{index-dnFOSpJs.js → index-AKm32GEX.js} +15 -15
- package/dist/dashboard/index.html +1 -1
- package/dist/mcp/tools.js +39 -32
- package/dist/telemetry.js +7 -3
- package/package.json +1 -1
- package/skills/a11y-auditor/SKILL.md +1 -0
- package/skills/apartment-finder/SKILL.md +211 -0
- package/skills/competitor-monitor/SKILL.md +1 -0
- package/skills/competitor-researcher/SKILL.md +434 -0
- package/skills/data-extractor/SKILL.md +1 -0
- package/skills/e2e-tester/SKILL.md +1 -0
- package/skills/hanzi-browse/SKILL.md +120 -112
- package/skills/job-applier/SKILL.md +1 -0
- package/skills/linkedin-prospector/SKILL.md +1 -0
- package/skills/seo-checker/SKILL.md +14 -1
- package/skills/social-poster/SKILL.md +1 -0
- package/skills/x-marketer/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -19,11 +19,11 @@ This installs the Chrome extension and configures your AI agent. One command, do
|
|
|
19
19
|
## Quick Start (API)
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install @hanzi/
|
|
22
|
+
npm install @hanzi-browse/sdk
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
|
-
import { HanziClient } from '@hanzi/
|
|
26
|
+
import { HanziClient } from '@hanzi-browse/sdk';
|
|
27
27
|
|
|
28
28
|
const client = new HanziClient({ apiKey: 'hic_live_...' });
|
|
29
29
|
|
|
@@ -129,6 +129,8 @@ browser_start("Check hotel prices in Shibuya")
|
|
|
129
129
|
| `HANZI_BROWSE_MAX_SESSIONS` | `5` | Max concurrent browser tasks |
|
|
130
130
|
| `HANZI_BROWSE_TIMEOUT_MS` | `300000` | Task timeout (ms) |
|
|
131
131
|
| `WS_RELAY_PORT` | `7862` | WebSocket relay port |
|
|
132
|
+
| `POSTHOG_API_KEY` | unset | Enables PostHog analytics for local CLI telemetry, the dashboard build, the managed backend, and example apps |
|
|
133
|
+
| `POSTHOG_HOST` | `https://us.i.posthog.com` | Override the PostHog host for all server-side capture calls and dashboard initialization |
|
|
132
134
|
|
|
133
135
|
## Skills
|
|
134
136
|
|
|
@@ -61,6 +61,32 @@
|
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
"domain": "amazon.com",
|
|
64
|
-
"skill": "Amazon
|
|
64
|
+
"skill": "Amazon UI patterns:\n- Prefer the top search bar or a direct search URL for product discovery\n- The first fold may be dominated by sponsored modules or Amazon brand carousels before standard results\n- Result cards can show total price, unit price, ratings, delivery promises, variation links, and inline Add to Cart buttons all at once\n- Watch carefully for subtle labels such as 'Sponsored' or 'Featured from Amazon brands'\n- Location/shipping prompts can change delivery wording and result context, so verify destination-sensitive text before extracting prices"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"domain": "ebay.com",
|
|
68
|
+
"skill": "eBay UI patterns:\n- Search results usually show title, condition, price, shipping, and location directly on the card\n- Headline price is often incomplete until you also check shipping cost, coupon text, and 'Best Offer' language\n- Sponsored placements and featured carousels can interrupt the organic result list\n- Use condition and shipping origin early to filter out weak matches\n- Be cautious with range prices and urgency labels like 'Last one' or watcher counts; they do not guarantee a clean fixed-price purchase path"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"domain": "walmart.ca",
|
|
72
|
+
"antiBot": true,
|
|
73
|
+
"skill": "Walmart.ca UI patterns:\n- Search results often expose enough information to compare products before opening a product page\n- Cards commonly include brand, title, price, ratings, fulfillment messaging, and promo details such as Subscribe to Save or pickup thresholds\n- Walmart.com may first present a storefront or region-selection step and later a human-verification blocker ('Press & Hold' challenge); validation was more reliable on Walmart.ca\n- On Walmart.ca product pages, verify one-time price separately from Subscribe to Save pricing and check pickup, express delivery, and standard delivery messages individually\n- Treat promo labels, fulfillment thresholds, and returns messaging as separate signals and verify them individually\n- If you hit a 'Press & Hold' or 'verify you are human' challenge, STOP and tell the user — never attempt to bypass"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"domain": "target.com",
|
|
77
|
+
"skill": "Target UI patterns:\n- Result cards usually include title, review count, price, promo messaging, and a persistent Add to Cart button\n- Price formatting can mix ranges, sale pricing, regular pricing, and per-unit math, so read the full price block carefully\n- Promo text such as Target gift card offers or 'Highly rated' badges appears frequently and can distract from the base price\n- Fulfillment filters are easy to see near the top, but card-level availability may still need a deeper click\n- Search results can include adjacent-category items, so confirm the exact product type from the title before acting"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"domain": "zillow.com",
|
|
81
|
+
"antiBot": true,
|
|
82
|
+
"skill": "Zillow UI patterns (rentals):\n- Sign in first for full contact info and saved searches; anonymous users see degraded data\n- Use 'For Rent' in the top nav, then refine by price, beds, and 'Move-in Date'\n- Results have a split map + list view — switch to list view for cleaner data extraction\n- Listing cards include price, beds/baths, address, and 'Apply' or 'Request a tour' buttons\n- 'Request a tour' and 'Contact' buttons submit forms — draft the message and wait for explicit user approval before clicking submit\n- Never enter SSN, payment info, or background check data without confirming the user is on an official Zillow Application flow\n- Zillow frequently serves a CAPTCHA / 'Press & Hold' challenge for automated traffic — if you hit one, STOP and tell the user, do not bypass"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"domain": "apartments.com",
|
|
86
|
+
"skill": "Apartments.com UI patterns:\n- Top filter bar: location, price, beds, move-in date, amenities — apply them before reading results\n- Listing cards include price range, beds/baths, address, and a 'Send Message' or 'Contact' button\n- Many listings have a built-in multi-step application flow — each step is clearly labeled; stop before 'Submit Application' and confirm with the user\n- 'Send Message' opens an inline form — draft the message, show it to the user, wait for approval before submitting\n- Pricing is often shown as a range ($X-$Y) because individual units vary; click into the listing for per-unit prices\n- Response times for large property managers are usually fast (hours); individual landlords slower\n- Do not enter SSN, credit-card, or bank info without explicit user confirmation that they want to proceed with a real application"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"domain": "craigslist.org",
|
|
90
|
+
"skill": "Craigslist UI patterns (apartments / housing):\n- URL pattern: https://[city].craigslist.org/search/apa?minAsk={min}&maxAsk={max}&bedrooms={n}\n- Sort by 'newest' to avoid stale listings — older listings are frequently scams or already rented\n- Listings may or may not have photos; listings without photos are higher risk, flag them\n- Contact is always via an anonymized email relay — no built-in application flow\n- Scam flags to warn the user about: price >25% below market, 'owner overseas', 'send deposit to hold', asks to text/WhatsApp before viewing, generic stock photos\n- Never follow links off craigslist for a 'verified listings site' — those are almost always phishing\n- Show the user any inquiry message before sending, and wait for explicit approval"
|
|
65
91
|
}
|
|
66
92
|
]
|
|
@@ -13,9 +13,13 @@ export function buildSystemPrompt(taskUrl) {
|
|
|
13
13
|
const blocks = [
|
|
14
14
|
{
|
|
15
15
|
type: "text",
|
|
16
|
-
text: `You are a
|
|
16
|
+
text: `You are Hanzi Browse, a browsing sub-agent driving the user's own Chrome browser — with all their logins, cookies, and sessions already in place. A host agent (Claude Code, Cursor, Codex, etc.) has delegated a task to you. Your job is to complete that task autonomously using the browser tools below, and return a concise answer.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
You are NOT a step-by-step executor reading a script. You are an agent. You decide what to click, what to type, what to wait for, and when you're done. The host agent gave you a goal in natural language; figure out the steps yourself and complete the goal.
|
|
19
|
+
|
|
20
|
+
When the host agent sends you a follow-up via browser_message, it's course-correcting or refining the task — treat it as the latest instruction from the user and continue from the current browser state.
|
|
21
|
+
|
|
22
|
+
You are persistent. Long or multi-step tasks are expected. The host agent expects you to work until the task is complete. Do not ask for permission — just do it.
|
|
19
23
|
|
|
20
24
|
<behavior_instructions>
|
|
21
25
|
The current date is ${dateStr}, ${timeStr}.
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface AgentConfig {
|
|
|
10
10
|
method: 'json-merge' | 'cli-command';
|
|
11
11
|
detect: () => boolean;
|
|
12
12
|
configPath?: () => string;
|
|
13
|
+
configSection?: 'mcpServers' | 'servers' | 'context_servers';
|
|
14
|
+
legacyConfigSections?: ('mcpServers' | 'servers' | 'context_servers')[];
|
|
13
15
|
cliCommand?: string;
|
|
14
16
|
skillsDir?: () => string;
|
|
15
17
|
}
|
|
@@ -22,6 +24,7 @@ interface AgentRegistryDeps {
|
|
|
22
24
|
home?: string;
|
|
23
25
|
plat?: NodeJS.Platform;
|
|
24
26
|
appData?: string;
|
|
27
|
+
xdgConfigHome?: string;
|
|
25
28
|
pathExists?: (path: string) => boolean;
|
|
26
29
|
runCommand?: (command: string, options?: any) => Buffer | string;
|
|
27
30
|
}
|
|
@@ -41,6 +44,7 @@ interface BrowserDetectionDeps {
|
|
|
41
44
|
}
|
|
42
45
|
export declare function getAgentRegistry(deps?: AgentRegistryDeps): AgentConfig[];
|
|
43
46
|
export declare function mergeJsonConfig(configPath: string, deps?: JsonConfigDeps): SetupResult;
|
|
47
|
+
export declare function mergeJsonConfigAtKey(configPath: string, configSection: 'mcpServers' | 'servers' | 'context_servers', deps?: JsonConfigDeps, legacyConfigSections?: ('mcpServers' | 'servers' | 'context_servers')[]): SetupResult;
|
|
44
48
|
interface BrowserInfo {
|
|
45
49
|
name: string;
|
|
46
50
|
slug: string;
|
|
@@ -57,5 +61,7 @@ export declare function buildSystemOpenCommand(url: string, plat: NodeJS.Platfor
|
|
|
57
61
|
export declare function runSetup(options?: {
|
|
58
62
|
only?: string;
|
|
59
63
|
yes?: boolean;
|
|
64
|
+
all?: boolean;
|
|
65
|
+
skills?: string[];
|
|
60
66
|
}): Promise<void>;
|
|
61
67
|
export {};
|
package/dist/cli/setup.js
CHANGED
|
@@ -69,6 +69,7 @@ export function getAgentRegistry(deps = {}) {
|
|
|
69
69
|
const home = deps.home ?? homedir();
|
|
70
70
|
const plat = deps.plat ?? platform();
|
|
71
71
|
const appData = deps.appData ?? process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
|
|
72
|
+
const xdgConfigHome = deps.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? join(home, '.config');
|
|
72
73
|
const pathExists = deps.pathExists ?? existsSync;
|
|
73
74
|
const runCommand = deps.runCommand ?? execSync;
|
|
74
75
|
const hasCli = (bin) => {
|
|
@@ -112,9 +113,39 @@ export function getAgentRegistry(deps = {}) {
|
|
|
112
113
|
slug: 'vscode',
|
|
113
114
|
method: 'json-merge',
|
|
114
115
|
configPath: () => join(home, '.vscode', 'mcp.json'),
|
|
116
|
+
configSection: 'servers',
|
|
117
|
+
legacyConfigSections: ['mcpServers'],
|
|
115
118
|
skillsDir: () => join(home, '.vscode', 'skills'),
|
|
116
119
|
detect: () => pathExists(join(home, '.vscode')),
|
|
117
120
|
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Zed',
|
|
123
|
+
slug: 'zed',
|
|
124
|
+
method: 'json-merge',
|
|
125
|
+
configPath: () => {
|
|
126
|
+
if (plat === 'darwin')
|
|
127
|
+
return join(home, 'Library', 'Application Support', 'Zed', 'settings.json');
|
|
128
|
+
if (plat === 'win32')
|
|
129
|
+
return join(appData, 'Zed', 'settings.json');
|
|
130
|
+
return join(xdgConfigHome, 'zed', 'settings.json');
|
|
131
|
+
},
|
|
132
|
+
configSection: 'context_servers',
|
|
133
|
+
detect: () => {
|
|
134
|
+
if (plat === 'darwin')
|
|
135
|
+
return pathExists(join(home, 'Library', 'Application Support', 'Zed'));
|
|
136
|
+
if (plat === 'win32')
|
|
137
|
+
return pathExists(join(appData, 'Zed'));
|
|
138
|
+
return pathExists(join(xdgConfigHome, 'zed'));
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'Neovim',
|
|
143
|
+
slug: 'neovim',
|
|
144
|
+
method: 'json-merge',
|
|
145
|
+
configPath: () => join(xdgConfigHome, 'mcphub', 'servers.json'),
|
|
146
|
+
configSection: 'servers',
|
|
147
|
+
detect: () => pathExists(join(xdgConfigHome, 'mcphub')),
|
|
148
|
+
},
|
|
118
149
|
{
|
|
119
150
|
name: 'Codex',
|
|
120
151
|
slug: 'codex',
|
|
@@ -181,6 +212,24 @@ function stripJsonComments(text) {
|
|
|
181
212
|
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
182
213
|
}
|
|
183
214
|
export function mergeJsonConfig(configPath, deps = {}) {
|
|
215
|
+
return mergeJsonConfigAtKey(configPath, 'mcpServers', deps);
|
|
216
|
+
}
|
|
217
|
+
function removeLegacyHanziEntries(config, configSection, legacyConfigSections = []) {
|
|
218
|
+
let changed = false;
|
|
219
|
+
for (const legacySection of legacyConfigSections) {
|
|
220
|
+
if (legacySection === configSection)
|
|
221
|
+
continue;
|
|
222
|
+
const section = config[legacySection];
|
|
223
|
+
if (section && typeof section === 'object' && section['hanzi-browser']) {
|
|
224
|
+
delete section['hanzi-browser'];
|
|
225
|
+
changed = true;
|
|
226
|
+
if (Object.keys(section).length === 0)
|
|
227
|
+
delete config[legacySection];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return changed;
|
|
231
|
+
}
|
|
232
|
+
export function mergeJsonConfigAtKey(configPath, configSection, deps = {}, legacyConfigSections = []) {
|
|
184
233
|
const agentName = configPath;
|
|
185
234
|
const pathExists = deps.pathExists ?? existsSync;
|
|
186
235
|
const readTextFile = deps.readTextFile ?? readFileSync;
|
|
@@ -190,7 +239,7 @@ export function mergeJsonConfig(configPath, deps = {}) {
|
|
|
190
239
|
try {
|
|
191
240
|
if (!pathExists(configPath)) {
|
|
192
241
|
ensureDir(join(configPath, '..'), { recursive: true });
|
|
193
|
-
const config = {
|
|
242
|
+
const config = { [configSection]: { "hanzi-browser": MCP_ENTRY } };
|
|
194
243
|
writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
195
244
|
return { agent: agentName, status: 'configured', detail: `created ${configPath}` };
|
|
196
245
|
}
|
|
@@ -206,20 +255,25 @@ export function mergeJsonConfig(configPath, deps = {}) {
|
|
|
206
255
|
catch {
|
|
207
256
|
const bakPath = configPath + '.bak';
|
|
208
257
|
copyFile(configPath, bakPath);
|
|
209
|
-
config = {
|
|
258
|
+
config = { [configSection]: { "hanzi-browser": MCP_ENTRY } };
|
|
210
259
|
writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
211
260
|
return { agent: agentName, status: 'configured', detail: `backed up malformed config to ${bakPath}` };
|
|
212
261
|
}
|
|
213
262
|
}
|
|
214
|
-
|
|
215
|
-
|
|
263
|
+
const removedLegacyEntry = removeLegacyHanziEntries(config, configSection, legacyConfigSections);
|
|
264
|
+
if (config[configSection]?.["hanzi-browser"]) {
|
|
265
|
+
const existing = config[configSection]["hanzi-browser"];
|
|
216
266
|
if (existing.command === MCP_ENTRY.command && JSON.stringify(existing.args) === JSON.stringify(MCP_ENTRY.args)) {
|
|
267
|
+
if (removedLegacyEntry) {
|
|
268
|
+
writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
269
|
+
return { agent: agentName, status: 'configured', detail: `migrated legacy hanzi-browser entry in ${configPath}` };
|
|
270
|
+
}
|
|
217
271
|
return { agent: agentName, status: 'already-configured', detail: configPath };
|
|
218
272
|
}
|
|
219
273
|
}
|
|
220
|
-
if (!config
|
|
221
|
-
config
|
|
222
|
-
config
|
|
274
|
+
if (!config[configSection])
|
|
275
|
+
config[configSection] = {};
|
|
276
|
+
config[configSection]["hanzi-browser"] = MCP_ENTRY;
|
|
223
277
|
writeTextFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
224
278
|
return { agent: agentName, status: 'configured', detail: `merged into ${configPath}` };
|
|
225
279
|
}
|
|
@@ -563,8 +617,10 @@ async function injectManagedKey(apiKey, agents) {
|
|
|
563
617
|
if (existsSync(configPath)) {
|
|
564
618
|
const raw = readFileSync(configPath, 'utf-8');
|
|
565
619
|
const config = JSON.parse(raw);
|
|
566
|
-
|
|
567
|
-
|
|
620
|
+
const configSection = agent.configSection ?? 'mcpServers';
|
|
621
|
+
removeLegacyHanziEntries(config, configSection, agent.legacyConfigSections ?? []);
|
|
622
|
+
if (config[configSection]?.["hanzi-browser"]) {
|
|
623
|
+
config[configSection]["hanzi-browser"] = managedEntry;
|
|
568
624
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
569
625
|
console.log(` ${c.green('✓')} Updated ${agent.name} with managed API key`);
|
|
570
626
|
}
|
|
@@ -697,7 +753,12 @@ function disconnectRelay() {
|
|
|
697
753
|
setTimeout(() => { console.error = origError; }, 500);
|
|
698
754
|
}
|
|
699
755
|
}
|
|
700
|
-
|
|
756
|
+
const VALID_CATEGORIES = ['core', 'productivity', 'marketing', 'life'];
|
|
757
|
+
const CATEGORY_BUNDLES = [
|
|
758
|
+
{ cat: 'productivity', label: 'Productivity', summary: 'testing, audits, data extraction, SEO' },
|
|
759
|
+
{ cat: 'marketing', label: 'Marketing & growth', summary: 'social posting, prospecting, competitor research' },
|
|
760
|
+
{ cat: 'life', label: 'Personal automation', summary: 'apartments, jobs' },
|
|
761
|
+
];
|
|
701
762
|
function getSkillsSource() {
|
|
702
763
|
// Skills are bundled in the npm package at ../skills/ relative to dist/cli/
|
|
703
764
|
const fromDist = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills');
|
|
@@ -707,36 +768,157 @@ function getSkillsSource() {
|
|
|
707
768
|
const fromSrc = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills');
|
|
708
769
|
return fromSrc;
|
|
709
770
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
771
|
+
function parseSkillFrontmatter(content) {
|
|
772
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
773
|
+
if (!match)
|
|
774
|
+
return null;
|
|
775
|
+
const result = {};
|
|
776
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
777
|
+
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
778
|
+
if (!m)
|
|
779
|
+
continue;
|
|
780
|
+
const key = m[1];
|
|
781
|
+
const value = m[2].trim();
|
|
782
|
+
if (key === 'description')
|
|
783
|
+
result.description = value;
|
|
784
|
+
else if (key === 'category' && VALID_CATEGORIES.includes(value)) {
|
|
785
|
+
result.category = value;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
function discoverSkills(skillsSource) {
|
|
713
791
|
if (!existsSync(skillsSource))
|
|
714
|
-
return;
|
|
792
|
+
return [];
|
|
793
|
+
const skills = [];
|
|
794
|
+
for (const entry of readdirSync(skillsSource, { withFileTypes: true })) {
|
|
795
|
+
if (!entry.isDirectory())
|
|
796
|
+
continue;
|
|
797
|
+
const skillPath = join(skillsSource, entry.name);
|
|
798
|
+
const skillMd = join(skillPath, 'SKILL.md');
|
|
799
|
+
if (!existsSync(skillMd))
|
|
800
|
+
continue;
|
|
801
|
+
const meta = parseSkillFrontmatter(readFileSync(skillMd, 'utf-8'));
|
|
802
|
+
if (!meta)
|
|
803
|
+
continue;
|
|
804
|
+
skills.push({
|
|
805
|
+
name: entry.name,
|
|
806
|
+
description: meta.description || '',
|
|
807
|
+
// Skills without an explicit category default to productivity — a safe
|
|
808
|
+
// "opt-in bundle" rather than forcing everyone to get it by default.
|
|
809
|
+
category: meta.category ?? 'productivity',
|
|
810
|
+
path: skillPath,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return skills;
|
|
814
|
+
}
|
|
815
|
+
async function promptSkillCategories(skills) {
|
|
816
|
+
const selected = new Set();
|
|
817
|
+
const coreSkills = skills.filter(s => s.category === 'core');
|
|
818
|
+
for (const s of coreSkills)
|
|
819
|
+
selected.add(s.name);
|
|
820
|
+
const byCategory = new Map();
|
|
821
|
+
for (const s of skills) {
|
|
822
|
+
if (s.category === 'core')
|
|
823
|
+
continue;
|
|
824
|
+
const cat = s.category;
|
|
825
|
+
if (!byCategory.has(cat))
|
|
826
|
+
byCategory.set(cat, []);
|
|
827
|
+
byCategory.get(cat).push(s);
|
|
828
|
+
}
|
|
829
|
+
const bundles = [];
|
|
830
|
+
for (const b of CATEGORY_BUNDLES) {
|
|
831
|
+
const catSkills = byCategory.get(b.cat);
|
|
832
|
+
if (catSkills && catSkills.length > 0)
|
|
833
|
+
bundles.push({ ...b, skills: catSkills });
|
|
834
|
+
}
|
|
835
|
+
if (bundles.length === 0)
|
|
836
|
+
return selected;
|
|
837
|
+
console.log('');
|
|
838
|
+
console.log(` ${c.dim('step 2b')} ${c.bold('Skills')}`);
|
|
839
|
+
console.log(` ${c.dim(' Skills tell your AI agent when and how to use Hanzi for specific workflows.')}\n`);
|
|
840
|
+
if (coreSkills.length > 0) {
|
|
841
|
+
console.log(` ${c.green('✓')} ${c.bold('Core')} ${c.dim(`(always installed)`)}`);
|
|
842
|
+
for (const s of coreSkills)
|
|
843
|
+
console.log(` ${c.dim(s.name)}`);
|
|
844
|
+
console.log('');
|
|
845
|
+
}
|
|
846
|
+
console.log(` ${c.dim('Optional bundles:')}`);
|
|
847
|
+
bundles.forEach((b, i) => {
|
|
848
|
+
console.log(` ${c.bold(String(i + 1))} ${b.label} ${c.dim(`(${b.skills.length} skills — ${b.summary})`)}`);
|
|
849
|
+
console.log(` ${c.dim(b.skills.map(s => s.name).join(', '))}`);
|
|
850
|
+
});
|
|
851
|
+
console.log('');
|
|
852
|
+
const answer = await ask('Install bundles (e.g. "1 2", "all", or "none"): ');
|
|
853
|
+
const normalized = answer.trim().toLowerCase();
|
|
854
|
+
if (normalized === 'all') {
|
|
855
|
+
for (const b of bundles)
|
|
856
|
+
for (const s of b.skills)
|
|
857
|
+
selected.add(s.name);
|
|
858
|
+
}
|
|
859
|
+
else if (normalized && normalized !== 'none') {
|
|
860
|
+
const picks = new Set(normalized.split(/[\s,]+/).map(n => parseInt(n, 10)).filter(n => !isNaN(n)));
|
|
861
|
+
for (const n of picks) {
|
|
862
|
+
const bundle = bundles[n - 1];
|
|
863
|
+
if (bundle)
|
|
864
|
+
for (const s of bundle.skills)
|
|
865
|
+
selected.add(s.name);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return selected;
|
|
869
|
+
}
|
|
870
|
+
async function installSkills(agents, isInteractive, options = {}) {
|
|
871
|
+
const skillsSource = getSkillsSource();
|
|
872
|
+
const discovered = discoverSkills(skillsSource);
|
|
873
|
+
if (discovered.length === 0)
|
|
874
|
+
return;
|
|
715
875
|
const agentsWithSkills = agents.filter(a => a.skillsDir);
|
|
716
876
|
if (agentsWithSkills.length === 0)
|
|
717
877
|
return;
|
|
718
|
-
|
|
878
|
+
// Decide which skills to install
|
|
879
|
+
let selected;
|
|
880
|
+
if (options.all) {
|
|
881
|
+
selected = new Set(discovered.map(s => s.name));
|
|
882
|
+
}
|
|
883
|
+
else if (options.skills && options.skills.length > 0) {
|
|
884
|
+
selected = new Set(options.skills);
|
|
885
|
+
for (const s of discovered)
|
|
886
|
+
if (s.category === 'core')
|
|
887
|
+
selected.add(s.name);
|
|
888
|
+
}
|
|
889
|
+
else if (isInteractive) {
|
|
890
|
+
selected = await promptSkillCategories(discovered);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
selected = new Set(discovered.filter(s => s.category === 'core').map(s => s.name));
|
|
894
|
+
}
|
|
895
|
+
if (selected.size === 0)
|
|
896
|
+
return;
|
|
897
|
+
const skillsToInstall = discovered.filter(s => selected.has(s.name));
|
|
719
898
|
if (isInteractive) {
|
|
720
899
|
console.log('');
|
|
721
|
-
console.log(` ${c.dim(' Installing
|
|
900
|
+
console.log(` ${c.dim(' Installing ' + skillsToInstall.length + ' skill' + (skillsToInstall.length === 1 ? '' : 's') + '...')}`);
|
|
722
901
|
}
|
|
723
902
|
else {
|
|
724
|
-
log(
|
|
903
|
+
log(`\n Installing ${skillsToInstall.length} skill${skillsToInstall.length === 1 ? '' : 's'}...`);
|
|
725
904
|
}
|
|
726
905
|
let installed = 0;
|
|
727
906
|
for (const agent of agentsWithSkills) {
|
|
728
907
|
const targetDir = agent.skillsDir();
|
|
729
908
|
try {
|
|
730
|
-
for (const
|
|
731
|
-
const
|
|
732
|
-
if (!existsSync(src))
|
|
733
|
-
continue;
|
|
734
|
-
const dest = join(targetDir, skillName);
|
|
909
|
+
for (const skill of skillsToInstall) {
|
|
910
|
+
const dest = join(targetDir, skill.name);
|
|
735
911
|
mkdirSync(dest, { recursive: true });
|
|
736
|
-
// Copy SKILL.md and any supporting files
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
912
|
+
// Copy SKILL.md and any flat supporting files. Subdirectories (e.g. a
|
|
913
|
+
// references/ folder) are skipped — copyFileSync on a dir throws and
|
|
914
|
+
// the skills we ship today don't need nested assets.
|
|
915
|
+
for (const file of readdirSync(skill.path)) {
|
|
916
|
+
try {
|
|
917
|
+
copyFileSync(join(skill.path, file), join(dest, file));
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
// Silently skip non-file entries (directories, symlinks, etc.)
|
|
921
|
+
}
|
|
740
922
|
}
|
|
741
923
|
}
|
|
742
924
|
installed++;
|
|
@@ -757,7 +939,7 @@ async function installSkills(agents, isInteractive) {
|
|
|
757
939
|
}
|
|
758
940
|
}
|
|
759
941
|
if (installed > 0) {
|
|
760
|
-
const msg = `${installed} agent${installed === 1 ? '' : 's'} got ${
|
|
942
|
+
const msg = `${installed} agent${installed === 1 ? '' : 's'} got ${skillsToInstall.length} skill${skillsToInstall.length === 1 ? '' : 's'}`;
|
|
761
943
|
if (isInteractive) {
|
|
762
944
|
console.log(`\n ${c.green('✓')} ${msg}`);
|
|
763
945
|
}
|
|
@@ -872,7 +1054,7 @@ export async function runSetup(options = {}) {
|
|
|
872
1054
|
result = runClaudeCodeSetup();
|
|
873
1055
|
}
|
|
874
1056
|
else {
|
|
875
|
-
result =
|
|
1057
|
+
result = mergeJsonConfigAtKey(agent.configPath(), agent.configSection ?? 'mcpServers', {}, agent.legacyConfigSections ?? []);
|
|
876
1058
|
}
|
|
877
1059
|
results.push({ ...result, agent: agent.name });
|
|
878
1060
|
await sleep(150);
|
|
@@ -905,7 +1087,7 @@ export async function runSetup(options = {}) {
|
|
|
905
1087
|
}
|
|
906
1088
|
}
|
|
907
1089
|
// ── Step 2b: Install skills ──
|
|
908
|
-
await installSkills(detected, interactive);
|
|
1090
|
+
await installSkills(detected, interactive, { all: options.all, skills: options.skills });
|
|
909
1091
|
// ── Step 3: Access mode ──
|
|
910
1092
|
let accessMode = 'byom';
|
|
911
1093
|
if (interactive) {
|
package/dist/cli.js
CHANGED
|
@@ -397,13 +397,24 @@ async function cmdSetup() {
|
|
|
397
397
|
const { runSetup } = await import('./cli/setup.js');
|
|
398
398
|
let only;
|
|
399
399
|
let yes = false;
|
|
400
|
+
let all = false;
|
|
401
|
+
let skills;
|
|
400
402
|
for (let i = 1; i < args.length; i++) {
|
|
401
|
-
|
|
403
|
+
const arg = args[i];
|
|
404
|
+
if (arg === '--only' && args[i + 1])
|
|
402
405
|
only = args[++i];
|
|
403
|
-
if (
|
|
406
|
+
else if (arg === '--yes' || arg === '-y')
|
|
404
407
|
yes = true;
|
|
408
|
+
else if (arg === '--all-skills' || arg === '--all')
|
|
409
|
+
all = true;
|
|
410
|
+
else if (arg === '--skills' && args[i + 1]) {
|
|
411
|
+
skills = args[++i].split(',').map(s => s.trim()).filter(Boolean);
|
|
412
|
+
}
|
|
413
|
+
else if (arg.startsWith('--skills=')) {
|
|
414
|
+
skills = arg.slice('--skills='.length).split(',').map(s => s.trim()).filter(Boolean);
|
|
415
|
+
}
|
|
405
416
|
}
|
|
406
|
-
await runSetup({ only, yes });
|
|
417
|
+
await runSetup({ only, yes, all, skills });
|
|
407
418
|
}
|
|
408
419
|
function cmdHelp() {
|
|
409
420
|
console.log(`
|
|
@@ -443,6 +454,9 @@ Commands:
|
|
|
443
454
|
|
|
444
455
|
setup Auto-detect AI agents and configure MCP
|
|
445
456
|
--only <agent> Only configure one agent (claude-code, cursor, windsurf, claude-desktop)
|
|
457
|
+
--yes, -y Non-interactive mode (installs core skill only)
|
|
458
|
+
--all Install every bundled skill (skip the prompt)
|
|
459
|
+
--skills a,b,c Install just these skills (core always included)
|
|
446
460
|
|
|
447
461
|
skills List available agent skills
|
|
448
462
|
skills install <name> Download a skill into your project
|