@youtyan/code-viewer 0.1.37 → 0.1.38

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/README.md CHANGED
@@ -149,6 +149,20 @@ than the current directory, or `--server <url>` to target a specific server.
149
149
  run `annotate start` again to begin a new session, or pass `--session <id>`
150
150
  to target a specific one.
151
151
 
152
+ ### Agent Skill
153
+
154
+ The package bundles an [Agent Skill](https://code.claude.com/docs/en/skills)
155
+ that teaches AI coding agents when and how to use `annotate`. Install it
156
+ into the current project (`.claude/skills/`):
157
+
158
+ ```sh
159
+ npx -y @youtyan/code-viewer skill install
160
+ ```
161
+
162
+ Or install it once for all projects with `--global`
163
+ (`~/.claude/skills/`). Running the same command again updates an existing
164
+ installation.
165
+
152
166
  ## Development
153
167
 
154
168
  ```sh
@@ -1624,6 +1624,125 @@ var init_annotate_cli = __esm(() => {
1624
1624
  init_server_registry();
1625
1625
  });
1626
1626
 
1627
+ // web-src/server/root.ts
1628
+ import { existsSync as existsSync4 } from "node:fs";
1629
+ import { dirname, join as join4, normalize } from "node:path";
1630
+ import { fileURLToPath } from "node:url";
1631
+ function findRoot(start) {
1632
+ let current = start;
1633
+ for (let i = 0;i < 5; i++) {
1634
+ if (existsSync4(join4(current, "package.json")) && existsSync4(join4(current, "web"))) {
1635
+ return normalize(current);
1636
+ }
1637
+ const parent = dirname(current);
1638
+ if (parent === current)
1639
+ break;
1640
+ current = parent;
1641
+ }
1642
+ return normalize(join4(start, "..", ".."));
1643
+ }
1644
+ var ROOT;
1645
+ var init_root = __esm(() => {
1646
+ ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
1647
+ });
1648
+
1649
+ // web-src/server/skill-cli.ts
1650
+ var exports_skill_cli = {};
1651
+ __export(exports_skill_cli, {
1652
+ runSkillCli: () => runSkillCli,
1653
+ parseSkillArgs: () => parseSkillArgs,
1654
+ installSkill: () => installSkill,
1655
+ SKILL_HELP: () => SKILL_HELP
1656
+ });
1657
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "node:fs";
1658
+ import { homedir as homedir2 } from "node:os";
1659
+ import { join as join5, resolve } from "node:path";
1660
+ function parseSkillArgs(argv) {
1661
+ if (argv.length === 0 || argv.includes("--help") || argv[0] === "help") {
1662
+ return { ok: true, args: { kind: "help" } };
1663
+ }
1664
+ const [command, ...rest] = argv;
1665
+ if (command !== "install") {
1666
+ return { ok: false, error: `unknown skill command: ${command}` };
1667
+ }
1668
+ let global = false;
1669
+ let cwd;
1670
+ for (let i = 0;i < rest.length; i++) {
1671
+ const arg = rest[i];
1672
+ if (arg === "--global") {
1673
+ global = true;
1674
+ } else if (arg === "--cwd") {
1675
+ cwd = rest[++i];
1676
+ if (!cwd)
1677
+ return { ok: false, error: "--cwd requires a directory" };
1678
+ } else {
1679
+ return { ok: false, error: `unknown option: ${arg}` };
1680
+ }
1681
+ }
1682
+ return { ok: true, args: { kind: "install", global, cwd } };
1683
+ }
1684
+ function installSkill(args, deps) {
1685
+ if (!existsSync5(join5(deps.sourceDir, "SKILL.md"))) {
1686
+ return {
1687
+ ok: false,
1688
+ error: `bundled skill not found at ${deps.sourceDir}`
1689
+ };
1690
+ }
1691
+ const base = args.global ? deps.homeDir : resolve(args.cwd ?? deps.projectDir);
1692
+ const target = join5(base, ".claude", "skills", SKILL_NAME);
1693
+ const action = existsSync5(target) ? "updated" : "installed";
1694
+ try {
1695
+ mkdirSync3(target, { recursive: true });
1696
+ cpSync(deps.sourceDir, target, { recursive: true });
1697
+ } catch (error) {
1698
+ return { ok: false, error: String(error) };
1699
+ }
1700
+ return { ok: true, action, target };
1701
+ }
1702
+ function runSkillCli(argv) {
1703
+ const parsed = parseSkillArgs(argv);
1704
+ if (parsed.ok === false) {
1705
+ console.error(parsed.error);
1706
+ console.error('Run "code-viewer skill --help" for usage.');
1707
+ process.exit(1);
1708
+ }
1709
+ if (parsed.args.kind === "help") {
1710
+ console.log(SKILL_HELP);
1711
+ return;
1712
+ }
1713
+ const result = installSkill(parsed.args, {
1714
+ sourceDir: join5(ROOT, "skills", SKILL_NAME),
1715
+ homeDir: homedir2(),
1716
+ projectDir: process.cwd()
1717
+ });
1718
+ if (result.ok === false) {
1719
+ console.error(result.error);
1720
+ process.exit(1);
1721
+ }
1722
+ console.log(`${result.action}: ${result.target}`);
1723
+ if (result.action === "installed") {
1724
+ console.log("Re-run the same command anytime to update the skill.");
1725
+ }
1726
+ }
1727
+ var SKILL_NAME = "code-viewer-annotate", SKILL_HELP;
1728
+ var init_skill_cli = __esm(() => {
1729
+ init_root();
1730
+ SKILL_HELP = `code-viewer skill — manage the bundled agent skill
1731
+
1732
+ Usage:
1733
+ code-viewer skill install [--global] [--cwd <dir>]
1734
+
1735
+ Installs the ${SKILL_NAME} skill (SKILL.md for AI coding agents) into
1736
+ .claude/skills/ of the current project, or into ~/.claude/skills/ with
1737
+ --global. Running install again overwrites the files, so the same command
1738
+ also updates an existing installation.
1739
+
1740
+ Options:
1741
+ --global install into ~/.claude/skills/ instead of the project
1742
+ --cwd <dir> project directory to install into (ignored with --global)
1743
+ `;
1744
+ });
1745
+
1627
1746
  // web-src/core/directory-name.ts
1628
1747
  function normalizeNewDirectoryName(name) {
1629
1748
  if (typeof name !== "string")
@@ -1650,7 +1769,7 @@ var init_routes = __esm(() => {
1650
1769
 
1651
1770
  // web-src/server/cache.ts
1652
1771
  import { lstatSync as lstatSync2 } from "node:fs";
1653
- import { join as join4 } from "node:path";
1772
+ import { join as join6 } from "node:path";
1654
1773
  function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
1655
1774
  return !!cached && now - cached.storedAt <= ttlMs;
1656
1775
  }
@@ -1665,7 +1784,7 @@ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MA
1665
1784
  }
1666
1785
  function worktreeFileSignature(path, cwd) {
1667
1786
  try {
1668
- const stats = lstatSync2(join4(cwd, path));
1787
+ const stats = lstatSync2(join6(cwd, path));
1669
1788
  const inode = "ino" in stats ? stats.ino : 0;
1670
1789
  return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
1671
1790
  } catch {
@@ -1921,28 +2040,6 @@ function collectLineRangeFromIndexedText(text, index, start, end) {
1921
2040
  return { lines, total: index.total, complete: end >= index.total };
1922
2041
  }
1923
2042
 
1924
- // web-src/server/root.ts
1925
- import { existsSync as existsSync4 } from "node:fs";
1926
- import { dirname, join as join5, normalize } from "node:path";
1927
- import { fileURLToPath } from "node:url";
1928
- function findRoot(start) {
1929
- let current = start;
1930
- for (let i = 0;i < 5; i++) {
1931
- if (existsSync4(join5(current, "package.json")) && existsSync4(join5(current, "web"))) {
1932
- return normalize(current);
1933
- }
1934
- const parent = dirname(current);
1935
- if (parent === current)
1936
- break;
1937
- current = parent;
1938
- }
1939
- return normalize(join5(start, "..", ".."));
1940
- }
1941
- var ROOT;
1942
- var init_root = __esm(() => {
1943
- ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
1944
- });
1945
-
1946
2043
  // web-src/server/search.ts
1947
2044
  function normalizeGrepMax(value) {
1948
2045
  const parsed = Number(value || "");
@@ -2069,7 +2166,7 @@ import {
2069
2166
  readdirSync as nodeReaddirSync,
2070
2167
  watch as nodeWatch
2071
2168
  } from "node:fs";
2072
- import { join as join6, relative } from "node:path";
2169
+ import { join as join7, relative } from "node:path";
2073
2170
  function normalizeRelativePath(path) {
2074
2171
  return path.replace(/\\/g, "/").replace(/^\/+/, "");
2075
2172
  }
@@ -2152,7 +2249,7 @@ function startWorktreeUpdateWatch(options) {
2152
2249
  for (const entry of entries) {
2153
2250
  if (!entry.isDirectory())
2154
2251
  continue;
2155
- children.push(join6(dir, entry.name));
2252
+ children.push(join7(dir, entry.name));
2156
2253
  }
2157
2254
  return children;
2158
2255
  };
@@ -2181,10 +2278,10 @@ function startWorktreeUpdateWatch(options) {
2181
2278
  scheduleUpdate();
2182
2279
  return;
2183
2280
  }
2184
- const changed = normalizeRelativePath(join6(rel, filename.toString()));
2281
+ const changed = normalizeRelativePath(join7(rel, filename.toString()));
2185
2282
  if (ignored(changed))
2186
2283
  return;
2187
- const fullChangedPath = join6(options.root, changed);
2284
+ const fullChangedPath = join7(options.root, changed);
2188
2285
  if (!isInsideRoot(options.root, fullChangedPath))
2189
2286
  return;
2190
2287
  const known = watchers.has(fullChangedPath);
@@ -2243,9 +2340,9 @@ var exports_preview = {};
2243
2340
  import {
2244
2341
  closeSync,
2245
2342
  constants,
2246
- existsSync as existsSync5,
2343
+ existsSync as existsSync6,
2247
2344
  lstatSync as lstatSync4,
2248
- mkdirSync as mkdirSync3,
2345
+ mkdirSync as mkdirSync4,
2249
2346
  openSync,
2250
2347
  readFileSync as readFileSync5,
2251
2348
  realpathSync as realpathSync2,
@@ -2255,8 +2352,8 @@ import {
2255
2352
  watch,
2256
2353
  writeFileSync as writeFileSync3
2257
2354
  } from "node:fs";
2258
- import { homedir as homedir2 } from "node:os";
2259
- import { basename as basename2, dirname as dirname2, extname, join as join7, relative as relative2 } from "node:path";
2355
+ import { homedir as homedir3 } from "node:os";
2356
+ import { basename as basename2, dirname as dirname2, extname, join as join8, relative as relative2 } from "node:path";
2260
2357
  function parseCli() {
2261
2358
  const rest = [];
2262
2359
  for (let i = 2;i < process.argv.length; i++) {
@@ -2395,8 +2492,8 @@ function staticFile(pathname) {
2395
2492
  const spec = map[pathname];
2396
2493
  if (!spec)
2397
2494
  return null;
2398
- const full = join7(WEB_ROOT, spec[0]);
2399
- if (!existsSync5(full))
2495
+ const full = join8(WEB_ROOT, spec[0]);
2496
+ if (!existsSync6(full))
2400
2497
  return text("not found", 404);
2401
2498
  return new Response(readFileSync5(full), {
2402
2499
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
@@ -2629,8 +2726,8 @@ function parseScopeExcludeNamesQuery(value) {
2629
2726
  return normalizeScopeExcludeNames(names);
2630
2727
  }
2631
2728
  function loadProjectConfig() {
2632
- const full = join7(cwd, ".code-viewer.json");
2633
- if (!existsSync5(full))
2729
+ const full = join8(cwd, ".code-viewer.json");
2730
+ if (!existsSync6(full))
2634
2731
  return null;
2635
2732
  let realCwd;
2636
2733
  let realConfig;
@@ -2694,8 +2791,8 @@ function safeWorktreePath(path) {
2694
2791
  return null;
2695
2792
  if (isGitInternalPath(path))
2696
2793
  return null;
2697
- const full = join7(cwd, path);
2698
- if (!existsSync5(full))
2794
+ const full = join8(cwd, path);
2795
+ if (!existsSync6(full))
2699
2796
  return null;
2700
2797
  let realCwd;
2701
2798
  let realFull;
@@ -2713,7 +2810,7 @@ function safeWorktreePath(path) {
2713
2810
  return realFull;
2714
2811
  }
2715
2812
  function worktreePath(path) {
2716
- return join7(cwd, path);
2813
+ return join8(cwd, path);
2717
2814
  }
2718
2815
  function safeOpenWorktreePath(path) {
2719
2816
  if (path === "") {
@@ -3476,10 +3573,10 @@ async function handleUploadFiles(req) {
3476
3573
  total += file.size;
3477
3574
  if (total > MAX_UPLOAD_TOTAL_BYTES)
3478
3575
  return text("upload too large", 413);
3479
- const target = join7(realDir, safeName);
3576
+ const target = join8(realDir, safeName);
3480
3577
  if (relative2(realDir, dirname2(target)) !== "")
3481
3578
  return text("invalid filename", 400);
3482
- if (existsSync5(target))
3579
+ if (existsSync6(target))
3483
3580
  return text("file exists", 409);
3484
3581
  uploads.push({ file, name: safeName, target });
3485
3582
  }
@@ -3597,11 +3694,11 @@ function triggerUpdate() {
3597
3694
  sendSse("update");
3598
3695
  }
3599
3696
  function moveMacPathIntoTrash(path) {
3600
- const trashDir = join7(homedir2(), ".Trash");
3697
+ const trashDir = join8(homedir3(), ".Trash");
3601
3698
  const base = basename2(path) || "code-viewer-trash-item";
3602
- const target = join7(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
3699
+ const target = join8(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
3603
3700
  try {
3604
- mkdirSync3(trashDir, { recursive: true });
3701
+ mkdirSync4(trashDir, { recursive: true });
3605
3702
  renameSync2(path, target);
3606
3703
  return { ok: true, trashPath: target };
3607
3704
  } catch (error) {
@@ -3633,19 +3730,19 @@ function restoreTrashPath(originalPath, trashPath) {
3633
3730
  if (!parentFullPath)
3634
3731
  return { ok: false, error: "invalid restore target" };
3635
3732
  const original = worktreePath(originalPath);
3636
- if (existsSync5(original))
3733
+ if (existsSync6(original))
3637
3734
  return { ok: false, error: "restore target exists" };
3638
3735
  if (trashPath) {
3639
3736
  if (process.platform !== "darwin")
3640
3737
  return { ok: false, error: "invalid trash handle" };
3641
- if (!existsSync5(trashPath))
3738
+ if (!existsSync6(trashPath))
3642
3739
  return { ok: false, error: "trash item not found" };
3643
3740
  try {
3644
- const trashRoot = join7(homedir2(), ".Trash");
3741
+ const trashRoot = join8(homedir3(), ".Trash");
3645
3742
  const trashRelative = relative2(trashRoot, trashPath);
3646
3743
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
3647
3744
  return { ok: false, error: "invalid trash handle" };
3648
- mkdirSync3(dirname2(original), { recursive: true });
3745
+ mkdirSync4(dirname2(original), { recursive: true });
3649
3746
  renameSync2(trashPath, original);
3650
3747
  return { ok: true };
3651
3748
  } catch (error) {
@@ -3791,11 +3888,11 @@ async function handleCreateDirectory(req) {
3791
3888
  const targetPath = dir ? `${dir}/${name}` : name;
3792
3889
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
3793
3890
  return text("invalid target", 400);
3794
- const target = join7(parent, name);
3795
- if (existsSync5(target))
3891
+ const target = join8(parent, name);
3892
+ if (existsSync6(target))
3796
3893
  return text("already exists", 409);
3797
3894
  try {
3798
- mkdirSync3(target, { recursive: false });
3895
+ mkdirSync4(target, { recursive: false });
3799
3896
  } catch (error) {
3800
3897
  if (error.code === "EEXIST")
3801
3898
  return text("already exists", 409);
@@ -3973,8 +4070,8 @@ var init_preview = __esm(async () => {
3973
4070
  init_search();
3974
4071
  init_server_registry();
3975
4072
  init_worktree_watcher();
3976
- WEB_ROOT = join7(ROOT, "web");
3977
- VERSION = JSON.parse(readFileSync5(join7(ROOT, "package.json"), "utf8")).version;
4073
+ WEB_ROOT = join8(ROOT, "web");
4074
+ VERSION = JSON.parse(readFileSync5(join8(ROOT, "package.json"), "utf8")).version;
3978
4075
  DEFAULT_ARGS = ["HEAD"];
3979
4076
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
3980
4077
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -4172,6 +4269,9 @@ data: ok
4172
4269
  if (process.argv[2] === "annotate") {
4173
4270
  const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
4174
4271
  await runAnnotateCli2(process.argv.slice(3));
4272
+ } else if (process.argv[2] === "skill") {
4273
+ const { runSkillCli: runSkillCli2 } = await Promise.resolve().then(() => (init_skill_cli(), exports_skill_cli));
4274
+ runSkillCli2(process.argv.slice(3));
4175
4275
  } else {
4176
4276
  await init_preview().then(() => exports_preview);
4177
4277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,8 @@
23
23
  "dist",
24
24
  "web",
25
25
  "README.md",
26
- "LICENSE"
26
+ "LICENSE",
27
+ "skills"
27
28
  ],
28
29
  "scripts": {
29
30
  "build": "bun run build:web && bun run build:server",
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: code-viewer-annotate
3
+ description: Use when walking a human through code in their browser with code-viewer annotations - explaining changes you just made, guiding a code review in reading order, or onboarding walkthroughs. Triggers on "annotate", "ウォークスルー", "コードを説明して", "変更を解説して", "レビューを案内して", "walkthrough", "explain this change in the browser".
4
+ ---
5
+
6
+ # code-viewer annotate
7
+
8
+ Create browser walkthroughs for a human: each annotation jumps every open
9
+ code-viewer tab to a file location and renders your explanation directly
10
+ under the annotated lines. The human can also replay a session with
11
+ text-to-speech playback.
12
+
13
+ ## When to use
14
+
15
+ - Explaining a change you just made (per-file, per-hunk commentary)
16
+ - Guiding a code review: point at the risky lines, in reading order
17
+ - Onboarding walkthroughs: "how does feature X flow through the code"
18
+
19
+ ## Requirements
20
+
21
+ - A code-viewer server must already be running for the repository
22
+ (the human starts it with: `code-viewer`). The CLI never starts one.
23
+ - Run from inside the repository, or pass `--cwd <repo>`.
24
+ - If `code-viewer` is not on PATH, prefix every command with
25
+ `npx -y @youtyan/code-viewer`.
26
+
27
+ ## Workflow
28
+
29
+ 1. Start a session per walkthrough topic (the title is shown to the human):
30
+
31
+ ```sh
32
+ code-viewer annotate start --title "How the cache invalidation works"
33
+ ```
34
+
35
+ 2. Add annotations in READING ORDER (the order the human should follow):
36
+
37
+ ```sh
38
+ code-viewer annotate add --file src/cache.ts --line 42-58 --body "..."
39
+ ```
40
+
41
+ ## Full reference
42
+
43
+ Before composing annotations, read the complete agent guide (all commands,
44
+ options, body formatting rules):
45
+
46
+ ```sh
47
+ code-viewer annotate agent-help
48
+ ```