Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion .github/repository-metrics-traffic.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,63 @@
"views_14d": 0,
"unique_cloners_14d": 11,
"clones_14d": 12,
"traffic_as_of": "2026-08-10T00:00:00Z"
"traffic_as_of": "2026-08-10T00:00:00Z",
"clone_series_14d": [
{
"count": 0,
"timestamp": "2026-07-27T00:00:00Z"
},
{
"count": 9,
"timestamp": "2026-07-28T00:00:00Z"
},
{
"count": 1,
"timestamp": "2026-07-29T00:00:00Z"
},
{
"count": 1,
"timestamp": "2026-07-30T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-07-31T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-01T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-02T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-03T00:00:00Z"
},
{
"count": 1,
"timestamp": "2026-08-04T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-05T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-06T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-07T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-08T00:00:00Z"
},
{
"count": 0,
"timestamp": "2026-08-09T00:00:00Z"
}
]
}
43 changes: 21 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,28 +48,6 @@ credential store, and deterministic per-agent projections.
> and [release checksums](https://github.com/JNHFlow21/agent-switch/releases/tag/v0.2.0)
> before installing.


## Project activity

| Public signal | Live or latest owner-visible value |
| --- | ---: |
| Stars / forks / commits / release downloads | Live badges above |
| README visits | Public counter above; may include bots and repeat visits |
| Unique repository visitors | **0** in the rolling 14-day GitHub Traffic window |
| Unique Git cloners | **11** (**12** total clones) in the rolling 14-day window |

<sub>Traffic snapshot: 2026-08-10. GitHub exposes clone and unique-visitor analytics only to maintainers, so those values are a dated, transparent snapshot rather than a token-backed public badge.</sub>

### Repository growth

<p align="center">
<a href="https://github.com/JNHFlow21/agent-switch/stargazers">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/agent-switch/metrics/repository-metrics.svg" alt="Agent Switch repository growth and traffic metrics">
</a>
</p>

<sub>Stars, forks, and commits refresh after every new star and once a week. Traffic cards use the dated aggregate owner snapshot shown in the chart because GitHub does not grant its Actions token access to repository Traffic. No long-lived token is embedded in this README.</sub>

The native app follows the macOS language preference and ships complete
**English** and **Simplified Chinese** interface catalogs.

Expand Down Expand Up @@ -334,6 +312,27 @@ The native app lives in [`macos-app/AgentSwitch`](macos-app/AgentSwitch) and
targets macOS 14+. See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete test
and privacy gate.

## Project activity

| Public signal | Live or latest owner-visible value |
| --- | ---: |
| Stars / forks / commits / release downloads | Live badges above |
| README visits | Public counter above; may include bots and repeat visits |
| Unique repository visitors | **0** in the rolling 14-day GitHub Traffic window |
| Unique Git cloners | **11** (**12** total clones) in the rolling 14-day window |

<sub>Traffic snapshot: 2026-08-10. GitHub exposes clone and unique-visitor analytics only to maintainers, so those values are a dated, transparent snapshot rather than a token-backed public badge.</sub>

### Clone activity

<p align="center">
<a href="https://github.com/JNHFlow21/agent-switch">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/agent-switch/metrics/repository-metrics.svg" alt="Agent Switch rolling 14-day clone activity and repository metrics">
</a>
</p>

<sub>The curve shows cumulative clones inside GitHub's rolling 14-day Traffic window. The Traffic series is a dated aggregate owner snapshot; stars, forks, and commits in the side cards refresh after every new star and weekly. No long-lived token is embedded in this README.</sub>

## License

[MIT](LICENSE) © 2026 JNHFlow21
43 changes: 21 additions & 22 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,6 @@ Agent Switch 用一个本地 MCP 注册表、一个私密凭据存储和可重
> quarantine。安装前请先检查[公开源码](https://github.com/JNHFlow21/agent-switch)
> 和[发行校验和](https://github.com/JNHFlow21/agent-switch/releases/tag/v0.2.0)。


## 项目数据

| 公开指标 | 实时数据或最近一次维护者可见数据 |
| --- | ---: |
| Star / Fork / Commit / 发行下载量 | 见上方实时徽章 |
| README 访问量 | 见上方公开计数器;可能包含机器人和重复访问 |
| 仓库独立访客 | GitHub Traffic 最近 14 天滚动窗口内为 **0** |
| 独立克隆者 | 最近 14 天滚动窗口内为 **11**(共 **12** 次克隆) |

<sub>数据快照日期:2026-08-10。GitHub 只向仓库维护者提供克隆与独立访客数据,因此这里采用注明日期的透明快照,而不是需要私密 Token 的公开徽章。</sub>

### 仓库增长曲线

<p align="center">
<a href="https://github.com/JNHFlow21/agent-switch/stargazers">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/agent-switch/metrics/repository-metrics.svg" alt="Agent Switch 仓库增长与流量指标">
</a>
</p>

<sub>Star、Fork 与 Commit 会在每次获得新 Star 后及每周自动刷新。由于 GitHub 不允许 Actions Token 读取仓库 Traffic,流量卡片使用图中注明日期的仓库所有者聚合快照;README 中不嵌入任何长期 Token。</sub>

原生应用会跟随 macOS 的语言偏好,并完整提供**英文**与**简体中文**界面。

## 快速开始
Expand Down Expand Up @@ -316,6 +294,27 @@ python -m unittest discover -s tests/integration
原生应用位于 [`macos-app/AgentSwitch`](macos-app/AgentSwitch),最低支持
macOS 14。完整测试和隐私门禁见 [CONTRIBUTING.md](CONTRIBUTING.md)。

## 项目数据

| 公开指标 | 实时数据或最近一次维护者可见数据 |
| --- | ---: |
| Star / Fork / Commit / 发行下载量 | 见上方实时徽章 |
| README 访问量 | 见上方公开计数器;可能包含机器人和重复访问 |
| 仓库独立访客 | GitHub Traffic 最近 14 天滚动窗口内为 **0** |
| 独立克隆者 | 最近 14 天滚动窗口内为 **11**(共 **12** 次克隆) |

<sub>数据快照日期:2026-08-10。GitHub 只向仓库维护者提供克隆与独立访客数据,因此这里采用注明日期的透明快照,而不是需要私密 Token 的公开徽章。</sub>

### 克隆趋势

<p align="center">
<a href="https://github.com/JNHFlow21/agent-switch">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/agent-switch/metrics/repository-metrics.svg" alt="Agent Switch 最近 14 天克隆趋势与仓库指标">
</a>
</p>

<sub>曲线展示 GitHub Traffic 最近 14 天滚动窗口内的累计克隆次数。Traffic 序列采用注明日期的仓库所有者聚合快照;侧边的 Star、Fork 与 Commit 会在每次获得新 Star 后及每周自动刷新。README 中不嵌入任何长期 Token。</sub>

## 许可证

[MIT](LICENSE) © 2026 JNHFlow21
89 changes: 43 additions & 46 deletions scripts/render_repository_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from __future__ import annotations

import argparse
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from html import escape
import json
import math
Expand Down Expand Up @@ -57,6 +57,14 @@ def fetch_snapshot(
"views_14d": int(views["count"]),
"unique_cloners_14d": int(clones["uniques"]),
"clones_14d": int(clones["count"]),
"clone_series_14d": [
{
"timestamp": str(item["timestamp"]),
"count": int(item["count"]),
}
for item in clones.get("clones", [])
if isinstance(item, dict) and "timestamp" in item and "count" in item
],
"traffic_as_of": generated_at,
}
except HTTPError as error:
Expand All @@ -68,6 +76,7 @@ def fetch_snapshot(
"views_14d",
"unique_cloners_14d",
"clones_14d",
"clone_series_14d",
"traffic_as_of",
}
missing = sorted(required - traffic_fallback.keys())
Expand All @@ -79,6 +88,7 @@ def fetch_snapshot(
"views_14d": int(traffic_fallback["views_14d"]),
"unique_cloners_14d": int(traffic_fallback["unique_cloners_14d"]),
"clones_14d": int(traffic_fallback["clones_14d"]),
"clone_series_14d": list(traffic_fallback["clone_series_14d"]),
"traffic_as_of": str(traffic_fallback["traffic_as_of"]),
}
_, commit_headers = _request_json(f"/repos/{encoded}/commits?per_page=1", token)
Expand All @@ -87,23 +97,6 @@ def fetch_snapshot(
last_page = re.search(r"[?&]page=(\d+)[^>]*>; rel=\"last\"", link)
commit_count = int(last_page.group(1)) if last_page else 1

starred_at: list[str] = []
page = 1
while True:
entries, _ = _request_json(
f"/repos/{encoded}/stargazers?per_page=100&page={page}",
token,
accept="application/vnd.github.star+json",
)
starred_at.extend(
item["starred_at"]
for item in entries
if isinstance(item, dict) and isinstance(item.get("starred_at"), str)
)
if len(entries) < 100:
break
page += 1

return {
"repository": repository,
"created_at": repo["created_at"],
Expand All @@ -113,7 +106,6 @@ def fetch_snapshot(
"commits": commit_count,
**traffic,
"traffic_live": traffic_live,
"starred_at": sorted(starred_at),
}


Expand All @@ -131,39 +123,45 @@ def _compact(value: int) -> str:

def render_svg(snapshot: dict[str, Any]) -> str:
repository = escape(str(snapshot["repository"]))
created = _parse_time(str(snapshot["created_at"]))
generated = _parse_time(str(snapshot["generated_at"]))
if generated <= created:
generated = created.replace(microsecond=0)

traffic_as_of = _parse_time(str(snapshot.get("traffic_as_of", snapshot["generated_at"])))
stars = max(0, int(snapshot["stars"]))
star_dates = sorted(
date for date in (_parse_time(str(value)) for value in snapshot.get("starred_at", []))
if created <= date <= generated
total_clones = max(0, int(snapshot["clones_14d"]))
clone_series = sorted(
(
_parse_time(str(item["timestamp"])),
max(0, int(item["count"])),
)
for item in snapshot.get("clone_series_14d", [])
if isinstance(item, dict) and "timestamp" in item and "count" in item
)
chart_start = clone_series[0][0] if clone_series else traffic_as_of - timedelta(days=13)
chart_end = clone_series[-1][0] if clone_series else traffic_as_of
if chart_end <= chart_start:
chart_end = chart_start + timedelta(days=1)

chart_x, chart_y, chart_w, chart_h = 64.0, 150.0, 540.0, 292.0
baseline = chart_y + chart_h
seconds = max((generated - created).total_seconds(), 1.0)
y_max = max(stars, 1)
seconds = max((chart_end - chart_start).total_seconds(), 1.0)
series_total = sum(count for _, count in clone_series)
y_max = max(total_clones, series_total, 1)

def x_for(date: datetime) -> float:
return chart_x + chart_w * max(0.0, min(1.0, (date - created).total_seconds() / seconds))
return chart_x + chart_w * max(0.0, min(1.0, (date - chart_start).total_seconds() / seconds))

def y_for(value: int) -> float:
return baseline - chart_h * max(0.0, min(1.0, value / y_max))

path = [f"M {chart_x:.1f} {y_for(0):.1f}"]
count = 0
for date in star_dates:
cumulative = 0
for date, count in clone_series:
x = x_for(date)
path.append(f"L {x:.1f} {y_for(count):.1f}")
count += 1
path.append(f"L {x:.1f} {y_for(count):.1f}")
if count < stars:
path.append(f"L {chart_x + chart_w:.1f} {y_for(stars):.1f}")
else:
path.append(f"L {chart_x + chart_w:.1f} {y_for(count):.1f}")
path.append(f"L {x:.1f} {y_for(cumulative):.1f}")
cumulative += count
path.append(f"L {x:.1f} {y_for(cumulative):.1f}")
if cumulative < total_clones:
cumulative = total_clones
path.append(f"L {chart_x + chart_w:.1f} {y_for(cumulative):.1f}")
line_path = " ".join(path)
area_path = f"{line_path} L {chart_x + chart_w:.1f} {baseline:.1f} Z"

Expand Down Expand Up @@ -202,20 +200,19 @@ def y_for(value: int) -> float:
)

empty_note = ""
if stars == 0:
empty_note = '<text x="334" y="292" text-anchor="middle" class="empty">Waiting for the first star</text>'
if total_clones == 0:
empty_note = '<text x="334" y="292" text-anchor="middle" class="empty">No clones in this 14-day window</text>'

updated = generated.strftime("%Y-%m-%d UTC")
traffic_as_of = _parse_time(str(snapshot.get("traffic_as_of", snapshot["generated_at"])))
if snapshot.get("traffic_live", False):
traffic_note = "GitHub Traffic: rolling 14-day owner view"
else:
traffic_note = f"GitHub Traffic owner snapshot: {traffic_as_of.strftime('%Y-%m-%d')}"
start_label = created.strftime("%Y-%m")
end_label = generated.strftime("%Y-%m")
start_label = chart_start.strftime("%Y-%m-%d")
end_label = chart_end.strftime("%Y-%m-%d")
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="960" height="560" viewBox="0 0 960 560" role="img" aria-labelledby="title desc">
<title id="title">{repository} repository metrics</title>
<desc id="desc">Star growth curve with stars, forks, commits, visitors, cloners, and clone totals.</desc>
<desc id="desc">Cumulative clone curve for the rolling 14-day Traffic window with stars, forks, commits, visitors, cloners, and clone totals.</desc>
<style>
.background {{ fill: #ffffff; }}
.outline {{ fill: none; stroke: #171717; stroke-width: 2.4; }}
Expand All @@ -239,8 +236,8 @@ def y_for(value: int) -> float:
<rect class="outline" x="12" y="12" width="936" height="536" rx="22"/>
<text x="54" y="61" class="title">Repository Pulse</text>
<text x="54" y="88" class="subtitle">{repository} · privacy-safe public activity</text>
<path d="M 844 47 l 8 16 18 3 -13 12 3 18 -16 -9 -16 9 3 -18 -13 -12 18 -3 z" fill="#18a558" opacity=".9"/>
<text x="64" y="128" class="section">Stars over time</text>
<path d="M 844 45 v34 m-13 -13 13 13 13 -13 M 825 92 h38" fill="none" stroke="#18a558" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="64" y="128" class="section">Total clones over time · rolling 14 days</text>
{''.join(grid)}
<line x1="{chart_x:.1f}" y1="{baseline:.1f}" x2="{chart_x + chart_w:.1f}" y2="{baseline:.1f}" class="outline"/>
<path d="{area_path}" class="area"/>
Expand Down
9 changes: 9 additions & 0 deletions tests/test_localization_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ def test_readmes_embed_the_privacy_safe_repository_metrics_chart(self) -> None:
self.assertIn(chart, content)
self.assertNotIn("api.star-history.com", content)

def test_activity_chart_stays_near_the_end_after_product_documentation(self) -> None:
english = (ROOT / "README.md").read_text()
self.assertLess(english.index("## Development"), english.index("## Project activity"))
self.assertLess(english.index("## Project activity"), english.index("## License"))

chinese = (ROOT / "README.zh-CN.md").read_text()
self.assertLess(chinese.index("## 开发"), chinese.index("## 项目数据"))
self.assertLess(chinese.index("## 项目数据"), chinese.index("## 许可证"))


if __name__ == "__main__":
unittest.main()
Loading