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 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/browser-agent
22
+ npm install @hanzi-browse/sdk
23
23
  ```
24
24
 
25
25
  ```typescript
26
- import { HanziClient } from '@hanzi/browser-agent';
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 best practices:\n- Use the search bar at the top for product search\n- Filter results using the left sidebar (price, ratings, Prime, etc.)\n- Click 'Add to Cart' or 'Buy Now' to purchase\n- Product details and reviews are on the product page\n- Check seller information and shipping times before purchasing"
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 web automation assistant with browser tools. Your priority is to complete the user's request efficiently and autonomously.
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
- Browser tasks often require long-running, agentic capabilities. When you encounter a user request that feels time-consuming or extensive in scope, you should be persistent and use all available context needed to accomplish the task. The user expects you to work autonomously until the task is complete. Do not ask for permission - just do it.
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}.
@@ -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 = { mcpServers: { "hanzi-browser": MCP_ENTRY } };
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 = { mcpServers: { "hanzi-browser": MCP_ENTRY } };
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
- if (config.mcpServers?.["hanzi-browser"]) {
215
- const existing = config.mcpServers["hanzi-browser"];
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.mcpServers)
221
- config.mcpServers = {};
222
- config.mcpServers["hanzi-browser"] = MCP_ENTRY;
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
- if (config.mcpServers?.["hanzi-browser"]) {
567
- config.mcpServers["hanzi-browser"] = managedEntry;
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
- // ── Skill installation ──────────────────────────────────────────────────
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
- const SKILL_NAMES = ['hanzi-browse', 'e2e-tester', 'social-poster', 'linkedin-prospector', 'a11y-auditor', 'x-marketer'];
711
- async function installSkills(agents, isInteractive) {
712
- const skillsSource = getSkillsSource();
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; // No skills bundled
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
- const out = isInteractive ? console.log : log;
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 browser automation skills...')}`);
900
+ console.log(` ${c.dim(' Installing ' + skillsToInstall.length + ' skill' + (skillsToInstall.length === 1 ? '' : 's') + '...')}`);
722
901
  }
723
902
  else {
724
- log('\n Installing skills...');
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 skillName of SKILL_NAMES) {
731
- const src = join(skillsSource, skillName);
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
- const files = readdirSync(src);
738
- for (const file of files) {
739
- copyFileSync(join(src, file), join(dest, file));
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 ${SKILL_NAMES.length} browser skills`;
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 = mergeJsonConfig(agent.configPath());
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
- if (args[i] === '--only' && args[i + 1])
403
+ const arg = args[i];
404
+ if (arg === '--only' && args[i + 1])
402
405
  only = args[++i];
403
- if (args[i] === '--yes' || args[i] === '-y')
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