harveyz-skill 0.32.0 → 0.34.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 (83) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/cli.js +170 -27
  3. package/lib/install-source.js +46 -0
  4. package/lib/version-check.js +2 -2
  5. package/package.json +7 -1
  6. package/skills/agent-canvas/agent-canvas-control/SKILL.md +235 -0
  7. package/skills/agent-canvas/capture-requirement/SKILL.md +51 -0
  8. package/skills/agent-canvas/close-node/SKILL.md +117 -0
  9. package/skills/agent-canvas/describe-node/SKILL.md +47 -0
  10. package/skills/agent-canvas/relate-node/SKILL.md +63 -0
  11. package/skills/agent-canvas/relation-review/SKILL.md +120 -0
  12. package/skills/coding/handoff/SKILL.md +122 -17
  13. package/skills/coding/handoff/assets/handoff-template.md +9 -3
  14. package/skills/coding/handoff/evals/evals.json +1 -1
  15. package/skills/coding/handoff/scripts/validate-handoff.sh +10 -0
  16. package/skills/coding/handoff/tests/validate-handoff.bats +65 -0
  17. package/skills/feed/sync-website/SKILL.md +14 -3
  18. package/skills/feed/sync-website/scripts/__pycache__/archive_articles.cpython-314.pyc +0 -0
  19. package/skills/feed/sync-website/scripts/__pycache__/articles_client.cpython-314.pyc +0 -0
  20. package/skills/feed/sync-website/scripts/__pycache__/browser_fetch_cli.cpython-314.pyc +0 -0
  21. package/skills/feed/sync-website/scripts/__pycache__/browser_fetch_locate.cpython-314.pyc +0 -0
  22. package/skills/feed/sync-website/scripts/__pycache__/calibration_gate.cpython-314.pyc +0 -0
  23. package/skills/feed/sync-website/scripts/__pycache__/config.cpython-314.pyc +0 -0
  24. package/skills/feed/sync-website/scripts/__pycache__/cursor.cpython-314.pyc +0 -0
  25. package/skills/feed/sync-website/scripts/__pycache__/digest.cpython-314.pyc +0 -0
  26. package/skills/feed/sync-website/scripts/__pycache__/fetch_new_articles.cpython-314.pyc +0 -0
  27. package/skills/feed/sync-website/scripts/__pycache__/roster_client.cpython-314.pyc +0 -0
  28. package/skills/feed/sync-website/scripts/__pycache__/roster_locate.cpython-314.pyc +0 -0
  29. package/skills/feed/sync-website/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
  30. package/skills/feed/sync-website/scripts/store_config.py +84 -2
  31. package/skills/feed/sync-website/tests/__pycache__/conftest.cpython-314-pytest-9.1.1.pyc +0 -0
  32. package/skills/feed/sync-website/tests/__pycache__/test_archive_articles.cpython-314-pytest-9.1.1.pyc +0 -0
  33. package/skills/feed/sync-website/tests/__pycache__/test_articles_client.cpython-314-pytest-9.1.1.pyc +0 -0
  34. package/skills/feed/sync-website/tests/__pycache__/test_browser_fetch_locate.cpython-314-pytest-9.1.1.pyc +0 -0
  35. package/skills/feed/sync-website/tests/__pycache__/test_calibration_gate.cpython-314-pytest-9.1.1.pyc +0 -0
  36. package/skills/feed/sync-website/tests/__pycache__/test_cursor.cpython-314-pytest-9.1.1.pyc +0 -0
  37. package/skills/feed/sync-website/tests/__pycache__/test_digest.cpython-314-pytest-9.1.1.pyc +0 -0
  38. package/skills/feed/sync-website/tests/__pycache__/test_fetch_new_articles.cpython-314-pytest-9.1.1.pyc +0 -0
  39. package/skills/feed/sync-website/tests/__pycache__/test_roster_client.cpython-314-pytest-9.1.1.pyc +0 -0
  40. package/skills/feed/sync-website/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
  41. package/skills/feed/sync-website/tests/test_store_config.py +101 -0
  42. package/skills/feed/sync-xtimeline/SKILL.md +6 -2
  43. package/skills/feed/sync-xtimeline/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
  44. package/skills/feed/sync-xtimeline/scripts/store_config.py +84 -2
  45. package/skills/feed/sync-xtimeline/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
  46. package/skills/feed/sync-xtimeline/tests/test_store_config.py +101 -0
  47. package/skills/feed/sync-ytchannel/SKILL.md +6 -2
  48. package/skills/feed/sync-ytchannel/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
  49. package/skills/feed/sync-ytchannel/scripts/store_config.py +84 -2
  50. package/skills/feed/sync-ytchannel/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
  51. package/skills/feed/sync-ytchannel/tests/test_store_config.py +101 -0
  52. package/skills/mint/init-skill/SKILL.md +1 -1
  53. package/skills/mint/learn-skill/SKILL.md +1 -1
  54. package/skills/research/clip-url/SKILL.md +10 -3
  55. package/skills/research/clip-url/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
  56. package/skills/research/clip-url/scripts/store_config.py +84 -2
  57. package/skills/research/clip-url/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
  58. package/skills/research/clip-url/tests/test_store_config.py +101 -0
  59. package/skills/research/learn-video/SKILL.md +8 -6
  60. package/skills/research/learn-video/scripts/__pycache__/archive.cpython-314.pyc +0 -0
  61. package/skills/research/learn-video/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
  62. package/skills/research/learn-video/scripts/store_config.py +84 -2
  63. package/skills/research/learn-video/tests/__pycache__/test_archive.cpython-314-pytest-9.1.1.pyc +0 -0
  64. package/skills/research/learn-video/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
  65. package/skills/research/learn-video/tests/test_store_config.py +101 -0
  66. package/skills/research/survey-skillrepo/SKILL.md +1 -1
  67. package/skills-index.json +44 -19
  68. package/tools/browser-fetch/browser_fetch/__pycache__/cli.cpython-314.pyc +0 -0
  69. package/tools/browser-fetch/browser_fetch/__pycache__/core.cpython-314.pyc +0 -0
  70. package/tools/browser-fetch/browser_fetch/__pycache__/extractors.cpython-314.pyc +0 -0
  71. package/tools/browser-fetch/browser_fetch/__pycache__/normalize.cpython-314.pyc +0 -0
  72. package/tools/browser-fetch/browser_fetch/__pycache__/site_rules.cpython-314.pyc +0 -0
  73. package/tools/browser-fetch/tests/__pycache__/conftest.cpython-314-pytest-9.1.1.pyc +0 -0
  74. package/tools/browser-fetch/tests/__pycache__/test_cli_articles.cpython-314-pytest-9.1.1.pyc +0 -0
  75. package/tools/browser-fetch/tests/__pycache__/test_cli_articles_rule.cpython-314-pytest-9.1.1.pyc +0 -0
  76. package/tools/browser-fetch/tests/__pycache__/test_cli_articles_transform.cpython-314-pytest-9.1.1.pyc +0 -0
  77. package/tools/browser-fetch/tests/__pycache__/test_extractors_articles.cpython-314-pytest-9.1.1.pyc +0 -0
  78. package/tools/browser-fetch/tests/__pycache__/test_normalize.cpython-314-pytest-9.1.1.pyc +0 -0
  79. package/tools/browser-fetch/tests/__pycache__/test_site_rules.cpython-314-pytest-9.1.1.pyc +0 -0
  80. package/tools/browser-fetch/tests/__pycache__/test_site_rules_dirs.cpython-314-pytest-9.1.1.pyc +0 -0
  81. package/tools/roster/roster/__pycache__/urls.cpython-314.pyc +0 -0
  82. package/tools/roster/tests/__pycache__/test_cli.cpython-314-pytest-9.1.1.pyc +0 -0
  83. package/tools/roster/tests/__pycache__/test_urls.cpython-314-pytest-9.1.1.pyc +0 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.34.0] - 2026-09-16
