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.
- package/CHANGELOG.md +23 -0
- package/bin/cli.js +26 -21
- package/bin/preview.mjs +7 -5
- package/lib/bundles.js +4 -3
- package/lib/targets.js +1 -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/{web-fetch → data-extraction}/url-extract/platforms/SKILL.claude.md +1 -2
- package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.codex.md +3 -1
- package/skills/{web-fetch → data-extraction}/url-extract/platforms/SKILL.hermes.md +3 -1
- package/skills/data-extraction/url-extract/scripts/detect_chrome_profile.py +105 -0
- package/skills/{web-fetch → data-extraction}/url-extract/vars.json +3 -7
- package/skills/meta/npm-release/SKILL.md +37 -13
- package/skills-index.json +2 -2
- package/tools/p-launch/__pycache__/p_launch.cpython-314.pyc +0 -0
- package/tools/p-launch/tests/__pycache__/test_p_launch.cpython-314-pytest-9.0.2.pyc +0 -0
- /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/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,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 {
|
|
162
|
-
const
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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('
|
|
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
|
|
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 +
|
|
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
|
|
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
|
-
?
|
|
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 =
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
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`、`
|
|
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`、`
|
|
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
|
|
15
|
-
"
|
|
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.
|
|
5
|
+
version: "1.1.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# npm-release
|
|
9
9
|
|
|
10
|
-
harveyz-skill 发布到 npm 的完整流程:版本号升级 → CHANGELOG 更新 → 分支提交 →
|
|
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 —
|
|
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 —
|
|
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 —
|
|
142
|
+
## Step 7 — 给出最终执行清单
|
|
140
143
|
|
|
141
|
-
|
|
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
|
-
|
|
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": "
|
|
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" },
|
|
Binary file
|
|
Binary file
|
|
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
|