Skip to content
Open
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
120 changes: 120 additions & 0 deletions .github/scripts/generate_star_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Generate a self-hosted GitHub star-history chart for the README."""

from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import urllib.request
from pathlib import Path


def fetch_stars(repository: str, token: str) -> list[dt.date]:
url = f"https://api.github.com/repos/{repository}/stargazers?per_page=100"
dates: list[dt.date] = []
while url:
request = urllib.request.Request(
url,
headers={
"Accept": "application/vnd.github.star+json",
"Authorization": f"Bearer {token}",
"User-Agent": "jcode-star-history",
"X-GitHub-Api-Version": "2022-11-28",
},
)
with urllib.request.urlopen(request) as response:
for star in json.load(response):
dates.append(dt.datetime.fromisoformat(star["starred_at"].replace("Z", "+00:00")).date())
links = response.headers.get("Link", "")
url = ""
for link in links.split(","):
if 'rel="next"' in link:
url = link[link.index("<") + 1 : link.index(">")]
break
return dates


def render_svg(repository: str, dates: list[dt.date]) -> str:
if not dates:
raise RuntimeError("GitHub returned no stargazers")
dates.sort()
start, end = dates[0], max(dates[-1], dt.date.today())
span = max((end - start).days, 1)
width, height = 800, 420
left, right, top, bottom = 72, 24, 38, 58
plot_w, plot_h = width - left - right, height - top - bottom

points: list[tuple[dt.date, int]] = []
for index, day in enumerate(dates, 1):
if index == len(dates) or dates[index] != day:
points.append((day, index))
if points[-1][0] < end:
points.append((end, len(dates)))

def x(day: dt.date) -> float:
return left + (day - start).days / span * plot_w

max_stars = len(dates)
grid_max = ((max_stars + 4999) // 5000) * 5000 or 1

def y(value: int) -> float:
return top + (1 - value / grid_max) * plot_h

path = " ".join(
("M" if index == 0 else "L") + f" {x(day):.1f} {y(count):.1f}"
for index, (day, count) in enumerate(points)
)
area = f"{path} L {x(end):.1f} {top + plot_h:.1f} L {x(start):.1f} {top + plot_h:.1f} Z"

y_ticks = []
for index in range(5):
value = round(grid_max * index / 4)
yy = y(value)
label = f"{value / 1000:g}k" if value >= 1000 else str(value)
y_ticks.append(f'<line x1="{left}" y1="{yy:.1f}" x2="{width-right}" y2="{yy:.1f}" class="grid"/><text x="{left-12}" y="{yy+5:.1f}" text-anchor="end">{label}</text>')

x_ticks = []
years = range(start.year, end.year + 1)
for year in years:
day = max(start, dt.date(year, 1, 1))
if day > end:
continue
xx = x(day)
x_ticks.append(f'<line x1="{xx:.1f}" y1="{top}" x2="{xx:.1f}" y2="{top+plot_h}" class="grid"/><text x="{xx:.1f}" y="{height-25}" text-anchor="middle">{day.year}</text>')

return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" role="img" aria-labelledby="title desc">
<title id="title">{repository} star history</title>
<desc id="desc">GitHub stars over time, currently {max_stars:,}</desc>
<style>
:root {{ color-scheme: light dark; }}
.bg {{ fill: #fff; }} text {{ fill: #57606a; font: 13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }}
.grid {{ stroke: #d8dee4; stroke-width: 1; }} .area {{ fill: #0969da; opacity: .12; }} .line {{ fill: none; stroke: #0969da; stroke-width: 3; }}
.heading {{ fill: #24292f; font-size: 17px; font-weight: 600; }}
@media (prefers-color-scheme: dark) {{ .bg {{ fill: #0d1117; }} text {{ fill: #8b949e; }} .grid {{ stroke: #30363d; }} .area {{ fill: #58a6ff; }} .line {{ stroke: #58a6ff; }} .heading {{ fill: #f0f6fc; }} }}
</style>
<rect class="bg" width="100%" height="100%" rx="6"/>
<text class="heading" x="{left}" y="25">GitHub stars over time</text>
{''.join(y_ticks)}{''.join(x_ticks)}
<path class="area" d="{area}"/><path class="line" d="{path}"/>
<circle cx="{x(points[-1][0]):.1f}" cy="{y(max_stars):.1f}" r="4" class="line"/>
<text x="{width-right}" y="25" text-anchor="end">{max_stars:,} stars</text>
</svg>
'''


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--repo", default="1jehuang/jcode")
parser.add_argument("--output", type=Path, default=Path("docs/images/star-history.svg"))
args = parser.parse_args()
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if not token:
raise SystemExit("GITHUB_TOKEN or GH_TOKEN is required")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(render_svg(args.repo, fetch_stars(args.repo, token)))


if __name__ == "__main__":
main()
22 changes: 18 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ jobs:
name: Quality Guardrails
runs-on: ubuntu-latest
timeout-minutes: 45
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
submodules: recursive

- name: Configure SSH for cargo git dependencies
if: ${{ env.DEPLOY_KEY != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
ssh-private-key: ${{ env.DEPLOY_KEY }}

- uses: dtolnay/rust-toolchain@stable
with:
Expand Down Expand Up @@ -137,6 +140,9 @@ jobs:
- name: Compile release automation scripts
run: python3 -m py_compile scripts/post_discord_release.py scripts/test_post_discord_release.py

- name: Test fork-compatible workflow contracts
run: python3 -m unittest -v scripts/test_fork_ci_workflow.py

build:
name: Build & Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand All @@ -158,6 +164,7 @@ jobs:
# pinning it here makes every step use this job's installed stable toolchain.
env:
RUSTUP_TOOLCHAIN: stable
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
strategy:
fail-fast: false
matrix:
Expand All @@ -175,9 +182,10 @@ jobs:
submodules: recursive

- name: Configure SSH for cargo git dependencies
if: ${{ env.DEPLOY_KEY != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
ssh-private-key: ${{ env.DEPLOY_KEY }}

- uses: dtolnay/rust-toolchain@stable
with:
Expand Down Expand Up @@ -406,16 +414,19 @@ jobs:
name: Build & Test (windows-latest)
runs-on: windows-latest
timeout-minutes: 150
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
submodules: recursive

- name: Configure SSH for cargo git dependencies
if: ${{ env.DEPLOY_KEY != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
ssh-private-key: ${{ env.DEPLOY_KEY }}

- uses: ilammy/msvc-dev-cmd@v1
with:
Expand Down Expand Up @@ -659,16 +670,19 @@ jobs:
name: Windows Cross-Target Check (Linux)
runs-on: ubuntu-latest
timeout-minutes: 35
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
submodules: recursive

- name: Configure SSH for cargo git dependencies
if: ${{ env.DEPLOY_KEY != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
ssh-private-key: ${{ env.DEPLOY_KEY }}

- uses: dtolnay/rust-toolchain@stable
with:
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/require-issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ jobs:

const owner = context.repo.owner;
const repo = context.repo.repo;

try {
const repositoryQuery = `query($owner:String!, $repo:String!) {
repository(owner:$owner, name:$repo) { hasIssuesEnabled }
}`;
const repositoryResult = await github.graphql(repositoryQuery, { owner, repo });
if (repositoryResult?.repository?.hasIssuesEnabled === false) {
core.info('Repository issues are disabled; skipping linked-issue requirement.');
return;
}
} catch (err) {
core.warning(`Could not query repository issue capability: ${err}`);
}

const text = `${pr.title || ''}\n\n${pr.body || ''}`;

// Collect candidate issue numbers that belong to THIS repo.
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/update-star-history.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Update star history

on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: update-star-history
cancel-in-progress: true

jobs:
update:
if: github.repository == '1jehuang/jcode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate chart using repository-authorized star data
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 .github/scripts/generate_star_history.py
- name: Commit updated chart
run: |
if git diff --quiet -- docs/images/star-history.svg; then
exit 0
fi
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git add docs/images/star-history.svg
git commit -m "docs: update star history chart"
git push
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jcode"
version = "0.75.5"
version = "0.76.0"
description = "Possibly the greatest coding agent ever built — blazing-fast TUI, multi-model, swarm coordination, 30+ tools"
edition = "2024"
autobins = false
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The most intelligent harness

<a href="https://trendshift.io/repositories/25042?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-25042" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/25042" alt="1jehuang/jcode | Trendshift" width="250" height="55"></a>

<a href="https://star-history.com/#1jehuang/jcode&Date"><img src="https://api.star-history.com/svg?repos=1jehuang/jcode&type=Date" alt="Stargazers over time" width="600"></a>
<a href="https://github.com/1jehuang/jcode/stargazers"><img src="docs/images/star-history.svg" alt="jcode GitHub stars over time" width="600"></a>

<a href="https://github.com/1jehuang/jcode/releases/download/readme-assets/jcode-yc-launch.mp4">
<img src="https://github.com/1jehuang/jcode/releases/download/readme-assets/jcode-yc-launch.webp" alt="jcode YC launch video" width="800">
Expand Down Expand Up @@ -456,6 +456,34 @@ id = "my-model-id"
context_window = 128000
```

Anthropic Messages-compatible gateways use the same named-profile surface with
`type = "anthropic-compatible"`. The profile can select bearer, custom-header,
or no authentication and attach gateway-specific headers to every request:

```toml
[provider]
default_provider = "corp-claude"
default_model = "claude-sonnet-4-6"

[providers.corp-claude]
type = "anthropic-compatible"
base_url = "https://gateway.example.com/anthropic/v1"
auth = "bearer"
api_key_env = "CORP_CLAUDE_TOKEN"
default_model = "claude-sonnet-4-6"

[providers.corp-claude.headers]
x-tenant-id = "tenant-42"

[[providers.corp-claude.models]]
id = "claude-sonnet-4-6"
context_window = 200000
```

For direct environment-based configuration, `ANTHROPIC_BASE_URL` overrides the
non-OAuth Messages endpoint and `ANTHROPIC_AUTH_TOKEN` is sent as a bearer token.
Claude OAuth traffic always continues to use Anthropic's official endpoints.

##### Extra request-body fields (`extra_body`)

Some OpenAI-compatible backends require non-standard top-level request fields. For example, NVIDIA NIM DeepSeek-V4 reasoning models (`deepseek-ai/deepseek-v4-flash`, `deepseek-ai/deepseek-v4-pro`) only enable thinking when the request includes `chat_template_kwargs`; without it they reply without reasoning (or, for some deployments, hang). jcode lets you inject arbitrary top-level fields two ways.
Expand Down
4 changes: 4 additions & 0 deletions changelog/index.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"entries": [
{
"version": "0.76.0",
"date": "2026-08-14"
},
{
"version": "0.75.5",
"date": "2026-08-12"
Expand Down
22 changes: 22 additions & 0 deletions changelog/v0.76.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"version": "0.76.0",
"date": "2026-08-14",
"title": "Transcript privacy and broader provider support",
"highlights": [
"Opt-in transcript telemetry now supports privacy-preserving collection with automatic secret redaction",
"Anthropic-compatible provider profiles can now connect to additional compatible services",
"Grok Build login can now be completed directly inside the TUI"
],
"improvements": [
"Startup update checks can now be disabled in configuration",
"Transient provider retries are more resilient and configurable",
"Z.AI Coding Plan supports reasoning effort controls and safely handles text-only models"
],
"fixes": [
"OpenRouter responses preserve tool outputs even when the corresponding tool call is unavailable",
"Prompt files are deduplicated and oversized skill context is clipped",
"Repeated paste placeholders expand correctly",
"Self-development builds promote artifacts to the correct paths",
"Grok model lists refresh after login and use the current managed backend"
]
}
15 changes: 15 additions & 0 deletions crates/jcode-base/src/mcp/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ pub struct McpHandle {
}

impl McpHandle {
/// Test-only constructor: a handle that is not backed by any process.
#[cfg(test)]
pub(crate) fn new_dummy_for_tests(name: String) -> Self {
let (writer_tx, _writer_rx) = tokio::sync::mpsc::channel::<String>(8);
Self {
name,
request_id: Arc::new(AtomicU64::new(1)),
pending: Arc::new(Mutex::new(HashMap::new())),
writer_tx,
server_info: Arc::new(std::sync::RwLock::new(None)),
capabilities: Arc::new(std::sync::RwLock::new(ServerCapabilities::default())),
tools: Arc::new(std::sync::RwLock::new(Vec::new())),
}
}

/// Send a request and wait for response
pub async fn request(&self, method: &str, params: Option<Value>) -> Result<JsonRpcResponse> {
let id = self.request_id.fetch_add(1, Ordering::SeqCst);
Expand Down
Loading
Loading