11
+
12
+ ### Added
13
+ - `hskill` 双来源安装与粘性更新:`hskill update --local <路径>` 从本地仓库 `npm pack` 出 tarball 再全局安装(真实拷贝,走 `prepack` 与 `files[]` 白名单,等于在发 npm 前预演一次真实发布),`hskill update --npm` 切回 registry,裸 `hskill update` 沿用当前来源不跨轨道。此前 `update` 是一行硬编码的 `npm install -g harveyz-skill@latest`,手工装的本地版会被它静默替换掉且事后无从察觉。来源痕迹(`.hskill-source.json` + 版本号后缀 `+local`)只写在全局安装目录内,因而任何一次 `npm install -g`——包括绕过 hskill 的手工安装——都会自动抹掉它,"记录的来源"与"实际的来源"不可能分叉。`hskill version` 展示来源与 branch/commit,`version --check` 在本地来源下比对 commit 而非版本号(开发分支上版本号常常不动)。设计见 `docs/superpowers/specs/2026-09-15-hskill-install-source-design.md`
14
+ - `agent-canvas` bundle:新增 `agent-canvas-control`、`capture-requirement`、`close-node`、`describe-node`、`relate-node`、`relation-review` 六个 skill,在 Agent Canvas 画布节点里操控节点实体、摆放画布、梳理节点间语义关系
15
+
16
+ ### Fixed
17
+ - `hskill <任意子命令> --json` 在输出走管道时被截断:Node 写管道是异步的,而每个子命令块以 `process.exit()` 结尾,不等缓冲区排空。`status --json` 的输出刚随 agent-canvas 六个 skill 涨到 66,739 字节、越过 64KiB 管道缓冲区,于是 `hskill status --json | jq` 拿到退出码 0 和一段从字符串中间断掉的 JSON——**静默失败**,退出码还宣称成功。16 处 `--json` 出口统一改走等待写入落地的 `emitJson()`;回归测试逐字节比对管道输出与直接重定向输出,且在负载跌回缓冲区以下时显式 skip 而非假绿
18
+ - `hskill version` / `hskill --version` 在 npm 不在 PATH 上时抛原始堆栈崩溃(cron、受限 PATH、精简镜像):来源追踪让 `readSource()` 每次都经 `globalRoot()` 打一次 `npm root -g`,而它只兜住了 JSON 解析失败。现在 `globalRoot()` 的失败并入同一个 `null`,即回落到缺省的 npm 来源
19
+ - `compareVersions()` 遇到 semver build metadata(`0.33.0+local`)会解析出 `NaN` 导致比较结果无意义,现按规范在比较前截断 `+` 及其后内容
20
+
21
+ ## [0.33.0] - 2026-09-15
22
+
23
+ ### Added
24
+ - `store_config.py`(`learn-video`/`clip-url`/`sync-xtimeline`/`sync-ytchannel`/`sync-website` 共用):新增 `check-downstream`,核对统一存储根的产物与 vdl / scholia 两个下游读取方是否同步,五份 SKILL.md 的前置检查步骤接入该核对
25
+
26
+ ### Changed
27
+ - `handoff`(1.8.0 → 1.11.0):新增 Phase 2.5 接手方自测闸门 + 打回复修回路(此前「接手方完工 → 待验收」这个状态转移毫无前置条件);建工作区改为显式给基线分支并 `EnterWorktree(path:)` 进去,三方指令统一,修复了两次因裸 git 命令落到主工作树而夹带他人未合并提交的翻车;关闭 `using-git-worktrees` 前置调用(它会另起一个 `.worktrees/` 下的第二工作区,与 handoff 自己的工作区约定冲突);平台专属指令(`EnterWorktree` 等 Claude Code harness 特性)拆进独立「平台适配」章节,避免 pi/hermes/Codex 上的接手方读到卡住
28
+ - `publish-skill` 审计:`init-skill`(1.2.0→1.2.1)、`learn-skill`(2.0.0→2.0.1)、`research/survey-skillrepo`(2.0.1→2.0.2)——内容已变但 version 未 bump,属于 F8 硬性检查项,现已按 patch 补齐;另有 6 个 skill(`handoff`/`sync-website`/`sync-xtimeline`/`sync-ytchannel`/`clip-url`/`learn-video`)此前 version 已 bump 但 `skills-index.json` 里的 `contentHash` 记录没跟上,已同步
29
+
30
+ ### Fixed
31
+ - `learn-video` / `clip-url` / `sync-xtimeline` / `sync-ytchannel` / `sync-website` 共用的 `knowledgeRoot` 默认建议路径从 `~/Documents/knowledge` 改为 `~/knowledge`:前者在无 Full Disk Access 的 agent 执行环境下写入会静默失败,且此前没有护栏阻止 agent 擅自把配置改到别的路径绕过,导致产物在用户不知情的情况下分裂到两个根目录;五份 SKILL.md 加"写入失败必须如实报告并停下问用户"的护栏指令
32
+ - `tests/install.bats` / `tests/interactive.bats`:`survey-skillrepo` 测试夹具的硬编码版本号随上面的 F8 审计一并更新为 `2.0.2`,此前的 bump 一度导致 3 个断言失败
33
+
10
34
  ## [0.32.0] - 2026-09-07
