harveyz-skill 0.10.0 → 0.12.0

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/CHANGELOG.md +23 -0
  2. package/bin/cli.js +26 -21
  3. package/bin/preview.mjs +7 -5
  4. package/lib/bundles.js +4 -3
  5. package/lib/targets.js +1 -1
  6. package/lib/vars.js +40 -1
  7. package/package.json +3 -3
  8. package/skills/{web-fetch → data-extraction}/article-fetcher/references/__pycache__/article_utils.cpython-314.pyc +0 -0
  9. package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.claude.md +1 -2
  10. package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.codex.md +3 -1
  11. package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.hermes.md +3 -1
  12. package/skills/data-extraction/url-extract/scripts/detect_chrome_profile.py +105 -0
  13. package/skills/{web-fetch → data-extraction}/url-extract/vars.json +3 -7
  14. package/skills/meta/npm-release/SKILL.md +37 -13
  15. package/skills-index.json +2 -2
  16. package/tools/p-launch/__pycache__/p_launch.cpython-314.pyc +0 -0
  17. package/tools/p-launch/tests/__pycache__/test_p_launch.cpython-314-pytest-9.0.2.pyc +0 -0
  18. /package/skills/{web-fetch → data-extraction}/article-fetcher/SKILL.md +0 -0
  19. /package/skills/{web-fetch → data-extraction}/article-fetcher/references/article_utils.py +0 -0
  20. /package/skills/{web-fetch → data-extraction}/article-fetcher/references/file-format.md +0 -0
  21. /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/import_reading.py +0 -0
  22. /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/init_db.py +0 -0
  23. /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/playwright_web.py +0 -0
  24. /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/playwright_xcom.py +0 -0
  25. /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/url-index.db +0 -0
  26. /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/url-index.db.bak +0 -0
  27. /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/validate_article.py +0 -0
  28. /package/skills/{web-fetch → data-extraction}/article-fetcher/tests/test_security.py +0 -0
  29. /package/skills/{web-fetch → data-extraction}/article-fetcher/vars.json +0 -0
  30. /package/skills/{web-fetch → data-extraction}/url-extract/SKILL.md +0 -0
  31. /package/skills/{web-fetch → data-extraction}/url-extract/references/__pycache__/article_utils.cpython-314.pyc +0 -0
  32. /package/skills/{web-fetch → data-extraction}/url-extract/references/article_utils.py +0 -0
  33. /package/skills/{web-fetch → data-extraction}/url-extract/references/core-flow.md +0 -0
  34. /package/skills/{web-fetch → data-extraction}/url-extract/references/file-format.md +0 -0
  35. /package/skills/{web-fetch → data-extraction}/url-extract/scripts/dedup_check.py +0 -0
  36. /package/skills/{web-fetch → data-extraction}/url-extract/scripts/playwright_web.py +0 -0
  37. /package/skills/{web-fetch → data-extraction}/url-extract/scripts/playwright_xcom.py +0 -0
  38. /package/skills/{web-fetch → data-extraction}/url-extract/scripts/validate_article.py +0 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-06-09
11
+
12
+ ### Added
13
+ - `url-extract`:新增 `detect_chrome_profile.py` 脚本,扫描本机 Chrome profile 并检测 X.com 登录态
14
+ - `url-extract`:安装时 `CHROME_PROFILE` 变量改为 select 选择列表(自动列出所有 profile + 对应 Google 账号),保留手动输入兜底
15
+ - `hskill`:安装/卸载完成后新增 "按 Enter 返回列表" 提示,避免 summary 被 fzf 立即覆盖
16
+
17
+ ### Changed
18
+ - `url-extract` `vars.json`:`CHROME_PROFILE` 默认值从 `Profile 2` 更新为 `Default`
19
+
20
+ ## [0.11.0] - 2026-06-09
21
+
22
+ ### Added
23
+ - `hskill list`:新增 Bundle 列并按 bundle 名称排序(替换旧的分组标题格式);`hskill status` skills 列表同步增加 Bundle 列
24
+
25
+ ### Changed
26
+ - `hskill` 内部重构:平台列表从 `SKILL_TARGETS` 单一来源派生,消除 `bundles.js` / `preview.mjs` 中的硬编码数组
27
+
28
+ ### Fixed
29
+ - `url-extract`:移除 `SKILL_DIR` 用户配置变量
30
+ - `preview`:安装状态侧边栏补全 openclaw、hermes 平台(USER LEVEL)
31
+ - `npm-release`:中间步骤跳过 push 操作,最终统一给出推送 + 发布指令清单
32
+
10
33
  ## [0.10.0] - 2026-06-08
