harveyz-skill 0.13.0 → 0.14.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 +9 -0
- package/lib/installer.js +15 -0
- package/package.json +3 -1
- package/skills/creative/add-todo/SKILL.md +52 -3
- package/skills/meta/npm-release/SKILL.md +15 -3
- package/skills/research/article-fetcher/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- package/skills/research/youtube-learner/SKILL.md +12 -3
- package/skills/writing/doc-forge/scripts/__pycache__/md_to_pdf.cpython-314.pyc +0 -0
- package/skills-index.json +11 -1
- package/tools/hub/hub/__init__.py +0 -0
- package/tools/hub/hub/__main__.py +24 -0
- package/tools/hub/hub/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/hub/hub/__pycache__/__main__.cpython-314.pyc +0 -0
- package/tools/hub/hub/cli/__init__.py +14 -0
- package/tools/hub/hub/cli/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/hub/hub/cli/__pycache__/projects.cpython-314.pyc +0 -0
- package/tools/hub/hub/cli/__pycache__/tasks.cpython-314.pyc +0 -0
- package/tools/hub/hub/cli/projects.py +100 -0
- package/tools/hub/hub/cli/tasks.py +114 -0
- package/tools/hub/hub/core/__init__.py +0 -0
- package/tools/hub/hub/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/hub/hub/core/__pycache__/db.cpython-314.pyc +0 -0
- package/tools/hub/hub/core/__pycache__/migrate.cpython-314.pyc +0 -0
- package/tools/hub/hub/core/__pycache__/projects.cpython-314.pyc +0 -0
- package/tools/hub/hub/core/__pycache__/tasks.cpython-314.pyc +0 -0
- package/tools/hub/hub/core/db.py +46 -0
- package/tools/hub/hub/core/migrate.py +49 -0
- package/tools/hub/hub/core/projects.py +65 -0
- package/tools/hub/hub/core/tasks.py +86 -0
- package/tools/hub/hub/tui/__init__.py +0 -0
- package/tools/hub/hub/tui/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/hub/hub/tui/__pycache__/app.cpython-314.pyc +0 -0
- package/tools/hub/hub/tui/__pycache__/git.cpython-314.pyc +0 -0
- package/tools/hub/hub/tui/app.py +78 -0
- package/tools/hub/hub/tui/git.py +300 -0
- package/tools/hub/hub/tui/panels/__init__.py +0 -0
- package/tools/hub/hub/tui/panels/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/hub/hub/tui/panels/__pycache__/git.cpython-314.pyc +0 -0
- package/tools/hub/hub/tui/panels/__pycache__/projects.cpython-314.pyc +0 -0
- package/tools/hub/hub/tui/panels/__pycache__/tasks.cpython-314.pyc +0 -0
- package/tools/hub/hub/tui/panels/git.py +118 -0
- package/tools/hub/hub/tui/panels/projects.py +58 -0
- package/tools/hub/hub/tui/panels/tasks.py +139 -0
- package/tools/hub/pyproject.toml +25 -0
- package/tools/hub/tests/__init__.py +0 -0
- package/tools/hub/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_db.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_migrate.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_projects.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_tasks.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_tui_app.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_tui_git.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_tui_git_panel.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_tui_projects_panel.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/__pycache__/test_tui_tasks_panel.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/hub/tests/test_cli.py +93 -0
- package/tools/hub/tests/test_db.py +35 -0
- package/tools/hub/tests/test_migrate.py +102 -0
- package/tools/hub/tests/test_projects.py +50 -0
- package/tools/hub/tests/test_tasks.py +74 -0
- package/tools/hub/tests/test_tui_app.py +66 -0
- package/tools/hub/tests/test_tui_git.py +58 -0
- package/tools/hub/tests/test_tui_git_panel.py +56 -0
- package/tools/hub/tests/test_tui_projects_panel.py +63 -0
- package/tools/hub/tests/test_tui_tasks_panel.py +136 -0
- package/tools/hub/tool.json +8 -0
- package/tools/p-launch/__pycache__/p_launch.cpython-314.pyc +0 -0
- package/tools/p-launch/p_launch.py +126 -3
- package/tools/p-launch/tests/__pycache__/test_p_launch.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/p-launch/tests/__pycache__/test_p_launch.cpython-314-pytest-9.1.0.pyc +0 -0
- package/tools/p-launch/tests/test_p_launch.py +139 -0
- package/tools/todo-tool/frontend/README.md +73 -0
- package/tools/todo-tool/frontend/components.json +25 -0
- package/tools/todo-tool/frontend/eslint.config.js +22 -0
- package/tools/todo-tool/frontend/index.html +13 -0
- package/tools/todo-tool/frontend/package-lock.json +6566 -0
- package/tools/todo-tool/frontend/package.json +42 -0
- package/tools/todo-tool/frontend/postcss.config.js +6 -0
- package/tools/todo-tool/frontend/public/favicon.svg +1 -0
- package/tools/todo-tool/frontend/public/icons.svg +24 -0
- package/tools/todo-tool/frontend/src/App.css +184 -0
- package/tools/todo-tool/frontend/src/App.tsx +78 -0
- package/tools/todo-tool/frontend/src/assets/hero.png +0 -0
- package/tools/todo-tool/frontend/src/assets/react.svg +1 -0
- package/tools/todo-tool/frontend/src/assets/vite.svg +1 -0
- package/tools/todo-tool/frontend/src/components/AddTaskDialog.tsx +92 -0
- package/tools/todo-tool/frontend/src/components/FilterBar.tsx +74 -0
- package/tools/todo-tool/frontend/src/components/TaskList.tsx +35 -0
- package/tools/todo-tool/frontend/src/components/TaskRow.tsx +44 -0
- package/tools/todo-tool/frontend/src/components/ui/badge.tsx +52 -0
- package/tools/todo-tool/frontend/src/components/ui/button.tsx +58 -0
- package/tools/todo-tool/frontend/src/components/ui/dialog.tsx +159 -0
- package/tools/todo-tool/frontend/src/components/ui/input.tsx +20 -0
- package/tools/todo-tool/frontend/src/components/ui/select.tsx +197 -0
- package/tools/todo-tool/frontend/src/index.css +90 -0
- package/tools/todo-tool/frontend/src/lib/api.ts +42 -0
- package/tools/todo-tool/frontend/src/lib/utils.ts +6 -0
- package/tools/todo-tool/frontend/src/main.tsx +10 -0
- package/tools/todo-tool/frontend/tailwind.config.js +66 -0
- package/tools/todo-tool/frontend/tsconfig.app.json +32 -0
- package/tools/todo-tool/frontend/tsconfig.json +7 -0
- package/tools/todo-tool/frontend/tsconfig.node.json +24 -0
- package/tools/todo-tool/frontend/vite.config.ts +17 -0
- package/tools/todo-tool/pyproject.toml +31 -0
- package/tools/todo-tool/tests/__init__.py +0 -0
- package/tools/todo-tool/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_api.cpython-311-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_api.cpython-311-pytest-9.0.3.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_api.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_cli.cpython-311-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_cli.cpython-311-pytest-9.0.3.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_cli.cpython-314.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_db.cpython-311-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_db.cpython-311-pytest-9.0.3.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_db.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_models.cpython-311-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_models.cpython-311-pytest-9.0.3.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_models.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_parser.cpython-311-pytest-9.0.3.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_parser.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_projects_index.cpython-311-pytest-9.0.3.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_sync.cpython-311-pytest-9.0.3.pyc +0 -0
- package/tools/todo-tool/tests/__pycache__/test_sync.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/todo-tool/tests/test_api.py +229 -0
- package/tools/todo-tool/tests/test_cli.py +214 -0
- package/tools/todo-tool/tests/test_db.py +214 -0
- package/tools/todo-tool/tests/test_models.py +24 -0
- package/tools/todo-tool/tests/test_parser.py +120 -0
- package/tools/todo-tool/tests/test_projects_index.py +185 -0
- package/tools/todo-tool/tests/test_sync.py +101 -0
- package/tools/todo-tool/todo/__init__.py +0 -0
- package/tools/todo-tool/todo/__pycache__/__init__.cpython-311.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/cli.cpython-311.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/cli.cpython-314.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/db.cpython-311.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/db.cpython-314.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/models.cpython-311.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/models.cpython-314.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/parser.cpython-311.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/parser.cpython-314.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/projects_index.cpython-311.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/projects_index.cpython-314.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/server.cpython-311.pyc +0 -0
- package/tools/todo-tool/todo/__pycache__/server.cpython-314.pyc +0 -0
- package/tools/todo-tool/todo/cli.py +208 -0
- package/tools/todo-tool/todo/db.py +289 -0
- package/tools/todo-tool/todo/models.py +41 -0
- package/tools/todo-tool/todo/parser.py +122 -0
- package/tools/todo-tool/todo/projects_index.py +152 -0
- package/tools/todo-tool/todo/server.py +98 -0
- package/tools/todo-tool/todo/todo_format.yaml +29 -0
- package/tools/todo-tool/todo-tool.sh +23 -0
- package/tools/todo-tool/tool.json +12 -0
- package/tools/todo-tool/zshrc.snippet +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.14.0] - 2026-06-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hub` tool(`tools` bundle):个人开发者 OS,整合项目管理、git 状态追踪、任务系统,替代 p-launch + todo-tool
|
|
14
|
+
- Phase 1:core 库(SQLite DB、projects CRUD、tasks CRUD)+ CLI(`hub project`、`hub task` 子命令,支持 `--json` 输出)
|
|
15
|
+
- Phase 2:三栏 Textual TUI(ProjectsPanel | GitPanel | TasksPanel),支持键盘导航、git fetch、任务增删改、项目切换联动
|
|
16
|
+
- 首次启动自动从 todo-tool DB 迁移数据(`migrate.py`)
|
|
17
|
+
- 62 个测试覆盖 core、CLI、TUI 各层
|
|
18
|
+
|
|
10
19
|
## [0.13.0] - 2026-06-12
|
|
11
20
|
|
|
12
21
|
### Fixed
|
package/lib/installer.js
CHANGED
|
@@ -146,6 +146,21 @@ export async function installTools(tools, targetDir, force = false) {
|
|
|
146
146
|
console.error(chalk.dim(` · Installed ${toolName}.py`))
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// Copy extra paths (files or dirs) declared in tool.json `extraPaths`
|
|
150
|
+
// Each entry is a relative path from the tool source; copied to ~/.hskill/tools/{toolName}/
|
|
151
|
+
try {
|
|
152
|
+
const toolMeta = await fs.readJson(path.join(srcPath, 'tool.json'))
|
|
153
|
+
for (const rel of (toolMeta.extraPaths ?? [])) {
|
|
154
|
+
const src = path.join(srcPath, rel)
|
|
155
|
+
const dest = path.join(dataDir, toolName, rel)
|
|
156
|
+
if (await fs.pathExists(src)) {
|
|
157
|
+
await fs.ensureDir(path.dirname(dest))
|
|
158
|
+
await fs.copy(src, dest, { overwrite: true })
|
|
159
|
+
console.error(chalk.dim(` · Installed ${toolName}/${rel}`))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch { /* tool.json missing or no extraPaths — skip silently */ }
|
|
163
|
+
|
|
149
164
|
installed.push(toolName)
|
|
150
165
|
await _patchZshrc(srcPath, toolName)
|
|
151
166
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harveyz-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Skill manager for Claude Code, Cursor, and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"skills/meta/npm-release/",
|
|
42
42
|
"skills/meta/opencode-runner/",
|
|
43
43
|
"tools/p-launch/",
|
|
44
|
+
"tools/todo-tool/",
|
|
45
|
+
"tools/hub/",
|
|
44
46
|
"hooks/check-similar-branch/"
|
|
45
47
|
],
|
|
46
48
|
"engines": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: add-todo
|
|
3
|
-
version: "4.
|
|
3
|
+
version: "4.5.0"
|
|
4
4
|
user_invocable: true
|
|
5
5
|
description: "Add a new requirement, task, or feature request to any project's TODO.md — from any working directory. Triggers whenever the user wants to capture a new need — even phrased casually like 'we should do X later', 'add this to the backlog', 'note this down', 'remember to build X', 'we need to do Y at some point', or 'record this for later'."
|
|
6
6
|
---
|
|
@@ -88,6 +88,36 @@ description: "Add a new requirement, task, or feature request to any project's T
|
|
|
88
88
|
todo project set-path [项目名] [本地路径]
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
### 切换到 chore/todo 分支
|
|
92
|
+
|
|
93
|
+
在写入前,在项目目录操作 git:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd {local_path}
|
|
97
|
+
|
|
98
|
+
# 1. 记录当前分支
|
|
99
|
+
ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
100
|
+
|
|
101
|
+
# 2. 确定 staging 基准分支(用于创建 chore/todo 及事后合并目标)
|
|
102
|
+
if git show-ref --verify --quiet refs/heads/staging; then
|
|
103
|
+
BASE_BRANCH=staging
|
|
104
|
+
else
|
|
105
|
+
BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null \
|
|
106
|
+
| sed 's@^refs/remotes/origin/@@' || echo "main")
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# 3. 切换到 chore/todo(不存在则从 BASE_BRANCH 创建)
|
|
110
|
+
if git show-ref --verify --quiet refs/heads/chore/todo; then
|
|
111
|
+
git checkout chore/todo
|
|
112
|
+
else
|
|
113
|
+
git checkout -b chore/todo "$BASE_BRANCH"
|
|
114
|
+
fi
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**非 git 仓库**:若 `local_path` 不在 git 仓库中,跳过此步骤,直接写入文件。
|
|
118
|
+
|
|
119
|
+
**BRANCH_GUARD hook**:执行 `git checkout -b chore/todo` 时,BRANCH_GUARD hook 可能会提示与其他分支语义相似。`chore/todo` 是永久追踪分支,不是功能分支,**直接确认新建,忽略相似分支警告**,继续执行后续步骤。
|
|
120
|
+
|
|
91
121
|
### 重复检查
|
|
92
122
|
|
|
93
123
|
加载文件,扫描是否有语义重叠的已有条目。若有:
|
|
@@ -118,6 +148,25 @@ todo project set-path [项目名] [本地路径]
|
|
|
118
148
|
## ✅ 已完成
|
|
119
149
|
```
|
|
120
150
|
|
|
121
|
-
|
|
151
|
+
### 提交、合并到 staging、切回原分支
|
|
152
|
+
|
|
153
|
+
写入后在项目目录执行:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
cd {local_path}
|
|
157
|
+
|
|
158
|
+
# 1. 在 chore/todo 上提交
|
|
159
|
+
git add TODO.md
|
|
160
|
+
git commit -m "todo: add [任务标题]"
|
|
161
|
+
|
|
162
|
+
# 2. 合并到 staging(或主分支)
|
|
163
|
+
git checkout "$BASE_BRANCH"
|
|
164
|
+
git merge --no-ff chore/todo -m "Merge chore/todo: add [任务标题]"
|
|
165
|
+
|
|
166
|
+
# 3. 切回原分支
|
|
167
|
+
git checkout "$ORIGINAL_BRANCH"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
完成后确认:
|
|
122
171
|
|
|
123
|
-
> "✅ 已将 **[任务标题]** 写入 `{local_path}/TODO.md`。"
|
|
172
|
+
> "✅ 已将 **[任务标题]** 写入 `{local_path}/TODO.md`,提交到 `chore/todo`,合并到 `$BASE_BRANCH`,当前回到 `$ORIGINAL_BRANCH`。"
|
|
@@ -2,7 +2,7 @@
|
|
|
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.2.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# npm-release
|
|
@@ -100,14 +100,26 @@ npm version <new-version> --no-git-tag-version
|
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
|
+
## Step 3.5 — 同步 .npmignore 和 package.json files 字段
|
|
104
|
+
|
|
105
|
+
运行生成脚本,确保 `.npmignore` 和 `package.json` 中的 `files` 字段与当前 `skills-index.json` 保持一致:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
node scripts/generate-npmignore.js
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
检查输出有无报错。此步骤必须在提交前完成,否则新增或重命名的 skill 路径不会被正确包含/排除。
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
103
115
|
## Step 4 — 创建 release 分支并提交
|
|
104
116
|
|
|
105
117
|
```bash
|
|
106
118
|
# 从当前 staging 切出 release 分支
|
|
107
119
|
git checkout -b release/<new-version>
|
|
108
120
|
|
|
109
|
-
#
|
|
110
|
-
git add CHANGELOG.md package.json package-lock.json
|
|
121
|
+
# 提交 CHANGELOG、package.json、package-lock.json 和 .npmignore
|
|
122
|
+
git add CHANGELOG.md package.json package-lock.json .npmignore
|
|
111
123
|
git commit -m "chore(release): bump version to <new-version>"
|
|
112
124
|
```
|
|
113
125
|
|
package/skills/research/article-fetcher/references/__pycache__/article_utils.cpython-314.pyc
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: youtube-learner
|
|
3
|
-
version: "1.3.
|
|
3
|
+
version: "1.3.1"
|
|
4
4
|
description: "Process a YouTube video using the vdl CLI: transcribe, generate article and summary. Triggers when the user provides a YouTube URL and wants to learn from, summarize, transcribe, or extract key points from the video — e.g. 'help me understand this talk', 'summarize this YouTube video', 'get the transcript', 'process this video', 'summarize it'."
|
|
5
5
|
user_invocable: true
|
|
6
6
|
---
|
|
@@ -35,7 +35,7 @@ npm link
|
|
|
35
35
|
| `url` | YouTube 链接 | 从用户消息提取 |
|
|
36
36
|
| `--focus` | 关注点(影响摘要内容) | 若用户未提供,主动询问:"你最想从这个视频中了解什么?(例如:核心论点、技术细节、行动项)" |
|
|
37
37
|
| `--mode` | 处理模式 | **必须询问用户**(见下方「模式选择」) |
|
|
38
|
-
| `--lang` | 输出语言 | 默认 `zh-CN`;若用户用英文交流则用 `en
|
|
38
|
+
| `--lang` | 输出语言 | 默认 `zh-CN`;若用户用英文交流则用 `en`;必须将解析结果赋给 `<LANG>` 占位符 |
|
|
39
39
|
|
|
40
40
|
### 模式选择
|
|
41
41
|
|
|
@@ -64,9 +64,18 @@ npm link
|
|
|
64
64
|
|
|
65
65
|
## 执行命令
|
|
66
66
|
|
|
67
|
+
将以下占位符全部替换后再执行:
|
|
68
|
+
|
|
69
|
+
| 占位符 | 替换为 |
|
|
70
|
+
|--------|--------|
|
|
71
|
+
| `<URL>` | YouTube 链接 |
|
|
72
|
+
| `<FOCUS>` | 用户填写的关注点 |
|
|
73
|
+
| `<MODE>` | `transcript` / `audio` / `media` / `full` |
|
|
74
|
+
| `<LANG>` | `zh-CN`(中文对话)或 `en`(英文对话) |
|
|
75
|
+
|
|
67
76
|
```bash
|
|
68
77
|
cd /Users/harveyzhang96/Projects/Video-Learner && \
|
|
69
|
-
vdl "<URL>" --focus "<FOCUS>" --mode <MODE> --lang
|
|
78
|
+
vdl "<URL>" --focus "<FOCUS>" --mode <MODE> --lang <LANG>
|
|
70
79
|
```
|
|
71
80
|
|
|
72
81
|
`vdl` 会自动:
|
|
Binary file
|
package/skills-index.json
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"toolBundleMeta": {
|
|
3
|
-
"shell-tools": "终端工具(p-launch
|
|
3
|
+
"shell-tools": "终端工具(p-launch 本地仓库管理器 + todo-tool 任务管理器)",
|
|
4
|
+
"tools": "开发者 OS 工具(hub — 项目管理 + git 状态 + 任务跟踪)"
|
|
4
5
|
},
|
|
5
6
|
"tools": [
|
|
6
7
|
{
|
|
7
8
|
"name": "p-launch",
|
|
8
9
|
"bundle": "shell-tools"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "todo-tool",
|
|
13
|
+
"bundle": "shell-tools"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "hub",
|
|
17
|
+
"path": "tools/hub",
|
|
18
|
+
"bundle": "tools"
|
|
9
19
|
}
|
|
10
20
|
],
|
|
11
21
|
"bundleMeta": {
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
from hub.core.db import HubDB
|
|
6
|
+
from hub.core.migrate import needs_migration, run_migration
|
|
7
|
+
|
|
8
|
+
db = HubDB()
|
|
9
|
+
if needs_migration():
|
|
10
|
+
n = run_migration(db)
|
|
11
|
+
if n:
|
|
12
|
+
import typer
|
|
13
|
+
typer.echo(f"hub: migrated {n} tasks from todo-tool ✓")
|
|
14
|
+
|
|
15
|
+
if len(sys.argv) == 1:
|
|
16
|
+
from hub.tui.app import HubApp
|
|
17
|
+
HubApp().run()
|
|
18
|
+
return
|
|
19
|
+
from hub.cli import app
|
|
20
|
+
app()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from hub.cli import projects as proj_cmd
|
|
6
|
+
from hub.cli import tasks as task_cmd
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
name="hub",
|
|
10
|
+
no_args_is_help=False,
|
|
11
|
+
add_completion=False,
|
|
12
|
+
)
|
|
13
|
+
app.add_typer(proj_cmd.app, name="projects")
|
|
14
|
+
app.add_typer(task_cmd.app, name="tasks")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from hub.core.db import HubDB
|
|
10
|
+
from hub.core import projects as proj
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
_DEFAULT_MD = Path.home() / ".hskill" / "public" / "PROJECTS.md"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _md_path() -> Path:
|
|
18
|
+
if env := os.environ.get("HUB_MD_PATH"):
|
|
19
|
+
return Path(env)
|
|
20
|
+
return _DEFAULT_MD
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _out(data, json_out: bool) -> None:
|
|
24
|
+
if json_out:
|
|
25
|
+
print(json.dumps({"ok": True, "data": data}))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _err(msg: str, json_out: bool) -> None:
|
|
29
|
+
if json_out:
|
|
30
|
+
print(json.dumps({"ok": False, "error": msg}))
|
|
31
|
+
else:
|
|
32
|
+
typer.echo(f"Error: {msg}", err=True)
|
|
33
|
+
raise SystemExit(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("list")
|
|
37
|
+
def projects_list(json_out: bool = typer.Option(False, "--json")):
|
|
38
|
+
"""List all registered projects."""
|
|
39
|
+
db = HubDB()
|
|
40
|
+
projects = proj.list_projects(db)
|
|
41
|
+
if json_out:
|
|
42
|
+
_out(projects, json_out)
|
|
43
|
+
else:
|
|
44
|
+
if not projects:
|
|
45
|
+
typer.echo("No projects. Use: hub projects add <name> --path <path>")
|
|
46
|
+
return
|
|
47
|
+
for p in projects:
|
|
48
|
+
typer.echo(f" {p['name']:<24} {p['path'] or ''}")
|
|
49
|
+
if p.get("description"):
|
|
50
|
+
typer.echo(f" {p['description']}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("add")
|
|
54
|
+
def projects_add(
|
|
55
|
+
name: str = typer.Argument(..., help="Project name (GitHub repo name)"),
|
|
56
|
+
path: str = typer.Option("", "--path", help="Local directory path"),
|
|
57
|
+
description: str = typer.Option("", "--desc", help="Short description"),
|
|
58
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
59
|
+
):
|
|
60
|
+
"""Register or update a project."""
|
|
61
|
+
db = HubDB()
|
|
62
|
+
result = proj.add_project(db, name, path=path, description=description, md_path=_md_path())
|
|
63
|
+
if json_out:
|
|
64
|
+
_out(result, json_out)
|
|
65
|
+
else:
|
|
66
|
+
typer.echo(f"✓ {name}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("path")
|
|
70
|
+
def projects_path(
|
|
71
|
+
name: str = typer.Argument(...),
|
|
72
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
73
|
+
):
|
|
74
|
+
"""Print the local path for a project (for shell cd / agent use)."""
|
|
75
|
+
db = HubDB()
|
|
76
|
+
p = proj.get_project_path(db, name)
|
|
77
|
+
if p is None:
|
|
78
|
+
_err(f"Project '{name}' not found", json_out)
|
|
79
|
+
if json_out:
|
|
80
|
+
_out(p, json_out)
|
|
81
|
+
else:
|
|
82
|
+
print(p)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("sync")
|
|
86
|
+
def projects_sync(json_out: bool = typer.Option(False, "--json")):
|
|
87
|
+
"""Re-scan configured dirs and update PROJECTS.md. (Requires p-launch config.)"""
|
|
88
|
+
try:
|
|
89
|
+
import sys as _sys
|
|
90
|
+
_sys.path.insert(0, str(Path(__file__).parents[4] / "p-launch"))
|
|
91
|
+
from p_launch import read_project_dirs, collect_repos, sync_to_index
|
|
92
|
+
dirs = read_project_dirs()
|
|
93
|
+
repos = collect_repos(dirs)
|
|
94
|
+
sync_to_index(repos, _md_path())
|
|
95
|
+
if json_out:
|
|
96
|
+
_out({"scanned": len(repos)}, json_out)
|
|
97
|
+
else:
|
|
98
|
+
typer.echo(f"✓ Scanned {len(repos)} repos")
|
|
99
|
+
except ImportError:
|
|
100
|
+
_err("p-launch not installed; cannot auto-scan", json_out)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from hub.core.db import HubDB
|
|
8
|
+
from hub.core import tasks as task
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _out(data, json_out: bool) -> None:
|
|
14
|
+
if json_out:
|
|
15
|
+
print(json.dumps({"ok": True, "data": data}))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _err(msg: str, json_out: bool) -> None:
|
|
19
|
+
if json_out:
|
|
20
|
+
print(json.dumps({"ok": False, "error": msg}))
|
|
21
|
+
else:
|
|
22
|
+
typer.echo(f"Error: {msg}", err=True)
|
|
23
|
+
raise SystemExit(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("list")
|
|
27
|
+
def tasks_list(
|
|
28
|
+
project: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
29
|
+
status: Optional[str] = typer.Option(None, "--status", "-s"),
|
|
30
|
+
priority: Optional[str] = typer.Option(None, "--priority", "-P"),
|
|
31
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
32
|
+
):
|
|
33
|
+
"""List tasks, optionally filtered."""
|
|
34
|
+
db = HubDB()
|
|
35
|
+
tasks = task.list_tasks(db, project=project, status=status, priority=priority)
|
|
36
|
+
if json_out:
|
|
37
|
+
_out(tasks, json_out)
|
|
38
|
+
else:
|
|
39
|
+
if not tasks:
|
|
40
|
+
typer.echo("No tasks.")
|
|
41
|
+
return
|
|
42
|
+
for t in tasks:
|
|
43
|
+
check = "✓" if t["status"] == "done" else "○"
|
|
44
|
+
typer.echo(f" [{t['id']:>4}] {check} [{t['priority']}] {t['title']} ({t['project']})")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("add")
|
|
48
|
+
def tasks_add(
|
|
49
|
+
title: str = typer.Argument(...),
|
|
50
|
+
project: str = typer.Option(..., "--project", "-p"),
|
|
51
|
+
priority: str = typer.Option("P2", "--priority", "-P"),
|
|
52
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
53
|
+
):
|
|
54
|
+
"""Add a new task."""
|
|
55
|
+
db = HubDB()
|
|
56
|
+
try:
|
|
57
|
+
t = task.add_task(db, title=title, project=project, priority=priority)
|
|
58
|
+
except ValueError as e:
|
|
59
|
+
_err(str(e), json_out)
|
|
60
|
+
if json_out:
|
|
61
|
+
_out(t, json_out)
|
|
62
|
+
else:
|
|
63
|
+
typer.echo(f"✓ [{t['id']}] {t['title']}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command("done")
|
|
67
|
+
def tasks_done(
|
|
68
|
+
task_id: int = typer.Argument(...),
|
|
69
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
70
|
+
):
|
|
71
|
+
"""Mark a task as done."""
|
|
72
|
+
db = HubDB()
|
|
73
|
+
result = task.mark_done(db, task_id)
|
|
74
|
+
if result is None:
|
|
75
|
+
_err(f"Task {task_id} not found", json_out)
|
|
76
|
+
if json_out:
|
|
77
|
+
_out(result, json_out)
|
|
78
|
+
else:
|
|
79
|
+
typer.echo(f"✓ done: {result['title']}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command("update")
|
|
83
|
+
def tasks_update(
|
|
84
|
+
task_id: int = typer.Argument(...),
|
|
85
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
86
|
+
priority: Optional[str] = typer.Option(None, "--priority"),
|
|
87
|
+
status: Optional[str] = typer.Option(None, "--status"),
|
|
88
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
89
|
+
):
|
|
90
|
+
"""Update task fields."""
|
|
91
|
+
db = HubDB()
|
|
92
|
+
result = task.update_task(db, task_id, title=title, priority=priority, status=status)
|
|
93
|
+
if result is None:
|
|
94
|
+
_err(f"Task {task_id} not found", json_out)
|
|
95
|
+
if json_out:
|
|
96
|
+
_out(result, json_out)
|
|
97
|
+
else:
|
|
98
|
+
typer.echo(f"✓ updated [{task_id}]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("rm")
|
|
102
|
+
def tasks_rm(
|
|
103
|
+
task_id: int = typer.Argument(...),
|
|
104
|
+
json_out: bool = typer.Option(False, "--json"),
|
|
105
|
+
):
|
|
106
|
+
"""Delete a task."""
|
|
107
|
+
db = HubDB()
|
|
108
|
+
deleted = task.delete_task(db, task_id)
|
|
109
|
+
if not deleted:
|
|
110
|
+
_err(f"Task {task_id} not found", json_out)
|
|
111
|
+
if json_out:
|
|
112
|
+
_out({"deleted": True}, json_out)
|
|
113
|
+
else:
|
|
114
|
+
typer.echo(f"✓ deleted [{task_id}]")
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_db_path() -> Path:
|
|
7
|
+
if env := os.environ.get("HUB_DB_PATH"):
|
|
8
|
+
return Path(env)
|
|
9
|
+
return Path.home() / ".hskill" / "hub" / "tasks.db"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HubDB:
|
|
13
|
+
def __init__(self, db_path: Path = None):
|
|
14
|
+
self.db_path = db_path or get_db_path()
|
|
15
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
self._init()
|
|
17
|
+
|
|
18
|
+
def _conn(self) -> sqlite3.Connection:
|
|
19
|
+
conn = sqlite3.connect(self.db_path)
|
|
20
|
+
conn.row_factory = sqlite3.Row
|
|
21
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
22
|
+
return conn
|
|
23
|
+
|
|
24
|
+
def _init(self) -> None:
|
|
25
|
+
with self._conn() as conn:
|
|
26
|
+
conn.execute("""
|
|
27
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
name TEXT NOT NULL UNIQUE,
|
|
30
|
+
path TEXT,
|
|
31
|
+
description TEXT DEFAULT '',
|
|
32
|
+
created_at TEXT NOT NULL
|
|
33
|
+
)
|
|
34
|
+
""")
|
|
35
|
+
conn.execute("""
|
|
36
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
title TEXT NOT NULL,
|
|
39
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
40
|
+
priority TEXT DEFAULT 'P2',
|
|
41
|
+
status TEXT DEFAULT 'todo',
|
|
42
|
+
created_at TEXT NOT NULL
|
|
43
|
+
)
|
|
44
|
+
""")
|
|
45
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_task_project ON tasks(project_id)")
|
|
46
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_task_status ON tasks(status)")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""One-time migration: copy tasks from todo-tool's DB into hub's DB."""
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_OLD_DB = Path.home() / ".local" / "share" / "todo" / "tasks.db"
|
|
7
|
+
_SENTINEL = Path.home() / ".hskill" / "hub" / ".migrated"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def needs_migration() -> bool:
|
|
11
|
+
return _OLD_DB.exists() and not _SENTINEL.exists()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_migration(hub_db) -> int:
|
|
15
|
+
"""Copy projects + tasks from todo-tool DB. Returns number of tasks migrated."""
|
|
16
|
+
if not _OLD_DB.exists():
|
|
17
|
+
return 0
|
|
18
|
+
old = sqlite3.connect(_OLD_DB)
|
|
19
|
+
old.row_factory = sqlite3.Row
|
|
20
|
+
migrated = 0
|
|
21
|
+
try:
|
|
22
|
+
from hub.core.projects import add_project
|
|
23
|
+
from hub.core.tasks import add_task, mark_done
|
|
24
|
+
from pathlib import Path as _Path
|
|
25
|
+
md = _Path.home() / ".hskill" / "public" / "PROJECTS.md"
|
|
26
|
+
|
|
27
|
+
for row in old.execute("SELECT repo_name, local_path FROM projects").fetchall():
|
|
28
|
+
try:
|
|
29
|
+
add_project(hub_db, row["repo_name"], path=row["local_path"] or "", md_path=md)
|
|
30
|
+
except Exception:
|
|
31
|
+
pass # already exists is fine
|
|
32
|
+
|
|
33
|
+
for row in old.execute(
|
|
34
|
+
"SELECT t.title, p.repo_name as project, t.priority, t.status "
|
|
35
|
+
"FROM tasks t JOIN projects p ON p.id = t.project_id"
|
|
36
|
+
).fetchall():
|
|
37
|
+
try:
|
|
38
|
+
t = add_task(hub_db, title=row["title"], project=row["project"], priority=row["priority"])
|
|
39
|
+
if row["status"] == "done":
|
|
40
|
+
mark_done(hub_db, t["id"])
|
|
41
|
+
migrated += 1
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
finally:
|
|
45
|
+
old.close()
|
|
46
|
+
|
|
47
|
+
_SENTINEL.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
_SENTINEL.touch()
|
|
49
|
+
return migrated
|