11
35
 
12
36
  ### Added
package/bin/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { select, input, confirm } from '@inquirer/prompts'
3
3
  import chalk from 'chalk'
4
4
  import { execSync, spawnSync } from 'child_process'
5
- import { existsSync, writeFileSync, unlinkSync } from 'fs'
5
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdtempSync } from 'fs'
6
6
  import { createRequire } from 'module'
7
7
  import os from 'os'
8
8
  import path from 'path'
@@ -24,6 +24,16 @@ const args = process.argv.slice(2)
24
24
  const subcommand = args[0]
25
25
  const jsonFlag = args.includes('--json')
26
26
 
27
+ // Writing to a pipe is asynchronous in Node, and process.exit() throws away
28
+ // whatever is still buffered. Anything past the 64KiB pipe buffer was being cut
29
+ // mid-string, so `hskill status --json | jq` got exit code 0 and unparseable
30
+ // JSON. Every --json emitter goes through here, which resolves only once the
31
+ // write has actually landed.
32
+ async function emitJson(value) {
33
+ const text = JSON.stringify(value, null, 2) + '\n'
34
+ await new Promise(resolve => process.stdout.write(text, resolve))
35
+ }
36
+
27
37
  // ── Help ─────────────────────────────────────────────────────────────────────
28
38
  function printHelp() {
29
39
  console.log(`
@@ -50,10 +60,12 @@ function printHelp() {
50
60
  hskill uninstall <tool> uninstall a shell tool and clean up all files
51
61
  hskill uninstall <tool> --yes skip all confirmations (incl. config files)
52
62
  hskill uninstall <skill> --scope <s> --target <t> uninstall a skill
53
- hskill update update hskill to the latest version
63
+ hskill update update hskill (sticky: refreshes from whichever source is installed)
64
+ hskill update --local <path> switch to / refresh a local repo (npm pack + install)
65
+ hskill update --npm switch back to npm registry
54
66
  hskill mcp start an MCP server (stdio) exposing hskill's tools to MCP-capable agent hosts
55
- hskill version show version
56
- hskill version --check compare local version against npm registry (no install)
67
+ hskill version show version (adds source/branch/commit lines when installed from a local repo)
68
+ hskill version --check compare against npm registry, or against the local source repo's HEAD
57
69
  hskill --help show this help
58
70
 
59
71
  ${chalk.cyan('Examples:')}
@@ -74,7 +86,7 @@ function printHelp() {
74
86
 
75
87
  if (args[0] === '--help' || args[0] === '-h') {
76
88
  if (jsonFlag || args.includes('--json')) {
77
- console.log(JSON.stringify({
89
+ await emitJson({
78
90
  name: 'hskill',
79
91
  version,
80
92
  description: 'Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi',
@@ -138,7 +150,11 @@ if (args[0] === '--help' || args[0] === '-h') {
138
150
  },
139
151
  {
140
152
  name: 'update',
141
- description: 'Update hskill to the latest version via npm',
153
+ description: 'Update hskill; sticky to whichever source (npm or local repo) is currently installed',
154
+ flags: [
155
+ { name: '--local', arg: '<path>', description: 'Switch to / refresh a local repo source (npm pack + install -g)' },
156
+ { name: '--npm', description: 'Switch back to the npm registry source' },
157
+ ],
142
158
  },
143
159
  {
144
160
  name: 'mcp',
@@ -149,7 +165,7 @@ if (args[0] === '--help' || args[0] === '-h') {
149
165
  description: 'Print version and exit',
150
166
  },
151
167
  ],
152
- }, null, 2))
168
+ })
153
169
  process.exit(0)
154
170
  }
155
171
  printHelp()
@@ -157,12 +173,37 @@ if (args[0] === '--help' || args[0] === '-h') {
157
173
  }
158
174
 
159
175
  if (args[0] === '--version' || args[0] === '-v' || subcommand === 'version') {
176
+ const { readSource, gitInfo } = await import('../lib/install-source.js')
177
+ const source = readSource()
178
+
160
179
  if (subcommand === 'version' && args.includes('--check')) {
180
+ if (source) {
181
+ let current
182
+ try {
183
+ current = gitInfo(source.repo)
184
+ } catch (err) {
185
+ console.error(chalk.red(` ✗ Could not read local source repo: ${err.message}`))
186
+ process.exit(1)
187
+ }
188
+ const upToDate = current.commit === source.commit && !current.dirty
189
+ if (jsonFlag) {
190
+ await emitJson({ source: 'local', repo: source.repo, installedCommit: source.commit, currentCommit: current.commit, dirty: current.dirty, upToDate })
191
+ } else if (upToDate) {
192
+ console.log(chalk.green(` ✔ hskill is up to date with local source (${source.repo}@${current.commit})`))
193
+ } else if (current.commit !== source.commit) {
194
+ console.log(chalk.yellow(` ⚠ local source has new commits: ${source.commit} → ${current.commit}`))
195
+ console.log(chalk.dim(' Run: hskill update'))
196
+ } else {
197
+ console.log(chalk.yellow(' ⚠ local source has uncommitted changes not yet packed'))
198
+ console.log(chalk.dim(' Run: hskill update'))
199
+ }
200
+ process.exit(0)
201
+ }
161
202
  try {
162
203
  const { checkNpmVersion } = await import('../lib/version-check.js')
163
204
  const { current, latest, upToDate } = await checkNpmVersion('harveyz-skill', version)
164
205
  if (jsonFlag) {
165
- console.log(JSON.stringify({ current, latest, upToDate }, null, 2))
206
+ await emitJson({ current, latest, upToDate })
166
207
  } else if (upToDate) {
167
208
  console.log(chalk.green(` ✔ hskill v${current} is up to date`))
168
209
  } else {
@@ -176,6 +217,11 @@ if (args[0] === '--version' || args[0] === '-v' || subcommand === 'version') {
176
217
  process.exit(0)
177
218
  }
178
219
  console.log(version)
220
+ if (source) {
221
+ console.log('')
222
+ console.log(`source: local ${source.repo}`)
223
+ console.log(`branch: ${source.branch} commit: ${source.commit}${source.dirty ? ' (dirty)' : ''}`)
224
+ }
179
225
  process.exit(0)
180
226
  }
181
227
 
@@ -223,15 +269,112 @@ async function checkArchivedInstalls() {
223
269
  }
224
270
 
225
271
  // ── Update ───────────────────────────────────────────────────────────────────
226
- if (subcommand === 'update') {
272
+ // Sticky: whichever source is currently installed (recorded in .hskill-source.json
273
+ // inside the global install dir) is what a bare `update` refreshes from. `--local`
274
+ // and `--npm` explicitly switch the source. See docs/superpowers/specs/2026-09-15-hskill-install-source-design.md
275
+ async function updateToNpm(priorSource) {
227
276
  console.log(chalk.dim(' · Updating hskill…'))
228
277
  try {
229
278
  execSync('npm install -g harveyz-skill@latest', { stdio: 'inherit' })
230
- console.log(chalk.green(' ✔ hskill updated'))
231
279
  } catch {
232
280
  console.error(chalk.red(' ✗ Update failed. Try: npm install -g harveyz-skill@latest'))
233
281
  process.exit(1)
234
282
  }
283
+ if (priorSource) {
284
+ const { globalRoot } = await import('../lib/install-source.js')
285
+ const newVersion = JSON.parse(readFileSync(path.join(globalRoot(), 'harveyz-skill', 'package.json'), 'utf8')).version
286
+ console.log(` ${priorSource.version}+local (${priorSource.branch}@${priorSource.commit}) → ${newVersion} (npm)`)
287
+ }
288
+ console.log(chalk.green(' ✔ hskill updated'))
289
+ }
290
+
291
+ async function updateToLocal(repoPath, priorSource) {
292
+ if (!existsSync(repoPath)) {
293
+ console.error(chalk.red(` ✗ 本地来源仓库不存在:${repoPath}`))
294
+ console.error(chalk.dim(' 改用 npm: hskill update --npm'))
295
+ console.error(chalk.dim(` 指向新路径: hskill update --local <新路径>`))
296
+ process.exit(1)
297
+ }
298
+
299
+ const { gitInfo, writeSource, globalRoot } = await import('../lib/install-source.js')
300
+ // git info must be collected before `npm pack` — its `prepack` hook rewrites
301
+ // the tracked .npmignore, which would otherwise poison the dirty check.
302
+ const info = gitInfo(repoPath)
303
+ const origVersion = JSON.parse(readFileSync(path.join(repoPath, 'package.json'), 'utf8')).version
304
+
305
+ console.log(chalk.dim(' · Packing local repo…'))
306
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'hskill-pack-'))
307
+ const packResult = spawnSync('npm', ['pack', '--pack-destination', tmpDir], { cwd: repoPath, encoding: 'utf8' })
308
+ if (packResult.status !== 0) {
309
+ console.error(chalk.red(' ✗ npm pack failed'))
310
+ process.exit(1)
311
+ }
312
+ const tarballName = packResult.stdout.trim().split('\n').pop()
313
+
314
+ console.log(chalk.dim(' · Installing packed tarball…'))
315
+ const installResult = spawnSync('npm', ['install', '-g', path.join(tmpDir, tarballName)], { stdio: 'inherit' })
316
+ if (installResult.status !== 0) {
317
+ console.error(chalk.red(' ✗ Install failed'))
318
+ process.exit(1)
319
+ }
320
+
321
+ const installedPkgPath = path.join(globalRoot(), 'harveyz-skill', 'package.json')
322
+ const installedPkg = JSON.parse(readFileSync(installedPkgPath, 'utf8'))
323
+ installedPkg.version = `${origVersion}+local`
324
+ writeFileSync(installedPkgPath, JSON.stringify(installedPkg, null, 2) + '\n')
325
+
326
+ writeSource({
327
+ repo: repoPath,
328
+ branch: info.branch,
329
+ commit: info.commit,
330
+ dirty: info.dirty,
331
+ version: origVersion,
332
+ installedAt: new Date().toISOString(),
333
+ })
334
+
335
+ if (priorSource === null) {
336
+ console.log(` ${version} (npm) → ${origVersion}+local (${info.branch}@${info.commit})`)
337
+ } else if (priorSource.repo !== repoPath) {
338
+ console.log(` ${priorSource.version}+local (${priorSource.branch}@${priorSource.commit}) → ${origVersion}+local (${info.branch}@${info.commit})`)
339
+ }
340
+
341
+ const npmignoreStatus = execSync('git status --porcelain -- .npmignore', { cwd: repoPath, encoding: 'utf8' }).trim()
342
+ if (npmignoreStatus) {
343
+ console.log(chalk.dim(' · .npmignore changed by prepack — left as-is, review with `git diff .npmignore`'))
344
+ }
345
+
346
+ console.log(chalk.green(' ✔ hskill updated'))
347
+ }
348
+
349
+ if (subcommand === 'update') {
350
+ const updateArgs = args.slice(1)
351
+ const localIdx = updateArgs.indexOf('--local')
352
+ const npmFlag = updateArgs.includes('--npm')
353
+ const localGiven = localIdx !== -1
354
+ const localPath = localGiven ? updateArgs[localIdx + 1] : undefined
355
+
356
+ if (localGiven && npmFlag) {
357
+ console.error(chalk.red(' ✗ --local and --npm are mutually exclusive'))
358
+ process.exit(1)
359
+ }
360
+ if (localGiven && (!localPath || localPath.startsWith('--'))) {
361
+ console.error(chalk.red(' ✗ --local requires a path: hskill update --local <path>'))
362
+ process.exit(1)
363
+ }
364
+
365
+ const { readSource } = await import('../lib/install-source.js')
366
+ const priorSource = readSource()
367
+
368
+ if (npmFlag) {
369
+ await updateToNpm(priorSource)
370
+ } else if (localGiven) {
371
+ await updateToLocal(localPath, priorSource)
372
+ } else if (priorSource) {
373
+ await updateToLocal(priorSource.repo, priorSource)
374
+ } else {
375
+ await updateToNpm(priorSource)
376
+ }
377
+
235
378
  // Run skill rename migrations
236
379
  const { renames = [], skills: skillDefs = [] } = require('../skills-index.json')
237
380
  if (renames.length > 0) {
@@ -276,10 +419,10 @@ if (subcommand === 'list') {
276
419
  const { skills, tools = [] } = require('../skills-index.json')
277
420
  const sorted = [...skills].sort((a, b) => a.bundle.localeCompare(b.bundle) || a.path.split('/').pop().localeCompare(b.path.split('/').pop()))
278
421
  if (jsonFlag) {
279
- console.log(JSON.stringify({
422
+ await emitJson({
280
423
  skills: sorted.map(s => ({ name: s.path.split('/').pop(), path: s.path, bundle: s.bundle, global: s.global ?? false })),
281
424
  tools: tools.map(t => t.name),
282
- }, null, 2))
425
+ })
283
426
  process.exit(0)
284
427
  }
285
428
  const nw = Math.max(...sorted.map(s => s.path.split('/').pop().length), 4)
@@ -380,12 +523,12 @@ if (subcommand === 'status' || subcommand === 'outdated') {
380
523
  return { name: h.name, description: h.description, user: inst.user, project: inst.project }
381
524
  })
382
525
  if (outdatedOnly) {
383
- console.log(JSON.stringify({
526
+ await emitJson({
384
527
  skills: jsonSkills.filter(s => Object.values(s.user).some(v => v.status === 'update') || Object.values(s.project).some(v => v.status === 'update')),
385
528
  tools: jsonTools.filter(t => t.status === 'update'),
386
- }, null, 2))
529
+ })
387
530
  } else {
388
- console.log(JSON.stringify({ skills: jsonSkills, tools: jsonTools, hooks: jsonHooks }, null, 2))
531
+ await emitJson({ skills: jsonSkills, tools: jsonTools, hooks: jsonHooks })
389
532
  }
390
533
  process.exit(0)
391
534
  }
@@ -513,17 +656,17 @@ if (subcommand === 'info') {
513
656
  if (jsonFlag) {
514
657
  if (skill) {
515
658
  const inst = checkInstalled(skill.skillName, skill.version ?? '—')
516
- console.log(JSON.stringify({
659
+ await emitJson({
517
660
  name: skill.skillName, type: 'skill', version: skill.version ?? '—',
518
661
  user: Object.fromEntries(targets.map(t => [t, inst.user[t]])),
519
662
  project: Object.fromEntries(targets.map(t => [t, inst.project[t]])),
520
- }, null, 2))
663
+ })
521
664
  } else {
522
665
  const inst = checkToolInstalled(tool.toolName, tool.srcPath)
523
- console.log(JSON.stringify({
666
+ await emitJson({
524
667
  name: tool.toolName, type: 'tool', version: tool.version ?? '—',
525
668
  installed: inst,
526
- }, null, 2))
669
+ })
527
670
  }
528
671
  process.exit(0)
529
672
  }
@@ -590,7 +733,7 @@ if (subcommand === 'uninstall') {
590
733
  const { removed, failed } = await uninstallTool(nameToRemove, { yes: yesFlag })
591
734
  if (jsonFlag) {
592
735
  console.error = originalError
593
- console.log(JSON.stringify({ removed: removed.length > 0, failed: failed.length > 0 }, null, 2))
736
+ await emitJson({ removed: removed.length > 0, failed: failed.length > 0 })
594
737
  } else if (removed.length > 0) {
595
738
  console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
596
739
  }
@@ -617,7 +760,7 @@ if (subcommand === 'uninstall') {
617
760
  }
618
761
  if (jsonFlag) {
619
762
  console.error = originalError2
620
- console.log(JSON.stringify({ removed: anyRemoved, failed: anyFailed }, null, 2))
763
+ await emitJson({ removed: anyRemoved, failed: anyFailed })
621
764
  } else if (anyRemoved) {
622
765
  console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
623
766
  }
@@ -663,7 +806,7 @@ if (subcommand === 'hooks') {
663
806
  codex: inst.codex,
664
807
  }
665
808
  })
666
- console.log(JSON.stringify({ hooks: out }, null, 2))
809
+ await emitJson({ hooks: out })
667
810
  process.exit(0)
668
811
  }
669
812
  function hookIcon(s) {
@@ -724,7 +867,7 @@ if (subcommand === 'hooks') {
724
867
  const { installed, skipped, failed } = await installHooksForTarget(toInstall, hookTargetArg, hookScopeArg, hookProjectArg, hookForce)
725
868
 
726
869
  if (hookJsonFlag) {
727
- console.log(JSON.stringify({ installed, skipped, failed }, null, 2))
870
+ await emitJson({ installed, skipped, failed })
728
871
  process.exit(failed.length ? 1 : 0)
729
872
  } else {
730
873
  if (installed.length) console.error(chalk.green.bold(`✔ Hooks installed (${hookScopeArg}):`), installed.join(', '))
@@ -746,7 +889,7 @@ if (subcommand === 'hooks') {
746
889
  }
747
890
  const { removed } = await uninstallHook(nameToRemove, hookScopeArg, hookProjectArg)
748
891
  if (hookJsonFlag) {
749
- console.log(JSON.stringify({ removed }, null, 2))
892
+ await emitJson({ removed })
750
893
  } else if (!removed) {
751
894
  console.log(chalk.dim(` · ${nameToRemove} was not installed in ${hookScopeArg} scope`))
752
895
  }
@@ -798,9 +941,9 @@ if (subcommand === 'upgrade') {
798
941
  const nothingUpgraded = Object.keys(summary).length === 0
799
942
  if (jsonFlag) {
800
943
  if (nothingUpgraded) {
801
- console.log(JSON.stringify({ skills: {}, upToDate: true }, null, 2))
944
+ await emitJson({ skills: {}, upToDate: true })
802
945
  } else {
803
- console.log(JSON.stringify({ skills: summary }, null, 2))
946
+ await emitJson({ skills: summary })
804
947
  }
805
948
  } else {
806
949
  if (nothingUpgraded) {
@@ -1512,7 +1655,7 @@ try {
1512
1655
  const out = {}
1513
1656
  if (skillSummary !== null) out.skills = skillSummary
1514
1657
  if (toolSummary !== null) out.tools = toolSummary
1515
- console.log(JSON.stringify(out, null, 2))
1658
+ await emitJson(out)
1516
1659
  } else {
1517
1660
  printSummary(skillSummary, toolSummary)
1518
1661
  }
@@ -0,0 +1,46 @@
1
+ import { execSync } from 'child_process'
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
3
+ import path from 'path'
4
+
5
+ let cachedGlobalRoot = null
6
+
7
+ export function globalRoot() {
8
+ if (process.env.HSKILL_GLOBAL_ROOT) return process.env.HSKILL_GLOBAL_ROOT
9
+ // stdio 'pipe' keeps npm's own stderr off ours: callers that tolerate failure
10
+ // (readSource) would otherwise leak "npm: command not found" into clean output.
11
+ if (!cachedGlobalRoot) cachedGlobalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: 'pipe' }).trim()
12
+ return cachedGlobalRoot
13
+ }
14
+
15
+ function sourceFilePath() {
16
+ return path.join(globalRoot(), 'harveyz-skill', '.hskill-source.json')
17
+ }
18
+
19
+ // null means "npm source" — the default. An unreadable trace file and an
20
+ // unreachable npm both collapse into it: `version` must keep working where npm
21
+ // is off PATH, and a source we cannot read is not a source we can update from.
22
+ export function readSource() {
23
+ let file
24
+ try {
25
+ file = sourceFilePath()
26
+ } catch {
27
+ return null
28
+ }
29
+ if (!existsSync(file)) return null
30
+ try {
31
+ return JSON.parse(readFileSync(file, 'utf8'))
32
+ } catch {
33
+ return null
34
+ }
35
+ }
36
+
37
+ export function writeSource(info) {
38
+ writeFileSync(sourceFilePath(), JSON.stringify(info, null, 2) + '\n')
39
+ }
40
+
41
+ export function gitInfo(repo) {
42
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repo, encoding: 'utf8' }).trim()
43
+ const commit = execSync('git rev-parse --short HEAD', { cwd: repo, encoding: 'utf8' }).trim()
44
+ const dirty = execSync('git status --porcelain', { cwd: repo, encoding: 'utf8' }).trim().length > 0
45
+ return { branch, commit, dirty }
46
+ }
@@ -2,8 +2,8 @@ const DEFAULT_REGISTRY = 'https://registry.npmjs.org'
2
2
  const REGISTRY_TIMEOUT_MS = 5000
3
3
 
4
4
  export function compareVersions(a, b) {
5
- const pa = a.split('.').map(Number)
6
- const pb = b.split('.').map(Number)
5
+ const pa = a.split('+')[0].split('.').map(Number)
6
+ const pb = b.split('+')[0].split('.').map(Number)
7
7
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
8
8
  const diff = (pa[i] || 0) - (pb[i] || 0)
9
9
  if (diff !== 0) return diff
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harveyz-skill",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,6 +62,12 @@
62
62
  "skills/coding/handoff/",
63
63
  "skills/feed/manage-creators/",
64
64
  "skills/feed/capture-opinion/",
65
+ "skills/agent-canvas/agent-canvas-control/",
66
+ "skills/agent-canvas/describe-node/",
67
+ "skills/agent-canvas/relate-node/",
68
+ "skills/agent-canvas/capture-requirement/",
69
+ "skills/agent-canvas/relation-review/",
70
+ "skills/agent-canvas/close-node/",
65
71
  "tools/hub/",
66
72
  "tools/sync-agent/",
67
73
  "tools/browser-fetch/",