11
34
 
12
35
  ### Added
package/bin/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { select } from '@inquirer/prompts'
2
+ import { select, input } from '@inquirer/prompts'
3
3
  import chalk from 'chalk'
4
4
  import { execSync, spawnSync } from 'child_process'
5
5
  import { createRequire } from 'module'
@@ -158,29 +158,31 @@ if (subcommand === 'update') {
158
158
 
159
159
  // ── List ─────────────────────────────────────────────────────────────────────
160
160
  if (subcommand === 'list') {
161
- const { bundleMeta, skills, tools = [] } = require('../skills-index.json')
162
- const byBundle = {}
163
- for (const s of skills) {
164
- if (!byBundle[s.bundle]) byBundle[s.bundle] = []
165
- byBundle[s.bundle].push(s.path)
166
- }
161
+ const { skills, tools = [] } = require('../skills-index.json')
162
+ const sorted = [...skills].sort((a, b) => a.bundle.localeCompare(b.bundle) || a.path.split('/').pop().localeCompare(b.path.split('/').pop()))
167
163
  if (jsonFlag) {
168
- const bundles = {}
169
- for (const [name, paths] of Object.entries(byBundle)) {
170
- bundles[name] = { description: bundleMeta[name] ?? name, skills: paths }
171
- }
172
- console.log(JSON.stringify({ bundles, tools: tools.map(t => t.name) }, null, 2))
164
+ console.log(JSON.stringify({
165
+ skills: sorted.map(s => ({ name: s.path.split('/').pop(), path: s.path, bundle: s.bundle })),
166
+ tools: tools.map(t => t.name),
167
+ }, null, 2))
173
168
  process.exit(0)
174
169
  }
175
- for (const [name, paths] of Object.entries(byBundle)) {
176
- console.log(chalk.bold(name) + ' — ' + (bundleMeta[name] ?? name))
177
- for (const p of paths) console.log(' ' + p)
170
+ const nw = Math.max(...sorted.map(s => s.path.split('/').pop().length), 4)
171
+ const bw = Math.max(...sorted.map(s => s.bundle.length), 6)
172
+ const sep = chalk.dim(' ' + '─'.repeat(nw + bw + 4))
173
+ console.log('')
174
+ console.log(' ' + chalk.bold('NAME'.padEnd(nw)) + ' ' + chalk.bold('BUNDLE'))
175
+ console.log(sep)
176
+ for (const s of sorted) {
177
+ console.log(' ' + s.path.split('/').pop().padEnd(nw) + ' ' + chalk.dim(s.bundle))
178
178
  }
179
179
  if (tools.length > 0) {
180
180
  console.log('')
181
- console.log(chalk.bold('shell tools:'))
181
+ console.log(' ' + chalk.bold('SHELL TOOLS'))
182
+ console.log(sep)
182
183
  for (const t of tools) console.log(' ' + t.name)
183
184
  }
185
+ console.log('')
184
186
  process.exit(0)
185
187
  }
186
188
 
@@ -208,11 +210,11 @@ if (subcommand === 'status' || subcommand === 'outdated') {
208
210
  const skillRows = skillItems.map(s => {
209
211
  const inst = checkInstalled(s.skillName, s.version ?? '—')
210
212
  return {
211
- name: s.skillName, version: s.version ?? '—',
213
+ name: s.skillName, bundle: s.bundle ?? '—', version: s.version ?? '—',
212
214
  userStatus: scopeSummary(inst.user), projectStatus: scopeSummary(inst.project),
213
215
  userDetail: inst.user, projectDetail: inst.project,
214
216
  }
215
- })
217
+ }).sort((a, b) => a.bundle.localeCompare(b.bundle) || a.name.localeCompare(b.name))
216
218
  const toolRows = toolItems.map(t => {
217
219
  const inst = checkToolInstalled(t.toolName, t.srcPath)
218
220
  return { name: t.toolName, version: t.version ?? '—', ...inst }
@@ -285,15 +287,16 @@ if (subcommand === 'status' || subcommand === 'outdated') {
285
287
  const allVers = [...skillRows, ...toolRows].map(r => r.version)
286
288
  const nw = Math.max(...allNames.map(n => n.length), 4)
287
289
  const vw = Math.max(...allVers.map(v => v.length), 7)
288
- const sep = chalk.dim(' ' + '─'.repeat(nw + vw + 20))
290
+ const bw = Math.max(...skillRows.map(r => r.bundle.length), 6)
291
+ const sep = chalk.dim(' ' + '─'.repeat(nw + vw + bw + 22))
289
292
 
290
293
  console.log('')
291
294
  console.log(' ' + chalk.bold('SKILLS') + chalk.dim(` — ${skillRows.length} available`))
292
295
  console.log(sep)
293
- console.log(' ' + ''.padEnd(nw + vw + 3) + chalk.dim('user project'))
296
+ console.log(' ' + ''.padEnd(nw + vw + bw + 5) + chalk.dim('user project'))
294
297
  for (const r of skillRows) {
295
298
  const u = icon(r.userStatus), p = icon(r.projectStatus)
296
- console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version.padEnd(vw)) + ' ' + u + ' ' + p)
299
+ console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version.padEnd(vw)) + ' ' + chalk.dim(r.bundle.padEnd(bw)) + ' ' + u + ' ' + p)
297
300
  }
