harveyz-skill 0.11.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.
- package/CHANGELOG.md +10 -0
- package/bin/cli.js +3 -1
- package/lib/vars.js +40 -1
- package/package.json +3 -3
- package/skills/{web-fetch → data-extraction}/article-fetcher/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- package/skills/data-extraction/url-extract/scripts/detect_chrome_profile.py +105 -0
- package/skills/{web-fetch → data-extraction}/url-extract/vars.json +3 -2
- package/skills-index.json +2 -2
- /package/skills/{web-fetch → data-extraction}/article-fetcher/SKILL.md +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/references/article_utils.py +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/references/file-format.md +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/import_reading.py +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/init_db.py +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/playwright_web.py +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/playwright_xcom.py +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/url-index.db +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/url-index.db.bak +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/scripts/validate_article.py +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/tests/test_security.py +0 -0
- /package/skills/{web-fetch → data-extraction}/article-fetcher/vars.json +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/SKILL.md +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.claude.md +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.codex.md +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.hermes.md +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/references/article_utils.py +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/references/core-flow.md +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/references/file-format.md +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/scripts/dedup_check.py +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/scripts/playwright_web.py +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/scripts/playwright_xcom.py +0 -0
- /package/skills/{web-fetch → data-extraction}/url-extract/scripts/validate_article.py +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ 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
|
+
|
|
10
20
|
## [0.11.0] - 2026-06-09
|
|
11
21
|
|
|
12
22
|
### 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'
|
|
@@ -1069,6 +1069,8 @@ try {
|
|
|
1069
1069
|
// In test mode with HSKILL_TEST_ACTION, run once and exit
|
|
1070
1070
|
if (process.env.HSKILL_TEST_ACTION) break
|
|
1071
1071
|
|
|
1072
|
+
if (process.stdout.isTTY) await input({ message: chalk.dim('按 Enter 返回列表…'), default: '' })
|
|
1073
|
+
|
|
1072
1074
|
// Loop back to skill selector automatically
|
|
1073
1075
|
}
|
|
1074
1076
|
|
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.
|
|
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/
|
|
31
|
-
"skills/
|
|
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/",
|
|
Binary file
|
|
@@ -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()
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
8
|
"name": "CHROME_PROFILE",
|
|
9
|
-
"description": "Chrome 用户配置目录(X.com
|
|
10
|
-
"
|
|
9
|
+
"description": "Chrome 用户配置目录(X.com 登录态所需)",
|
|
10
|
+
"type": "chrome_profile_select",
|
|
11
|
+
"default": "{{HOME}}/Library/Application Support/Google/Chrome/Default"
|
|
11
12
|
}
|
|
12
13
|
]
|
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": "
|
|
37
|
-
{ "path": "
|
|
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" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|