diff --git a/README.ko.md b/README.ko.md index 3f890ae..d8b8184 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,24 +1,23 @@ -# AI Agent Context Hub — Phase 1 (로컬 MCP 프로토타입) +# AI Agent Context Hub [![CI](https://github.com/junseo2323/claudexhub/actions/workflows/ci.yml/badge.svg)](https://github.com/junseo2323/claudexhub/actions/workflows/ci.yml) [English README](./README.md) **에이전트 중심 개발자 지식 플랫폼**입니다. AI 코딩 에이전트(Claude Code, -Codex, Cursor)가 MCP 서버를 통해 구조화된 문제 해결 단위인 **Context Card**를 +Codex, Cursor, Antigravity)가 MCP 서버를 통해 구조화된 문제 해결 단위인 **Context Card**를 읽고 씁니다. 한 번 해결한 문제를 처음부터 다시 분석하는 대신, 나중에 검색하여 재사용할 수 있습니다. -이 저장소는 **Phase 1**으로, Claude Code에 연결할 수 있는 stdio MCP 서버와 -동일한 데이터를 보여주는 **웹 앱**을 중심으로 한 로컬 우선 프로토타입입니다. -모든 데이터는 로컬 SQLite 저장소를 사용합니다. +호스팅 Hub는 GitHub 로그인, API 토큰, 원격 MCP 엔드포인트와 함께 공유된 +엔지니어링 지식을 검색·검토·게시하는 웹 앱을 제공합니다. ## 구성 요소 > 📄 제품 명세: [`docs/PLANNING.md`](./docs/PLANNING.md) · 명세와 구현 차이 분석: > [`docs/SPEC-GAP.md`](./docs/SPEC-GAP.md) -- stdio 방식으로 7개 도구를 제공하는 **MCP 서버** (`src/index.ts`) +- 호스팅 HTTP와 로컬 stdio 방식으로 7개 도구를 제공하는 **MCP 서버** - FTS5 키워드 검색과 sqlite-vec 임베딩 유사도를 신뢰도 점수로 결합하는 **하이브리드 검색** 기반의 **로컬 SQLite** 저장소 - **요약 우선 검색**: `search_context`는 간결한 요약만 반환하며, 토큰 절약을 @@ -43,18 +42,21 @@ Codex, Cursor)가 MCP 서버를 통해 구조화된 문제 해결 단위인 **Co | `record_feedback` | 재사용 결과(`success`, `partial`, `failed`)를 기록하고 재사용 횟수, 누적 절약 토큰, 신뢰도를 갱신합니다. | | `mark_stale` | 해결책이 오래되었거나 잘못된 경우 카드를 오래된 상태로 표시합니다. 오래된 카드는 검색 결과에서 제외됩니다. | -## 빠른 시작 +## 에이전트 연결 -배포된 MCP 서버를 `npx`로 실행할 수 있습니다. 아래 명령 하나로 로컬 DB -스키마와 시드 카드를 준비한 다음 에이전트에 연결하세요. +명령어 한 줄을 실행하세요. GitHub 로그인을 위한 브라우저가 열리고, CLI가 +호스팅 API 토큰을 만든 뒤 Context Hub를 자동 등록합니다. ```bash -npx -y ai-agent-context-hub context-hub-cli init # 스키마 + 시드 카드 20개 +npx -y ai-agent-context-hub connect claude +npx -y ai-agent-context-hub connect codex +npx -y ai-agent-context-hub connect cursor +npx -y ai-agent-context-hub connect antigravity ``` -바로 복사해 쓸 수 있는 Claude Code와 Cursor 설정은 -[`examples/`](./examples)에 있습니다. 패키지는 `context-hub`(기본 MCP 서버)와 -`context-hub-cli`(개발/관리 CLI), 두 개의 실행 파일을 제공합니다. +지원되는 에이전트를 모두 설정하려면 `connect all`을 사용하세요. JSON 편집이나 +로컬 DB 설정은 필요 없습니다. 자세한 사용법은 +[claudexhub.fly.dev](https://claudexhub.fly.dev/)에서 확인할 수 있습니다. ## 소스에서 설치 @@ -93,7 +95,7 @@ npm run cli -- eval --k 5 # 검색 품질 자체 검색 평가(hit@k, npm run cli -- reindex ``` -## Claude Code에 등록 +## 로컬 개발: Claude Code에 등록 이 저장소에는 프로젝트 범위의 `.mcp.json`이 포함되어 있습니다. diff --git a/README.md b/README.md index bc58824..35ec2bb 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,23 @@ -# AI Agent Context Hub — Phase 1 (Local MCP Prototype) +# AI Agent Context Hub [![CI](https://github.com/junseo2323/claudexhub/actions/workflows/ci.yml/badge.svg)](https://github.com/junseo2323/claudexhub/actions/workflows/ci.yml) [한국어 README](./README.ko.md) An **agent-first developer knowledge platform**. AI coding agents (Claude Code, -Codex, Cursor) read and write **Context Cards** — structured problem-solving +Codex, Cursor, Antigravity) read and write **Context Cards** — structured problem-solving units — through an MCP server, so a fix solved once can be searched and reused later instead of re-derived from scratch. -This repository is **Phase 1**: a local-first prototype centered on a stdio MCP -server you can plug into Claude Code, plus a read-only **web view** over the same -data. No OAuth or hosted endpoint yet — everything runs against a local SQLite store. +The hosted Hub provides GitHub sign-in, API tokens, a remote MCP endpoint, and a +web app for searching, reviewing, and publishing shared engineering knowledge. ## What's here > 📄 Product spec: [`docs/PLANNING.md`](./docs/PLANNING.md) · spec-vs-build gap > analysis: [`docs/SPEC-GAP.md`](./docs/SPEC-GAP.md). -- **MCP server** (`src/index.ts`) exposing 7 tools over stdio. +- **MCP server** exposing 7 tools over hosted HTTP and local stdio. - **Local SQLite** store with **hybrid search**: FTS5 keyword + sqlite-vec embedding similarity, fused into a confidence score. - **Brief-first retrieval**: `search_context` returns compact briefs; full card @@ -27,8 +26,8 @@ data. No OAuth or hosted endpoint yet — everything runs against a local SQLite card is stored or published. - **Human approval**: agents create *drafts*; publishing requires an explicit approval step. -- A **read-only web app** (`app/`, Next.js) — dashboard, leaderboard/stats, card - browse, detail, and search — reusing the exact same domain layer. +- A **web app** (`app/`, Next.js) for authentication, tokens, card authoring, + review, search, teams, profiles, and operational status. - A **dev CLI** and **seed data** (20 example cards). ## MCP tools @@ -43,18 +42,21 @@ data. No OAuth or hosted endpoint yet — everything runs against a local SQLite | `record_feedback` | Record reuse outcome (success/partial/failed); updates reuse counts, accumulated tokens saved, and confidence. | | `mark_stale` | Mark a card stale when its fix is outdated/wrong; stale cards drop out of search. | -## Quickstart +## Connect an agent -Run the published MCP server with `npx` — one command sets up the local DB -(schema + seed cards), then point your agent at it: +Run one command. A browser opens for GitHub sign-in, then the CLI creates a +hosted API token and registers Context Hub automatically: ```bash -npx -y ai-agent-context-hub context-hub-cli init # schema + 20 seed cards +npx -y ai-agent-context-hub connect claude +npx -y ai-agent-context-hub connect codex +npx -y ai-agent-context-hub connect cursor +npx -y ai-agent-context-hub connect antigravity ``` -Ready-to-copy agent configs are in [`examples/`](./examples) (Claude Code, Cursor). -The package ships two bins: `context-hub` (the MCP server, the default) and -`context-hub-cli` (the dev/admin CLI). +Use `connect all` to configure every supported agent. No JSON editing or local +database setup is required. See the live guide at +[claudexhub.fly.dev](https://claudexhub.fly.dev/). ## Setup (from source) @@ -92,7 +94,7 @@ npm run cli -- eval --k 5 # search-quality self-retrieval eval (hit@k npm run cli -- reindex ``` -## Register in Claude Code +## Local development: register in Claude Code This repo ships a project-scoped `.mcp.json`: diff --git a/app/globals.css b/app/globals.css index ca219c3..b36d74a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -553,6 +553,57 @@ th { position: relative; } +.docs-subnav { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 42px; + margin-top: -10px; + border-bottom: 1px solid rgba(37, 46, 61, 0.7); +} + +.docs-subnav nav { + display: flex; + gap: 20px; +} + +.docs-subnav nav a { + color: var(--muted); + font-size: 12px; + font-weight: 600; +} + +.docs-subnav nav a:hover { + color: var(--text); + text-decoration: none; +} + +.locale-toggle { + display: inline-flex; + padding: 3px; + border: 1px solid var(--border); + border-radius: 8px; + background: #0a0e14; +} + +.locale-toggle button { + min-width: 42px; + padding: 5px 8px; + border: 0; + border-radius: 5px; + color: #6f7b8e; + background: transparent; + cursor: pointer; + font-size: 10px; + font-weight: 700; +} + +.locale-toggle button.active { + color: var(--text); + background: var(--panel-2); + box-shadow: 0 0 0 1px #2b3545 inset; +} + .docs-hero { position: relative; display: grid; @@ -936,7 +987,7 @@ th { .quickstart-shell { display: grid; - grid-template-columns: 245px 1fr; + grid-template-columns: 1fr; overflow: hidden; border: 1px solid var(--border); border-radius: 15px; @@ -950,24 +1001,29 @@ th { background: #0b1016; } -.quickstart-steps > div { +.quickstart-steps > button { display: flex; + width: 100%; gap: 12px; + border: 0; padding: 13px 10px; border-radius: 9px; color: #687487; + background: transparent; + cursor: pointer; + text-align: left; } -.quickstart-steps > div + div { +.quickstart-steps > button + button { margin-top: 8px; } -.quickstart-steps > div.active { +.quickstart-steps > button.active { color: var(--text); background: rgba(138, 164, 255, 0.08); } -.quickstart-steps > div > span { +.quickstart-steps > button > span { display: grid; place-items: center; flex: 0 0 26px; @@ -977,13 +1033,13 @@ th { font-size: 11px; } -.quickstart-steps .active > span { +.quickstart-steps button.active > span { color: #0b0f15; border-color: transparent; background: var(--accent-strong); } -.quickstart-steps div div { +.quickstart-steps button div { display: flex; flex-direction: column; } @@ -1001,6 +1057,35 @@ th { padding: 38px 42px 42px; } +.prerequisite { + display: flex; + gap: 12px; + align-items: baseline; + margin-bottom: 28px; + padding: 12px 14px; + border: 1px solid #273244; + border-radius: 8px; + color: #8490a3; + background: rgba(138, 164, 255, 0.035); + font-size: 11px; +} + +.prerequisite b { + flex: 0 0 auto; + color: #b9c5f0; +} + +.token-button { + width: fit-content; + margin-top: 18px; +} + +.verify-copy { + margin: 10px 0 0; + color: var(--muted); + font-size: 13px; +} + .step-label { color: var(--accent-strong); font-size: 10px; @@ -1098,6 +1183,7 @@ th { .tab-list { display: flex; + flex-wrap: wrap; gap: 4px; margin-top: 15px; padding: 4px; @@ -1106,7 +1192,7 @@ th { } .tab-list button { - flex: 1; + flex: 1 1 140px; padding: 9px 12px; border: 0; border-radius: 7px; @@ -1131,6 +1217,105 @@ th { font-size: 11px; } +.usage-steps { + overflow: hidden; + border-top: 1px solid var(--border); +} + +.usage-step { + display: grid; + grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr); + gap: 54px; + align-items: start; + padding: 44px 0; + border-bottom: 1px solid var(--border); +} + +.usage-step .copy-block { + margin-top: 0; +} + +.usage-copy h3 { + margin: 14px 0 10px; + font-size: 21px; +} + +.usage-copy p { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.75; +} + +.capture-section { + border-bottom: 1px solid var(--border); +} + +.capture-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 15px; +} + +.capture-card { + display: flex; + flex-direction: column; + min-width: 0; + padding: 23px; + border: 1px solid var(--border); + border-radius: 13px; + background: linear-gradient(145deg, rgba(20, 27, 38, 0.9), rgba(12, 17, 24, 0.85)); +} + +.capture-card h3 { + margin: 0 0 10px; + font-size: 17px; +} + +.capture-card > p { + min-height: 92px; + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.7; +} + +.capture-card .copy-block { + flex: 1; +} + +.capture-card .copy-block pre { + font-size: 9px; +} + +.stale-guide { + display: grid; + grid-template-columns: 0.8fr 1.2fr; + gap: 50px; + align-items: center; + margin-top: 16px; + padding: 28px; + border: 1px solid rgba(251, 191, 36, 0.2); + border-radius: 13px; + background: rgba(251, 191, 36, 0.035); +} + +.stale-guide h3 { + margin: 0 0 9px; + font-size: 18px; +} + +.stale-guide p { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.75; +} + +.stale-guide .copy-block { + margin-top: 0; +} + .first-prompt b { color: #b9c3d3; } @@ -1331,6 +1516,44 @@ th { justify-content: center; } +.tips-section { + border-bottom: 1px solid var(--border); +} + +.tips-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1px; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 13px; + background: var(--border); +} + +.tips-grid article { + min-height: 190px; + padding: 24px; + background: #0d1219; +} + +.tips-grid article > span { + color: #4f5c70; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 10px; +} + +.tips-grid h3 { + margin: 28px 0 9px; + font-size: 16px; +} + +.tips-grid p { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.7; +} + @media (max-width: 900px) { .nav { gap: 14px; @@ -1357,16 +1580,33 @@ th { } .quickstart-steps { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); border-right: 0; border-bottom: 1px solid var(--border); } - .quickstart-steps > div + div { + .quickstart-steps > button + button { margin-top: 0; } .workflow-grid { grid-template-columns: 1fr; } + .usage-step { + grid-template-columns: 1fr; + gap: 22px; + } + .capture-grid { + grid-template-columns: 1fr; + } + .capture-card > p { + min-height: 0; + } + .stale-guide { + grid-template-columns: 1fr; + gap: 20px; + } + .tips-grid { + grid-template-columns: repeat(2, 1fr); + } .workflow-card { min-height: 245px; } @@ -1395,6 +1635,12 @@ th { min-height: auto; padding: 38px 0 60px; } + .docs-subnav nav { + gap: 12px; + } + .docs-subnav nav a { + font-size: 10px; + } .docs-hero h1 { font-size: 40px; } @@ -1442,12 +1688,14 @@ th { grid-template-columns: 1fr; padding: 12px; } - .quickstart-steps > div:not(.active) { - display: none; - } .quickstart-content { padding: 27px 18px 30px; } + .prerequisite { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } .copy-block pre { font-size: 10px; } @@ -1461,6 +1709,12 @@ th { .safety-card { padding: 32px 20px; } + .tips-grid { + grid-template-columns: 1fr; + } + .tips-grid article { + min-height: 0; + } .redaction-demo > div { grid-template-columns: 1fr auto; } diff --git a/app/landing-client.tsx b/app/landing-client.tsx index e8bb6b1..b9f156d 100644 --- a/app/landing-client.tsx +++ b/app/landing-client.tsx @@ -1,38 +1,407 @@ "use client"; -import { useState } from "react"; - -const configs = { - claude: `{ - "mcpServers": { - "context-hub": { - "command": "npx", - "args": ["-y", "ai-agent-context-hub"], - "env": { - "EMBEDDING_PROVIDER": "local", - "HUB_DB_PATH": "./data/hub.db" - } - } - } -}`, - cursor: `{ - "mcpServers": { - "context-hub": { - "command": "npx", - "args": ["-y", "ai-agent-context-hub"], - "env": { - "EMBEDDING_PROVIDER": "local", - "HUB_DB_PATH": "./data/hub.db" - } - } - } -}`, +import Link from "next/link"; +import { useEffect, useState } from "react"; + +type Locale = "en" | "ko"; +type Agent = "claude" | "codex" | "cursor" | "antigravity"; + +const connectCommands: Record = { + claude: "npx -y ai-agent-context-hub connect claude", + codex: "npx -y ai-agent-context-hub connect codex", + cursor: "npx -y ai-agent-context-hub connect cursor", + antigravity: "npx -y ai-agent-context-hub connect antigravity", }; -export function CopyBlock({ code, language }: { code: string; language?: string }) { +const copy = { + en: { + nav: { guide: "Usage guide", tools: "Tools", safety: "Safety" }, + hero: { + eyebrow: "Shared memory for AI coding agents", + titleA: "Stop solving the same", + titleB: "engineering problem twice.", + body: "Context Hub lets Claude Code, Codex, Cursor, and Antigravity search your team's proven fixes, apply only the relevant context, and save new solutions for the next agent.", + start: "Start in 3 minutes", + github: "View on GitHub", + proof: ["Hosted MCP", "Automatic secret redaction", "Human-approved publishing"], + prompt: "OAuth cookies are missing only in production", + searching: "Searching verified solutions...", + result: "Check trust proxy and secure cookie settings together behind a reverse proxy.", + response: "I found a verified fix from the same deployment environment. I’ll use this card as reference and validate it against the current code.", + saved: "About 8,400 tokens saved", + aria: "Context Hub usage example", + }, + metrics: ["MCP tools", "secret scan", "keyword + semantic", "live context cards"], + quick: { + kicker: "QUICKSTART", + title: "One command connects your agent.", + body: "Sign in through the browser once. The CLI creates a hosted API token and registers Context Hub in your selected agent automatically.", + prereq: "Prerequisites", + prereqBody: "Node.js 20 or newer and at least one supported agent installed.", + tokenLabel: "STEP 1 · CHOOSE YOUR AGENT", + tokenTitle: "Run the matching connect command.", + tokenBody: "A browser window opens for GitHub sign-in. After approval, the CLI stores the token securely and updates the agent's global MCP configuration.", + connectLabel: "STEP 2 · CONNECT", + agentHelp: { + claude: "Registers the hosted HTTP MCP server in Claude Code user scope.", + codex: "Registers the server with Codex CLI and configures its bearer-token environment variable.", + cursor: "Adds Context Hub to ~/.cursor/mcp.json while preserving your existing servers.", + antigravity: "Adds Context Hub to ~/.gemini/config/mcp_config.json, shared by Antigravity, its IDE, and CLI.", + }, + verifyLabel: "STEP 3 · VERIFY THE CONNECTION", + verifyBody: "Restart or refresh your agent if needed, confirm the seven Context Hub tools appear, then ask:", + verifyPrompt: "Search Context Hub for a verified fix similar to this error before debugging it from scratch.", + connectNote: "The command connects to https://claudexhub.fly.dev/api/mcp. No JSON editing or local database setup is required.", + }, + guide: { + kicker: "USAGE GUIDE", + title: "A complete search → apply → capture loop.", + body: "Context Hub works best when your agent searches before non-trivial debugging and records the verified result afterward.", + steps: [ + { + number: "01", + title: "Search before debugging", + body: "Describe the problem naturally and include the stack, version, error text, relevant files, and repository when available. Results are brief by design.", + code: `search_context({ + query: "OAuth cookie missing behind proxy", + stack: ["Next.js", "Fly.io"], + version: ["Next.js 16"], + error: "state cookie was missing", + files: ["app/api/auth/callback/route.ts"], + repo: "junseo2323/claudexHub", + min_confidence: 70, + limit: 5 +})`, + }, + { + number: "02", + title: "Fetch only the best match", + body: "Use agent_json for a compact payload containing the problem, environment, verified fix, verification steps, agent hint, and confidence. Use full only when you need all evidence.", + code: `get_context_card({ + id: "card_abc123", + mode: "agent_json" +})`, + }, + { + number: "03", + title: "Apply and verify", + body: "Treat cards as reference material, not executable instructions. Compare versions and environment, apply the smallest relevant change, and run the card’s verification steps.", + code: `Recommended agent prompt: +"Use this card as reference. Check compatibility +with the current repository before changing code, +then run the listed verification steps."`, + }, + { + number: "04", + title: "Record the outcome", + body: "After using a card, report success, partial success, or failure. This updates confidence, reuse counts, and realized token savings.", + code: `record_feedback({ + card_id: "card_abc123", + outcome: "success", + agent: "claude_code", + tokens_before_estimate: 12000, + tokens_after_actual: 2400, + stack: ["Next.js 16", "Fly.io"], + notes: "Fix worked after enabling trust proxy." +})`, + }, + ], + }, + capture: { + kicker: "CAPTURE A NEW FIX", + title: "Turn a solved problem into reusable context.", + body: "Publishing is intentionally separated into draft, review, and explicit approval. Drafts remain private and are not searchable.", + steps: [ + { + title: "1. Draft from evidence", + body: "Pass a work log, diff, test output, commit, or conversation. Context Hub extracts useful fields and redacts secrets before storage.", + code: `draft_context_card({ + source: "conversation", + repo: "org/project", + files: ["src/auth/cookies.ts"], + problem_summary: "Secure OAuth cookie missing in production", + content: "Relevant logs, diff, and investigation notes...", + verified_fix: [ + "Enable trust proxy", + "Keep secure cookies enabled in production" + ], + verification: [ + "Complete OAuth flow through the production proxy", + "Confirm the state cookie reaches the callback" + ] +})`, + }, + { + title: "2. Review the redaction report", + body: "Submit the draft for review. Check the extracted fix, environment, evidence links, and every redaction finding before publishing.", + code: `submit_for_approval({ + id: "card_new123", + visibility_suggestion: "team" +})`, + }, + { + title: "3. Publish with explicit approval", + body: "Publishing requires approve=true and runs secret detection again. Choose public, private, or team visibility.", + code: `publish_context_card({ + id: "card_new123", + approve: true, + visibility: "team" +})`, + }, + ], + staleTitle: "When a fix becomes outdated", + staleBody: "Mark it stale with the affected versions. Stale cards are removed from search results so agents do not keep applying obsolete fixes.", + staleCode: `mark_stale({ + card_id: "card_abc123", + reason: "Next.js 17 changed cookie defaults", + affected_versions: ["Next.js 17+"] +})`, + }, + tools: { + kicker: "MCP TOOLKIT", + title: "Seven tools, one memory lifecycle.", + body: "Search returns compact briefs first. Full context is fetched only when useful, while approval and feedback keep the shared memory trustworthy.", + browse: "Browse public cards", + rows: [ + ["search_context", "Hybrid search with stack, version, error, file, repository, and confidence filters."], + ["get_context_card", "Read a brief, the full card, or a compact agent_json payload."], + ["draft_context_card", "Create a private, redacted draft from logs, diffs, commits, or conversations."], + ["submit_for_approval", "Move a draft into human review and inspect the redaction report."], + ["publish_context_card", "Publish only after explicit approval and a second secret scan."], + ["record_feedback", "Record success, partial success, failure, and estimated token savings."], + ["mark_stale", "Remove an outdated or incorrect fix from future search results."], + ], + }, + safety: { + kicker: "SAFE BY DEFAULT", + title: "Keep the knowledge. Leave the secrets out.", + body: "Context Hub scans drafts before storage and scans again before publishing. API keys, JWTs, database URLs, emails, and other sensitive values are redacted.", + }, + tips: { + kicker: "BEST PRACTICES", + title: "Get better results from day one.", + items: [ + ["Search with evidence", "Include the exact error, framework version, file paths, and repository. These signals improve ranking."], + ["Prefer agent_json", "Use the compact mode for agent context. Fetch full cards only when deeper evidence is necessary."], + ["Verify before trusting", "A high-confidence card is still reference material. Check environment and run verification steps."], + ["Close the loop", "Always record feedback. Real reuse outcomes improve confidence for every future agent."], + ["Capture proven fixes", "Publish only after the solution has been tested. Keep uncertain hypotheses in private drafts."], + ["Retire stale knowledge", "Mark fixes stale as soon as versions or platform behavior make them unreliable."], + ], + }, + cta: { + overline: "YOUR AGENTS ALREADY SOLVE HARD PROBLEMS", + title: "Make sure the next agent remembers.", + body: "Connect Context Hub, run your first search, and start building reusable engineering memory today.", + start: "Get started", + search: "Search public knowledge", + }, + }, + ko: { + nav: { guide: "사용 가이드", tools: "도구", safety: "보안" }, + hero: { + eyebrow: "AI 코딩 에이전트를 위한 공유 메모리", + titleA: "한 번 해결한 문제를", + titleB: "다시 풀지 마세요.", + body: "Claude Code, Codex, Cursor, Antigravity가 팀의 검증된 해결책을 검색하고, 필요한 컨텍스트만 적용하고, 새로운 해결책을 다음 에이전트를 위해 저장합니다.", + start: "3분 만에 시작하기", + github: "GitHub에서 보기", + proof: ["호스팅 MCP", "민감 정보 자동 제거", "사람의 게시 승인"], + prompt: "프로덕션에서만 OAuth 쿠키가 사라져", + searching: "검증된 해결책을 검색하는 중...", + result: "리버스 프록시 환경에서는 trust proxy와 secure cookie 설정을 함께 확인하세요.", + response: "같은 배포 환경에서 검증된 해결책을 찾았습니다. 현재 코드와 호환되는지 확인한 뒤 참고 자료로 적용할게요.", + saved: "약 8,400 tokens 절약", + aria: "Context Hub 사용 예시", + }, + metrics: ["MCP 도구", "민감 정보 검사", "키워드 + 의미 검색", "공개 Context Card"], + quick: { + kicker: "빠른 시작", + title: "명령어 한 줄로 에이전트를 연결하세요.", + body: "브라우저에서 한 번 로그인하면 CLI가 호스팅 API 토큰을 만들고 선택한 에이전트에 Context Hub를 자동 등록합니다.", + prereq: "준비 사항", + prereqBody: "Node.js 20 이상과 지원되는 에이전트 중 하나가 설치되어 있어야 합니다.", + tokenLabel: "STEP 1 · 에이전트 선택", + tokenTitle: "에이전트에 맞는 연결 명령어를 실행하세요.", + tokenBody: "GitHub 로그인을 위한 브라우저가 열립니다. 승인하면 CLI가 토큰을 안전하게 저장하고 에이전트의 전역 MCP 설정을 갱신합니다.", + connectLabel: "STEP 2 · 연결", + agentHelp: { + claude: "Claude Code 사용자 범위에 호스팅 HTTP MCP 서버를 등록합니다.", + codex: "Codex CLI에 서버를 등록하고 bearer token 환경 변수를 설정합니다.", + cursor: "기존 서버를 보존하면서 ~/.cursor/mcp.json에 Context Hub를 추가합니다.", + antigravity: "Antigravity, IDE, CLI가 공유하는 ~/.gemini/config/mcp_config.json에 Context Hub를 추가합니다.", + }, + verifyLabel: "STEP 3 · 연결 확인", + verifyBody: "필요하면 에이전트를 재시작하거나 새로고침하고 Context Hub 도구 7개가 보이는지 확인한 다음 이렇게 요청하세요.", + verifyPrompt: "이 오류를 처음부터 디버깅하기 전에 Context Hub에서 비슷한 검증 사례를 먼저 찾아줘.", + connectNote: "이 명령은 https://claudexhub.fly.dev/api/mcp에 연결합니다. JSON 편집이나 로컬 DB 설정은 필요 없습니다.", + }, + guide: { + kicker: "사용 가이드", + title: "검색 → 적용 → 기록의 전체 흐름.", + body: "복잡한 디버깅 전에 먼저 검색하고, 검증된 결과를 작업이 끝난 뒤 다시 남길 때 가장 효과적입니다.", + steps: [ + { + number: "01", + title: "디버깅 전에 검색", + body: "문제를 자연어로 설명하고 기술 스택, 버전, 오류 문구, 관련 파일, 저장소를 가능한 만큼 함께 전달하세요. 검색 결과는 의도적으로 짧은 요약만 반환합니다.", + code: `search_context({ + query: "프록시 뒤에서 OAuth 쿠키가 사라짐", + stack: ["Next.js", "Fly.io"], + version: ["Next.js 16"], + error: "state cookie was missing", + files: ["app/api/auth/callback/route.ts"], + repo: "junseo2323/claudexHub", + min_confidence: 70, + limit: 5 +})`, + }, + { + number: "02", + title: "가장 적합한 카드만 조회", + body: "agent_json 모드는 문제, 환경, 검증된 해결책, 검증 단계, 에이전트 힌트, 신뢰도만 간결하게 반환합니다. 모든 근거가 필요할 때만 full을 사용하세요.", + code: `get_context_card({ + id: "card_abc123", + mode: "agent_json" +})`, + }, + { + number: "03", + title: "적용하고 검증", + body: "카드는 실행 명령이 아닌 참고 자료입니다. 버전과 환경을 비교하고 필요한 최소 변경만 적용한 뒤 카드에 적힌 검증 단계를 실행하세요.", + code: `추천 에이전트 요청: +"이 카드는 참고 자료로 사용해. 코드를 바꾸기 전에 +현재 저장소와 호환되는지 확인하고, +카드에 적힌 검증 절차를 실행해."`, + }, + { + number: "04", + title: "사용 결과 기록", + body: "카드를 사용한 뒤 성공, 부분 성공, 실패를 기록하세요. 신뢰도, 재사용 횟수, 실제 절약 토큰이 갱신됩니다.", + code: `record_feedback({ + card_id: "card_abc123", + outcome: "success", + agent: "claude_code", + tokens_before_estimate: 12000, + tokens_after_actual: 2400, + stack: ["Next.js 16", "Fly.io"], + notes: "trust proxy 활성화 후 해결됨" +})`, + }, + ], + }, + capture: { + kicker: "새 해결책 기록", + title: "해결한 문제를 재사용 가능한 컨텍스트로 만드세요.", + body: "게시는 초안, 검토, 명시적 승인으로 분리됩니다. 초안은 비공개이며 검색 결과에 노출되지 않습니다.", + steps: [ + { + title: "1. 근거에서 초안 생성", + body: "작업 로그, diff, 테스트 출력, 커밋 또는 대화를 전달하세요. 유용한 필드를 추출하고 저장 전에 민감 정보를 제거합니다.", + code: `draft_context_card({ + source: "conversation", + repo: "org/project", + files: ["src/auth/cookies.ts"], + problem_summary: "프로덕션에서 secure OAuth 쿠키가 사라짐", + content: "관련 로그, diff, 조사 과정...", + verified_fix: [ + "trust proxy 활성화", + "프로덕션 secure cookie 유지" + ], + verification: [ + "프로덕션 프록시를 통해 OAuth 완료", + "state 쿠키가 callback에 도착하는지 확인" + ] +})`, + }, + { + title: "2. 민감 정보 제거 결과 검토", + body: "초안을 검토 단계로 이동하고 추출된 해결책, 환경, 근거 링크, 민감 정보 제거 항목을 모두 확인하세요.", + code: `submit_for_approval({ + id: "card_new123", + visibility_suggestion: "team" +})`, + }, + { + title: "3. 명시적으로 승인하고 게시", + body: "게시에는 approve=true가 필요하며 민감 정보를 다시 검사합니다. 공개, 비공개, 팀 공개 범위를 선택할 수 있습니다.", + code: `publish_context_card({ + id: "card_new123", + approve: true, + visibility: "team" +})`, + }, + ], + staleTitle: "해결책이 오래되었을 때", + staleBody: "영향받는 버전과 함께 stale로 표시하세요. 오래된 카드는 검색에서 제외되어 에이전트가 잘못된 해결책을 반복 적용하지 않습니다.", + staleCode: `mark_stale({ + card_id: "card_abc123", + reason: "Next.js 17에서 쿠키 기본값이 변경됨", + affected_versions: ["Next.js 17+"] +})`, + }, + tools: { + kicker: "MCP 도구", + title: "7개의 도구로 관리하는 지식의 전체 수명주기.", + body: "검색은 먼저 짧은 요약을 반환하고 필요할 때만 전체 컨텍스트를 가져옵니다. 승인과 피드백 흐름이 공유 지식의 신뢰도를 유지합니다.", + browse: "공개 카드 둘러보기", + rows: [ + ["search_context", "스택, 버전, 오류, 파일, 저장소, 신뢰도 필터를 지원하는 하이브리드 검색"], + ["get_context_card", "요약, 전체 카드 또는 간결한 agent_json 형식으로 조회"], + ["draft_context_card", "로그, diff, 커밋, 대화에서 민감 정보가 제거된 비공개 초안 생성"], + ["submit_for_approval", "초안을 사람의 검토 단계로 이동하고 제거 결과 확인"], + ["publish_context_card", "명시적 승인과 두 번째 민감 정보 검사 후 게시"], + ["record_feedback", "성공, 부분 성공, 실패와 예상 절약 토큰 기록"], + ["mark_stale", "오래되거나 잘못된 해결책을 이후 검색에서 제외"], + ], + }, + safety: { + kicker: "기본으로 안전하게", + title: "지식은 남기고 비밀은 남기지 않습니다.", + body: "초안 저장 전과 게시 직전에 두 번 검사해 API 키, JWT, DB URL, 이메일 등 민감한 값을 제거합니다.", + }, + tips: { + kicker: "권장 사용법", + title: "처음부터 더 정확한 결과를 얻으세요.", + items: [ + ["근거와 함께 검색", "정확한 오류, 프레임워크 버전, 파일 경로, 저장소를 포함하면 검색 순위가 좋아집니다."], + ["agent_json 우선", "에이전트 컨텍스트에는 간결한 모드를 사용하고 깊은 근거가 필요할 때만 전체 카드를 가져오세요."], + ["적용 전 검증", "신뢰도가 높아도 카드는 참고 자료입니다. 환경을 비교하고 검증 절차를 실행하세요."], + ["피드백으로 마무리", "항상 사용 결과를 기록하세요. 실제 재사용 결과가 이후 모든 에이전트의 신뢰도를 개선합니다."], + ["검증된 해결책만 게시", "테스트가 끝난 해결책만 게시하고 불확실한 가설은 비공개 초안으로 유지하세요."], + ["오래된 지식 정리", "버전이나 플랫폼 동작이 바뀌면 즉시 stale로 표시하세요."], + ], + }, + cta: { + overline: "에이전트는 이미 어려운 문제를 해결하고 있습니다", + title: "다음 에이전트가 그 해결책을 기억하게 하세요.", + body: "Context Hub를 연결하고 첫 검색을 실행해 재사용 가능한 엔지니어링 메모리를 만들어 보세요.", + start: "지금 시작하기", + search: "공개 지식 검색", + }, + }, +} as const; + +function ArrowIcon() { + return ; +} + +function CheckIcon() { + return ; +} + +export function CopyBlock({ + code, + language = "json", + locale = "en", +}: { + code: string; + language?: string; + locale?: Locale; +}) { const [copied, setCopied] = useState(false); - async function copy() { + async function handleCopy() { await navigator.clipboard.writeText(code); setCopied(true); window.setTimeout(() => setCopied(false), 1600); @@ -41,9 +410,9 @@ export function CopyBlock({ code, language }: { code: string; language?: string return (
- {language ?? "json"} -
{code}
@@ -51,40 +420,228 @@ export function CopyBlock({ code, language }: { code: string; language?: string ); } -export function SetupTabs() { - const [tab, setTab] = useState("claude"); +function SetupPanel({ locale }: { locale: Locale }) { + const [agent, setAgent] = useState("claude"); + const t = copy[locale].quick; return ( -
-
- - +
+
+
+ {t.prereq} + {t.prereqBody} +
+ +
{t.tokenLabel}
+

{t.tokenTitle}

+

{t.tokenBody}

+
+ + + + +
+

{t.agentHelp[agent]}

+ +
+
{t.connectLabel}
+ +
i

{t.connectNote}

+ +
+
{t.verifyLabel}
+

{t.verifyBody}

+
“{t.verifyPrompt}”
+
+
+ ); +} + +function LocaleToggle({ locale, onChange }: { locale: Locale; onChange: (locale: Locale) => void }) { + return ( +
+ + +
+ ); +} + +export function DocsLanding({ cardsPublished }: { cardsPublished: number }) { + const [locale, setLocale] = useState("en"); + const t = copy[locale]; + + useEffect(() => { + const saved = window.localStorage.getItem("context-hub-locale"); + if (saved === "en" || saved === "ko") { + setLocale(saved); + document.documentElement.lang = saved; + } + }, []); + + function changeLocale(next: Locale) { + setLocale(next); + window.localStorage.setItem("context-hub-locale", next); + document.documentElement.lang = next; + } + + return ( +
+ -

- {tab === "claude" - ? "프로젝트 루트의 .mcp.json에 아래 설정을 추가하세요." - : "프로젝트의 .cursor/mcp.json 또는 전역 ~/.cursor/mcp.json에 추가하세요."} -

- -

- 연결 후 이렇게 말해보세요: “이 오류와 비슷한 해결 사례를 Context Hub에서 먼저 찾아줘.” -

+ +
+
+
+
{t.hero.eyebrow}
+

{t.hero.titleA}
{t.hero.titleB}

+

{t.hero.body}

+ +
+ {t.hero.proof.map((item) => {item})} +
+
+ +
+
+
+ agent · context-hub + ● connected +
+
+
{t.hero.prompt}
+
search_context{t.hero.searching}
+
+
+ 92 +
Secure cookie behind reverse proxyNext.js · OAuth · Fly.io
+ best match +
+

{t.hero.result}

+
+

{t.hero.response}

+
context saved{t.hero.saved}
+
+
+
+ +
+
7{t.metrics[0]}
+
2-step{t.metrics[1]}
+
Hybrid{t.metrics[2]}
+
{cardsPublished.toLocaleString(locale === "ko" ? "ko-KR" : "en-US")}{t.metrics[3]}
+
+ +
+
+ {t.quick.kicker} +

{t.quick.title}

+

{t.quick.body}

+
+ +
+ +
+
+ {t.guide.kicker} +

{t.guide.title}

+

{t.guide.body}

+
+
+ {t.guide.steps.map((item) => ( +
+
+ {item.number} +

{item.title}

+

{item.body}

+
+ +
+ ))} +
+
+ +
+
+ {t.capture.kicker} +

{t.capture.title}

+

{t.capture.body}

+
+
+ {t.capture.steps.map((item) => ( +
+

{item.title}

+

{item.body}

+ +
+ ))} +
+
+

{t.capture.staleTitle}

{t.capture.staleBody}

+ +
+
+ +
+
+ {t.tools.kicker} +

{t.tools.title}

+

{t.tools.body}

+ {t.tools.browse} +
+
+ {t.tools.rows.map(([name, description], index) => ( +
+ {String(index + 1).padStart(2, "0")}{name}

{description}

+
+ ))} +
+
+ +
+
+
+ {t.safety.kicker} +

{t.safety.title}

+

{t.safety.body}

+
+
+
OPENAI_API_KEYsk-proj-••••••••••redacted
+
DATABASE_URLpostgres://••••••redacted
+
frameworkNext.js 16kept
+
+
+
+ +
+
+ {t.tips.kicker} +

{t.tips.title}

+
+
+ {t.tips.items.map(([title, body], index) => ( +
{String(index + 1).padStart(2, "0")}

{title}

{body}

+ ))} +
+
+ +
+ {t.cta.overline} +

{t.cta.title}

+

{t.cta.body}

+
+ {t.cta.start} + {t.cta.search} +
+
); } diff --git a/app/layout.tsx b/app/layout.tsx index 187fc12..c3bdbc0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,15 +6,16 @@ import { getUnreadNotificationCount } from "./lib/hub"; import { Avatar } from "./components"; export const metadata = { - title: "Context Hub — AI 에이전트를 위한 공유 메모리", - description: "한 번 해결한 개발 문제를 Claude Code와 Cursor가 검색하고 재사용하는 MCP 기반 컨텍스트 허브.", + title: "Context Hub — Shared memory for AI coding agents", + description: + "A hosted MCP context hub where Claude Code, Codex, Cursor, and Antigravity search, reuse, and improve verified engineering solutions.", }; export default async function RootLayout({ children }: { children: ReactNode }) { const user = await getCurrentUser(); const unread = user ? getUnreadNotificationCount(user.id) : 0; return ( - +
diff --git a/app/page.tsx b/app/page.tsx index 23ac143..a81f3a4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,241 +1,10 @@ -import Link from "next/link"; import { getStats } from "./lib/hub"; -import { CopyBlock, SetupTabs } from "./landing-client"; +import { DocsLanding } from "./landing-client"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; -const tools = [ - ["search_context", "키워드와 의미를 함께 이해하는 하이브리드 검색"], - ["get_context_card", "필요한 순간에만 전체 해결 과정 불러오기"], - ["draft_context_card", "작업 로그에서 해결책 초안 자동 생성"], - ["submit_for_approval", "게시 전 민감 정보 제거 결과 검토"], - ["publish_context_card", "검증된 해결책을 팀의 기억으로 저장"], - ["record_feedback", "재사용 성공 여부와 절약 토큰 기록"], - ["mark_stale", "오래되거나 잘못된 해결책 검색에서 제외"], -]; - -const workflow = [ - { - number: "01", - title: "먼저 검색합니다", - body: "에이전트가 새 문제를 분석하기 전에 Context Hub에서 유사한 증상과 환경을 찾습니다.", - code: 'search_context({ query: "OAuth cookie missing in production", stack: ["Next.js"] })', - }, - { - number: "02", - title: "해결책을 적용합니다", - body: "검색 결과는 짧은 요약으로 받고, 관련성이 높은 카드만 전체 내용을 불러와 토큰을 아낍니다.", - code: 'get_context_card({ id: "card_...", mode: "agent_json" })', - }, - { - number: "03", - title: "결과를 다시 남깁니다", - body: "실제로 통했는지 기록하고, 새로운 해결책은 사람의 승인 후 다음 작업에서 재사용합니다.", - code: 'record_feedback({ card_id: "card_...", outcome: "success" })', - }, -]; - -function ArrowIcon() { - return ( - - ); -} - -function CheckIcon() { - return ( - - ); -} - export default function DocsHome() { const stats = getStats(); - - return ( -
-
-
-
-
- - MCP 기반 AI 에이전트 메모리 -
-

- 한 번 해결한 문제를 -
- 다시 풀지 마세요. -

-

- Claude Code와 Cursor가 팀의 해결책을 검색하고, 적용하고, 더 나은 지식으로 - 되돌려 놓는 공유 컨텍스트 허브입니다. -

- -
- 로컬 우선 - 민감 정보 자동 제거 - 사람의 게시 승인 -
-
- -
-
-
- agent · context-hub - ● connected -
-
-
- - 프로덕션에서 OAuth 쿠키가 저장되지 않아 -
-
- -
- search_context - 관련 해결책을 검색하는 중... -
-
-
-
- 92 -
- Secure cookie behind reverse proxy - Next.js · OAuth · Fly.io -
- best match -
-

프록시 환경에서 trust proxy와 secure cookie 설정을 함께 확인하세요.

-
-
- -

같은 배포 환경에서 검증된 해결책을 찾았어요. 이 카드를 기준으로 설정을 확인할게요.

-
-
- context saved - 약 8,400 tokens 절약 -
-
-
-
- -
-
7MCP tools
-
2-stepsecret scan
-
Hybridkeyword + semantic
-
- {stats.cardsPublished.toLocaleString()} - live context cards -
-
- -
-
- QUICKSTART -

복사하고, 붙여넣고, 바로 사용하세요.

-

별도 서버 구성 없이 npx 한 줄과 에이전트 설정 파일만으로 시작할 수 있습니다.

-
- -
- -
-
STEP 1 · 로컬 허브 준비
-

명령어 한 줄이면 충분합니다.

-

Node.js 20 이상에서 스키마와 예제 Context Card 20개를 자동으로 준비합니다.

- -
- i -

처음 실행할 때 로컬 임베딩 모델을 내려받습니다. API 키 없이 내 컴퓨터 안에서 동작합니다.

-
-
-
STEP 2 · 사용하는 에이전트에 연결
- -
-
-
- -
-
- HOW IT WORKS -

에이전트의 작업 흐름 안에 자연스럽게 들어갑니다.

-

새로운 도구를 따로 배우기보다, 기존 대화 속에서 검색과 기록이 이어집니다.

-
-
- {workflow.map((item) => ( -
- {item.number} -

{item.title}

-

{item.body}

- {item.code} -
- ))} -
-
- -
-
- MCP TOOLKIT -

검색부터 검증까지,
기억의 전체 수명주기.

-

- Context Card는 단순 메모가 아닙니다. 환경, 증상, 실패한 시도, 해결책, - 검증 결과를 구조화해 에이전트가 바로 사용할 수 있습니다. -

- 공개 카드 둘러보기 -
-
- {tools.map(([name, description], index) => ( -
- {String(index + 1).padStart(2, "0")} - {name} -

{description}

-
- ))} -
-
- -
-
-
- SAFE BY DEFAULT -

팀의 지식은 남기고,
비밀은 남기지 않습니다.

-

초안 생성과 게시 직전에 두 번 검사해 API 키, JWT, DB URL, 이메일 같은 민감 정보를 제거합니다.

-
-
-
OPENAI_API_KEYsk-proj-••••••••••redacted
-
DATABASE_URLpostgres://••••••redacted
-
frameworkNext.js 16kept
-
-
-
- -
- YOUR AGENTS ALREADY SOLVE HARD PROBLEMS -

이제 그 해결책을 잊지 않게 하세요.

-

첫 Context Hub를 만들고, 다음 작업부터 바로 재사용하세요.

-
- 지금 시작하기 - 공개 지식 검색 -
-
-
- ); + return ; } diff --git a/examples/README.md b/examples/README.md index 1d2b9e9..4446314 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,23 +1,22 @@ -# Example agent configs +# Connect an agent -Drop-in MCP server configs for connecting an agent to the Context Hub. +Connect an agent directly to the hosted Context Hub. The command opens a +browser for GitHub sign-in, creates an API token, and updates the selected +agent's global MCP configuration. ## Quickstart ```bash -# 1. Create the local DB (schema + seed cards) -npx -y ai-agent-context-hub context-hub-cli init - -# 2. Point your agent at the server (see the JSON files here) +npx -y ai-agent-context-hub connect claude +npx -y ai-agent-context-hub connect codex +npx -y ai-agent-context-hub connect cursor +npx -y ai-agent-context-hub connect antigravity ``` -- **`claude-code.mcp.json`** — Claude Code (project `.mcp.json`, or `claude mcp add`). -- **`cursor.mcp.json`** — Cursor (`~/.cursor/mcp.json` or project `.cursor/mcp.json`). - -Both launch the published server via `npx -y ai-agent-context-hub`. Set -`HUB_DB_PATH` to a shared absolute path if multiple agents should read the same -hub, and `EMBEDDING_PROVIDER=openai` (with `OPENAI_API_KEY`) for hosted embeddings. +Use `connect all` to configure every supported agent. Claude Code and Codex are +registered through their native CLIs. Cursor and Antigravity configuration files +are merged without removing existing MCP servers. Once connected, the agent has the `search_context`, `get_context_card`, -`draft_context_card`, `publish_context_card`, `record_feedback`, and `mark_stale` -tools. +`draft_context_card`, `submit_for_approval`, `publish_context_card`, +`record_feedback`, and `mark_stale` tools. diff --git a/examples/claude-code.mcp.json b/examples/claude-code.mcp.json deleted file mode 100644 index 8ce8212..0000000 --- a/examples/claude-code.mcp.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "//": "Claude Code: project-scoped .mcp.json, or register globally with `claude mcp add`.", - "mcpServers": { - "context-hub": { - "command": "npx", - "args": ["-y", "ai-agent-context-hub"], - "env": { - "EMBEDDING_PROVIDER": "local", - "HUB_DB_PATH": "./data/hub.db" - } - } - } -} diff --git a/examples/cursor.mcp.json b/examples/cursor.mcp.json deleted file mode 100644 index 66aa446..0000000 --- a/examples/cursor.mcp.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "//": "Cursor: add to ~/.cursor/mcp.json (global) or .cursor/mcp.json (project).", - "mcpServers": { - "context-hub": { - "command": "npx", - "args": ["-y", "ai-agent-context-hub"], - "env": { - "EMBEDDING_PROVIDER": "local", - "HUB_DB_PATH": "./data/hub.db" - } - } - } -} diff --git a/package-lock.json b/package-lock.json index d80e2ed..1ab11c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai-agent-context-hub", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-agent-context-hub", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@huggingface/transformers": "^4.2.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -21,7 +21,8 @@ "zod": "^3.25.0" }, "bin": { - "context-hub": "dist/index.js" + "context-hub": "dist/index.js", + "context-hub-cli": "dist/cli/index.js" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", diff --git a/package.json b/package.json index 54d11d0..bd6c55a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ai-agent-context-hub", - "version": "0.1.0", + "version": "0.2.0", "description": "Agent-first developer knowledge platform — an MCP server + web app for searchable, reusable Context Cards", "type": "module", "license": "MIT", @@ -9,6 +9,9 @@ "model-context-protocol", "ai-agents", "claude", + "codex", + "cursor", + "antigravity", "knowledge-base", "developer-tools", "semantic-search" diff --git a/src/cli/agent-connect.ts b/src/cli/agent-connect.ts new file mode 100644 index 0000000..5a6dc59 --- /dev/null +++ b/src/cli/agent-connect.ts @@ -0,0 +1,96 @@ +import { + chmodSync, + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +export const HOSTED_ORIGIN = "https://claudexhub.fly.dev"; +export const MCP_SERVER_NAME = "context-hub"; +export const CODEX_TOKEN_ENV = "CONTEXT_HUB_TOKEN"; + +export type AgentClient = "claude" | "codex" | "cursor" | "antigravity"; + +type JsonObject = Record; + +function readJsonObject(file: string): JsonObject { + if (!existsSync(file)) return {}; + const parsed: unknown = JSON.parse(readFileSync(file, "utf8")); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error(`${file} must contain a JSON object`); + } + return parsed as JsonObject; +} + +/** + * Add or replace one MCP server without disturbing other user configuration. + * Config files contain credentials, so newly written files are owner-only. + */ +export function writeMcpJsonConfig( + file: string, + server: JsonObject, +): string { + const root = readJsonObject(file); + const existing = root.mcpServers; + const mcpServers = + existing && typeof existing === "object" && !Array.isArray(existing) + ? (existing as JsonObject) + : {}; + + root.mcpServers = { + ...mcpServers, + [MCP_SERVER_NAME]: server, + }; + + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, `${JSON.stringify(root, null, 2)}\n`, { mode: 0o600 }); + chmodSync(file, 0o600); + return file; +} + +export function registerCursorConfig( + mcpUrl: string, + token: string, + home = os.homedir(), +): string { + return writeMcpJsonConfig(path.join(home, ".cursor", "mcp.json"), { + url: mcpUrl, + headers: { Authorization: `Bearer ${token}` }, + }); +} + +export function registerAntigravityConfig( + mcpUrl: string, + token: string, + home = os.homedir(), +): string { + return writeMcpJsonConfig( + path.join(home, ".gemini", "config", "mcp_config.json"), + { + serverUrl: mcpUrl, + headers: { Authorization: `Bearer ${token}` }, + }, + ); +} + +export function parseClientSelection(value: string): AgentClient[] { + const normalized = value.trim().toLowerCase(); + if (normalized === "all" || normalized === "both") { + return ["claude", "codex", "cursor", "antigravity"]; + } + if ( + normalized === "claude" || + normalized === "codex" || + normalized === "cursor" || + normalized === "antigravity" + ) { + return [normalized]; + } + if (normalized === "none" || normalized === "auto") return []; + throw new Error( + `Unknown client "${value}". Use claude, codex, cursor, antigravity, all, or none.`, + ); +} diff --git a/src/cli/index.ts b/src/cli/index.ts index bb3ee23..bce9c6d 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,4 +1,10 @@ -import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { + chmodSync, + readFileSync, + writeFileSync, + mkdirSync, + existsSync, +} from "node:fs"; import http from "node:http"; import crypto from "node:crypto"; import os from "node:os"; @@ -17,6 +23,15 @@ import { config } from "../config.js"; import { extractDraft } from "../domain/extraction.js"; import { redact, redactCard, mergeReports, reportFromFindings } from "../domain/redaction.js"; import type { CardInput } from "../domain/card-schema.js"; +import { + CODEX_TOKEN_ENV, + HOSTED_ORIGIN, + MCP_SERVER_NAME, + parseClientSelection, + registerAntigravityConfig, + registerCursorConfig, + type AgentClient, +} from "./agent-connect.js"; function repo(): Repository { const db = getDb(); @@ -25,7 +40,10 @@ function repo(): Repository { } const program = new Command(); -program.name("context-hub").description("Dev CLI for the AI Agent Context Hub").version("0.1.0"); +program + .name("context-hub") + .description("CLI for the AI Agent Context Hub") + .version("0.2.0"); program .command("init") @@ -210,7 +228,11 @@ program .command("feedback ") .description("Record agent reuse feedback for a card") .requiredOption("--outcome ", "success|partial|failed") - .option("--agent ", "claude_code|codex|cursor|other", "claude_code") + .option( + "--agent ", + "claude_code|codex|cursor|antigravity|other", + "claude_code", + ) .option("--before ", "Estimated tokens without the card") .option("--after ", "Actual tokens used") .action( @@ -219,7 +241,12 @@ program opts: { outcome: "success" | "partial" | "failed"; agent: string; before?: string; after?: string }, ) => { const { card, usage } = await repo().recordUsage(id, { - agent: opts.agent as "claude_code" | "codex" | "cursor" | "other", + agent: opts.agent as + | "claude_code" + | "codex" + | "cursor" + | "antigravity" + | "other", outcome: opts.outcome, tokensBeforeEstimate: opts.before ? Number(opts.before) : undefined, tokensAfterActual: opts.after ? Number(opts.after) : undefined, @@ -328,6 +355,7 @@ function saveCredential(origin: string, data: { token: string; login?: string }) } store[origin] = { ...data, createdAt: new Date().toISOString() }; writeFileSync(file, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 }); + chmodSync(file, 0o600); return file; } @@ -343,9 +371,6 @@ function openBrowser(url: string): void { } } -const MCP_SERVER_NAME = "context-hub"; -const CODEX_TOKEN_ENV = "CONTEXT_HUB_TOKEN"; - /** True if a command resolves on PATH (no ENOENT when invoked). */ function commandExists(cmd: string): boolean { const r = spawnSync(cmd, ["--version"], { stdio: "ignore" }); @@ -390,6 +415,28 @@ function registerCodex(mcpUrl: string, token: string): boolean { return true; } +function registerCursor(mcpUrl: string, token: string): boolean { + try { + const file = registerCursorConfig(mcpUrl, token); + console.log(`✅ Registered with Cursor in ${file}.`); + return true; + } catch (error) { + console.error(`⚠️ Cursor registration failed: ${String(error)}`); + return false; + } +} + +function registerAntigravity(mcpUrl: string, token: string): boolean { + try { + const file = registerAntigravityConfig(mcpUrl, token); + console.log(`✅ Registered with Antigravity in ${file}.`); + return true; + } catch (error) { + console.error(`⚠️ Antigravity registration failed: ${String(error)}`); + return false; + } +} + /** Idempotently persist `export NAME=value` to the user's shell rc file. */ function persistShellEnv(name: string, value: string): string | null { const shell = process.env.SHELL ?? ""; @@ -412,97 +459,144 @@ function persistShellEnv(name: string, value: string): string | null { return file; } -program - .command("login") - .description("Obtain an API token via the browser and register the MCP server") - .option("--host ", "Hub base URL", process.env.HUB_URL || "http://localhost:3000") - .option("--name ", "Token name", `cli@${os.hostname()}`) - .option("--client ", "Register with: claude | codex | both | none (default: auto-detect)") - .option("--no-open", "Print the URL instead of opening a browser") - .option("--print", "Print the token only; don't save or register") - .action( - async (opts: { - host: string; - name: string; - client?: string; - open: boolean; - print?: boolean; - }) => { - const origin = opts.host.replace(/\/+$/, ""); - const state = crypto.randomBytes(16).toString("hex"); - - const token = await new Promise<{ token: string; login?: string }>((resolve, reject) => { - const server = http.createServer((req, res) => { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - const got = url.searchParams.get("state"); - const tok = url.searchParams.get("token"); - res.setHeader("Content-Type", "text/html; charset=utf-8"); - if (got !== state || !tok) { - res.statusCode = 400; - res.end("

Invalid login response. You can close this window.

"); - return; - } - res.end("

✅ Logged in to Context Hub. You can close this window.

"); - server.close(); - resolve({ token: tok, login: url.searchParams.get("login") ?? undefined }); - }); - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const addr = server.address(); - const port = typeof addr === "object" && addr ? addr.port : 0; - const loginUrl = - `${origin}/settings/tokens/cli?port=${port}` + - `&state=${state}&name=${encodeURIComponent(opts.name)}`; - console.log(`Opening ${loginUrl}`); - console.log("If your browser didn't open, visit the URL above to authorize."); - if (opts.open) openBrowser(loginUrl); - }); - setTimeout(() => { - server.close(); - reject(new Error("Timed out waiting for browser login (5 min).")); - }, 5 * 60_000).unref(); - }); +interface ConnectOptions { + host: string; + name: string; + client?: string; + open: boolean; + print?: boolean; +} + +function detectedClients(): AgentClient[] { + const clients: AgentClient[] = []; + if (commandExists("claude")) clients.push("claude"); + if (commandExists("codex")) clients.push("codex"); + if ( + commandExists("cursor") || + commandExists("cursor-agent") || + existsSync(path.join(os.homedir(), ".cursor")) + ) { + clients.push("cursor"); + } + if ( + commandExists("agy") || + existsSync(path.join(os.homedir(), ".gemini", "config")) + ) { + clients.push("antigravity"); + } + return clients; +} - if (opts.print) { - console.log(token.token); +async function connectAgent(opts: ConnectOptions): Promise { + const origin = opts.host.replace(/\/+$/, ""); + const want = (opts.client ?? "auto").toLowerCase(); + const clients = want === "auto" ? detectedClients() : parseClientSelection(want); + const state = crypto.randomBytes(16).toString("hex"); + + const token = await new Promise<{ token: string; login?: string }>((resolve, reject) => { + const server = http.createServer((req, res) => { + const url = new URL(req.url ?? "/", "http://127.0.0.1"); + const got = url.searchParams.get("state"); + const tok = url.searchParams.get("token"); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + if (got !== state || !tok) { + res.statusCode = 400; + res.end("

Invalid login response. You can close this window.

"); return; } + res.end("

✅ Logged in to Context Hub. You can close this window.

"); + server.close(); + resolve({ token: tok, login: url.searchParams.get("login") ?? undefined }); + }); + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + const loginUrl = + `${origin}/settings/tokens/cli?port=${port}` + + `&state=${state}&name=${encodeURIComponent(opts.name)}`; + console.log(`Opening ${loginUrl}`); + console.log("If your browser didn't open, visit the URL above to authorize."); + if (opts.open) openBrowser(loginUrl); + }); + setTimeout(() => { + server.close(); + reject(new Error("Timed out waiting for browser login (5 min).")); + }, 5 * 60_000).unref(); + }); - const file = saveCredential(origin, token); - console.log(`\n✅ Token saved to ${file}${token.login ? ` (as ${token.login})` : ""}`); - - const mcpUrl = `${origin}/api/mcp`; - const want = (opts.client ?? "auto").toLowerCase(); - const doClaude = - want === "claude" || want === "both" || (want === "auto" && commandExists("claude")); - const doCodex = - want === "codex" || want === "both" || (want === "auto" && commandExists("codex")); + if (opts.print) { + console.log(token.token); + return; + } - let registered = false; - if (want !== "none") { - if (doClaude) registered = registerClaude(mcpUrl, token.token) || registered; - if (doCodex) registered = registerCodex(mcpUrl, token.token) || registered; - } + const file = saveCredential(origin, token); + console.log(`\n✅ Token saved to ${file}${token.login ? ` (as ${token.login})` : ""}`); + + const mcpUrl = `${origin}/api/mcp`; + + let registered = false; + for (const client of clients) { + if (client === "claude") { + registered = registerClaude(mcpUrl, token.token) || registered; + } else if (client === "codex") { + registered = registerCodex(mcpUrl, token.token) || registered; + } else if (client === "cursor") { + registered = registerCursor(mcpUrl, token.token) || registered; + } else if (client === "antigravity") { + registered = registerAntigravity(mcpUrl, token.token) || registered; + } + } - if (!registered) { - console.log("\nNo agent CLI registered. Add this MCP server manually:"); - console.log( - JSON.stringify( - { - mcpServers: { - [MCP_SERVER_NAME]: { - url: mcpUrl, - headers: { Authorization: `Bearer ${token.token}` }, - }, - }, + if (!registered) { + console.log("\nNo agent was registered. Re-run with a client name, for example:"); + console.log(" context-hub connect codex"); + console.log( + JSON.stringify( + { + mcpServers: { + [MCP_SERVER_NAME]: { + url: mcpUrl, + headers: { Authorization: `Bearer ${token.token}` }, }, - null, - 2, - ), - ); - } - }, - ); + }, + }, + null, + 2, + ), + ); + } +} + +function addConnectOptions(command: Command): Command { + return command + .option("--host ", "Hub base URL", process.env.HUB_URL || HOSTED_ORIGIN) + .option("--name ", "Token name", `cli@${os.hostname()}`) + .option("--no-open", "Print the URL instead of opening a browser") + .option("--print", "Print the token only; don't save or register"); +} + +addConnectOptions( + program + .command("connect [client]") + .description( + "Sign in and connect Claude, Codex, Cursor, or Antigravity to the hosted Hub", + ), +).action(async (client: string | undefined, opts: ConnectOptions) => { + await connectAgent({ ...opts, client: client ?? "auto" }); +}); + +addConnectOptions( + program + .command("login") + .description("Legacy alias for connect") + .option( + "--client ", + "claude | codex | cursor | antigravity | all | none (default: auto)", + ), +).action(async (opts: ConnectOptions) => { + await connectAgent(opts); +}); program.parseAsync().catch((err) => { console.error(err); diff --git a/src/db/schema.sql b/src/db/schema.sql index c075e2e..281a03b 100644 --- a/src/db/schema.sql +++ b/src/db/schema.sql @@ -48,7 +48,7 @@ CREATE INDEX IF NOT EXISTS idx_evidence_card ON source_evidence(card_id); CREATE TABLE IF NOT EXISTS agent_usage ( id TEXT PRIMARY KEY, card_id TEXT NOT NULL REFERENCES context_cards(id) ON DELETE CASCADE, - agent TEXT NOT NULL, -- claude_code | codex | cursor | other + agent TEXT NOT NULL, -- claude_code | codex | cursor | antigravity | other outcome TEXT NOT NULL, -- success | partial | failed tokens_before_estimate INTEGER, tokens_after_actual INTEGER, diff --git a/src/domain/types.ts b/src/domain/types.ts index ec127ae..ec36ceb 100644 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -10,7 +10,12 @@ export type EvidenceSource = | "issue" | "test" | "official_doc"; -export type AgentName = "claude_code" | "codex" | "cursor" | "other"; +export type AgentName = + | "claude_code" + | "codex" + | "cursor" + | "antigravity" + | "other"; export type UsageOutcome = "success" | "partial" | "failed"; export interface AgentUsage { diff --git a/src/index.ts b/src/index.ts index 784f663..47edea4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,14 @@ import { buildServer } from "./mcp/server.js"; import { config, logStderr } from "./config.js"; async function main(): Promise { + // The package's default binary doubles as the hosted setup CLI when a + // subcommand is present. With no arguments it remains the stdio MCP server, + // preserving compatibility for existing local integrations. + if (process.argv.length > 2) { + await import("./cli/index.js"); + return; + } + const db = getDb(); migrate(db); // idempotent — ensure schema exists const server = buildServer(db); diff --git a/src/mcp/tools/record-feedback.ts b/src/mcp/tools/record-feedback.ts index bc94ad8..e07e536 100644 --- a/src/mcp/tools/record-feedback.ts +++ b/src/mcp/tools/record-feedback.ts @@ -7,7 +7,7 @@ export const recordFeedbackSchema = { .enum(["success", "partial", "failed"]) .describe("Did applying the card solve the problem?"), agent: z - .enum(["claude_code", "codex", "cursor", "other"]) + .enum(["claude_code", "codex", "cursor", "antigravity", "other"]) .default("claude_code") .describe("Which agent used the card"), tokens_before_estimate: z diff --git a/test/agent-connect.test.ts b/test/agent-connect.test.ts new file mode 100644 index 0000000..d49ec38 --- /dev/null +++ b/test/agent-connect.test.ts @@ -0,0 +1,69 @@ +import { + mkdtempSync, + mkdirSync, + readFileSync, + statSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { + parseClientSelection, + registerAntigravityConfig, + registerCursorConfig, +} from "../src/cli/agent-connect.js"; + +function tempHome(): string { + return mkdtempSync(path.join(os.tmpdir(), "context-hub-connect-")); +} + +describe("agent connection config", () => { + it("adds Cursor without replacing existing MCP servers", () => { + const home = tempHome(); + const dir = path.join(home, ".cursor"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + path.join(dir, "mcp.json"), + JSON.stringify({ mcpServers: { existing: { url: "https://example.test/mcp" } } }), + ); + + const file = registerCursorConfig("https://hub.test/api/mcp", "cxh_test", home); + const config = JSON.parse(readFileSync(file, "utf8")); + + expect(config.mcpServers.existing.url).toBe("https://example.test/mcp"); + expect(config.mcpServers["context-hub"]).toEqual({ + url: "https://hub.test/api/mcp", + headers: { Authorization: "Bearer cxh_test" }, + }); + if (process.platform !== "win32") { + expect(statSync(file).mode & 0o777).toBe(0o600); + } + }); + + it("writes the Antigravity remote-server schema", () => { + const home = tempHome(); + const file = registerAntigravityConfig( + "https://hub.test/api/mcp", + "cxh_test", + home, + ); + const config = JSON.parse(readFileSync(file, "utf8")); + + expect(config.mcpServers["context-hub"]).toEqual({ + serverUrl: "https://hub.test/api/mcp", + headers: { Authorization: "Bearer cxh_test" }, + }); + }); + + it("parses one client or every supported client", () => { + expect(parseClientSelection("codex")).toEqual(["codex"]); + expect(parseClientSelection("all")).toEqual([ + "claude", + "codex", + "cursor", + "antigravity", + ]); + expect(() => parseClientSelection("unknown")).toThrow("Unknown client"); + }); +});