298
301
 
299
302
  console.log('')
@@ -1066,6 +1069,8 @@ try {
1066
1069
  // In test mode with HSKILL_TEST_ACTION, run once and exit
1067
1070
  if (process.env.HSKILL_TEST_ACTION) break
1068
1071
 
1072
+ if (process.stdout.isTTY) await input({ message: chalk.dim('按 Enter 返回列表…'), default: '' })
1073
+
1069
1074
  // Loop back to skill selector automatically
1070
1075
  }
1071
1076
 
package/bin/preview.mjs CHANGED
@@ -3,6 +3,7 @@ import fs from 'fs'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
5
  import { fileURLToPath } from 'url'
6
+ import { SKILL_TARGETS, USER_ONLY_TARGETS } from '../lib/targets.js'
6
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
9
 
@@ -72,10 +73,11 @@ function statusLine(version, status) {
72
73
  return ver + ' ' + D + '— not installed' + R
73
74
  }
74
75
 
75
- const targets = ['claude', 'cursor', 'codex']
76
+ const userTargets = SKILL_TARGETS
77
+ const projectTargets = SKILL_TARGETS.filter(t => !USER_ONLY_TARGETS.has(t))
76
78
  const home = os.homedir()
77
79
 
78
- function checkScope(dirFn) {
80
+ function checkScope(targets, dirFn) {
79
81
  return targets.map(t => {
80
82
  const ver = readVersion(path.join(dirFn(t), skillName))
81
83
  const status = ver === '—' ? 'none'
@@ -86,10 +88,10 @@ function checkScope(dirFn) {
86
88
  }
87
89
 
88
90
  const cwd = process.cwd()
89
- const userDetails = checkScope(t => path.join(home, `.${t}`, 'skills'))
91
+ const userDetails = checkScope(userTargets, t => path.join(home, `.${t}`, 'skills'))
90
92
  const projectDetails = cwd === home
91
- ? targets.map(t => ({ tool: t, version: '—', status: 'none' }))
92
- : checkScope(t => path.join(cwd, `.${t}`, 'skills'))
93
+ ? projectTargets.map(t => ({ tool: t, version: '—', status: 'none' }))
94
+ : checkScope(projectTargets, t => path.join(cwd, `.${t}`, 'skills'))
93
95
 
94
96
  console.log(B + skillName + R)
95
97
  console.log(D + 'available: ' + R + availableVersion)
package/lib/bundles.js CHANGED
@@ -3,7 +3,7 @@ import os from 'os'
3
3
  import { createRequire } from 'module'
4
4
  import path from 'path'
5
5
  import { fileURLToPath } from 'url'
6
- import { USER_ONLY_TARGETS } from './targets.js'
6
+ import { SKILL_TARGETS, USER_ONLY_TARGETS } from './targets.js'
7
7
 
8
8
  const require = createRequire(import.meta.url)
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -101,13 +101,14 @@ export function buildAllChoices() {
101
101
  return choices
102
102
  }
103
103
 
104
- // 所有 skill 展开为 { kind:'skill', skillName, srcPath } 列表(对应 all 选项)
104
+ // 所有 skill 展开为 { kind:'skill', skillName, bundle, srcPath } 列表(对应 all 选项)
105
105
  export function getAllSkillItems() {
106
106
  return skillDefs.map(skill => {
107
107
  const srcPath = path.join(skillsRoot, skill.path)
108
108
  return {
109
109
  kind: 'skill',
110
110
  skillName: skill.path.split('/').pop(),
111
+ bundle: skill.bundle,
111
112
  srcPath,
112
113
  version: readVersion(srcPath),
113
114
  }
@@ -132,7 +133,7 @@ export function getAllToolItems() {
132
133
  // 每个工具: { version: string, status: 'up-to-date'|'update'|'none' }
133
134
  export function checkInstalled(skillName, availableVersion) {
134
135
  const home = os.homedir()
135
- const targets = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
136
+ const targets = SKILL_TARGETS
136
137
 
137
138
  function checkScope(dirFn) {
138
139
  const result = {}
package/lib/targets.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import os from 'os'
2
2
  import path from 'path'
3
3
 
4
- const SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
4
+ export const SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
5
5
 
6
6
  export const USER_ONLY_TARGETS = new Set(['openclaw', 'hermes'])
7
7
 
package/lib/vars.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from 'fs-extra'
3
3
  import path from 'path'
4
4
  import os from 'os'
5
- import { input } from '@inquirer/prompts'
5
+ import { input, select } from '@inquirer/prompts'
6
6
 
7
7
  export function buildAutoVars() {
8
8
  return { HOME: os.homedir() }
@@ -20,10 +20,49 @@ export function substituteVars(text, varsMap) {
20
20
  return text.replace(/\{\{(\w+)\}\}/g, (_, name) => varsMap[name] ?? `{{${name}}}`)
21
21
  }
22
22
 
23
+ async function detectChromeProfiles() {
24
+ const chromeBase = path.join(os.homedir(), 'Library/Application Support/Google/Chrome')
25
+ if (!await fs.pathExists(chromeBase)) return []
26
+
27
+ const entries = await fs.readdir(chromeBase)
28
+ const profileDirs = entries.filter(e => e === 'Default' || e.startsWith('Profile '))
29
+ profileDirs.sort((a, b) => (a === 'Default' ? -1 : b === 'Default' ? 1 : a.localeCompare(b)))
30
+
31
+ const profiles = []
32
+ for (const name of profileDirs) {
33
+ const dir = path.join(chromeBase, name)
34
+ let email = ''
35
+ try {
36
+ const prefs = await fs.readJson(path.join(dir, 'Preferences'))
37
+ const accounts = prefs.account_info ?? []
38
+ email = accounts[0]?.email ?? prefs.user_name ?? ''
39
+ } catch { /* unreadable */ }
40
+ profiles.push({ name, dir, email })
41
+ }
42
+ return profiles
43
+ }
44
+
23
45
  export async function resolveVars(varDefs = [], autoVars) {
24
46
  const result = { ...autoVars }
25
47
  for (const def of varDefs) {
26
48
  const defaultVal = substituteVars(def.default ?? '', autoVars)
49
+
50
+ if (def.type === 'chrome_profile_select') {
51
+ const profiles = await detectChromeProfiles()
52
+ if (profiles.length > 0) {
53
+ const choices = profiles.map(p => ({
54
+ name: p.email ? `${p.name} (${p.email})` : p.name,
55
+ value: p.dir,
56
+ }))
57
+ choices.push({ name: '手动输入路径…', value: '__manual__' })
58
+ const selected = await select({ message: `${def.description}:`, choices })
59
+ if (selected !== '__manual__') {
60
+ result[def.name] = selected
61
+ continue
62
+ }
63
+ }
64
+ }
65
+
27
66
  const value = await input({
28
67
  message: `${def.description}:`,
29
68
  default: defaultVal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harveyz-skill",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Skill manager for Claude Code, Cursor, and Codex",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,8 @@
27
27
  "skills/superpowers-fork/writing-plans/",
28
28
  "skills/task/pm-task-dispatch/",
29
29
  "skills/task/task-close/",
30
- "skills/web-fetch/article-fetcher/",
31
- "skills/web-fetch/url-extract/",
30
+ "skills/data-extraction/article-fetcher/",
31
+ "skills/data-extraction/url-extract/",
32
32
  "skills/data-extraction/vision-extract/",
33
33
  "skills/writing/mermaid-diagram/",
34
34
  "skills/design/sync-design-html/",
@@ -25,7 +25,6 @@ sessions_spawn \
25
25
  | 抽象变量 | 实际语法 |
26
26
  |----------|----------|
27
27
  | `VAULT_PATH` | `{{VAULT_PATH}}` |
28
- | `SKILL_DIR` | `{{SKILL_DIR}}` |
29
28
  | `CHROME_PROFILE` | `{{CHROME_PROFILE}}` |
30
29
 
31
- 执行 SKILL.md 中的代码时,将所有 `VAULT_PATH`、`SKILL_DIR`、`CHROME_PROFILE` 占位替换为对应的 `{{变量名}}`,由 vars.json 注入实际值。
30
+ `SKILL_DIR` 由平台自动提供,无需用户配置:值固定为 `$HOME/.claude/skills/url-extract`(Claude Code skill 的标准安装路径)。执行代码时直接将 `SKILL_DIR` 替换为该路径。
@@ -14,4 +14,6 @@
14
14
 
15
15
  ## ③ 变量注入
16
16
 
17
- 通过环境变量或 Codex 平台的变量系统注入,执行 SKILL.md 中的代码时直接使用实际值替换 `VAULT_PATH`、`SKILL_DIR`、`CHROME_PROFILE` 占位。
17
+ 通过环境变量或 Codex 平台的变量系统注入,执行 SKILL.md 中的代码时直接使用实际值替换 `VAULT_PATH`、`CHROME_PROFILE` 占位。
18
+
19
+ `SKILL_DIR` 由平台自动提供,无需用户配置:值为 Codex 安装本 skill 的目录(即包含 `scripts/` 的那一级目录)。
@@ -14,4 +14,6 @@
14
14
 
15
15
  ## ③ 变量注入
16
16
 
17
- 通过 Hermes vars.json 或环境变量注入,执行 SKILL.md 中的代码时直接使用实际值替换 `VAULT_PATH`、`SKILL_DIR`、`CHROME_PROFILE` 占位。
17
+ 通过 Hermes vars.json 或环境变量注入,执行 SKILL.md 中的代码时直接使用实际值替换 `VAULT_PATH`、`CHROME_PROFILE` 占位。
18
+
19
+ `SKILL_DIR` 由平台自动提供,无需用户配置:值为 Hermes 安装本 skill 的目录(即包含 `scripts/` 的那一级目录)。
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 检测哪个 Chrome profile 登录了 X.com (Twitter)。
4
+ 用法: python3 detect_chrome_profile.py
5
+ """
6
+ import os
7
+ import shutil
8
+ import sqlite3
9
+ import tempfile
10
+ import json
11
+ from pathlib import Path
12
+
13
+ CHROME_BASE = Path.home() / "Library/Application Support/Google/Chrome"
14
+ XCOM_HOSTS = [".twitter.com", ".x.com"]
15
+ AUTH_COOKIES = {"auth_token", "ct0", "twid"}
16
+
17
+
18
+ def get_profile_email(profile_dir: Path) -> str:
19
+ prefs = profile_dir / "Preferences"
20
+ try:
21
+ data = json.loads(prefs.read_text(errors="ignore"))
22
+ accounts = data.get("account_info", [])
23
+ if accounts:
24
+ return accounts[0].get("email", "")
25
+ return data.get("user_name", "")
26
+ except Exception:
27
+ return ""
28
+
29
+
30
+ def check_xcom_cookies(profile_dir: Path) -> dict:
31
+ """返回在 X.com 找到的 auth cookie 名称集合(不解密,只检查存在性)"""
32
+ cookies_db = profile_dir / "Cookies"
33
+ if not cookies_db.exists():
34
+ return {}
35
+
36
+ # 复制到 /tmp 避免 Chrome 锁文件冲突
37
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
38
+ tmp_path = f.name
39
+ try:
40
+ shutil.copy2(cookies_db, tmp_path)
41
+ conn = sqlite3.connect(tmp_path)
42
+ conn.row_factory = sqlite3.Row
43
+ cur = conn.cursor()
44
+ cur.execute(
45
+ "SELECT name, host_key FROM cookies WHERE host_key IN ({})".format(
46
+ ",".join("?" * len(XCOM_HOSTS))
47
+ ),
48
+ XCOM_HOSTS,
49
+ )
50
+ rows = cur.fetchall()
51
+ conn.close()
52
+ found = {row["name"] for row in rows}
53
+ return found
54
+ except Exception as e:
55
+ return {"_error": str(e)}
56
+ finally:
57
+ os.unlink(tmp_path)
58
+
59
+
60
+ def main():
61
+ if not CHROME_BASE.exists():
62
+ print("未找到 Chrome 目录:", CHROME_BASE)
63
+ return
64
+
65
+ profiles = sorted(
66
+ [d for d in CHROME_BASE.iterdir() if d.is_dir() and (d.name == "Default" or d.name.startswith("Profile"))],
67
+ key=lambda d: (d.name != "Default", d.name),
68
+ )
69
+
70
+ print(f"{'Profile':<12} {'账号':<38} {'X.com cookies':<30} {'推荐'}")
71
+ print("-" * 95)
72
+
73
+ best = None
74
+ for p in profiles:
75
+ email = get_profile_email(p) or "(未登录 Google)"
76
+ cookies = check_xcom_cookies(p)
77
+
78
+ if "_error" in cookies:
79
+ status = f"[错误: {cookies['_error'][:40]}]"
80
+ has_auth = False
81
+ else:
82
+ auth_found = AUTH_COOKIES & cookies
83
+ has_auth = bool(auth_found)
84
+ all_found = cookies
85
+ if all_found:
86
+ status = ", ".join(sorted(all_found))
87
+ else:
88
+ status = "(无 X.com cookie)"
89
+
90
+ recommend = "<-- 推荐" if has_auth and best is None else ""
91
+ if has_auth and best is None:
92
+ best = p
93
+
94
+ print(f"{p.name:<12} {email:<38} {status:<30} {recommend}")
95
+
96
+ print()
97
+ if best:
98
+ print(f"建议 CHROME_PROFILE 设置为:")
99
+ print(f" {best}")
100
+ else:
101
+ print("未在任何 profile 中找到 X.com auth_token,请先在 Chrome 中登录 X.com。")
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -4,14 +4,10 @@
4
4
  "description": "Obsidian Reading 目录完整路径(例如 /Users/you/Vault/Product/Reading)",
5
5
  "default": "{{HOME}}/Vault/Product/Reading"
6
6
  },
7
- {
8
- "name": "SKILL_DIR",
9
- "description": "url-extract skill 安装后的完整路径(用于定位辅助脚本和数据库文件)",
10
- "default": "{{HOME}}/.claude/skills/url-extract"
11
- },
12
7
  {
13
8
  "name": "CHROME_PROFILE",
14
- "description": "Chrome 用户配置目录(X.com 登录态所需)。macOS 默认:~/Library/Application Support/Google/Chrome/Profile 2",
15
- "default": "{{HOME}}/Library/Application Support/Google/Chrome/Profile 2"
9
+ "description": "Chrome 用户配置目录(X.com 登录态所需)",
10
+ "type": "chrome_profile_select",
11
+ "default": "{{HOME}}/Library/Application Support/Google/Chrome/Default"
16
12
  }
17
13
  ]
@@ -2,12 +2,14 @@
2
2
  name: npm-release
3
3
  description: "Complete npm publish workflow for harveyz-skill: bump version, update CHANGELOG, create release branch, merge to staging then main, tag, and publish to npm. Use this skill whenever the user wants to release, publish, cut a version, bump version, ship to npm, or deploy a new package version."
4
4
  user_invocable: true
5
- version: "1.0.0"
5
+ version: "1.1.0"
6
6
  ---
7
7
 
8
8
  # npm-release
9
9
 
10
- harveyz-skill 发布到 npm 的完整流程:版本号升级 → CHANGELOG 更新 → 分支提交 → 合并到 staging 合并到 main → 打 tag → npm publish。
10
+ harveyz-skill 发布到 npm 的完整流程:版本号升级 → CHANGELOG 更新 → 分支提交 → 本地合并到 staging main → 打 tag → 给出推送 + 发布指令供用户执行。
11
+
12
+ **说明**:所有 `git push` 和 `npm publish` 操作由用户自行执行,Claude 只做本地操作,最后统一给出指令清单。
11
13
 
12
14
  ---
13
15
 
@@ -111,44 +113,66 @@ git commit -m "chore(release): bump version to <new-version>"
111
113
 
112
114
  ---
113
115
 
114
- ## Step 5 — 合并到 staging
116
+ ## Step 5 — 本地合并到 staging(不推送)
115
117
 
116
118
  ```bash
117
119
  git checkout staging
118
120
  git merge release/<new-version>
119
- git push origin staging
120
121
  ```
121
122
 
123
+ 只做本地合并,不执行 push。
124
+
122
125
  ---
123
126
 
124
- ## Step 6 — 合并到 main 并打 tag
127
+ ## Step 6 — 本地合并到 main 并打 tag(不推送)
125
128
 
126
129
  ```bash
127
130
  git checkout main
128
- git pull origin main
131
+ git pull origin main # 只拉取,确保本地 main 是最新的
129
132
  git merge staging
130
133
  git tag -a v<new-version> -m "v<new-version>"
131
- git push origin main
132
- git push origin v<new-version>
133
134
  ```
134
135
 
135
136
  tag 使用 annotated tag(`-a`),符合项目 workflow-config.yml 的 `require_annotated: true` 规则。
136
137
 
138
+ **不执行任何 push**,推送操作统一在最后由用户执行。
139
+
137
140
  ---
138
141
 
139
- ## Step 7 — npm publish
142
+ ## Step 7 — 给出最终执行清单
140
143
 
141
- ```bash
144
+ 本地准备工作已完成。向用户展示以下指令,请用户依次确认并手动执行:
145
+
146
+ ---
147
+
148
+ ```
149
+ == 待执行:推送 + 发布 ==
150
+
151
+ # 1. 推送 staging
152
+ git push origin staging
153
+
154
+ # 2. 推送 main 和 tag
155
+ git push origin main
156
+ git push origin v<new-version>
157
+
158
+ # 3. 发布到 npm(需要已登录:npm login)
142
159
  npm publish
143
160
  ```
144
161
 
145
- 如果报错(比如登录态过期),提示用户先运行 `npm login`,然后重试。
162
+ ---
163
+
164
+ 说明:
165
+ - 如果 `npm login` 状态已过期,先运行 `npm login` 再执行 `npm publish`
166
+ - 如果 `git push origin main` 被 pre-push hook 拒绝,检查 tag 格式是否为 `v<X>.<Y>.<Z>`,以及是否是 annotated tag
167
+ - 以上命令需在项目根目录执行
168
+
169
+ 用户执行完毕后,告知 Claude,Claude 会输出最终摘要。
146
170
 
147
171
  ---
148
172
 
149
- ## Step 8 — 收尾
173
+ ## Step 8 — 收尾(用户执行完毕后)
150
174
 
151
- 发布成功后输出摘要:
175
+ 用户确认推送和发布成功后输出摘要:
152
176
 
153
177
  ```
154
178
  ✓ 版本 v<new-version> 已发布
package/skills-index.json CHANGED
@@ -33,8 +33,8 @@
33
33
  { "path": "superpowers-fork/writing-plans", "bundle": "brainstorming" },
34
34
  { "path": "task/pm-task-dispatch", "bundle": "task" },
35
35
  { "path": "task/task-close", "bundle": "task" },
36
- { "path": "web-fetch/article-fetcher", "bundle": "data-extraction" },
37
- { "path": "web-fetch/url-extract", "bundle": "data-extraction" },
36
+ { "path": "data-extraction/article-fetcher", "bundle": "data-extraction" },
37
+ { "path": "data-extraction/url-extract", "bundle": "data-extraction" },
38
38
  { "path": "data-extraction/vision-extract", "bundle": "data-extraction" },
39
39
  { "path": "writing/mermaid-diagram", "bundle": "writing" },
40
40
  { "path": "design/sync-design-html", "bundle": "design" },