@stupidloud/codegraph 0.7.15 → 0.7.20

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 (115) hide show
  1. package/README.md +44 -10
  2. package/dist/bin/codegraph.js +102 -24
  3. package/dist/bin/codegraph.js.map +1 -1
  4. package/dist/bin/node-version-check.d.ts +3 -0
  5. package/dist/bin/node-version-check.d.ts.map +1 -1
  6. package/dist/bin/node-version-check.js +5 -2
  7. package/dist/bin/node-version-check.js.map +1 -1
  8. package/dist/bin/uninstall.d.ts +7 -7
  9. package/dist/bin/uninstall.d.ts.map +1 -1
  10. package/dist/bin/uninstall.js +23 -135
  11. package/dist/bin/uninstall.js.map +1 -1
  12. package/dist/context/index.d.ts.map +1 -1
  13. package/dist/context/index.js +4 -2
  14. package/dist/context/index.js.map +1 -1
  15. package/dist/db/queries.d.ts.map +1 -1
  16. package/dist/db/queries.js +7 -1
  17. package/dist/db/queries.js.map +1 -1
  18. package/dist/extraction/index.d.ts +1 -1
  19. package/dist/extraction/index.d.ts.map +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/installer/claude-md-template.d.ts +10 -6
  24. package/dist/installer/claude-md-template.d.ts.map +1 -1
  25. package/dist/installer/claude-md-template.js +15 -40
  26. package/dist/installer/claude-md-template.js.map +1 -1
  27. package/dist/installer/config-writer.d.ts +17 -24
  28. package/dist/installer/config-writer.d.ts.map +1 -1
  29. package/dist/installer/config-writer.js +44 -239
  30. package/dist/installer/config-writer.js.map +1 -1
  31. package/dist/installer/index.d.ts +45 -4
  32. package/dist/installer/index.d.ts.map +1 -1
  33. package/dist/installer/index.js +216 -79
  34. package/dist/installer/index.js.map +1 -1
  35. package/dist/installer/instructions-template.d.ts +28 -0
  36. package/dist/installer/instructions-template.d.ts.map +1 -0
  37. package/dist/installer/instructions-template.js +63 -0
  38. package/dist/installer/instructions-template.js.map +1 -0
  39. package/dist/installer/targets/claude.d.ts +27 -0
  40. package/dist/installer/targets/claude.d.ts.map +1 -0
  41. package/dist/installer/targets/claude.js +246 -0
  42. package/dist/installer/targets/claude.js.map +1 -0
  43. package/dist/installer/targets/codex.d.ts +18 -0
  44. package/dist/installer/targets/codex.d.ts.map +1 -0
  45. package/dist/installer/targets/codex.js +185 -0
  46. package/dist/installer/targets/codex.js.map +1 -0
  47. package/dist/installer/targets/cursor.d.ts +35 -0
  48. package/dist/installer/targets/cursor.d.ts.map +1 -0
  49. package/dist/installer/targets/cursor.js +229 -0
  50. package/dist/installer/targets/cursor.js.map +1 -0
  51. package/dist/installer/targets/opencode.d.ts +30 -0
  52. package/dist/installer/targets/opencode.d.ts.map +1 -0
  53. package/dist/installer/targets/opencode.js +235 -0
  54. package/dist/installer/targets/opencode.js.map +1 -0
  55. package/dist/installer/targets/registry.d.ts +35 -0
  56. package/dist/installer/targets/registry.d.ts.map +1 -0
  57. package/dist/installer/targets/registry.js +83 -0
  58. package/dist/installer/targets/registry.js.map +1 -0
  59. package/dist/installer/targets/shared.d.ts +77 -0
  60. package/dist/installer/targets/shared.d.ts.map +1 -0
  61. package/dist/installer/targets/shared.js +246 -0
  62. package/dist/installer/targets/shared.js.map +1 -0
  63. package/dist/installer/targets/toml.d.ts +52 -0
  64. package/dist/installer/targets/toml.d.ts.map +1 -0
  65. package/dist/installer/targets/toml.js +147 -0
  66. package/dist/installer/targets/toml.js.map +1 -0
  67. package/dist/installer/targets/types.d.ts +116 -0
  68. package/dist/installer/targets/types.d.ts.map +1 -0
  69. package/dist/installer/targets/types.js +16 -0
  70. package/dist/installer/targets/types.js.map +1 -0
  71. package/dist/mcp/index.d.ts +4 -0
  72. package/dist/mcp/index.d.ts.map +1 -1
  73. package/dist/mcp/index.js +34 -9
  74. package/dist/mcp/index.js.map +1 -1
  75. package/dist/mcp/server-instructions.d.ts +1 -1
  76. package/dist/mcp/server-instructions.d.ts.map +1 -1
  77. package/dist/mcp/server-instructions.js +6 -6
  78. package/dist/mcp/tools.d.ts +61 -5
  79. package/dist/mcp/tools.d.ts.map +1 -1
  80. package/dist/mcp/tools.js +389 -81
  81. package/dist/mcp/tools.js.map +1 -1
  82. package/dist/search/query-utils.d.ts.map +1 -1
  83. package/dist/search/query-utils.js +29 -26
  84. package/dist/search/query-utils.js.map +1 -1
  85. package/dist/ui/glyphs.d.ts +42 -0
  86. package/dist/ui/glyphs.d.ts.map +1 -0
  87. package/dist/ui/glyphs.js +78 -0
  88. package/dist/ui/glyphs.js.map +1 -0
  89. package/dist/ui/shimmer-progress.d.ts +1 -0
  90. package/dist/ui/shimmer-progress.d.ts.map +1 -1
  91. package/dist/ui/shimmer-progress.js +7 -0
  92. package/dist/ui/shimmer-progress.js.map +1 -1
  93. package/dist/ui/shimmer-worker.js +20 -11
  94. package/dist/ui/shimmer-worker.js.map +1 -1
  95. package/dist/ui/types.d.ts +1 -0
  96. package/dist/ui/types.d.ts.map +1 -1
  97. package/dist/vectors/embedder.d.ts +11 -1
  98. package/dist/vectors/embedder.d.ts.map +1 -1
  99. package/dist/vectors/embedder.js +48 -18
  100. package/dist/vectors/embedder.js.map +1 -1
  101. package/dist/vectors/index.d.ts +1 -1
  102. package/dist/vectors/index.d.ts.map +1 -1
  103. package/dist/vectors/index.js.map +1 -1
  104. package/dist/vectors/manager.d.ts +5 -0
  105. package/dist/vectors/manager.d.ts.map +1 -1
  106. package/dist/vectors/manager.js +44 -23
  107. package/dist/vectors/manager.js.map +1 -1
  108. package/package.json +2 -1
  109. package/scripts/agent-eval/itrun.sh +107 -0
  110. package/scripts/agent-eval/parse-run.mjs +45 -0
  111. package/scripts/agent-eval/parse-session.mjs +93 -0
  112. package/scripts/agent-eval/run-agent.sh +34 -0
  113. package/scripts/extract-release-notes.mjs +130 -0
  114. package/scripts/local-install.sh +41 -0
  115. package/scripts/release.sh +68 -0
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // Parse the newest Claude Code session log for a project + its subagent logs,
3
+ // and report the tool-call breakdown (main + subagents). Works for interactive
4
+ // runs (driven via itrun.sh) — Claude Code writes full transcripts to
5
+ // ~/.claude/projects/<escaped-cwd>/<session>.jsonl with subagents/ alongside.
6
+ import { readFileSync, readdirSync, statSync, existsSync, realpathSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+
10
+ const projectArg = process.argv[2];
11
+ if (!projectArg) { console.error('usage: parse-session.mjs <project-dir>'); process.exit(1); }
12
+
13
+ // Claude Code escapes the (real) cwd by replacing every "/" with "-".
14
+ const real = realpathSync(projectArg);
15
+ const escaped = real.replace(/\//g, '-');
16
+ const projDir = join(homedir(), '.claude', 'projects', escaped);
17
+ if (!existsSync(projDir)) { console.error('no session logs at', projDir); process.exit(1); }
18
+
19
+ // Newest top-level session .jsonl
20
+ const sessions = readdirSync(projDir)
21
+ .filter(f => f.endsWith('.jsonl'))
22
+ .map(f => ({ f, m: statSync(join(projDir, f)).mtimeMs }))
23
+ .sort((a, b) => b.m - a.m);
24
+ if (sessions.length === 0) { console.error('no .jsonl sessions in', projDir); process.exit(1); }
25
+ const sessionId = sessions[0].f.replace('.jsonl', '');
26
+
27
+ function tally(file) {
28
+ const counts = {};
29
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
30
+ if (!line) continue;
31
+ let ev; try { ev = JSON.parse(line); } catch { continue; }
32
+ const content = ev.message?.content;
33
+ if (!Array.isArray(content)) continue;
34
+ for (const b of content) {
35
+ if (b.type === 'tool_use') counts[b.name] = (counts[b.name] || 0) + 1;
36
+ }
37
+ }
38
+ return counts;
39
+ }
40
+
41
+ // Sum token usage from a transcript. The TUI's "Done (…Xk tokens…)" line only
42
+ // covers a subagent's throughput; this works for main-thread runs too and is
43
+ // consistent across both paths. `gen` = output, `fresh` = uncached input
44
+ // (input + cache_creation), `cached` = cache reads (≈free), `total` = all.
45
+ function sumTokens(file) {
46
+ const t = { gen: 0, fresh: 0, cached: 0 };
47
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
48
+ if (!line) continue;
49
+ let ev; try { ev = JSON.parse(line); } catch { continue; }
50
+ const u = ev.message?.usage;
51
+ if (!u) continue;
52
+ t.gen += u.output_tokens || 0;
53
+ t.fresh += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
54
+ t.cached += u.cache_read_input_tokens || 0;
55
+ }
56
+ return t;
57
+ }
58
+
59
+ const mainCounts = tally(join(projDir, sessionId + '.jsonl'));
60
+
61
+ // Subagent transcripts live under <session>/subagents/*.jsonl
62
+ const subDir = join(projDir, sessionId, 'subagents');
63
+ const subCounts = {};
64
+ let subAgentFiles = 0;
65
+ if (existsSync(subDir)) {
66
+ for (const f of readdirSync(subDir).filter(f => f.endsWith('.jsonl'))) {
67
+ subAgentFiles++;
68
+ const c = tally(join(subDir, f));
69
+ for (const [k, v] of Object.entries(c)) subCounts[k] = (subCounts[k] || 0) + v;
70
+ }
71
+ }
72
+
73
+ const fmt = (counts) => Object.entries(counts).sort((a, b) => b[1] - a[1])
74
+ .map(([k, v]) => ` ${String(v).padStart(3)} ${k}`).join('\n') || ' (none)';
75
+
76
+ console.log(`session: ${sessionId}`);
77
+ console.log(`\nMAIN thread tools:\n${fmt(mainCounts)}`);
78
+ console.log(`\nSUBAGENT tools (${subAgentFiles} subagent transcript${subAgentFiles === 1 ? '' : 's'}):\n${fmt(subCounts)}`);
79
+
80
+ const explore = subCounts['mcp__codegraph__codegraph_explore'] || mainCounts['mcp__codegraph__codegraph_explore'] || 0;
81
+ const reads = (subCounts['Read'] || 0) + (mainCounts['Read'] || 0);
82
+ const greps = (subCounts['Grep'] || 0) + (mainCounts['Grep'] || 0) + (subCounts['Bash'] || 0) + (mainCounts['Bash'] || 0);
83
+ console.log(`\nVERDICT: codegraph_explore used ${explore}x | Read ${reads} | Grep/Bash ${greps}`);
84
+
85
+ // Token totals (main + subagents), consistent across main-thread and subagent runs.
86
+ const tok = { gen: 0, fresh: 0, cached: 0 };
87
+ const addTok = (t) => { tok.gen += t.gen; tok.fresh += t.fresh; tok.cached += t.cached; };
88
+ addTok(sumTokens(join(projDir, sessionId + '.jsonl')));
89
+ if (existsSync(subDir)) {
90
+ for (const f of readdirSync(subDir).filter(f => f.endsWith('.jsonl'))) addTok(sumTokens(join(subDir, f)));
91
+ }
92
+ const k = (n) => (n / 1000).toFixed(1) + 'k';
93
+ console.log(`TOKENS: gen ${k(tok.gen)} | fresh-in ${k(tok.fresh)} | cached-in ${k(tok.cached)} | billable≈ ${k(tok.gen + tok.fresh)}`);
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # Headless Claude Code run against a repo with codegraph MCP, capturing the
3
+ # full stream-json so we can see tool calls + token usage. Complements the
4
+ # interactive itrun.sh: headless gives a clean per-tool breakdown + exact
5
+ # tokens/cost, but defaults to the general-purpose subagent (not Explore).
6
+ # To force the Explore path, ask for it in the prompt.
7
+ #
8
+ # Usage: run-agent.sh <repo-path> <label> "<prompt>"
9
+ # Env: AGENT_EVAL_OUT (default /tmp/agent-eval), CG_BIN (codegraph dist binary)
10
+ set -uo pipefail
11
+
12
+ REPO="$1"; LABEL="$2"; PROMPT="$3"
13
+ CG_BIN="${CG_BIN:-$(command -v codegraph || echo /usr/local/bin/codegraph)}"
14
+ OUT_DIR="${AGENT_EVAL_OUT:-/tmp/agent-eval}"; mkdir -p "$OUT_DIR"
15
+ OUT="$OUT_DIR/run-${LABEL}.jsonl"
16
+
17
+ MCP_CONFIG=$(cat <<JSON
18
+ {"mcpServers":{"codegraph":{"command":"${CG_BIN}","args":["serve","--mcp","--path","${REPO}"]}}}
19
+ JSON
20
+ )
21
+
22
+ echo "→ running [$LABEL] in $REPO"
23
+ cd "$REPO" || exit 1
24
+
25
+ claude -p "$PROMPT" \
26
+ --output-format stream-json --verbose \
27
+ --permission-mode bypassPermissions \
28
+ --model opus \
29
+ --max-budget-usd 2 \
30
+ --strict-mcp-config --mcp-config "$MCP_CONFIG" \
31
+ > "$OUT" 2>"$OUT_DIR/run-${LABEL}.err"
32
+
33
+ echo "exit: $? | wrote $OUT ($(wc -l < "$OUT") lines)"
34
+ node "$(cd "$(dirname "$0")" && pwd)/parse-run.mjs" "$OUT" 2>/dev/null || true
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extract a release-notes block from CHANGELOG.md for a given version
4
+ * (or unwrap text supplied on stdin), then join hard-wrapped paragraphs.
5
+ *
6
+ * Why: GitHub renders release-note Markdown with GFM hard breaks, so
7
+ * every `\n` becomes `<br>`. The CHANGELOG is hard-wrapped at ~75
8
+ * chars for readable diffs, which then renders as awkward visible
9
+ * line breaks on the release page. This script joins indented
10
+ * continuation lines into a single line per bullet so the GFM
11
+ * renderer produces clean paragraphs.
12
+ *
13
+ * Repo-level CHANGELOG.md viewing is unaffected (CommonMark treats
14
+ * newlines as spaces there).
15
+ *
16
+ * Usage:
17
+ * extract-release-notes.mjs <version> # read CHANGELOG.md
18
+ * extract-release-notes.mjs --stdin # read from stdin (any text)
19
+ */
20
+
21
+ import { readFileSync } from 'fs';
22
+
23
+ const arg = process.argv[2];
24
+ if (!arg) {
25
+ console.error('usage: extract-release-notes.mjs <version> | --stdin');
26
+ process.exit(1);
27
+ }
28
+
29
+ let block;
30
+ if (arg === '--stdin') {
31
+ block = readFileSync(0, 'utf8').replace(/\r\n?/g, '\n').split('\n');
32
+ } else {
33
+ const version = arg;
34
+ const escaped = version.replace(/\./g, '\\.');
35
+ const headerRe = new RegExp(`^## \\[${escaped}\\]`);
36
+ const anyHeaderRe = /^## \[/;
37
+ const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n');
38
+ const start = lines.findIndex((l) => headerRe.test(l));
39
+ if (start === -1) {
40
+ console.error(`no '## [${version}]' entry found in CHANGELOG.md`);
41
+ process.exit(1);
42
+ }
43
+ const after = lines.findIndex((l, i) => i > start && anyHeaderRe.test(l));
44
+ block = lines.slice(start, after === -1 ? lines.length : after);
45
+ }
46
+
47
+ // Track a stack of `{ indent: number }` frames so a continuation line
48
+ // can attach to the right ancestor. Handles the post-nested-list
49
+ // continuation pattern:
50
+ //
51
+ // - top-level
52
+ // - nested
53
+ // back to top-level <- 2-space indent, joins the top-level bullet
54
+ const out = [];
55
+ let buf = '';
56
+ let stack = [];
57
+
58
+ function flushBuf() {
59
+ if (buf !== '') {
60
+ out.push(buf);
61
+ buf = '';
62
+ }
63
+ }
64
+
65
+ function leadingSpaces(s) {
66
+ const m = s.match(/^(\s*)/);
67
+ return m ? m[1].length : 0;
68
+ }
69
+
70
+ // Bullets: `-`, `*`, `digit.` only. `+` is intentionally excluded — the
71
+ // CHANGELOG uses literal `+` inline (`config + instructions`) and we
72
+ // don't want to misread those as nested bullets.
73
+ const listItemRe = /^(\s*)([-*]|\d+\.)\s+/;
74
+ const fenceRe = /^\s*```/;
75
+
76
+ let inFence = false;
77
+
78
+ for (const line of block) {
79
+ // Fenced code blocks: pass through verbatim, no joining.
80
+ if (fenceRe.test(line)) {
81
+ flushBuf();
82
+ stack = [];
83
+ out.push(line);
84
+ inFence = !inFence;
85
+ continue;
86
+ }
87
+ if (inFence) {
88
+ out.push(line);
89
+ continue;
90
+ }
91
+ if (/^\s*$/.test(line)) {
92
+ flushBuf();
93
+ out.push('');
94
+ continue;
95
+ }
96
+ if (/^#/.test(line)) {
97
+ flushBuf();
98
+ stack = [];
99
+ out.push(line);
100
+ continue;
101
+ }
102
+ const itemMatch = line.match(listItemRe);
103
+ if (itemMatch) {
104
+ flushBuf();
105
+ const indent = itemMatch[1].length;
106
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
107
+ stack.pop();
108
+ }
109
+ stack.push({ indent });
110
+ buf = line;
111
+ continue;
112
+ }
113
+ if (/^\s/.test(line)) {
114
+ const indent = leadingSpaces(line);
115
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
116
+ flushBuf();
117
+ stack.pop();
118
+ }
119
+ const trimmed = line.replace(/^\s+/, '');
120
+ buf = buf === '' ? trimmed : `${buf} ${trimmed}`;
121
+ continue;
122
+ }
123
+ flushBuf();
124
+ stack = [];
125
+ out.push(line);
126
+ }
127
+ flushBuf();
128
+
129
+ process.stdout.write(out.join('\n'));
130
+ if (!out[out.length - 1]?.endsWith('\n')) process.stdout.write('\n');
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # Build the current branch and link it as the global `codegraph` for
3
+ # hands-on testing. Replaces any existing global install for as long
4
+ # as the symlink is in place.
5
+ #
6
+ # Usage:
7
+ # ./scripts/local-install.sh # build + link
8
+ # ./scripts/local-install.sh --undo # unlink + restore the published version
9
+
10
+ set -euo pipefail
11
+
12
+ cd "$(dirname "$0")/.."
13
+
14
+ PKG=$(node -p "require('./package.json').name")
15
+ VERSION=$(node -p "require('./package.json').version")
16
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
17
+
18
+ if [ "${1:-}" = "--undo" ]; then
19
+ echo "→ unlinking ${PKG}"
20
+ npm unlink -g "${PKG}" >/dev/null 2>&1 || true
21
+ echo "→ reinstalling published ${PKG}"
22
+ npm install -g "${PKG}"
23
+ echo "done: global codegraph -> $(command -v codegraph)"
24
+ exit 0
25
+ fi
26
+
27
+ echo "→ building ${PKG} ${VERSION} (${BRANCH})"
28
+ npm run build
29
+
30
+ echo "→ linking globally"
31
+ npm link
32
+
33
+ LINKED=$(command -v codegraph || echo "(not on PATH)")
34
+ echo
35
+ echo "✓ global codegraph now points to this branch"
36
+ echo " binary: ${LINKED}"
37
+ echo " branch: ${BRANCH}"
38
+ echo " version: ${VERSION}"
39
+ echo
40
+ echo "To restore the published version:"
41
+ echo " ./scripts/local-install.sh --undo"
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Tag the current commit with the version in package.json and publish a
3
+ # matching GitHub Release whose body is the corresponding CHANGELOG.md entry.
4
+ #
5
+ # Run AFTER you have:
6
+ # - bumped package.json
7
+ # - added a `## [X.Y.Z] - YYYY-MM-DD` block at the top of CHANGELOG.md
8
+ # - committed, pushed to origin, and run `npm publish`
9
+ #
10
+ # Idempotent: safe to re-run after a partial failure. Skips steps that are
11
+ # already done (tag created, tag pushed, release published).
12
+ #
13
+ # Usage: ./scripts/release.sh
14
+
15
+ set -euo pipefail
16
+
17
+ cd "$(dirname "$0")/.."
18
+
19
+ VERSION=$(node -p "require('./package.json').version")
20
+ TAG="v${VERSION}"
21
+
22
+ REPO=$(git remote get-url origin | sed -E 's|.*github\.com[:/]||; s|\.git$||')
23
+ if [ -z "${REPO}" ]; then
24
+ echo "error: could not derive owner/repo from origin remote URL" >&2
25
+ exit 1
26
+ fi
27
+
28
+ if ! grep -q "^## \[${VERSION}\]" CHANGELOG.md; then
29
+ echo "error: no '## [${VERSION}]' entry found in CHANGELOG.md" >&2
30
+ exit 1
31
+ fi
32
+
33
+ # Extract notes with paragraph unwrapping — GitHub Releases render with
34
+ # GFM hard-breaks, so the CHANGELOG's hard-wrapped lines would show as
35
+ # visible `<br>` breaks otherwise. The helper joins continuation lines
36
+ # into a single line per bullet.
37
+ NOTES=$(node scripts/extract-release-notes.mjs "${VERSION}")
38
+
39
+ if [ -z "${NOTES}" ]; then
40
+ echo "error: failed to extract changelog notes for ${VERSION}" >&2
41
+ exit 1
42
+ fi
43
+
44
+ if git rev-parse "${TAG}" >/dev/null 2>&1; then
45
+ echo "✓ tag ${TAG} already exists locally"
46
+ else
47
+ echo "→ tagging ${TAG}"
48
+ git tag "${TAG}"
49
+ fi
50
+
51
+ if git ls-remote --exit-code --tags origin "${TAG}" >/dev/null 2>&1; then
52
+ echo "✓ tag ${TAG} already on origin"
53
+ else
54
+ echo "→ pushing ${TAG} to origin"
55
+ git push origin "${TAG}"
56
+ fi
57
+
58
+ if gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1; then
59
+ echo "✓ release ${TAG} already published"
60
+ else
61
+ echo "→ creating GitHub Release ${TAG} on ${REPO}"
62
+ gh release create "${TAG}" \
63
+ --repo "${REPO}" \
64
+ --title "${TAG}" \
65
+ --notes "${NOTES}"
66
+ fi
67
+
68
+ echo "done: https://github.com/${REPO}/releases/tag/${TAG}"