mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
fix(ci): fix codespell and prettier issues
- Fix typos: parellel→parallel, neccessary→necessary, mulitple→multiple, responible→responsible, geniune→genuine, yeild→yield - Add ignore words: Nd, nd, edn, lightening (valid terms) - Run prettier --write to fix all formatting issues
This commit is contained in:
+5
-1
@@ -4,4 +4,8 @@ psuedo
|
||||
shouldBe
|
||||
amountIn
|
||||
MapPin
|
||||
summar
|
||||
summar
|
||||
Nd
|
||||
nd
|
||||
edn
|
||||
lightening
|
||||
@@ -8,33 +8,33 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'hanzo-node/**'
|
||||
- 'console/**'
|
||||
- 'platform/**'
|
||||
- 'deploy/**'
|
||||
- '.github/workflows/do-deploy.yml'
|
||||
- "hanzo-node/**"
|
||||
- "console/**"
|
||||
- "platform/**"
|
||||
- "deploy/**"
|
||||
- ".github/workflows/do-deploy.yml"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'deploy/terraform/**'
|
||||
- 'deploy/do/**'
|
||||
- "deploy/terraform/**"
|
||||
- "deploy/do/**"
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: 'Deployment environment'
|
||||
description: "Deployment environment"
|
||||
required: true
|
||||
default: 'staging'
|
||||
default: "staging"
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
action:
|
||||
description: 'Deployment action'
|
||||
description: "Deployment action"
|
||||
required: true
|
||||
default: 'deploy'
|
||||
default: "deploy"
|
||||
type: choice
|
||||
options:
|
||||
- deploy
|
||||
@@ -43,10 +43,10 @@ on:
|
||||
- destroy
|
||||
|
||||
env:
|
||||
DOCTL_VERSION: '1.101.0'
|
||||
TERRAFORM_VERSION: '1.6.6'
|
||||
NODE_VERSION: '20'
|
||||
RUST_VERSION: '1.82'
|
||||
DOCTL_VERSION: "1.101.0"
|
||||
TERRAFORM_VERSION: "1.6.6"
|
||||
NODE_VERSION: "20"
|
||||
RUST_VERSION: "1.82"
|
||||
|
||||
jobs:
|
||||
# ==========================================================================
|
||||
@@ -223,7 +223,6 @@ jobs:
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
github.event.inputs.action == 'apply'
|
||||
environment: ${{ github.event.inputs.environment || 'staging' }}
|
||||
|
||||
env:
|
||||
TF_VAR_do_token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
TF_VAR_spaces_access_id: ${{ secrets.SPACES_ACCESS_ID }}
|
||||
@@ -260,7 +259,6 @@ jobs:
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
github.event.inputs.action == 'deploy'
|
||||
environment: ${{ github.event.inputs.environment || 'staging' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -7,26 +7,26 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'hanzo-node/**'
|
||||
- '.github/workflows/hanzo-node-release.yml'
|
||||
- '!hanzo-node/**/*.md'
|
||||
- '!hanzo-node/**/*.test.*'
|
||||
- '!hanzo-node/**/test/**'
|
||||
- "hanzo-node/**"
|
||||
- ".github/workflows/hanzo-node-release.yml"
|
||||
- "!hanzo-node/**/*.md"
|
||||
- "!hanzo-node/**/*.test.*"
|
||||
- "!hanzo-node/**/test/**"
|
||||
|
||||
# Allow manual trigger with optional version override
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (leave empty for auto-increment)'
|
||||
description: "Version to release (leave empty for auto-increment)"
|
||||
required: false
|
||||
type: string
|
||||
skip_npm:
|
||||
description: 'Skip npm publish'
|
||||
description: "Skip npm publish"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
dry_run:
|
||||
description: 'Dry run (build only, no publish)'
|
||||
description: "Dry run (build only, no publish)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -57,8 +57,8 @@ jobs:
|
||||
- name: Setup Node.js for auth check
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Validate npm authentication
|
||||
if: inputs.skip_npm != true && inputs.dry_run != true
|
||||
@@ -93,8 +93,8 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Determine next version
|
||||
id: version
|
||||
@@ -349,8 +349,8 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: "20"
|
||||
|
||||
- name: Cache npm (npx) downloads
|
||||
uses: actions/cache@v4
|
||||
@@ -234,7 +234,6 @@ jobs:
|
||||
# Wait briefly for readiness
|
||||
for i in {1..30}; do if nc -z 127.0.0.1 5057; then break; else sleep 0.2; fi; done || true
|
||||
|
||||
|
||||
- name: Run Code agent (workspace-write)
|
||||
id: run_agent
|
||||
continue-on-error: true
|
||||
|
||||
@@ -292,7 +292,6 @@ jobs:
|
||||
echo '### openai-proxy.log (tail)' >> "$GITHUB_STEP_SUMMARY"
|
||||
{ echo '```'; tail -n 80 .github/auto/openai-proxy.log || true; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
|
||||
- name: Generate comment with Code (read-only)
|
||||
id: gen
|
||||
if: steps.meta.outputs.pr_number && env.OPENAI_API_KEY != ''
|
||||
|
||||
@@ -62,9 +62,9 @@ jobs:
|
||||
# Default is strict (no fallback) to preserve bot authorship.
|
||||
# Authorship: PAT only (no fallbacks). Ensure CODE_GH_PAT has org repo write perms.
|
||||
# Rate limiting configuration
|
||||
RATE_WINDOW_HOURS: '24'
|
||||
RATE_PER_ACTOR: '5' # Max issues per actor in window to auto-act
|
||||
RATE_TOTAL_AUTO_PRS: '50' # Max auto PRs opened by bot in window
|
||||
RATE_WINDOW_HOURS: "24"
|
||||
RATE_PER_ACTOR: "5" # Max issues per actor in window to auto-act
|
||||
RATE_TOTAL_AUTO_PRS: "50" # Max auto PRs opened by bot in window
|
||||
# Path policy configuration
|
||||
PROTECTED_GLOBS: |
|
||||
.github/**
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: "20"
|
||||
|
||||
# No Rust or heavy npm usage in triage; skip related caches and toolchains.
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch: {} # Allow manual trigger
|
||||
workflow_dispatch: {} # Allow manual trigger
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
# Ignore common non-release paths, but DO NOT ignore release.yml so that
|
||||
# edits to this workflow can intentionally trigger a new release run.
|
||||
paths-ignore:
|
||||
- '.github/workflows/issue-triage.yml'
|
||||
- '.github/workflows/preview-build.yml'
|
||||
- '.github/workflows/upstream-merge.yml'
|
||||
- 'examples/**'
|
||||
- '**/*.test.ts'
|
||||
- 'test/**'
|
||||
- '*.md'
|
||||
- 'CHANGELOG.md'
|
||||
- 'docs/release-notes/**'
|
||||
- 'codex-cli/package.json'
|
||||
- ".github/workflows/issue-triage.yml"
|
||||
- ".github/workflows/preview-build.yml"
|
||||
- ".github/workflows/upstream-merge.yml"
|
||||
- "examples/**"
|
||||
- "**/*.test.ts"
|
||||
- "test/**"
|
||||
- "*.md"
|
||||
- "CHANGELOG.md"
|
||||
- "docs/release-notes/**"
|
||||
- "codex-cli/package.json"
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
@@ -43,8 +43,8 @@ jobs:
|
||||
- name: Setup Node.js for auth check
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Validate npm authentication (must fail fast if missing/invalid)
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }}
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
- name: Setup Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Read Rust toolchain channel
|
||||
id: rust_toolchain
|
||||
@@ -175,8 +175,8 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Determine next version (across all npm packages; ensure unique)
|
||||
id: version
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
- name: Setup Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Read Rust toolchain channel
|
||||
id: rust_toolchain
|
||||
@@ -385,7 +385,7 @@ jobs:
|
||||
- name: Build binaries (with timings)
|
||||
shell: bash
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0" # keep off in CI; release builds + sccache
|
||||
CARGO_INCREMENTAL: "0" # keep off in CI; release builds + sccache
|
||||
RUST_BACKTRACE: "1"
|
||||
run: |
|
||||
cd hanzo-dev
|
||||
@@ -508,8 +508,8 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Start local OpenAI proxy for release (hardened)
|
||||
if: env.OPENAI_API_KEY != ''
|
||||
@@ -1046,7 +1046,7 @@ jobs:
|
||||
test -s Formula/Code.rb || { echo "Formula/Code.rb missing" >&2; exit 1; }
|
||||
|
||||
- name: Publish Homebrew formula to just-every/homebrew-tap
|
||||
continue-on-error: true # Non-blocking: npm publish is higher priority
|
||||
continue-on-error: true # Non-blocking: npm publish is higher priority
|
||||
shell: bash
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -3,17 +3,17 @@ name: Upstream Merge
|
||||
on:
|
||||
# Poll upstream regularly; also allow manual and external triggers
|
||||
schedule:
|
||||
- cron: '*/30 * * * *'
|
||||
- cron: "*/30 * * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upstream_repo:
|
||||
description: 'Upstream repo (owner/name)'
|
||||
description: "Upstream repo (owner/name)"
|
||||
required: false
|
||||
default: 'openai/codex'
|
||||
default: "openai/codex"
|
||||
upstream_branch:
|
||||
description: 'Upstream branch'
|
||||
description: "Upstream branch"
|
||||
required: false
|
||||
default: 'main'
|
||||
default: "main"
|
||||
repository_dispatch:
|
||||
types: [upstream-push]
|
||||
|
||||
@@ -35,9 +35,9 @@ env:
|
||||
# skips due to no upstream changes and the branch is zero‑diff vs base.
|
||||
# This used to be true when we reset the branch each run; now we carry
|
||||
# forward the branch for incremental merges, so default this to false.
|
||||
CLOSE_ZERO_DIFF_ON_SKIP: 'false'
|
||||
RG_VERSION: '14.1.0'
|
||||
JQ_VERSION: '1.7.1'
|
||||
CLOSE_ZERO_DIFF_ON_SKIP: "false"
|
||||
RG_VERSION: "14.1.0"
|
||||
JQ_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
precheck:
|
||||
@@ -354,7 +354,7 @@ jobs:
|
||||
if: steps.check_upstream.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: "20"
|
||||
|
||||
- name: Cache npm (npx) downloads
|
||||
if: steps.check_upstream.outputs.skip != 'true'
|
||||
@@ -383,7 +383,6 @@ jobs:
|
||||
# Wait briefly for readiness
|
||||
for i in {1..30}; do if nc -z 127.0.0.1 5055; then break; else sleep 0.2; fi; done || true
|
||||
|
||||
|
||||
- name: Print proxy startup log tail
|
||||
if: steps.check_upstream.outputs.skip != 'true'
|
||||
run: |
|
||||
@@ -603,7 +602,7 @@ jobs:
|
||||
- If Git reports no merge-base between origin/DEFAULT_BRANCH and upstream/UPSTREAM_BRANCH, you may use `--allow-unrelated-histories` to graft histories and proceed with all policies/guards.
|
||||
- If the initial `git merge --no-ff --no-commit upstream/UPSTREAM_BRANCH` fails with unrelated histories, re-run the merge in a separate command with `--allow-unrelated-histories` (do not chain with `||`).
|
||||
- Use only the provided GH_TOKEN for push; do not echo it.
|
||||
|
||||
|
||||
<tools>
|
||||
- To search for API usages before removing exports:
|
||||
rg -n "^(use\\s+codex_core::|codex_core::)(ModelClient|Prompt|Response(Event|Stream))\\b" codex-rs
|
||||
@@ -835,7 +834,7 @@ jobs:
|
||||
fi
|
||||
popd >/dev/null
|
||||
git worktree remove -f "$wt"
|
||||
|
||||
|
||||
- name: Branding report (guide-only) and perma-deleted guard
|
||||
if: steps.check_upstream.outputs.skip != 'true'
|
||||
env:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Hanzo Dev - LLM Context
|
||||
|
||||
## Project Overview
|
||||
|
||||
Hanzo Dev is a powerful fork of OpenAI's Codex CLI, enhanced with enterprise features and maintained by Hanzo AI. This is a Rust-based CLI tool that provides AI-powered development assistance directly in the terminal.
|
||||
|
||||
## Recent Updates (January 2026)
|
||||
@@ -35,6 +36,7 @@ Added `hanzo-node` npm package for Node.js project integration:
|
||||
- Validates binary headers (ELF/Mach-O/PE)
|
||||
|
||||
5. **Usage**
|
||||
|
||||
```bash
|
||||
# Global installation
|
||||
npm install -g hanzo-node
|
||||
@@ -48,6 +50,7 @@ Added `hanzo-node` npm package for Node.js project integration:
|
||||
```
|
||||
|
||||
### Branding + Tooling Refresh (January 2026)
|
||||
|
||||
- Updated docs and CLI guidance to use Hanzo Dev branding, `dev` command, and `~/.hanzo` as primary config home (legacy `~/.code`/`~/.codex` still read).
|
||||
- MCP Rust crate moved to Rust 2024 edition with a `rust-toolchain.toml` pinned to `stable`.
|
||||
|
||||
@@ -77,7 +80,8 @@ Added `hanzo-node` npm package for Node.js project integration:
|
||||
|
||||
## Hanzod - Unified AI Blockchain with Lux Consensus (January 2025)
|
||||
|
||||
### Complete AI Blockchain Infrastructure
|
||||
### Complete AI Blockchain Infrastructure
|
||||
|
||||
Hanzod has evolved into a complete AI blockchain system with native KuzuDB ledger and full Lux consensus integration:
|
||||
|
||||
1. **Native KuzuDB Ledger Integration**
|
||||
@@ -87,7 +91,7 @@ Hanzod has evolved into a complete AI blockchain system with native KuzuDB ledge
|
||||
- Cypher queries for complex graph analysis
|
||||
- In-process execution with zero network overhead
|
||||
|
||||
2. **Lux Consensus Protocol**
|
||||
2. **Lux Consensus Protocol**
|
||||
- Full compatibility with luxfi/consensus
|
||||
- Snow consensus with parameters: k=20, α=15, β=15/20
|
||||
- Quantum finality for immutable state
|
||||
@@ -120,6 +124,7 @@ Hanzod has evolved into a complete AI blockchain system with native KuzuDB ledge
|
||||
## Unified Chain Architecture (December 2025)
|
||||
|
||||
### Clean Architecture Refactoring
|
||||
|
||||
Complete blockchain system with KuzuDB as the default database backend implementation. Major refactoring to remove branding and use generic, concise names:
|
||||
|
||||
1. **Naming Convention Updates** (Completed)
|
||||
@@ -166,16 +171,17 @@ Complete blockchain system with KuzuDB as the default database backend implement
|
||||
- **Architecture**: Clean abstraction with KuzuDB as default backend
|
||||
- **Tests**: Pure Rust tests in `/tests` directory
|
||||
|
||||
3. **AI Blockchain Pipeline** (`/src/rs/hanzod/src/ai_blockchain.rs`)
|
||||
5. **AI Blockchain Pipeline** (`/src/rs/hanzod/src/ai_blockchain.rs`)
|
||||
- **Single Process**: Text → Embedding → Storage → Search → Inference
|
||||
- **Zero Latency**: All operations in-memory with no network calls
|
||||
- **Integrated Engine**: Embedded Hanzo Engine with mistralrs
|
||||
- **Metal Acceleration**: Native Apple Silicon GPU support
|
||||
|
||||
```rust
|
||||
pub async fn process_text(&self, text: &str, search_similar: bool, run_inference: bool)
|
||||
```
|
||||
|
||||
4. **API Gateway System** (`/src/rs/hanzod/src/api_gateway.rs`)
|
||||
6. **API Gateway System** (`/src/rs/hanzod/src/api_gateway.rs`)
|
||||
- **Key-Based Billing**: Usage tracking and rate limiting per API key
|
||||
- **Multi-Tenant**: Encrypted namespaces with data isolation
|
||||
- **Privacy Modes**:
|
||||
@@ -190,11 +196,12 @@ Complete blockchain system with KuzuDB as the default database backend implement
|
||||
- **Pricing Tiers**: Pay-as-you-go and subscription plans
|
||||
- **Service Types**: Native agents, LLMs, compute, VPN, vector search
|
||||
|
||||
5. **KuzuDB Vector Store** (`/src/rs/hanzod/src/kuzu_vector.rs`)
|
||||
7. **KuzuDB Vector Store** (`/src/rs/hanzod/src/kuzu_vector.rs`)
|
||||
- **HNSW Index**: Fast k-NN similarity search
|
||||
- **Graph Embeddings**: Connect vectors in knowledge graphs
|
||||
- **Blockchain Checkpoints**: Immutable snapshots with merkle roots
|
||||
- **Cypher Queries**: Complex graph traversals
|
||||
|
||||
```rust
|
||||
// Unified operations
|
||||
store.add_vector(id, embedding, content, metadata).await?;
|
||||
@@ -202,7 +209,7 @@ Complete blockchain system with KuzuDB as the default database backend implement
|
||||
store.cypher_query("MATCH (n:Node)-[r]->(m) RETURN n,r,m").await?;
|
||||
```
|
||||
|
||||
6. **Key Features Implemented**
|
||||
8. **Key Features Implemented**
|
||||
- ✅ Real Ed25519 cryptographic signatures (no placeholders)
|
||||
- ✅ Real embedding engine with Snowflake Arctic Embed L model
|
||||
- ✅ Real mistralrs integration for embedded inference
|
||||
@@ -213,22 +220,27 @@ Complete blockchain system with KuzuDB as the default database backend implement
|
||||
- ✅ TEE/confidential computing support
|
||||
|
||||
### Service Architecture
|
||||
|
||||
- **Port 3690**: Unified hanzod service endpoint
|
||||
- **Embedded Engine**: Hanzo Engine with Metal acceleration
|
||||
- **Models**:
|
||||
- **Models**:
|
||||
- `phi-3.5-mini` for inference (embedded)
|
||||
- `snowflake-arctic-embed-l` for embeddings (1536 dimensions)
|
||||
- **Database**: KuzuDB with full graph/vector/blockchain capabilities
|
||||
|
||||
### Testing & Integration
|
||||
|
||||
Created comprehensive test infrastructure:
|
||||
|
||||
- `test_integration.sh` - Shell script for endpoint testing
|
||||
- `test_ai_blockchain.py` - Python integration test suite
|
||||
- Unit tests for all major components
|
||||
- End-to-end pipeline validation
|
||||
|
||||
### Vision Realized
|
||||
|
||||
The system demonstrates the "absolutely fastest execution path" requested:
|
||||
|
||||
- **Native Agents**: Direct in-process execution
|
||||
- **LLM Operations**: Embedded inference with Metal acceleration
|
||||
- **Vector Search**: HNSW index with sub-millisecond queries
|
||||
@@ -239,6 +251,7 @@ The system demonstrates the "absolutely fastest execution path" requested:
|
||||
## Hanzo Compute Integration (December 2025)
|
||||
|
||||
### Unified Compute Layer Infrastructure
|
||||
|
||||
The dev project now uses hanzod as its primary compute layer:
|
||||
|
||||
1. **Hanzod Connection**
|
||||
@@ -265,7 +278,9 @@ The dev project now uses hanzod as its primary compute layer:
|
||||
- Post-quantum secure for future-proof infrastructure
|
||||
|
||||
### Post-Quantum Cryptography Stack
|
||||
|
||||
Implemented comprehensive post-quantum security:
|
||||
|
||||
- **HPKE** (Hybrid Public Key Encryption) for secure channels
|
||||
- **ML-KEM-768** (NIST FIPS 203) for key encapsulation
|
||||
- **ChaCha20-Poly1305** for authenticated encryption
|
||||
@@ -274,6 +289,7 @@ Implemented comprehensive post-quantum security:
|
||||
- All cryptographic tests passing
|
||||
|
||||
### Compute SDK Enhancement
|
||||
|
||||
```rust
|
||||
use hanzo_inference::{HanzoManager, HanzoConfig};
|
||||
|
||||
@@ -289,7 +305,9 @@ let results = manager.vector.search(query, 10, None).await?;
|
||||
```
|
||||
|
||||
### TUI Compilation Fixes
|
||||
|
||||
Resolved major compilation issues after merge:
|
||||
|
||||
- Added `standard_terminal_mode` field
|
||||
- Implemented `maybe_show_history_nav_hint_on_first_scroll` method
|
||||
- Fixed pulldown-cmark API compatibility for v0.9
|
||||
@@ -301,6 +319,7 @@ Resolved major compilation issues after merge:
|
||||
### Running with Hanzod Compute Layer
|
||||
|
||||
1. **Start Hanzod**:
|
||||
|
||||
```bash
|
||||
cd /Users/z/work/hanzo/hanzod
|
||||
cargo build --release
|
||||
@@ -309,6 +328,7 @@ Resolved major compilation issues after merge:
|
||||
```
|
||||
|
||||
2. **Run Dev with Unified Compute**:
|
||||
|
||||
```bash
|
||||
cd /Users/z/work/hanzo/dev
|
||||
cargo build --release
|
||||
@@ -322,6 +342,7 @@ Resolved major compilation issues after merge:
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
```bash
|
||||
# Hanzod compute layer settings
|
||||
HANZOD_HTTP_URL=http://localhost:8080
|
||||
@@ -340,6 +361,7 @@ HANZOD_ENABLE_BITDELTA=true # Model personalization
|
||||
## Recent Updates (September 2025)
|
||||
|
||||
### Merged from OpenAI Codex Upstream
|
||||
|
||||
Successfully incorporated key improvements from OpenAI Codex (up to commit 9bbeb75361):
|
||||
|
||||
1. **Reasoning Effort Tracking**
|
||||
@@ -361,6 +383,7 @@ Successfully incorporated key improvements from OpenAI Codex (up to commit 9bbeb
|
||||
## Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
/Users/z/work/hanzo/dev/
|
||||
├── src/
|
||||
@@ -378,6 +401,7 @@ Successfully incorporated key improvements from OpenAI Codex (up to commit 9bbeb
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
- **Core** (`dev-core`): Main business logic, config, auth
|
||||
- **TUI** (`dev-tui`): Terminal interface with Ratatui 0.29.0
|
||||
- **MCP** (`mcp-server/client/types`): Full MCP protocol support
|
||||
@@ -388,17 +412,20 @@ Successfully incorporated key improvements from OpenAI Codex (up to commit 9bbeb
|
||||
## Technical Decisions
|
||||
|
||||
### Build System
|
||||
|
||||
- Using Rust 2024 edition across all crates
|
||||
- Fast development profile with incremental compilation
|
||||
- Production profile with full LTO and symbol stripping
|
||||
- Workspace-level lints: `unwrap_used = "deny"`, `expect_used = "deny"`
|
||||
|
||||
### Package Distribution
|
||||
|
||||
- npm package: `@hanzo/dev` v3.0.0
|
||||
- Platform-specific binaries via optionalDependencies
|
||||
- GitHub Actions for multi-platform builds (Linux musl, macOS, Windows)
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- Unit tests per crate
|
||||
- Integration tests in `tests/suite/`
|
||||
- Snapshot testing with insta (from upstream)
|
||||
@@ -409,6 +436,7 @@ Successfully incorporated key improvements from OpenAI Codex (up to commit 9bbeb
|
||||
Based on analysis of OpenAI Codex at `/Users/z/work/openai/codex`:
|
||||
|
||||
### High Priority
|
||||
|
||||
1. **Unified Execution** (commit c09ed74a1)
|
||||
- PTY-backed interactive exec with session reuse
|
||||
- Bounded output (128 KiB) and timeout clamping
|
||||
@@ -425,6 +453,7 @@ Based on analysis of OpenAI Codex at `/Users/z/work/openai/codex`:
|
||||
- Better ChatGPT/API key reconciliation
|
||||
|
||||
### Medium Priority
|
||||
|
||||
1. **IDE Integration**
|
||||
- UriBasedFileOpener for VS Code, Cursor, Windsurf
|
||||
- Already partially implemented
|
||||
@@ -438,6 +467,7 @@ Based on analysis of OpenAI Codex at `/Users/z/work/openai/codex`:
|
||||
## Development Notes
|
||||
|
||||
### Compilation
|
||||
|
||||
```bash
|
||||
# Fast development build
|
||||
./build-fast.sh
|
||||
@@ -450,18 +480,22 @@ cargo test --all
|
||||
```
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Build takes significant time on first compilation
|
||||
- Some upstream features require config flags to enable
|
||||
- Directory structure differs from upstream (codex-rs/ → src/rs/)
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. Complete testing of current merge
|
||||
2. Consider importing unified execution feature
|
||||
3. Evaluate TUI onboarding improvements
|
||||
4. Update documentation for new features
|
||||
|
||||
## Merge Strategy
|
||||
|
||||
When syncing with upstream:
|
||||
|
||||
1. Fetch from `https://github.com/openai/codex`
|
||||
2. Cherry-pick or manually apply changes
|
||||
3. Update paths from `codex-rs/` to `src/rs/`
|
||||
@@ -469,6 +503,7 @@ When syncing with upstream:
|
||||
5. Test thoroughly with `./build-fast.sh`
|
||||
|
||||
## Contact
|
||||
|
||||
- Repository: https://github.com/hanzoai/dev
|
||||
- Organization: Hanzo AI (https://hanzo.ai)
|
||||
- Upstream: https://github.com/openai/codex (for reference)
|
||||
@@ -476,6 +511,7 @@ When syncing with upstream:
|
||||
## Context for All AI Assistants
|
||||
|
||||
This file (`LLM.md`) is symlinked as:
|
||||
|
||||
- `.AGENTS.md`
|
||||
- `CLAUDE.md`
|
||||
- `QWEN.md`
|
||||
|
||||
+14
-14
@@ -163,11 +163,11 @@ extractor.export(ConversationFormat::Jsonl).await?;
|
||||
Located at: `github.com/hanzoai/mcp`
|
||||
|
||||
```typescript
|
||||
import { createMCPServer, Guard } from '@hanzo/mcp';
|
||||
import { createMCPServer, Guard } from "@hanzo/mcp";
|
||||
|
||||
const server = await createMCPServer({
|
||||
tools: ['filesystem', 'git', 'docker'],
|
||||
guard: { enabled: true, pii: true }
|
||||
tools: ["filesystem", "git", "docker"],
|
||||
guard: { enabled: true, pii: true },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -194,15 +194,15 @@ interface ProviderCapabilities {
|
||||
|
||||
### Implemented Adapters
|
||||
|
||||
| Provider | Package | CLI Tool | Status |
|
||||
|----------|---------|----------|--------|
|
||||
| Claude | `@anthropic-ai/claude-code` | `claude` | ✅ |
|
||||
| Codex | `@hanzo/dev` | `dev/codex` | ✅ |
|
||||
| Qwen | `qwen-cli` | `qwen` | ✅ |
|
||||
| Gemini | `@google/gemini-cli` | `gemini` | ✅ |
|
||||
| Vibe | `vibe` | `vibe` | ✅ |
|
||||
| Copilot | `gh (extension)` | `gh copilot` | ✅ |
|
||||
| Ollama | `ollama` | `ollama` | ✅ |
|
||||
| Provider | Package | CLI Tool | Status |
|
||||
| -------- | --------------------------- | ------------ | ------ |
|
||||
| Claude | `@anthropic-ai/claude-code` | `claude` | ✅ |
|
||||
| Codex | `@hanzo/dev` | `dev/codex` | ✅ |
|
||||
| Qwen | `qwen-cli` | `qwen` | ✅ |
|
||||
| Gemini | `@google/gemini-cli` | `gemini` | ✅ |
|
||||
| Vibe | `vibe` | `vibe` | ✅ |
|
||||
| Copilot | `gh (extension)` | `gh copilot` | ✅ |
|
||||
| Ollama | `ollama` | `ollama` | ✅ |
|
||||
|
||||
## Data Flow
|
||||
|
||||
@@ -281,11 +281,11 @@ guard:
|
||||
|
||||
# Extract settings
|
||||
extract:
|
||||
enabled: false # opt-in
|
||||
enabled: false # opt-in
|
||||
anonymize: true
|
||||
output: ~/.hanzo/conversations/
|
||||
format: jsonl
|
||||
|
||||
|
||||
# MCP settings
|
||||
mcp:
|
||||
servers:
|
||||
|
||||
@@ -18,6 +18,7 @@ This document outlines the architecture for an automated system that tracks open
|
||||
Modern software projects depend on hundreds of open-source packages, yet the authors of these packages are rarely compensated. Current funding mechanisms (GitHub Sponsors, Open Collective) are fragmented and require manual setup by each maintainer.
|
||||
|
||||
**Goals**:
|
||||
|
||||
1. Automatically identify all OSS dependencies in a project
|
||||
2. Attribute contributions to their authors
|
||||
3. Calculate fair distribution weights
|
||||
@@ -29,6 +30,7 @@ Modern software projects depend on hundreds of open-source packages, yet the aut
|
||||
### 3.1 Dependency Tracking Patterns
|
||||
|
||||
**Rust/Cargo Ecosystem**:
|
||||
|
||||
```toml
|
||||
# Cargo.toml metadata provides:
|
||||
[package]
|
||||
@@ -39,6 +41,7 @@ license = "MIT OR Apache-2.0"
|
||||
```
|
||||
|
||||
**Node.js/npm Ecosystem**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "package-name",
|
||||
@@ -53,6 +56,7 @@ license = "MIT OR Apache-2.0"
|
||||
```
|
||||
|
||||
**GitHub FUNDING.yml Standard**:
|
||||
|
||||
```yaml
|
||||
github: [dtolnay]
|
||||
open_collective: project-name
|
||||
@@ -62,14 +66,14 @@ custom: ["https://example.com/donate"]
|
||||
|
||||
### 3.2 Existing Hanzo Infrastructure
|
||||
|
||||
| Component | Location | Capabilities |
|
||||
|-----------|----------|--------------|
|
||||
| **Commerce** | `/hanzo/commerce` | Payment processing, wallets, transactions |
|
||||
| **PaymentProcessor** | `commerce/payment/processor` | Stripe, PayPal, Bitcoin, Ethereum |
|
||||
| **CryptoProcessor** | `commerce/payment/processor` | Address generation, balance checking |
|
||||
| **Wallet Model** | `commerce/models/wallet` | Multi-chain account management |
|
||||
| **Events System** | `commerce/events` | ClickHouse-based analytics |
|
||||
| **Lux Network** | `/lux/wallet` | Blockchain settlement layer |
|
||||
| Component | Location | Capabilities |
|
||||
| -------------------- | ---------------------------- | ----------------------------------------- |
|
||||
| **Commerce** | `/hanzo/commerce` | Payment processing, wallets, transactions |
|
||||
| **PaymentProcessor** | `commerce/payment/processor` | Stripe, PayPal, Bitcoin, Ethereum |
|
||||
| **CryptoProcessor** | `commerce/payment/processor` | Address generation, balance checking |
|
||||
| **Wallet Model** | `commerce/models/wallet` | Multi-chain account management |
|
||||
| **Events System** | `commerce/events` | ClickHouse-based analytics |
|
||||
| **Lux Network** | `/lux/wallet` | Blockchain settlement layer |
|
||||
|
||||
### 3.3 Attribution Challenges
|
||||
|
||||
@@ -115,12 +119,14 @@ custom: ["https://example.com/donate"]
|
||||
**Purpose**: Extract dependency graphs from project manifests
|
||||
|
||||
**Supported Formats**:
|
||||
|
||||
- `Cargo.toml` + `Cargo.lock` (Rust)
|
||||
- `package.json` + `package-lock.json` / `pnpm-lock.yaml` (Node.js)
|
||||
- `pyproject.toml` + `uv.lock` / `requirements.txt` (Python)
|
||||
- `go.mod` + `go.sum` (Go)
|
||||
|
||||
**Data Model**:
|
||||
|
||||
```rust
|
||||
struct Dependency {
|
||||
name: String,
|
||||
@@ -146,6 +152,7 @@ struct DependencyGraph {
|
||||
**Purpose**: Map dependencies to their authors and funding information
|
||||
|
||||
**Data Sources**:
|
||||
|
||||
1. Package registry metadata (crates.io, npm, PyPI)
|
||||
2. GitHub repository FUNDING.yml files
|
||||
3. Package manifest author/contributors fields
|
||||
@@ -153,6 +160,7 @@ struct DependencyGraph {
|
||||
5. Open Collective API
|
||||
|
||||
**Data Model**:
|
||||
|
||||
```rust
|
||||
struct Author {
|
||||
id: Uuid,
|
||||
@@ -188,6 +196,7 @@ struct AuthorShare {
|
||||
```
|
||||
|
||||
**Attribution Algorithm**:
|
||||
|
||||
```python
|
||||
def calculate_author_shares(package: Package) -> List[AuthorShare]:
|
||||
"""
|
||||
@@ -243,16 +252,17 @@ def calculate_author_shares(package: Package) -> List[AuthorShare]:
|
||||
|
||||
**Weighting Factors**:
|
||||
|
||||
| Factor | Weight | Description |
|
||||
|--------|--------|-------------|
|
||||
| **Direct Dependency** | 1.5x | Explicitly chosen by developer |
|
||||
| **Transitive Depth** | 1.0 / depth | Deeper = less weight |
|
||||
| **Usage Frequency** | log(n) | Import/require counts in codebase |
|
||||
| **Criticality** | 1.0-3.0x | Security, performance, or core functionality |
|
||||
| **Maintenance Activity** | 0.5-1.5x | Active vs abandoned |
|
||||
| **License Compatibility** | 1.0x or 0x | Exclude non-OSS |
|
||||
| Factor | Weight | Description |
|
||||
| ------------------------- | ----------- | -------------------------------------------- |
|
||||
| **Direct Dependency** | 1.5x | Explicitly chosen by developer |
|
||||
| **Transitive Depth** | 1.0 / depth | Deeper = less weight |
|
||||
| **Usage Frequency** | log(n) | Import/require counts in codebase |
|
||||
| **Criticality** | 1.0-3.0x | Security, performance, or core functionality |
|
||||
| **Maintenance Activity** | 0.5-1.5x | Active vs abandoned |
|
||||
| **License Compatibility** | 1.0x or 0x | Exclude non-OSS |
|
||||
|
||||
**Distribution Algorithm**:
|
||||
|
||||
```python
|
||||
def calculate_distribution(
|
||||
project: Project,
|
||||
@@ -597,7 +607,7 @@ paths:
|
||||
package_lock: { type: string, format: binary }
|
||||
pyproject_toml: { type: string, format: binary }
|
||||
responses:
|
||||
'202':
|
||||
"202":
|
||||
description: Scan initiated
|
||||
|
||||
/api/v1/oss/projects/{project_id}/dependencies:
|
||||
@@ -791,13 +801,13 @@ code oss payments --period 2026-01
|
||||
|
||||
### 9.1 Key Metrics
|
||||
|
||||
| Metric | Description | Target |
|
||||
|--------|-------------|--------|
|
||||
| Scan Coverage | % of dependencies with attribution | >95% |
|
||||
| Payment Success Rate | Successful / Total payments | >99% |
|
||||
| Author Coverage | Authors with payment addresses | >70% |
|
||||
| Distribution Latency | Time from trigger to completion | <1 hour |
|
||||
| Author Onboarding | New authors claiming packages | Growing |
|
||||
| Metric | Description | Target |
|
||||
| -------------------- | ---------------------------------- | ------- |
|
||||
| Scan Coverage | % of dependencies with attribution | >95% |
|
||||
| Payment Success Rate | Successful / Total payments | >99% |
|
||||
| Author Coverage | Authors with payment addresses | >70% |
|
||||
| Distribution Latency | Time from trigger to completion | <1 hour |
|
||||
| Author Onboarding | New authors claiming packages | Growing |
|
||||
|
||||
### 9.2 Alerting
|
||||
|
||||
@@ -844,11 +854,11 @@ code oss payments --period 2026-01
|
||||
For a project with 100 dependencies and $500 monthly budget:
|
||||
|
||||
| Package | Depth | Usage | Activity | Score | Amount |
|
||||
|---------|-------|-------|----------|-------|--------|
|
||||
| tokio | 0 | 45 | Active | 8.25 | $41.25 |
|
||||
| serde | 0 | 120 | Active | 11.22 | $56.10 |
|
||||
| rand | 1 | 12 | Active | 3.12 | $15.60 |
|
||||
| ... | ... | ... | ... | ... | ... |
|
||||
| ------- | ----- | ----- | -------- | ----- | ------ |
|
||||
| tokio | 0 | 45 | Active | 8.25 | $41.25 |
|
||||
| serde | 0 | 120 | Active | 11.22 | $56.10 |
|
||||
| rand | 1 | 12 | Active | 3.12 | $15.60 |
|
||||
| ... | ... | ... | ... | ... | ... |
|
||||
|
||||
### C. Integration with Lux Network
|
||||
|
||||
@@ -861,4 +871,4 @@ The system integrates with Lux Network for crypto-native settlements:
|
||||
|
||||
---
|
||||
|
||||
*This document is maintained as part of the Hanzo Dev project. Updates should be reflected in LLM.md.*
|
||||
_This document is maintained as part of the Hanzo Dev project. Updates should be reflected in LLM.md._
|
||||
|
||||
@@ -20,6 +20,7 @@ This document specifies the Quality of Service (QoS) Challenge System for Hanzo
|
||||
## Problem Statement
|
||||
|
||||
In a decentralized compute marketplace, providers may:
|
||||
|
||||
- Claim capabilities they do not possess (GPU TFLOPS, VRAM, etc.)
|
||||
- Provide inconsistent service quality over time
|
||||
- Game the reputation system through selective performance
|
||||
@@ -32,6 +33,7 @@ The QoS Challenge System addresses these issues by continuously verifying provid
|
||||
## Design Goals
|
||||
|
||||
### Primary Goals
|
||||
|
||||
1. **Verifiable Performance**: Prove providers can deliver claimed compute capabilities
|
||||
2. **Continuous Monitoring**: Ongoing verification, not just registration-time
|
||||
3. **Sybil Resistance**: Prevent gaming through multiple identities
|
||||
@@ -39,6 +41,7 @@ The QoS Challenge System addresses these issues by continuously verifying provid
|
||||
5. **Low Overhead**: Minimal impact on actual compute workloads
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Real-time latency guarantees (handled by SLA monitoring)
|
||||
- Content verification (handled by result verifier)
|
||||
- Network topology optimization (handled by routing layer)
|
||||
@@ -81,12 +84,12 @@ The QoS Challenge System addresses these issues by continuously verifying provid
|
||||
|
||||
The QoS Challenge System integrates with:
|
||||
|
||||
| Component | Integration Point | Purpose |
|
||||
|-----------|------------------|---------|
|
||||
| `hanzo-compute` | `ComputeSwarm`, `Peer` | Challenge issuance, result collection |
|
||||
| `hanzo-mining` | `MiningManager`, `Consensus` | On-chain settlement, stake management |
|
||||
| `hanzo-libp2p` | P2P messaging | Challenge delivery, proof collection |
|
||||
| `hanzo-pqc` | Cryptographic primitives | Quantum-safe signatures for proofs |
|
||||
| Component | Integration Point | Purpose |
|
||||
| --------------- | ---------------------------- | ------------------------------------- |
|
||||
| `hanzo-compute` | `ComputeSwarm`, `Peer` | Challenge issuance, result collection |
|
||||
| `hanzo-mining` | `MiningManager`, `Consensus` | On-chain settlement, stake management |
|
||||
| `hanzo-libp2p` | P2P messaging | Challenge delivery, proof collection |
|
||||
| `hanzo-pqc` | Cryptographic primitives | Quantum-safe signatures for proofs |
|
||||
|
||||
---
|
||||
|
||||
@@ -95,6 +98,7 @@ The QoS Challenge System integrates with:
|
||||
### Challenge Types
|
||||
|
||||
#### 1. Compute Challenge (CC)
|
||||
|
||||
Verifies actual compute capability (TFLOPS/GFLOPS)
|
||||
|
||||
```rust
|
||||
@@ -144,6 +148,7 @@ pub enum ChallengeType {
|
||||
**Verification Method**: Provider executes a deterministic computation kernel (e.g., matrix multiplication with specific seed). Result hash and execution time are submitted.
|
||||
|
||||
**Difficulty Scaling**:
|
||||
|
||||
```python
|
||||
# Difficulty determines problem size
|
||||
def compute_problem_size(difficulty: int, metric: str) -> int:
|
||||
@@ -161,6 +166,7 @@ def compute_problem_size(difficulty: int, metric: str) -> int:
|
||||
```
|
||||
|
||||
#### 2. Latency Challenge (LC)
|
||||
|
||||
Verifies network latency and response time
|
||||
|
||||
```rust
|
||||
@@ -181,6 +187,7 @@ pub struct LatencyChallenge {
|
||||
**Verification Method**: Echo-response pattern with cryptographic binding. Provider must respond within deadline with signed nonce.
|
||||
|
||||
#### 3. Bandwidth Challenge (BC)
|
||||
|
||||
Verifies data transfer capabilities
|
||||
|
||||
```rust
|
||||
@@ -205,6 +212,7 @@ pub enum TransferDirection {
|
||||
```
|
||||
|
||||
#### 4. Availability Challenge (AC)
|
||||
|
||||
Verifies provider is online and ready to serve
|
||||
|
||||
```rust
|
||||
@@ -223,6 +231,7 @@ pub struct AvailabilityChallenge {
|
||||
**Verification Method**: Heartbeat-style challenges issued at random intervals. Missing responses count against availability score.
|
||||
|
||||
#### 5. Model Capability Challenge (MC)
|
||||
|
||||
Verifies provider can run specific AI models
|
||||
|
||||
```rust
|
||||
@@ -280,12 +289,12 @@ pub struct ModelChallenge {
|
||||
|
||||
### Challenge Frequency
|
||||
|
||||
| Provider Tier | Challenge Frequency | Challenge Mix |
|
||||
|---------------|--------------------|--------------|
|
||||
| New (< 10 jobs) | Every 10 minutes | 40% Compute, 30% Avail, 20% Latency, 10% Model |
|
||||
| Established (10-100) | Every 30 minutes | 30% Compute, 30% Avail, 25% Latency, 15% Model |
|
||||
| Trusted (100+) | Every 60 minutes | 25% Compute, 35% Avail, 20% Latency, 20% Model |
|
||||
| Elite (1000+, TEE) | Every 2 hours | 20% Compute, 40% Avail, 20% Latency, 20% Model |
|
||||
| Provider Tier | Challenge Frequency | Challenge Mix |
|
||||
| -------------------- | ------------------- | ---------------------------------------------- |
|
||||
| New (< 10 jobs) | Every 10 minutes | 40% Compute, 30% Avail, 20% Latency, 10% Model |
|
||||
| Established (10-100) | Every 30 minutes | 30% Compute, 30% Avail, 25% Latency, 15% Model |
|
||||
| Trusted (100+) | Every 60 minutes | 25% Compute, 35% Avail, 20% Latency, 20% Model |
|
||||
| Elite (1000+, TEE) | Every 2 hours | 20% Compute, 40% Avail, 20% Latency, 20% Model |
|
||||
|
||||
### Proof Structure
|
||||
|
||||
@@ -348,6 +357,7 @@ pub enum ProofData {
|
||||
### Verification Methods
|
||||
|
||||
#### 1. Hash Verification
|
||||
|
||||
For compute challenges, verifiers execute the same deterministic computation:
|
||||
|
||||
```rust
|
||||
@@ -387,6 +397,7 @@ impl ComputeChallengeVerifier {
|
||||
```
|
||||
|
||||
#### 2. Optimistic Verification with Fraud Proofs
|
||||
|
||||
For efficiency, most verifications are optimistic:
|
||||
|
||||
```rust
|
||||
@@ -427,6 +438,7 @@ pub struct FraudProof {
|
||||
```
|
||||
|
||||
#### 3. TEE Attestation Verification
|
||||
|
||||
For TEE-enabled providers:
|
||||
|
||||
```rust
|
||||
@@ -1081,15 +1093,15 @@ contract QoSChallengeSettlement is ReentrancyGuard {
|
||||
|
||||
### Threat Model
|
||||
|
||||
| Threat | Impact | Mitigation |
|
||||
|--------|--------|------------|
|
||||
| **Sybil Attack** | Multiple fake identities to game reputation | Minimum stake requirement, unique hardware attestation |
|
||||
| Threat | Impact | Mitigation |
|
||||
| --------------------------- | -------------------------------------------- | ----------------------------------------------------------- |
|
||||
| **Sybil Attack** | Multiple fake identities to game reputation | Minimum stake requirement, unique hardware attestation |
|
||||
| **Computation Outsourcing** | Provider outsources challenge to faster node | Strict deadlines, random challenge timing, TEE verification |
|
||||
| **Selective Participation** | Only respond to easy challenges | Random assignment, penalty for missed challenges |
|
||||
| **Collusion** | Verifiers and providers collude | Multiple independent verifiers, fraud proofs, slashing |
|
||||
| **Replay Attacks** | Reuse old challenge responses | Unique nonces, timestamp validation, challenge expiry |
|
||||
| **Eclipse Attack** | Isolate provider from network | Multiple bootstrap nodes, diverse verifier set |
|
||||
| **TEE Compromise** | Fake attestation quotes | Quote freshness checks, revocation lists, multi-vendor TEE |
|
||||
| **Selective Participation** | Only respond to easy challenges | Random assignment, penalty for missed challenges |
|
||||
| **Collusion** | Verifiers and providers collude | Multiple independent verifiers, fraud proofs, slashing |
|
||||
| **Replay Attacks** | Reuse old challenge responses | Unique nonces, timestamp validation, challenge expiry |
|
||||
| **Eclipse Attack** | Isolate provider from network | Multiple bootstrap nodes, diverse verifier set |
|
||||
| **TEE Compromise** | Fake attestation quotes | Quote freshness checks, revocation lists, multi-vendor TEE |
|
||||
|
||||
### Security Properties
|
||||
|
||||
@@ -1165,12 +1177,14 @@ impl ChallengeSelector {
|
||||
### Phase 1: Foundation (Weeks 1-4)
|
||||
|
||||
#### Week 1-2: Core Data Structures
|
||||
|
||||
- [ ] Define `Challenge`, `Proof`, `ChallengeResult` types in `hanzo-compute`
|
||||
- [ ] Implement `ChallengeGenerator` with VRF-based randomness
|
||||
- [ ] Add `QoSScore` and `ProviderState` to peer management
|
||||
- [ ] Unit tests for all data structures
|
||||
|
||||
#### Week 3-4: Challenge Protocol
|
||||
|
||||
- [ ] Implement P2P challenge delivery via `hanzo-libp2p`
|
||||
- [ ] Build `ComputeChallengeVerifier` with reference computation
|
||||
- [ ] Implement `LatencyChallengeVerifier` with timing verification
|
||||
@@ -1179,12 +1193,14 @@ impl ChallengeSelector {
|
||||
### Phase 2: Verification (Weeks 5-8)
|
||||
|
||||
#### Week 5-6: Verification Engine
|
||||
|
||||
- [ ] Build optimistic verification framework
|
||||
- [ ] Implement fraud proof generation and validation
|
||||
- [ ] Add TEE attestation verification (Intel SGX, AMD SEV)
|
||||
- [ ] Integration with `hanzo-pqc` for quantum-safe signatures
|
||||
|
||||
#### Week 7-8: Scoring System
|
||||
|
||||
- [ ] Implement `QoSScoreCalculator` with component weighting
|
||||
- [ ] Build `ConsistencyCalculator` with time-series analysis
|
||||
- [ ] Add score persistence to `hanzo-database`
|
||||
@@ -1193,12 +1209,14 @@ impl ChallengeSelector {
|
||||
### Phase 3: Economics (Weeks 9-12)
|
||||
|
||||
#### Week 9-10: On-Chain Contracts
|
||||
|
||||
- [ ] Deploy `QoSChallengeSettlement` contract to testnet
|
||||
- [ ] Implement reward distribution logic
|
||||
- [ ] Add slashing mechanics with circuit breakers
|
||||
- [ ] Security audit of smart contracts
|
||||
|
||||
#### Week 11-12: Integration & Testing
|
||||
|
||||
- [ ] End-to-end integration with `hanzo-mining`
|
||||
- [ ] Stress testing with simulated adversaries
|
||||
- [ ] Documentation and API reference
|
||||
@@ -1206,24 +1224,24 @@ impl ChallengeSelector {
|
||||
|
||||
### Milestones
|
||||
|
||||
| Milestone | Deliverable | Date |
|
||||
|-----------|-------------|------|
|
||||
| M1 | Challenge protocol specification finalized | Week 2 |
|
||||
| M2 | Testnet deployment with basic challenges | Week 6 |
|
||||
| M3 | Full verification engine operational | Week 8 |
|
||||
| M4 | Economic settlement on testnet | Week 10 |
|
||||
| M5 | Security audit complete | Week 11 |
|
||||
| M6 | Mainnet launch | Week 12 |
|
||||
| Milestone | Deliverable | Date |
|
||||
| --------- | ------------------------------------------ | ------- |
|
||||
| M1 | Challenge protocol specification finalized | Week 2 |
|
||||
| M2 | Testnet deployment with basic challenges | Week 6 |
|
||||
| M3 | Full verification engine operational | Week 8 |
|
||||
| M4 | Economic settlement on testnet | Week 10 |
|
||||
| M5 | Security audit complete | Week 11 |
|
||||
| M6 | Mainnet launch | Week 12 |
|
||||
|
||||
### Success Metrics
|
||||
|
||||
| Metric | Target | Measurement |
|
||||
|--------|--------|-------------|
|
||||
| Challenge completion rate | > 98% | Successful responses / total challenges |
|
||||
| False positive rate | < 0.1% | Incorrect failures / total failures |
|
||||
| Verification latency | < 5 seconds | Time from proof submission to verification |
|
||||
| Provider churn | < 5% monthly | Providers leaving due to failed challenges |
|
||||
| Fraud detection rate | > 99% | Detected fraudulent proofs / total fraudulent |
|
||||
| Metric | Target | Measurement |
|
||||
| ------------------------- | ------------ | --------------------------------------------- |
|
||||
| Challenge completion rate | > 98% | Successful responses / total challenges |
|
||||
| False positive rate | < 0.1% | Incorrect failures / total failures |
|
||||
| Verification latency | < 5 seconds | Time from proof submission to verification |
|
||||
| Provider churn | < 5% monthly | Providers leaving due to failed challenges |
|
||||
| Fraud detection rate | > 99% | Detected fraudulent proofs / total fraudulent |
|
||||
|
||||
---
|
||||
|
||||
@@ -1313,14 +1331,14 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Active challenges
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Challenge'
|
||||
$ref: "#/components/schemas/Challenge"
|
||||
|
||||
/v1/challenges/{challenge_id}/proof:
|
||||
post:
|
||||
@@ -1336,11 +1354,11 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChallengeProof'
|
||||
$ref: "#/components/schemas/ChallengeProof"
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Proof accepted
|
||||
'400':
|
||||
"400":
|
||||
description: Invalid proof
|
||||
|
||||
/v1/providers/{provider_id}/qos:
|
||||
@@ -1353,12 +1371,12 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: QoS score
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QoSScore'
|
||||
$ref: "#/components/schemas/QoSScore"
|
||||
|
||||
/v1/providers/{provider_id}/history:
|
||||
get:
|
||||
@@ -1375,14 +1393,14 @@ paths:
|
||||
type: integer
|
||||
default: 100
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Challenge history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ChallengeResult'
|
||||
$ref: "#/components/schemas/ChallengeResult"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
@@ -1581,9 +1599,9 @@ ChallengeProof execute_compute_challenge(ComputeChallenge* challenge) {
|
||||
|
||||
## Document History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-01-24 | Architecture Team | Initial design document |
|
||||
| Version | Date | Author | Changes |
|
||||
| ------- | ---------- | ----------------- | ----------------------- |
|
||||
| 1.0 | 2026-01-24 | Architecture Team | Initial design document |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+248
-211
@@ -16,267 +16,304 @@
|
||||
* wire_api = "chat"
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const http = require("http");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const PORT = parseInt(process.env.PORT || process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '8080');
|
||||
const PORT = parseInt(
|
||||
process.env.PORT ||
|
||||
process.argv.find((a) => a.startsWith("--port="))?.split("=")[1] ||
|
||||
"8080",
|
||||
);
|
||||
|
||||
/**
|
||||
* Extract non-JSON text from Claude CLI output (filter telemetry)
|
||||
*/
|
||||
function extractResponse(output) {
|
||||
const lines = output.split('\n');
|
||||
const textLines = [];
|
||||
let inJson = false;
|
||||
let braceCount = 0;
|
||||
const lines = output.split("\n");
|
||||
const textLines = [];
|
||||
let inJson = false;
|
||||
let braceCount = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Skip empty lines at start
|
||||
if (!trimmed && textLines.length === 0) continue;
|
||||
// Skip empty lines at start
|
||||
if (!trimmed && textLines.length === 0) continue;
|
||||
|
||||
// Track JSON blocks
|
||||
if (trimmed.startsWith('{')) {
|
||||
inJson = true;
|
||||
braceCount = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inJson) {
|
||||
braceCount += (trimmed.match(/{/g) || []).length;
|
||||
braceCount -= (trimmed.match(/}/g) || []).length;
|
||||
if (braceCount <= 0) {
|
||||
inJson = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip lines that are just JSON artifacts (brackets, commas)
|
||||
if (/^[\[\]\{\},\s]*$/.test(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not in JSON block - this is actual content
|
||||
if (trimmed && !trimmed.startsWith('}')) {
|
||||
textLines.push(line);
|
||||
}
|
||||
// Track JSON blocks
|
||||
if (trimmed.startsWith("{")) {
|
||||
inJson = true;
|
||||
braceCount = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clean up any trailing bracket artifacts
|
||||
let result = textLines.join('\n').trim();
|
||||
result = result.replace(/\s*[\[\]\{\}]+\s*$/g, '').trim();
|
||||
if (inJson) {
|
||||
braceCount += (trimmed.match(/{/g) || []).length;
|
||||
braceCount -= (trimmed.match(/}/g) || []).length;
|
||||
if (braceCount <= 0) {
|
||||
inJson = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return result;
|
||||
// Skip lines that are just JSON artifacts (brackets, commas)
|
||||
if (/^[\[\]\{\},\s]*$/.test(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not in JSON block - this is actual content
|
||||
if (trimmed && !trimmed.startsWith("}")) {
|
||||
textLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any trailing bracket artifacts
|
||||
let result = textLines.join("\n").trim();
|
||||
result = result.replace(/\s*[\[\]\{\}]+\s*$/g, "").trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map model names to Claude CLI supported models
|
||||
*/
|
||||
function mapModel(model) {
|
||||
const modelMap = {
|
||||
'claude-cli': null, // Use default
|
||||
'claude-opus': 'claude-opus-4-5-20251101',
|
||||
'claude-sonnet': 'claude-sonnet-4-20250514',
|
||||
'claude-haiku': 'claude-haiku-4-5-20251001',
|
||||
'gpt-4': null, // Use default
|
||||
'gpt-4o': null,
|
||||
'gpt-4o-mini': null,
|
||||
};
|
||||
const modelMap = {
|
||||
"claude-cli": null, // Use default
|
||||
"claude-opus": "claude-opus-4-5-20251101",
|
||||
"claude-sonnet": "claude-sonnet-4-20250514",
|
||||
"claude-haiku": "claude-haiku-4-5-20251001",
|
||||
"gpt-4": null, // Use default
|
||||
"gpt-4o": null,
|
||||
"gpt-4o-mini": null,
|
||||
};
|
||||
|
||||
// If model is in map, use mapped value (null means use default)
|
||||
if (model in modelMap) {
|
||||
return modelMap[model];
|
||||
}
|
||||
// If model is in map, use mapped value (null means use default)
|
||||
if (model in modelMap) {
|
||||
return modelMap[model];
|
||||
}
|
||||
|
||||
// If model starts with 'claude-', pass it through
|
||||
if (model && model.startsWith('claude-')) {
|
||||
return model;
|
||||
}
|
||||
// If model starts with 'claude-', pass it through
|
||||
if (model && model.startsWith("claude-")) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Default: use Claude's default model
|
||||
return null;
|
||||
// Default: use Claude's default model
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke Claude CLI and return the response
|
||||
*/
|
||||
function invokeClaude(prompt, model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = ['-p', '--output-format', 'text'];
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = ["-p", "--output-format", "text"];
|
||||
|
||||
// Map model name to Claude CLI format
|
||||
const mappedModel = mapModel(model);
|
||||
if (mappedModel) {
|
||||
args.push('--model', mappedModel);
|
||||
}
|
||||
// Map model name to Claude CLI format
|
||||
const mappedModel = mapModel(model);
|
||||
if (mappedModel) {
|
||||
args.push("--model", mappedModel);
|
||||
}
|
||||
|
||||
// Create clean env without ANTHROPIC_API_KEY to force subscription mode
|
||||
const cleanEnv = { ...process.env };
|
||||
delete cleanEnv.ANTHROPIC_API_KEY;
|
||||
delete cleanEnv.ANTHROPIC_AUTH_TOKEN;
|
||||
cleanEnv.OTEL_SDK_DISABLED = 'true';
|
||||
// Create clean env without ANTHROPIC_API_KEY to force subscription mode
|
||||
const cleanEnv = { ...process.env };
|
||||
delete cleanEnv.ANTHROPIC_API_KEY;
|
||||
delete cleanEnv.ANTHROPIC_AUTH_TOKEN;
|
||||
cleanEnv.OTEL_SDK_DISABLED = "true";
|
||||
|
||||
const claude = spawn('claude', args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: cleanEnv
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
claude.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
claude.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
claude.on('close', (code) => {
|
||||
if (code !== 0 && !stdout) {
|
||||
reject(new Error(`Claude CLI exited with code ${code}: ${stderr}`));
|
||||
} else {
|
||||
const response = extractResponse(stdout);
|
||||
resolve(response || stdout);
|
||||
}
|
||||
});
|
||||
|
||||
claude.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// Send prompt via stdin
|
||||
claude.stdin.write(prompt);
|
||||
claude.stdin.end();
|
||||
const claude = spawn("claude", args, {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: cleanEnv,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
claude.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
claude.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
claude.on("close", (code) => {
|
||||
if (code !== 0 && !stdout) {
|
||||
reject(new Error(`Claude CLI exited with code ${code}: ${stderr}`));
|
||||
} else {
|
||||
const response = extractResponse(stdout);
|
||||
resolve(response || stdout);
|
||||
}
|
||||
});
|
||||
|
||||
claude.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// Send prompt via stdin
|
||||
claude.stdin.write(prompt);
|
||||
claude.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming HTTP requests
|
||||
*/
|
||||
async function handleRequest(req, res) {
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
// CORS headers
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Health check
|
||||
if (url.pathname === '/health' || url.pathname === '/') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', provider: 'claude-cli' }));
|
||||
return;
|
||||
}
|
||||
// Health check
|
||||
if (url.pathname === "/health" || url.pathname === "/") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "ok", provider: "claude-cli" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat completions endpoint
|
||||
if (url.pathname === '/v1/chat/completions' && req.method === 'POST') {
|
||||
let body = '';
|
||||
// Chat completions endpoint
|
||||
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
||||
let body = "";
|
||||
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const request = JSON.parse(body);
|
||||
req.on("end", async () => {
|
||||
try {
|
||||
const request = JSON.parse(body);
|
||||
|
||||
// Extract prompt from messages - combine all for context
|
||||
const messages = request.messages || [];
|
||||
const model = request.model;
|
||||
// Extract prompt from messages - combine all for context
|
||||
const messages = request.messages || [];
|
||||
const model = request.model;
|
||||
|
||||
// Build a combined prompt with role prefixes for context
|
||||
let prompt = messages.map(msg => {
|
||||
const role = msg.role || 'user';
|
||||
const content = msg.content || '';
|
||||
if (role === 'system') {
|
||||
return `[System]: ${content}`;
|
||||
} else if (role === 'assistant') {
|
||||
return `[Assistant]: ${content}`;
|
||||
} else {
|
||||
return content; // User messages without prefix
|
||||
}
|
||||
}).join('\n\n');
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Request: model=${model}, prompt=${prompt.substring(0, 50)}...`);
|
||||
|
||||
// Invoke Claude CLI
|
||||
const response = await invokeClaude(prompt, model);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Response: ${response.substring(0, 100)}...`);
|
||||
|
||||
// Return OpenAI-compatible response
|
||||
const completion = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model || 'claude-cli',
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: response
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil(prompt.length / 4),
|
||||
completion_tokens: Math.ceil(response.length / 4),
|
||||
total_tokens: Math.ceil((prompt.length + response.length) / 4)
|
||||
}
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(completion));
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
message: error.message,
|
||||
type: 'server_error',
|
||||
code: 'internal_error'
|
||||
}
|
||||
}));
|
||||
// Build a combined prompt with role prefixes for context
|
||||
let prompt = messages
|
||||
.map((msg) => {
|
||||
const role = msg.role || "user";
|
||||
const content = msg.content || "";
|
||||
if (role === "system") {
|
||||
return `[System]: ${content}`;
|
||||
} else if (role === "assistant") {
|
||||
return `[Assistant]: ${content}`;
|
||||
} else {
|
||||
return content; // User messages without prefix
|
||||
}
|
||||
});
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Request: model=${model}, prompt=${prompt.substring(0, 50)}...`,
|
||||
);
|
||||
|
||||
// Models endpoint (for compatibility)
|
||||
if (url.pathname === '/v1/models' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
object: 'list',
|
||||
data: [
|
||||
{ id: 'claude-cli', object: 'model', created: Date.now(), owned_by: 'anthropic' },
|
||||
{ id: 'claude-opus', object: 'model', created: Date.now(), owned_by: 'anthropic' },
|
||||
{ id: 'claude-sonnet', object: 'model', created: Date.now(), owned_by: 'anthropic' },
|
||||
{ id: 'claude-haiku', object: 'model', created: Date.now(), owned_by: 'anthropic' }
|
||||
]
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// Invoke Claude CLI
|
||||
const response = await invokeClaude(prompt, model);
|
||||
|
||||
// 404 for unknown endpoints
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'Not found', type: 'not_found' } }));
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Response: ${response.substring(0, 100)}...`,
|
||||
);
|
||||
|
||||
// Return OpenAI-compatible response
|
||||
const completion = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model || "claude-cli",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: response,
|
||||
},
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil(prompt.length / 4),
|
||||
completion_tokens: Math.ceil(response.length / 4),
|
||||
total_tokens: Math.ceil((prompt.length + response.length) / 4),
|
||||
},
|
||||
};
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(completion));
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: error.message,
|
||||
type: "server_error",
|
||||
code: "internal_error",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Models endpoint (for compatibility)
|
||||
if (url.pathname === "/v1/models" && req.method === "GET") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "claude-cli",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "anthropic",
|
||||
},
|
||||
{
|
||||
id: "claude-opus",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "anthropic",
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "anthropic",
|
||||
},
|
||||
{
|
||||
id: "claude-haiku",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "anthropic",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 404 for unknown endpoints
|
||||
res.writeHead(404, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({ error: { message: "Not found", type: "not_found" } }),
|
||||
);
|
||||
}
|
||||
|
||||
// Create server
|
||||
const server = http.createServer(handleRequest);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`
|
||||
console.log(`
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ Claude CLI → OpenAI API Proxy ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
@@ -296,9 +333,9 @@ server.listen(PORT, () => {
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
server.close(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\nShutting down...");
|
||||
server.close(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
+319
-265
@@ -16,20 +16,24 @@
|
||||
* Configure ~/.code/config.toml with any supported provider.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const http = require("http");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const PORT = parseInt(process.env.PORT || process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '9999');
|
||||
const PORT = parseInt(
|
||||
process.env.PORT ||
|
||||
process.argv.find((a) => a.startsWith("--port="))?.split("=")[1] ||
|
||||
"9999",
|
||||
);
|
||||
|
||||
/**
|
||||
* API keys to clean per backend (forces subscription/login mode)
|
||||
*/
|
||||
const API_KEYS_TO_CLEAN = {
|
||||
claude: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
|
||||
codex: ['OPENAI_API_KEY', 'OPENAI_ORG_ID'],
|
||||
gemini: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
||||
vibe: ['MISTRAL_API_KEY'],
|
||||
qwen: ['DASHSCOPE_API_KEY', 'QWEN_API_KEY'],
|
||||
claude: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
||||
codex: ["OPENAI_API_KEY", "OPENAI_ORG_ID"],
|
||||
gemini: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
||||
vibe: ["MISTRAL_API_KEY"],
|
||||
qwen: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,330 +41,380 @@ const API_KEYS_TO_CLEAN = {
|
||||
* All backends use subscription/login mode by default (API keys removed from env)
|
||||
*/
|
||||
const CLI_BACKENDS = {
|
||||
// Claude Code CLI - uses Anthropic subscription
|
||||
'claude': {
|
||||
command: 'claude',
|
||||
args: (prompt, model) => {
|
||||
const args = ['-p', '--output-format', 'text'];
|
||||
if (model && model !== 'claude-cli' && model.startsWith('claude-')) {
|
||||
args.push('--model', model);
|
||||
}
|
||||
return args;
|
||||
},
|
||||
env: { OTEL_SDK_DISABLED: 'true' },
|
||||
inputMode: 'stdin',
|
||||
models: ['claude-cli', 'claude-opus', 'claude-sonnet', 'claude-haiku'],
|
||||
// Claude Code CLI - uses Anthropic subscription
|
||||
claude: {
|
||||
command: "claude",
|
||||
args: (prompt, model) => {
|
||||
const args = ["-p", "--output-format", "text"];
|
||||
if (model && model !== "claude-cli" && model.startsWith("claude-")) {
|
||||
args.push("--model", model);
|
||||
}
|
||||
return args;
|
||||
},
|
||||
env: { OTEL_SDK_DISABLED: "true" },
|
||||
inputMode: "stdin",
|
||||
models: ["claude-cli", "claude-opus", "claude-sonnet", "claude-haiku"],
|
||||
},
|
||||
|
||||
// Codex CLI (OpenAI) - uses OpenAI subscription
|
||||
'codex': {
|
||||
command: 'codex',
|
||||
args: (prompt, model) => {
|
||||
const args = ['-q', '--full-auto']; // -q for quiet, final output only
|
||||
if (model && model !== 'codex' && model.startsWith('codex-')) {
|
||||
args.push('-m', model);
|
||||
}
|
||||
args.push(prompt);
|
||||
return args;
|
||||
},
|
||||
env: { OTEL_SDK_DISABLED: 'true' },
|
||||
inputMode: 'args',
|
||||
models: ['codex', 'codex-mini', 'codex-mini-latest', 'gpt-4o', 'gpt-4o-mini', 'o1', 'o3-mini'],
|
||||
// Codex CLI (OpenAI) - uses OpenAI subscription
|
||||
codex: {
|
||||
command: "codex",
|
||||
args: (prompt, model) => {
|
||||
const args = ["-q", "--full-auto"]; // -q for quiet, final output only
|
||||
if (model && model !== "codex" && model.startsWith("codex-")) {
|
||||
args.push("-m", model);
|
||||
}
|
||||
args.push(prompt);
|
||||
return args;
|
||||
},
|
||||
env: { OTEL_SDK_DISABLED: "true" },
|
||||
inputMode: "args",
|
||||
models: [
|
||||
"codex",
|
||||
"codex-mini",
|
||||
"codex-mini-latest",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"o1",
|
||||
"o3-mini",
|
||||
],
|
||||
},
|
||||
|
||||
// Google Gemini CLI - uses Google account
|
||||
'gemini': {
|
||||
command: 'gemini',
|
||||
args: (prompt, model) => {
|
||||
const args = ['-p', prompt, '-y']; // -y for auto-approve
|
||||
if (model && model !== 'gemini' && model.startsWith('gemini-')) {
|
||||
args.push('-m', model);
|
||||
}
|
||||
return args;
|
||||
},
|
||||
env: {},
|
||||
inputMode: 'args',
|
||||
models: ['gemini', 'gemini-2.5-pro', 'gemini-2.0-flash', 'gemini-1.5-pro'],
|
||||
// Google Gemini CLI - uses Google account
|
||||
gemini: {
|
||||
command: "gemini",
|
||||
args: (prompt, model) => {
|
||||
const args = ["-p", prompt, "-y"]; // -y for auto-approve
|
||||
if (model && model !== "gemini" && model.startsWith("gemini-")) {
|
||||
args.push("-m", model);
|
||||
}
|
||||
return args;
|
||||
},
|
||||
env: {},
|
||||
inputMode: "args",
|
||||
models: ["gemini", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"],
|
||||
},
|
||||
|
||||
// Mistral Vibe CLI - uses Mistral subscription
|
||||
'vibe': {
|
||||
command: 'vibe',
|
||||
args: (prompt, model) => ['-p', prompt, '--output', 'text', '--auto-approve'],
|
||||
env: {},
|
||||
inputMode: 'args',
|
||||
models: ['vibe', 'mistral', 'mistral-large', 'mistral-small'],
|
||||
},
|
||||
// Mistral Vibe CLI - uses Mistral subscription
|
||||
vibe: {
|
||||
command: "vibe",
|
||||
args: (prompt, model) => [
|
||||
"-p",
|
||||
prompt,
|
||||
"--output",
|
||||
"text",
|
||||
"--auto-approve",
|
||||
],
|
||||
env: {},
|
||||
inputMode: "args",
|
||||
models: ["vibe", "mistral", "mistral-large", "mistral-small"],
|
||||
},
|
||||
|
||||
// Qwen CLI - uses Alibaba account
|
||||
'qwen': {
|
||||
command: 'qwen',
|
||||
args: (prompt, model) => ['-p', prompt],
|
||||
env: { OTEL_SDK_DISABLED: 'true' },
|
||||
inputMode: 'args',
|
||||
models: ['qwen', 'qwen-cli', 'qwen-plus', 'qwen-turbo', 'qwen3'],
|
||||
},
|
||||
// Qwen CLI - uses Alibaba account
|
||||
qwen: {
|
||||
command: "qwen",
|
||||
args: (prompt, model) => ["-p", prompt],
|
||||
env: { OTEL_SDK_DISABLED: "true" },
|
||||
inputMode: "args",
|
||||
models: ["qwen", "qwen-cli", "qwen-plus", "qwen-turbo", "qwen3"],
|
||||
},
|
||||
|
||||
// Ollama (local models) - no API keys needed
|
||||
'ollama': {
|
||||
command: 'ollama',
|
||||
args: (prompt, model) => ['run', model || 'llama3.2', prompt],
|
||||
env: {},
|
||||
inputMode: 'args',
|
||||
models: ['ollama', 'llama3.2', 'llama3.1', 'codellama', 'mixtral', 'phi3'],
|
||||
},
|
||||
// Ollama (local models) - no API keys needed
|
||||
ollama: {
|
||||
command: "ollama",
|
||||
args: (prompt, model) => ["run", model || "llama3.2", prompt],
|
||||
env: {},
|
||||
inputMode: "args",
|
||||
models: ["ollama", "llama3.2", "llama3.1", "codellama", "mixtral", "phi3"],
|
||||
},
|
||||
|
||||
// GitHub Copilot - uses GitHub subscription via gh extension
|
||||
'copilot': {
|
||||
command: 'gh',
|
||||
args: (prompt, model) => ['copilot', 'suggest', '--type', 'shell', prompt],
|
||||
env: {},
|
||||
inputMode: 'args',
|
||||
models: ['copilot', 'gh-copilot', 'github-copilot'],
|
||||
},
|
||||
// GitHub Copilot - uses GitHub subscription via gh extension
|
||||
copilot: {
|
||||
command: "gh",
|
||||
args: (prompt, model) => ["copilot", "suggest", "--type", "shell", prompt],
|
||||
env: {},
|
||||
inputMode: "args",
|
||||
models: ["copilot", "gh-copilot", "github-copilot"],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Map model name to CLI backend
|
||||
*/
|
||||
function getBackend(model) {
|
||||
if (!model) return { ...CLI_BACKENDS['claude'], name: 'claude' };
|
||||
if (!model) return { ...CLI_BACKENDS["claude"], name: "claude" };
|
||||
|
||||
// Direct backend match
|
||||
for (const [name, backend] of Object.entries(CLI_BACKENDS)) {
|
||||
if (backend.models.includes(model)) {
|
||||
return { ...backend, name };
|
||||
}
|
||||
// Direct backend match
|
||||
for (const [name, backend] of Object.entries(CLI_BACKENDS)) {
|
||||
if (backend.models.includes(model)) {
|
||||
return { ...backend, name };
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix matching
|
||||
if (model.startsWith('claude')) return { ...CLI_BACKENDS['claude'], name: 'claude' };
|
||||
if (model.startsWith('codex') || model.startsWith('gpt') || model.startsWith('o1') || model.startsWith('o3')) {
|
||||
return { ...CLI_BACKENDS['codex'], name: 'codex' };
|
||||
}
|
||||
if (model.startsWith('gemini')) return { ...CLI_BACKENDS['gemini'], name: 'gemini' };
|
||||
if (model.startsWith('vibe') || model.startsWith('mistral')) return { ...CLI_BACKENDS['vibe'], name: 'vibe' };
|
||||
if (model.startsWith('qwen')) return { ...CLI_BACKENDS['qwen'], name: 'qwen' };
|
||||
if (model.startsWith('llama') || model.startsWith('ollama') || model.startsWith('phi')) {
|
||||
return { ...CLI_BACKENDS['ollama'], name: 'ollama' };
|
||||
}
|
||||
if (model.startsWith('copilot') || model.startsWith('gh-copilot')) {
|
||||
return { ...CLI_BACKENDS['copilot'], name: 'copilot' };
|
||||
}
|
||||
// Prefix matching
|
||||
if (model.startsWith("claude"))
|
||||
return { ...CLI_BACKENDS["claude"], name: "claude" };
|
||||
if (
|
||||
model.startsWith("codex") ||
|
||||
model.startsWith("gpt") ||
|
||||
model.startsWith("o1") ||
|
||||
model.startsWith("o3")
|
||||
) {
|
||||
return { ...CLI_BACKENDS["codex"], name: "codex" };
|
||||
}
|
||||
if (model.startsWith("gemini"))
|
||||
return { ...CLI_BACKENDS["gemini"], name: "gemini" };
|
||||
if (model.startsWith("vibe") || model.startsWith("mistral"))
|
||||
return { ...CLI_BACKENDS["vibe"], name: "vibe" };
|
||||
if (model.startsWith("qwen"))
|
||||
return { ...CLI_BACKENDS["qwen"], name: "qwen" };
|
||||
if (
|
||||
model.startsWith("llama") ||
|
||||
model.startsWith("ollama") ||
|
||||
model.startsWith("phi")
|
||||
) {
|
||||
return { ...CLI_BACKENDS["ollama"], name: "ollama" };
|
||||
}
|
||||
if (model.startsWith("copilot") || model.startsWith("gh-copilot")) {
|
||||
return { ...CLI_BACKENDS["copilot"], name: "copilot" };
|
||||
}
|
||||
|
||||
// Default to Claude
|
||||
return { ...CLI_BACKENDS['claude'], name: 'claude' };
|
||||
// Default to Claude
|
||||
return { ...CLI_BACKENDS["claude"], name: "claude" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract clean text from CLI output (filter telemetry/JSON artifacts)
|
||||
*/
|
||||
function extractResponse(output) {
|
||||
const lines = output.split('\n');
|
||||
const textLines = [];
|
||||
let inJson = false;
|
||||
let braceCount = 0;
|
||||
const lines = output.split("\n");
|
||||
const textLines = [];
|
||||
let inJson = false;
|
||||
let braceCount = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed && textLines.length === 0) continue;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed && textLines.length === 0) continue;
|
||||
|
||||
if (trimmed.startsWith('{')) {
|
||||
inJson = true;
|
||||
braceCount = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inJson) {
|
||||
braceCount += (trimmed.match(/{/g) || []).length;
|
||||
braceCount -= (trimmed.match(/}/g) || []).length;
|
||||
if (braceCount <= 0) inJson = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[\[\]\{\},\s]*$/.test(trimmed)) continue;
|
||||
if (trimmed && !trimmed.startsWith('}')) {
|
||||
textLines.push(line);
|
||||
}
|
||||
if (trimmed.startsWith("{")) {
|
||||
inJson = true;
|
||||
braceCount = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = textLines.join('\n').trim();
|
||||
result = result.replace(/\s*[\[\]\{\}]+\s*$/g, '').trim();
|
||||
return result;
|
||||
if (inJson) {
|
||||
braceCount += (trimmed.match(/{/g) || []).length;
|
||||
braceCount -= (trimmed.match(/}/g) || []).length;
|
||||
if (braceCount <= 0) inJson = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[\[\]\{\},\s]*$/.test(trimmed)) continue;
|
||||
if (trimmed && !trimmed.startsWith("}")) {
|
||||
textLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
let result = textLines.join("\n").trim();
|
||||
result = result.replace(/\s*[\[\]\{\}]+\s*$/g, "").trim();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke CLI backend and return response
|
||||
*/
|
||||
function invokeCLI(prompt, model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const backend = getBackend(model);
|
||||
const args = backend.args(prompt, model);
|
||||
return new Promise((resolve, reject) => {
|
||||
const backend = getBackend(model);
|
||||
const args = backend.args(prompt, model);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Backend: ${backend.name}, Command: ${backend.command} ${args.slice(0, 2).join(' ')}...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Backend: ${backend.name}, Command: ${backend.command} ${args.slice(0, 2).join(" ")}...`,
|
||||
);
|
||||
|
||||
// Build environment - clean API keys to force subscription/login mode
|
||||
let spawnEnv = { ...process.env, ...backend.env };
|
||||
const keysToClean = API_KEYS_TO_CLEAN[backend.name] || [];
|
||||
for (const key of keysToClean) {
|
||||
delete spawnEnv[key];
|
||||
}
|
||||
// Build environment - clean API keys to force subscription/login mode
|
||||
let spawnEnv = { ...process.env, ...backend.env };
|
||||
const keysToClean = API_KEYS_TO_CLEAN[backend.name] || [];
|
||||
for (const key of keysToClean) {
|
||||
delete spawnEnv[key];
|
||||
}
|
||||
|
||||
const proc = spawn(backend.command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: spawnEnv
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code !== 0 && !stdout) {
|
||||
reject(new Error(`${backend.name} CLI exited with code ${code}: ${stderr}`));
|
||||
} else {
|
||||
const response = extractResponse(stdout);
|
||||
resolve(response || stdout.trim());
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(new Error(`Failed to spawn ${backend.name}: ${err.message}`));
|
||||
});
|
||||
|
||||
// Send prompt via stdin if backend uses stdin mode
|
||||
if (backend.inputMode === 'stdin') {
|
||||
proc.stdin.write(prompt);
|
||||
proc.stdin.end();
|
||||
}
|
||||
const proc = spawn(backend.command, args, {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: spawnEnv,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0 && !stdout) {
|
||||
reject(
|
||||
new Error(`${backend.name} CLI exited with code ${code}: ${stderr}`),
|
||||
);
|
||||
} else {
|
||||
const response = extractResponse(stdout);
|
||||
resolve(response || stdout.trim());
|
||||
}
|
||||
});
|
||||
|
||||
proc.on("error", (err) => {
|
||||
reject(new Error(`Failed to spawn ${backend.name}: ${err.message}`));
|
||||
});
|
||||
|
||||
// Send prompt via stdin if backend uses stdin mode
|
||||
if (backend.inputMode === "stdin") {
|
||||
proc.stdin.write(prompt);
|
||||
proc.stdin.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt from messages array
|
||||
*/
|
||||
function buildPrompt(messages) {
|
||||
return messages.map(msg => {
|
||||
const role = msg.role || 'user';
|
||||
const content = msg.content || '';
|
||||
if (role === 'system') return `[System]: ${content}`;
|
||||
if (role === 'assistant') return `[Assistant]: ${content}`;
|
||||
return content;
|
||||
}).join('\n\n');
|
||||
return messages
|
||||
.map((msg) => {
|
||||
const role = msg.role || "user";
|
||||
const content = msg.content || "";
|
||||
if (role === "system") return `[System]: ${content}`;
|
||||
if (role === "assistant") return `[Assistant]: ${content}`;
|
||||
return content;
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTTP requests
|
||||
*/
|
||||
async function handleRequest(req, res) {
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
|
||||
// CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
// CORS
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Health check
|
||||
if (url.pathname === '/health' || url.pathname === '/') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
backends: Object.keys(CLI_BACKENDS),
|
||||
default: 'claude'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// Health check
|
||||
if (url.pathname === "/health" || url.pathname === "/") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: "ok",
|
||||
backends: Object.keys(CLI_BACKENDS),
|
||||
default: "claude",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat completions
|
||||
if (url.pathname === '/v1/chat/completions' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk.toString(); });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const request = JSON.parse(body);
|
||||
const messages = request.messages || [];
|
||||
const model = request.model;
|
||||
const prompt = buildPrompt(messages);
|
||||
// Chat completions
|
||||
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
||||
let body = "";
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on("end", async () => {
|
||||
try {
|
||||
const request = JSON.parse(body);
|
||||
const messages = request.messages || [];
|
||||
const model = request.model;
|
||||
const prompt = buildPrompt(messages);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Request: model=${model}, prompt=${prompt.substring(0, 50)}...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Request: model=${model}, prompt=${prompt.substring(0, 50)}...`,
|
||||
);
|
||||
|
||||
const response = await invokeCLI(prompt, model);
|
||||
const response = await invokeCLI(prompt, model);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Response: ${response.substring(0, 100)}...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Response: ${response.substring(0, 100)}...`,
|
||||
);
|
||||
|
||||
const completion = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model || 'claude-cli',
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: response },
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil(prompt.length / 4),
|
||||
completion_tokens: Math.ceil(response.length / 4),
|
||||
total_tokens: Math.ceil((prompt.length + response.length) / 4)
|
||||
}
|
||||
};
|
||||
const completion = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model || "claude-cli",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: response },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil(prompt.length / 4),
|
||||
completion_tokens: Math.ceil(response.length / 4),
|
||||
total_tokens: Math.ceil((prompt.length + response.length) / 4),
|
||||
},
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(completion));
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(completion));
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: error.message,
|
||||
type: "server_error",
|
||||
code: "internal_error",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: { message: error.message, type: 'server_error', code: 'internal_error' }
|
||||
}));
|
||||
}
|
||||
// Models endpoint
|
||||
if (url.pathname === "/v1/models" && req.method === "GET") {
|
||||
const allModels = [];
|
||||
for (const [name, backend] of Object.entries(CLI_BACKENDS)) {
|
||||
for (const model of backend.models) {
|
||||
allModels.push({
|
||||
id: model,
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ object: "list", data: allModels }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Models endpoint
|
||||
if (url.pathname === '/v1/models' && req.method === 'GET') {
|
||||
const allModels = [];
|
||||
for (const [name, backend] of Object.entries(CLI_BACKENDS)) {
|
||||
for (const model of backend.models) {
|
||||
allModels.push({
|
||||
id: model,
|
||||
object: 'model',
|
||||
created: Date.now(),
|
||||
owned_by: name
|
||||
});
|
||||
}
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ object: 'list', data: allModels }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 404
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'Not found', type: 'not_found' } }));
|
||||
// 404
|
||||
res.writeHead(404, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({ error: { message: "Not found", type: "not_found" } }),
|
||||
);
|
||||
}
|
||||
|
||||
// Start server
|
||||
const server = http.createServer(handleRequest);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
const backends = Object.keys(CLI_BACKENDS).join(', ');
|
||||
console.log(`
|
||||
const backends = Object.keys(CLI_BACKENDS).join(", ");
|
||||
console.log(`
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ Multi-CLI → OpenAI API Proxy (Subscription Mode) ║
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
@@ -379,7 +433,7 @@ server.listen(PORT, () => {
|
||||
`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
server.close(() => process.exit(0));
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\nShutting down...");
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
|
||||
+169
-152
@@ -6,10 +6,10 @@
|
||||
* which uses your Claude subscription instead of API credits.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const pty = require('node-pty');
|
||||
const http = require("http");
|
||||
const pty = require("node-pty");
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '9999');
|
||||
const PORT = parseInt(process.env.PORT || "9999");
|
||||
|
||||
// Track active PTY sessions
|
||||
const sessions = new Map();
|
||||
@@ -18,191 +18,208 @@ const sessions = new Map();
|
||||
* Send a message to Claude via PTY and get response
|
||||
*/
|
||||
function sendToClaude(prompt) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = 120000; // 2 minutes
|
||||
let output = '';
|
||||
let responseStarted = false;
|
||||
let lastDataTime = Date.now();
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = 120000; // 2 minutes
|
||||
let output = "";
|
||||
let responseStarted = false;
|
||||
let lastDataTime = Date.now();
|
||||
|
||||
// Spawn Claude in interactive mode
|
||||
const proc = pty.spawn('claude', ['--dangerously-skip-permissions'], {
|
||||
name: 'xterm-256color',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
TERM: 'xterm-256color',
|
||||
NO_COLOR: '1', // Disable colors for easier parsing
|
||||
}
|
||||
});
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
proc.kill();
|
||||
reject(new Error('Timeout waiting for Claude response'));
|
||||
}, timeout);
|
||||
|
||||
// Detect when Claude is ready for input
|
||||
let ready = false;
|
||||
let sentPrompt = false;
|
||||
|
||||
proc.onData((data) => {
|
||||
lastDataTime = Date.now();
|
||||
output += data;
|
||||
|
||||
// Debug logging
|
||||
if (process.env.DEBUG) {
|
||||
console.log('[PTY DATA]:', JSON.stringify(data.substring(0, 100)));
|
||||
}
|
||||
|
||||
// Detect ready state (Claude shows prompt)
|
||||
if (!ready && (output.includes('>') || output.includes('Claude'))) {
|
||||
ready = true;
|
||||
}
|
||||
|
||||
// Send prompt once ready
|
||||
if (ready && !sentPrompt) {
|
||||
sentPrompt = true;
|
||||
setTimeout(() => {
|
||||
proc.write(prompt + '\n');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Detect response completion (new prompt appears after response)
|
||||
if (sentPrompt && output.split(prompt).length > 1) {
|
||||
// Look for completion indicators
|
||||
const afterPrompt = output.split(prompt).pop();
|
||||
if (afterPrompt && afterPrompt.includes('>')) {
|
||||
// Response complete
|
||||
clearTimeout(timeoutId);
|
||||
proc.kill();
|
||||
|
||||
// Extract response between prompt and next >
|
||||
const response = extractResponse(output, prompt);
|
||||
resolve(response);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proc.onExit(({ exitCode }) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (exitCode !== 0 && !output) {
|
||||
reject(new Error(`Claude exited with code ${exitCode}`));
|
||||
} else {
|
||||
const response = extractResponse(output, prompt);
|
||||
resolve(response || 'No response received');
|
||||
}
|
||||
});
|
||||
// Spawn Claude in interactive mode
|
||||
const proc = pty.spawn("claude", ["--dangerously-skip-permissions"], {
|
||||
name: "xterm-256color",
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
TERM: "xterm-256color",
|
||||
NO_COLOR: "1", // Disable colors for easier parsing
|
||||
},
|
||||
});
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
proc.kill();
|
||||
reject(new Error("Timeout waiting for Claude response"));
|
||||
}, timeout);
|
||||
|
||||
// Detect when Claude is ready for input
|
||||
let ready = false;
|
||||
let sentPrompt = false;
|
||||
|
||||
proc.onData((data) => {
|
||||
lastDataTime = Date.now();
|
||||
output += data;
|
||||
|
||||
// Debug logging
|
||||
if (process.env.DEBUG) {
|
||||
console.log("[PTY DATA]:", JSON.stringify(data.substring(0, 100)));
|
||||
}
|
||||
|
||||
// Detect ready state (Claude shows prompt)
|
||||
if (!ready && (output.includes(">") || output.includes("Claude"))) {
|
||||
ready = true;
|
||||
}
|
||||
|
||||
// Send prompt once ready
|
||||
if (ready && !sentPrompt) {
|
||||
sentPrompt = true;
|
||||
setTimeout(() => {
|
||||
proc.write(prompt + "\n");
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Detect response completion (new prompt appears after response)
|
||||
if (sentPrompt && output.split(prompt).length > 1) {
|
||||
// Look for completion indicators
|
||||
const afterPrompt = output.split(prompt).pop();
|
||||
if (afterPrompt && afterPrompt.includes(">")) {
|
||||
// Response complete
|
||||
clearTimeout(timeoutId);
|
||||
proc.kill();
|
||||
|
||||
// Extract response between prompt and next >
|
||||
const response = extractResponse(output, prompt);
|
||||
resolve(response);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proc.onExit(({ exitCode }) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (exitCode !== 0 && !output) {
|
||||
reject(new Error(`Claude exited with code ${exitCode}`));
|
||||
} else {
|
||||
const response = extractResponse(output, prompt);
|
||||
resolve(response || "No response received");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Claude's response from PTY output
|
||||
*/
|
||||
function extractResponse(output, prompt) {
|
||||
// Remove ANSI escape codes
|
||||
const clean = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
|
||||
.replace(/\x1B\][^\x07]*\x07/g, '')
|
||||
.replace(/[\x00-\x1F]/g, ' ');
|
||||
// Remove ANSI escape codes
|
||||
const clean = output
|
||||
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
|
||||
.replace(/\x1B\][^\x07]*\x07/g, "")
|
||||
.replace(/[\x00-\x1F]/g, " ");
|
||||
|
||||
// Find the response after the prompt
|
||||
const promptIndex = clean.indexOf(prompt);
|
||||
if (promptIndex === -1) return clean.trim();
|
||||
// Find the response after the prompt
|
||||
const promptIndex = clean.indexOf(prompt);
|
||||
if (promptIndex === -1) return clean.trim();
|
||||
|
||||
let response = clean.substring(promptIndex + prompt.length);
|
||||
let response = clean.substring(promptIndex + prompt.length);
|
||||
|
||||
// Remove trailing prompt indicator
|
||||
response = response.replace(/>\s*$/, '').trim();
|
||||
// Remove trailing prompt indicator
|
||||
response = response.replace(/>\s*$/, "").trim();
|
||||
|
||||
// Remove any leading/trailing whitespace and common artifacts
|
||||
response = response.split('\n')
|
||||
.filter(line => line.trim() && !line.includes('Claude') && !line.startsWith('>'))
|
||||
.join('\n')
|
||||
.trim();
|
||||
// Remove any leading/trailing whitespace and common artifacts
|
||||
response = response
|
||||
.split("\n")
|
||||
.filter(
|
||||
(line) =>
|
||||
line.trim() && !line.includes("Claude") && !line.startsWith(">"),
|
||||
)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
return response || 'Response received';
|
||||
return response || "Response received";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt from messages
|
||||
*/
|
||||
function buildPrompt(messages) {
|
||||
const lastUserMsg = messages.filter(m => m.role === 'user').pop();
|
||||
return lastUserMsg?.content || messages[messages.length - 1]?.content || '';
|
||||
const lastUserMsg = messages.filter((m) => m.role === "user").pop();
|
||||
return lastUserMsg?.content || messages[messages.length - 1]?.content || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request handler
|
||||
*/
|
||||
async function handleRequest(req, res) {
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/health' || url.pathname === '/') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', mode: 'pty-interactive' }));
|
||||
return;
|
||||
}
|
||||
if (url.pathname === "/health" || url.pathname === "/") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "ok", mode: "pty-interactive" }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/v1/chat/completions' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk.toString(); });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const request = JSON.parse(body);
|
||||
const prompt = buildPrompt(request.messages || []);
|
||||
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
||||
let body = "";
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on("end", async () => {
|
||||
try {
|
||||
const request = JSON.parse(body);
|
||||
const prompt = buildPrompt(request.messages || []);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] PTY Request: ${prompt.substring(0, 50)}...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] PTY Request: ${prompt.substring(0, 50)}...`,
|
||||
);
|
||||
|
||||
const response = await sendToClaude(prompt);
|
||||
const response = await sendToClaude(prompt);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] PTY Response: ${response.substring(0, 100)}...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] PTY Response: ${response.substring(0, 100)}...`,
|
||||
);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: request.model || 'claude-interactive',
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: response },
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil(prompt.length / 4),
|
||||
completion_tokens: Math.ceil(response.length / 4),
|
||||
total_tokens: Math.ceil((prompt.length + response.length) / 4)
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: { message: error.message, type: 'server_error' }
|
||||
}));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: request.model || "claude-interactive",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: response },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil(prompt.length / 4),
|
||||
completion_tokens: Math.ceil(response.length / 4),
|
||||
total_tokens: Math.ceil((prompt.length + response.length) / 4),
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Error:`, error.message);
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: { message: error.message, type: "server_error" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'Not found' } }));
|
||||
res.writeHead(404, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: { message: "Not found" } }));
|
||||
}
|
||||
|
||||
const server = http.createServer(handleRequest);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`
|
||||
console.log(`
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ Claude CLI PTY Proxy (Interactive Mode) ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
@@ -215,7 +232,7 @@ server.listen(PORT, () => {
|
||||
`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
server.close(() => process.exit(0));
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\nShutting down...");
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
|
||||
+665
-380
File diff suppressed because it is too large
Load Diff
+15
-14
@@ -1,38 +1,39 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@hanzo/pools-ui'],
|
||||
transpilePackages: ["@hanzo/pools-ui"],
|
||||
|
||||
// Environment variables available at build time
|
||||
env: {
|
||||
HANZO_NODE_URL: process.env.HANZO_NODE_URL || 'http://localhost:8080',
|
||||
HANZO_NODE_GRPC_URL: process.env.HANZO_NODE_GRPC_URL || 'http://localhost:9090',
|
||||
HANZO_NODE_URL: process.env.HANZO_NODE_URL || "http://localhost:8080",
|
||||
HANZO_NODE_GRPC_URL:
|
||||
process.env.HANZO_NODE_GRPC_URL || "http://localhost:9090",
|
||||
},
|
||||
|
||||
// API rewrites to proxy requests to hanzo-node
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/node/:path*',
|
||||
destination: `${process.env.HANZO_NODE_URL || 'http://localhost:8080'}/:path*`,
|
||||
source: "/api/node/:path*",
|
||||
destination: `${process.env.HANZO_NODE_URL || "http://localhost:8080"}/:path*`,
|
||||
},
|
||||
]
|
||||
];
|
||||
},
|
||||
|
||||
// Security headers
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||
{ key: "X-Frame-Options", value: "DENY" },
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
||||
],
|
||||
},
|
||||
]
|
||||
];
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -3,4 +3,4 @@ module.exports = {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
+43
-43
@@ -1,79 +1,79 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ['class'],
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./node_modules/@hanzo/pools-ui/src/**/*.{js,ts,jsx,tsx}',
|
||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./node_modules/@hanzo/pools-ui/src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
hanzo: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
950: '#082f49',
|
||||
50: "#f0f9ff",
|
||||
100: "#e0f2fe",
|
||||
200: "#bae6fd",
|
||||
300: "#7dd3fc",
|
||||
400: "#38bdf8",
|
||||
500: "#0ea5e9",
|
||||
600: "#0284c7",
|
||||
700: "#0369a1",
|
||||
800: "#075985",
|
||||
900: "#0c4a6e",
|
||||
950: "#082f49",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
|
||||
mono: ['var(--font-mono)', 'monospace'],
|
||||
sans: ["var(--font-sans)", "system-ui", "sans-serif"],
|
||||
mono: ["var(--font-mono)", "monospace"],
|
||||
},
|
||||
keyframes: {
|
||||
'pulse-slow': {
|
||||
'0%, 100%': { opacity: 1 },
|
||||
'50%': { opacity: 0.5 },
|
||||
"pulse-slow": {
|
||||
"0%, 100%": { opacity: 1 },
|
||||
"50%": { opacity: 0.5 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'pulse-slow': 'pulse-slow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
"pulse-slow": "pulse-slow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
};
|
||||
|
||||
+399
-185
@@ -1,26 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
// Postinstall script for @hanzo/dev
|
||||
|
||||
import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync, copyFileSync, fsyncSync, renameSync, realpathSync } from 'fs';
|
||||
import { join, dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { get } from 'https';
|
||||
import { platform, arch, tmpdir } from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { createRequire } from 'module';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
createWriteStream,
|
||||
chmodSync,
|
||||
readFileSync,
|
||||
readSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
statSync,
|
||||
openSync,
|
||||
closeSync,
|
||||
copyFileSync,
|
||||
fsyncSync,
|
||||
renameSync,
|
||||
realpathSync,
|
||||
} from "fs";
|
||||
import { join, dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { get } from "https";
|
||||
import { platform, arch, tmpdir } from "os";
|
||||
import { execSync } from "child_process";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function getTargetTriple() {
|
||||
const platformMap = {
|
||||
'darwin': 'apple-darwin',
|
||||
'linux': 'unknown-linux-musl',
|
||||
'win32': 'pc-windows-msvc'
|
||||
darwin: "apple-darwin",
|
||||
linux: "unknown-linux-musl",
|
||||
win32: "pc-windows-msvc",
|
||||
};
|
||||
|
||||
const archMap = {
|
||||
'x64': 'x86_64',
|
||||
'arm64': 'aarch64'
|
||||
x64: "x86_64",
|
||||
arm64: "aarch64",
|
||||
};
|
||||
|
||||
const rustArch = archMap[arch()] || arch();
|
||||
@@ -31,47 +47,54 @@ function getTargetTriple() {
|
||||
|
||||
function getCacheDir(version) {
|
||||
const plt = platform();
|
||||
const home = process.env.HOME || process.env.USERPROFILE || '';
|
||||
let base = '';
|
||||
if (plt === 'win32') {
|
||||
base = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
||||
} else if (plt === 'darwin') {
|
||||
base = join(home, 'Library', 'Caches');
|
||||
const home = process.env.HOME || process.env.USERPROFILE || "";
|
||||
let base = "";
|
||||
if (plt === "win32") {
|
||||
base = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
||||
} else if (plt === "darwin") {
|
||||
base = join(home, "Library", "Caches");
|
||||
} else {
|
||||
base = process.env.XDG_CACHE_HOME || join(home, '.cache');
|
||||
base = process.env.XDG_CACHE_HOME || join(home, ".cache");
|
||||
}
|
||||
const dir = join(base, 'hanzo', 'dev', version);
|
||||
const dir = join(base, "hanzo", "dev", version);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function getCachedBinaryPath(version, targetTriple, isWindows) {
|
||||
const ext = isWindows ? '.exe' : '';
|
||||
const ext = isWindows ? ".exe" : "";
|
||||
const cacheDir = getCacheDir(version);
|
||||
return join(cacheDir, `dev-${targetTriple}${ext}`);
|
||||
}
|
||||
|
||||
function isWSL() {
|
||||
if (platform() !== 'linux') return false;
|
||||
if (platform() !== "linux") return false;
|
||||
try {
|
||||
const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
|
||||
return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
||||
} catch { return false; }
|
||||
const ver = readFileSync("/proc/version", "utf8").toLowerCase();
|
||||
return ver.includes("microsoft") || !!process.env.WSL_DISTRO_NAME;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isPathOnWindowsFs(p) {
|
||||
try {
|
||||
const mounts = readFileSync('/proc/mounts', 'utf8').split(/\n/).filter(Boolean);
|
||||
let best = { mount: '/', type: 'unknown', len: 1 };
|
||||
const mounts = readFileSync("/proc/mounts", "utf8")
|
||||
.split(/\n/)
|
||||
.filter(Boolean);
|
||||
let best = { mount: "/", type: "unknown", len: 1 };
|
||||
for (const line of mounts) {
|
||||
const parts = line.split(' ');
|
||||
const parts = line.split(" ");
|
||||
if (parts.length < 3) continue;
|
||||
const mnt = parts[1];
|
||||
const typ = parts[2];
|
||||
if (p.startsWith(mnt) && mnt.length > best.len) best = { mount: mnt, type: typ, len: mnt.length };
|
||||
if (p.startsWith(mnt) && mnt.length > best.len)
|
||||
best = { mount: mnt, type: typ, len: mnt.length };
|
||||
}
|
||||
return best.type === 'drvfs' || best.type === 'cifs';
|
||||
} catch { return false; }
|
||||
return best.type === "drvfs" || best.type === "cifs";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeCacheAtomic(srcPath, cachePath) {
|
||||
@@ -82,141 +105,185 @@ async function writeCacheAtomic(srcPath, cachePath) {
|
||||
}
|
||||
} catch {}
|
||||
const dir = dirname(cachePath);
|
||||
if (!existsSync(dir)) { try { mkdirSync(dir, { recursive: true }); } catch {} }
|
||||
const tmp = cachePath + '.tmp-' + Math.random().toString(36).slice(2, 8);
|
||||
if (!existsSync(dir)) {
|
||||
try {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
} catch {}
|
||||
}
|
||||
const tmp = cachePath + ".tmp-" + Math.random().toString(36).slice(2, 8);
|
||||
copyFileSync(srcPath, tmp);
|
||||
try { const fd = openSync(tmp, 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } } catch {}
|
||||
try {
|
||||
const fd = openSync(tmp, "r");
|
||||
try {
|
||||
fsyncSync(fd);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
} catch {}
|
||||
const delays = [100, 200, 400, 800, 1200, 1600];
|
||||
for (let i = 0; i < delays.length; i++) {
|
||||
try {
|
||||
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
||||
if (existsSync(cachePath)) {
|
||||
try {
|
||||
unlinkSync(cachePath);
|
||||
} catch {}
|
||||
}
|
||||
renameSync(tmp, cachePath);
|
||||
return;
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, delays[i]));
|
||||
await new Promise((r) => setTimeout(r, delays[i]));
|
||||
}
|
||||
}
|
||||
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
||||
if (existsSync(cachePath)) {
|
||||
try {
|
||||
unlinkSync(cachePath);
|
||||
} catch {}
|
||||
}
|
||||
renameSync(tmp, cachePath);
|
||||
}
|
||||
|
||||
function resolveGlobalBinDir() {
|
||||
const plt = platform();
|
||||
const userAgent = process.env.npm_config_user_agent || '';
|
||||
const userAgent = process.env.npm_config_user_agent || "";
|
||||
|
||||
const fromPrefix = (prefixPath) => {
|
||||
if (!prefixPath) return '';
|
||||
return plt === 'win32' ? prefixPath : join(prefixPath, 'bin');
|
||||
if (!prefixPath) return "";
|
||||
return plt === "win32" ? prefixPath : join(prefixPath, "bin");
|
||||
};
|
||||
|
||||
const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX || '';
|
||||
const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX || "";
|
||||
const direct = fromPrefix(prefixEnv);
|
||||
if (direct) return direct;
|
||||
|
||||
const tryExec = (command) => {
|
||||
try {
|
||||
return execSync(command, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
shell: true,
|
||||
}).toString().trim();
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
} catch {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const prefixFromNpm = fromPrefix(tryExec('npm prefix -g'));
|
||||
const prefixFromNpm = fromPrefix(tryExec("npm prefix -g"));
|
||||
if (prefixFromNpm) return prefixFromNpm;
|
||||
|
||||
const binFromNpm = tryExec('npm bin -g');
|
||||
const binFromNpm = tryExec("npm bin -g");
|
||||
if (binFromNpm) return binFromNpm;
|
||||
|
||||
if (userAgent.includes('pnpm')) {
|
||||
const pnpmBin = tryExec('pnpm bin --global');
|
||||
if (userAgent.includes("pnpm")) {
|
||||
const pnpmBin = tryExec("pnpm bin --global");
|
||||
if (pnpmBin) return pnpmBin;
|
||||
}
|
||||
|
||||
if (userAgent.includes('yarn')) {
|
||||
const yarnBin = tryExec('yarn global bin');
|
||||
if (userAgent.includes("yarn")) {
|
||||
const yarnBin = tryExec("yarn global bin");
|
||||
if (yarnBin) return yarnBin;
|
||||
}
|
||||
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const doAttempt = () => new Promise((resolve, reject) => {
|
||||
const attempt = (currentUrl, redirectsLeft) => {
|
||||
const req = get(currentUrl, (response) => {
|
||||
const status = response.statusCode || 0;
|
||||
const location = response.headers.location;
|
||||
const doAttempt = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const attempt = (currentUrl, redirectsLeft) => {
|
||||
const req = get(currentUrl, (response) => {
|
||||
const status = response.statusCode || 0;
|
||||
const location = response.headers.location;
|
||||
|
||||
if ((status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && location) {
|
||||
if (redirectsLeft <= 0) {
|
||||
reject(new Error(`Too many redirects while downloading ${currentUrl}`));
|
||||
if (
|
||||
(status === 301 ||
|
||||
status === 302 ||
|
||||
status === 303 ||
|
||||
status === 307 ||
|
||||
status === 308) &&
|
||||
location
|
||||
) {
|
||||
if (redirectsLeft <= 0) {
|
||||
reject(
|
||||
new Error(`Too many redirects while downloading ${currentUrl}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
attempt(location, redirectsLeft - 1);
|
||||
return;
|
||||
}
|
||||
attempt(location, redirectsLeft - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 200) {
|
||||
const expected = parseInt(response.headers['content-length'] || '0', 10) || 0;
|
||||
let bytes = 0;
|
||||
let timer;
|
||||
const timeoutMs = 30000;
|
||||
if (status === 200) {
|
||||
const expected =
|
||||
parseInt(response.headers["content-length"] || "0", 10) || 0;
|
||||
let bytes = 0;
|
||||
let timer;
|
||||
const timeoutMs = 30000;
|
||||
|
||||
const resetTimer = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
req.destroy(new Error('download stalled'));
|
||||
}, timeoutMs);
|
||||
};
|
||||
const resetTimer = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
req.destroy(new Error("download stalled"));
|
||||
}, timeoutMs);
|
||||
};
|
||||
|
||||
resetTimer();
|
||||
response.on('data', (chunk) => {
|
||||
bytes += chunk.length;
|
||||
resetTimer();
|
||||
});
|
||||
response.on("data", (chunk) => {
|
||||
bytes += chunk.length;
|
||||
resetTimer();
|
||||
});
|
||||
|
||||
const file = createWriteStream(dest);
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
file.close();
|
||||
if (expected && bytes !== expected) {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(new Error(`incomplete download: got ${bytes} of ${expected} bytes`));
|
||||
} else if (bytes === 0) {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(new Error('empty download'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Failed to download: HTTP ${status}`));
|
||||
}
|
||||
});
|
||||
const file = createWriteStream(dest);
|
||||
response.pipe(file);
|
||||
file.on("finish", () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
file.close();
|
||||
if (expected && bytes !== expected) {
|
||||
try {
|
||||
unlinkSync(dest);
|
||||
} catch {}
|
||||
reject(
|
||||
new Error(
|
||||
`incomplete download: got ${bytes} of ${expected} bytes`,
|
||||
),
|
||||
);
|
||||
} else if (bytes === 0) {
|
||||
try {
|
||||
unlinkSync(dest);
|
||||
} catch {}
|
||||
reject(new Error("empty download"));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
file.on("error", (err) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
try {
|
||||
unlinkSync(dest);
|
||||
} catch {}
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Failed to download: HTTP ${status}`));
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(err);
|
||||
});
|
||||
req.on("error", (err) => {
|
||||
try {
|
||||
unlinkSync(dest);
|
||||
} catch {}
|
||||
reject(err);
|
||||
});
|
||||
|
||||
req.setTimeout(120000, () => {
|
||||
req.destroy(new Error('download timed out'));
|
||||
});
|
||||
};
|
||||
req.setTimeout(120000, () => {
|
||||
req.destroy(new Error("download timed out"));
|
||||
});
|
||||
};
|
||||
|
||||
attempt(url, maxRedirects);
|
||||
});
|
||||
attempt(url, maxRedirects);
|
||||
});
|
||||
|
||||
let attemptNum = 0;
|
||||
while (true) {
|
||||
@@ -235,22 +302,38 @@ function validateDownloadedBinary(p) {
|
||||
try {
|
||||
const st = statSync(p);
|
||||
if (!st.isFile() || st.size === 0) {
|
||||
return { ok: false, reason: 'empty or not a regular file' };
|
||||
return { ok: false, reason: "empty or not a regular file" };
|
||||
}
|
||||
const fd = openSync(p, 'r');
|
||||
const fd = openSync(p, "r");
|
||||
try {
|
||||
const buf = Buffer.alloc(4);
|
||||
const n = readSync(fd, buf, 0, 4, 0);
|
||||
if (n < 2) return { ok: false, reason: 'too short' };
|
||||
if (n < 2) return { ok: false, reason: "too short" };
|
||||
const plt = platform();
|
||||
if (plt === 'win32') {
|
||||
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: 'invalid PE header (missing MZ)' };
|
||||
} else if (plt === 'linux' || plt === 'android') {
|
||||
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: 'invalid ELF header' };
|
||||
} else if (plt === 'darwin') {
|
||||
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
||||
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
||||
if (!isMachO) return { ok: false, reason: 'invalid Mach-O header' };
|
||||
if (plt === "win32") {
|
||||
if (!(buf[0] === 0x4d && buf[1] === 0x5a))
|
||||
return { ok: false, reason: "invalid PE header (missing MZ)" };
|
||||
} else if (plt === "linux" || plt === "android") {
|
||||
if (
|
||||
!(
|
||||
buf[0] === 0x7f &&
|
||||
buf[1] === 0x45 &&
|
||||
buf[2] === 0x4c &&
|
||||
buf[3] === 0x46
|
||||
)
|
||||
)
|
||||
return { ok: false, reason: "invalid ELF header" };
|
||||
} else if (plt === "darwin") {
|
||||
const isMachO =
|
||||
(buf[0] === 0xcf &&
|
||||
buf[1] === 0xfa &&
|
||||
buf[2] === 0xed &&
|
||||
buf[3] === 0xfe) ||
|
||||
(buf[0] === 0xca &&
|
||||
buf[1] === 0xfe &&
|
||||
buf[2] === 0xba &&
|
||||
buf[3] === 0xbe);
|
||||
if (!isMachO) return { ok: false, reason: "invalid Mach-O header" };
|
||||
}
|
||||
return { ok: true };
|
||||
} finally {
|
||||
@@ -263,32 +346,35 @@ function validateDownloadedBinary(p) {
|
||||
|
||||
export async function runPostinstall(options = {}) {
|
||||
const { skipGlobalAlias = false, invokedByRuntime = false } = options;
|
||||
if (process.env.DEV_POSTINSTALL_DRY_RUN === '1') {
|
||||
if (process.env.DEV_POSTINSTALL_DRY_RUN === "1") {
|
||||
return { skipped: true };
|
||||
}
|
||||
|
||||
if (invokedByRuntime) {
|
||||
process.env.DEV_RUNTIME_POSTINSTALL = process.env.DEV_RUNTIME_POSTINSTALL || '1';
|
||||
process.env.DEV_RUNTIME_POSTINSTALL =
|
||||
process.env.DEV_RUNTIME_POSTINSTALL || "1";
|
||||
}
|
||||
|
||||
const ua = process.env.npm_config_user_agent || '';
|
||||
const isNpx = ua.includes('npx');
|
||||
const isGlobal = process.env.npm_config_global === 'true';
|
||||
const ua = process.env.npm_config_user_agent || "";
|
||||
const isNpx = ua.includes("npx");
|
||||
const isGlobal = process.env.npm_config_global === "true";
|
||||
|
||||
const targetTriple = getTargetTriple();
|
||||
const isWindows = platform() === 'win32';
|
||||
const binaryExt = isWindows ? '.exe' : '';
|
||||
const isWindows = platform() === "win32";
|
||||
const binaryExt = isWindows ? ".exe" : "";
|
||||
|
||||
const binDir = join(__dirname, 'bin');
|
||||
const binDir = join(__dirname, "bin");
|
||||
if (!existsSync(binDir)) {
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, "package.json"), "utf8"),
|
||||
);
|
||||
const version = packageJson.version;
|
||||
|
||||
// The release produces 'code-*' binaries; we'll download and rename to 'dev-*'
|
||||
const binaries = ['code'];
|
||||
const binaries = ["code"];
|
||||
|
||||
console.log(`Installing @hanzo/dev v${version} for ${targetTriple}...`);
|
||||
|
||||
@@ -304,11 +390,22 @@ export async function runPostinstall(options = {}) {
|
||||
const valid = validateDownloadedBinary(cachePath);
|
||||
if (valid.ok) {
|
||||
const wsl = isWSL();
|
||||
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
||||
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
||||
const binDirReal = (() => {
|
||||
try {
|
||||
return realpathSync(binDir);
|
||||
} catch {
|
||||
return binDir;
|
||||
}
|
||||
})();
|
||||
const mirrorToLocal = !(
|
||||
isWindows ||
|
||||
(wsl && isPathOnWindowsFs(binDirReal))
|
||||
);
|
||||
if (mirrorToLocal) {
|
||||
copyFileSync(cachePath, localPath);
|
||||
try { chmodSync(localPath, 0o755); } catch {}
|
||||
try {
|
||||
chmodSync(localPath, 0o755);
|
||||
} catch {}
|
||||
}
|
||||
console.log(`✓ ${devBinaryName} ready from user cache`);
|
||||
continue;
|
||||
@@ -322,13 +419,16 @@ export async function runPostinstall(options = {}) {
|
||||
const require = createRequire(import.meta.url);
|
||||
const platformPkg = (() => {
|
||||
const name = (() => {
|
||||
if (isWindows) return '@hanzo/dev-win32-x64';
|
||||
if (isWindows) return "@hanzo/dev-win32-x64";
|
||||
const plt = platform();
|
||||
const cpu = arch();
|
||||
if (plt === 'darwin' && cpu === 'arm64') return '@hanzo/dev-darwin-arm64';
|
||||
if (plt === 'darwin' && cpu === 'x64') return '@hanzo/dev-darwin-x64';
|
||||
if (plt === 'linux' && cpu === 'x64') return '@hanzo/dev-linux-x64-musl';
|
||||
if (plt === 'linux' && cpu === 'arm64') return '@hanzo/dev-linux-arm64-musl';
|
||||
if (plt === "darwin" && cpu === "arm64")
|
||||
return "@hanzo/dev-darwin-arm64";
|
||||
if (plt === "darwin" && cpu === "x64") return "@hanzo/dev-darwin-x64";
|
||||
if (plt === "linux" && cpu === "x64")
|
||||
return "@hanzo/dev-linux-x64-musl";
|
||||
if (plt === "linux" && cpu === "arm64")
|
||||
return "@hanzo/dev-linux-arm64-musl";
|
||||
return null;
|
||||
})();
|
||||
if (!name) return null;
|
||||
@@ -343,81 +443,155 @@ export async function runPostinstall(options = {}) {
|
||||
|
||||
if (platformPkg) {
|
||||
try {
|
||||
const src = join(platformPkg.dir, 'bin', devBinaryName);
|
||||
const src = join(platformPkg.dir, "bin", devBinaryName);
|
||||
if (!existsSync(src)) {
|
||||
throw new Error(`platform package missing binary: ${platformPkg.name}`);
|
||||
throw new Error(
|
||||
`platform package missing binary: ${platformPkg.name}`,
|
||||
);
|
||||
}
|
||||
await writeCacheAtomic(src, cachePath);
|
||||
const wsl = isWSL();
|
||||
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
||||
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
||||
const binDirReal = (() => {
|
||||
try {
|
||||
return realpathSync(binDir);
|
||||
} catch {
|
||||
return binDir;
|
||||
}
|
||||
})();
|
||||
const mirrorToLocal = !(
|
||||
isWindows ||
|
||||
(wsl && isPathOnWindowsFs(binDirReal))
|
||||
);
|
||||
if (mirrorToLocal) {
|
||||
copyFileSync(cachePath, localPath);
|
||||
try { chmodSync(localPath, 0o755); } catch {}
|
||||
try {
|
||||
chmodSync(localPath, 0o755);
|
||||
} catch {}
|
||||
}
|
||||
console.log(`✓ Installed ${devBinaryName} from ${platformPkg.name} (cached)`);
|
||||
console.log(
|
||||
`✓ Installed ${devBinaryName} from ${platformPkg.name} (cached)`,
|
||||
);
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
|
||||
console.warn(
|
||||
`⚠ Failed platform package install (${e.message}), falling back to GitHub download`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Download from GitHub release
|
||||
const isWin = isWindows;
|
||||
const detectedWSL = isWSL();
|
||||
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
||||
const mirrorToLocal = !(isWin || (detectedWSL && isPathOnWindowsFs(binDirReal)));
|
||||
const binDirReal = (() => {
|
||||
try {
|
||||
return realpathSync(binDir);
|
||||
} catch {
|
||||
return binDir;
|
||||
}
|
||||
})();
|
||||
const mirrorToLocal = !(
|
||||
isWin ||
|
||||
(detectedWSL && isPathOnWindowsFs(binDirReal))
|
||||
);
|
||||
let useZst = false;
|
||||
if (!isWin) {
|
||||
try {
|
||||
execSync('zstd --version', { stdio: 'ignore', shell: true });
|
||||
execSync("zstd --version", { stdio: "ignore", shell: true });
|
||||
useZst = true;
|
||||
} catch {
|
||||
useZst = false;
|
||||
}
|
||||
}
|
||||
const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
|
||||
const archiveName = isWin
|
||||
? `${binaryName}.zip`
|
||||
: useZst
|
||||
? `${binaryName}.zst`
|
||||
: `${binaryName}.tar.gz`;
|
||||
const downloadUrl = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
|
||||
|
||||
console.log(`Downloading ${archiveName}...`);
|
||||
try {
|
||||
const needsIsolation = isWin || (!isWin && !mirrorToLocal);
|
||||
let safeTempDir = needsIsolation ? join(tmpdir(), 'hanzo', 'dev', version) : binDir;
|
||||
let safeTempDir = needsIsolation
|
||||
? join(tmpdir(), "hanzo", "dev", version)
|
||||
: binDir;
|
||||
if (needsIsolation) {
|
||||
try {
|
||||
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
||||
if (!existsSync(safeTempDir))
|
||||
mkdirSync(safeTempDir, { recursive: true });
|
||||
} catch {
|
||||
try {
|
||||
safeTempDir = getCacheDir(version);
|
||||
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
||||
if (!existsSync(safeTempDir))
|
||||
mkdirSync(safeTempDir, { recursive: true });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
const tmpPath = join(needsIsolation ? safeTempDir : binDir, `.${archiveName}.part`);
|
||||
const tmpPath = join(
|
||||
needsIsolation ? safeTempDir : binDir,
|
||||
`.${archiveName}.part`,
|
||||
);
|
||||
await downloadBinary(downloadUrl, tmpPath);
|
||||
|
||||
if (isWin) {
|
||||
const unzipDest = safeTempDir;
|
||||
try {
|
||||
const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
|
||||
const psFull = join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
||||
const sysRoot =
|
||||
process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
||||
const psFull = join(
|
||||
sysRoot,
|
||||
"System32",
|
||||
"WindowsPowerShell",
|
||||
"v1.0",
|
||||
"powershell.exe",
|
||||
);
|
||||
const psCmd = `Expand-Archive -Path '${tmpPath}' -DestinationPath '${unzipDest}' -Force`;
|
||||
let ok = false;
|
||||
try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
|
||||
if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
if (!ok) { execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
|
||||
try {
|
||||
execSync(
|
||||
`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`,
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
ok = true;
|
||||
} catch {}
|
||||
if (!ok) {
|
||||
try {
|
||||
execSync(
|
||||
`powershell -NoProfile -NonInteractive -Command "${psCmd}"`,
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
ok = true;
|
||||
} catch {}
|
||||
}
|
||||
if (!ok) {
|
||||
try {
|
||||
execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, {
|
||||
stdio: "ignore",
|
||||
});
|
||||
ok = true;
|
||||
} catch {}
|
||||
}
|
||||
if (!ok) {
|
||||
execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, {
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`failed to unzip archive: ${e.message}`);
|
||||
} finally {
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
try {
|
||||
unlinkSync(tmpPath);
|
||||
} catch {}
|
||||
}
|
||||
try {
|
||||
const extractedPath = join(unzipDest, binaryName);
|
||||
// Rename code-* to dev-* in cache
|
||||
const devCachePath = cachePath;
|
||||
await writeCacheAtomic(extractedPath, devCachePath);
|
||||
try { unlinkSync(extractedPath); } catch {}
|
||||
try {
|
||||
unlinkSync(extractedPath);
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
throw new Error(`failed to move binary to cache: ${e.message}`);
|
||||
}
|
||||
@@ -425,20 +599,36 @@ export async function runPostinstall(options = {}) {
|
||||
const downloadedPath = join(binDir, binaryName);
|
||||
if (useZst) {
|
||||
try {
|
||||
execSync(`zstd -d '${tmpPath}' -o '${downloadedPath}'`, { stdio: 'ignore', shell: true });
|
||||
execSync(`zstd -d '${tmpPath}' -o '${downloadedPath}'`, {
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
});
|
||||
} catch (e) {
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
throw new Error(`failed to decompress .zst (need zstd CLI): ${e.message}`);
|
||||
try {
|
||||
unlinkSync(tmpPath);
|
||||
} catch {}
|
||||
throw new Error(
|
||||
`failed to decompress .zst (need zstd CLI): ${e.message}`,
|
||||
);
|
||||
}
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
try {
|
||||
unlinkSync(tmpPath);
|
||||
} catch {}
|
||||
} else {
|
||||
try {
|
||||
execSync(`tar -xzf '${tmpPath}' -C '${binDir}'`, { stdio: 'ignore', shell: true });
|
||||
execSync(`tar -xzf '${tmpPath}' -C '${binDir}'`, {
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
});
|
||||
} catch (e) {
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
try {
|
||||
unlinkSync(tmpPath);
|
||||
} catch {}
|
||||
throw new Error(`failed to extract .tar.gz: ${e.message}`);
|
||||
}
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
try {
|
||||
unlinkSync(tmpPath);
|
||||
} catch {}
|
||||
}
|
||||
// Rename code-* to dev-*
|
||||
if (existsSync(downloadedPath)) {
|
||||
@@ -447,14 +637,22 @@ export async function runPostinstall(options = {}) {
|
||||
} else {
|
||||
const extractedPath = downloadedPath;
|
||||
await writeCacheAtomic(extractedPath, cachePath);
|
||||
try { unlinkSync(extractedPath); } catch {}
|
||||
try {
|
||||
unlinkSync(extractedPath);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const valid = validateDownloadedBinary(isWin ? cachePath : (mirrorToLocal ? localPath : cachePath));
|
||||
const valid = validateDownloadedBinary(
|
||||
isWin ? cachePath : mirrorToLocal ? localPath : cachePath,
|
||||
);
|
||||
if (!valid.ok) {
|
||||
try { (isWin || !mirrorToLocal) ? unlinkSync(cachePath) : unlinkSync(localPath); } catch {}
|
||||
try {
|
||||
isWin || !mirrorToLocal
|
||||
? unlinkSync(cachePath)
|
||||
: unlinkSync(localPath);
|
||||
} catch {}
|
||||
throw new Error(`invalid binary (${valid.reason})`);
|
||||
}
|
||||
|
||||
@@ -462,9 +660,13 @@ export async function runPostinstall(options = {}) {
|
||||
chmodSync(localPath, 0o755);
|
||||
}
|
||||
|
||||
console.log(`✓ Installed ${devBinaryName}${(isWin || !mirrorToLocal) ? ' (cached)' : ''}`);
|
||||
console.log(
|
||||
`✓ Installed ${devBinaryName}${isWin || !mirrorToLocal ? " (cached)" : ""}`,
|
||||
);
|
||||
if (!isWin && mirrorToLocal) {
|
||||
try { await writeCacheAtomic(localPath, cachePath); } catch {}
|
||||
try {
|
||||
await writeCacheAtomic(localPath, cachePath);
|
||||
} catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to install ${devBinaryName}: ${error.message}`);
|
||||
@@ -475,23 +677,35 @@ export async function runPostinstall(options = {}) {
|
||||
const mainBinary = `dev-${targetTriple}${binaryExt}`;
|
||||
const mainBinaryPath = join(binDir, mainBinary);
|
||||
|
||||
if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
|
||||
if (
|
||||
existsSync(mainBinaryPath) ||
|
||||
existsSync(
|
||||
getCachedBinaryPath(version, targetTriple, platform() === "win32"),
|
||||
)
|
||||
) {
|
||||
try {
|
||||
const probePath = existsSync(mainBinaryPath) ? mainBinaryPath : getCachedBinaryPath(version, targetTriple, platform() === 'win32');
|
||||
const probePath = existsSync(mainBinaryPath)
|
||||
? mainBinaryPath
|
||||
: getCachedBinaryPath(version, targetTriple, platform() === "win32");
|
||||
const stats = statSync(probePath);
|
||||
if (!stats.size) throw new Error('binary is empty (download likely failed)');
|
||||
if (!stats.size)
|
||||
throw new Error("binary is empty (download likely failed)");
|
||||
const valid = validateDownloadedBinary(probePath);
|
||||
if (!valid.ok) {
|
||||
console.warn(`⚠ Main dev binary appears invalid: ${valid.reason}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
console.warn(
|
||||
" Try reinstalling or check your network/proxy settings.",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`⚠ Main dev binary appears invalid: ${e.message}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
console.warn(" Try reinstalling or check your network/proxy settings.");
|
||||
}
|
||||
console.log('✓ Installation complete!');
|
||||
console.log("✓ Installation complete!");
|
||||
} else {
|
||||
console.warn('⚠ Main dev binary not found. You may need to build from source.');
|
||||
console.warn(
|
||||
"⚠ Main dev binary not found. You may need to build from source.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,8 +720,8 @@ function isExecutedDirectly() {
|
||||
}
|
||||
|
||||
if (isExecutedDirectly()) {
|
||||
runPostinstall().catch(error => {
|
||||
console.error('Installation failed:', error);
|
||||
runPostinstall().catch((error) => {
|
||||
console.error("Installation failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
+97
-96
@@ -17,23 +17,23 @@ Hanzo Dev is an AI-powered coding assistant that runs in your terminal. When inv
|
||||
|
||||
These options can be used with any subcommand or in interactive mode:
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--model <MODEL>` | `-m` | Model the agent should use |
|
||||
| `--profile <PROFILE>` | `-p` | Configuration profile from config.toml |
|
||||
| `--sandbox <MODE>` | `-s` | Sandbox policy for shell commands |
|
||||
| `--ask-for-approval <POLICY>` | `-a` | When to require human approval |
|
||||
| `--cd <DIR>` | `-C` | Working directory for the agent |
|
||||
| `--image <FILE>` | `-i` | Attach image(s) to initial prompt (comma-separated) |
|
||||
| `--oss` | | Use local open source model provider (Ollama) |
|
||||
| `--full-auto` | | Enable low-friction sandboxed automatic execution |
|
||||
| `--dangerously-bypass-approvals-and-sandbox` | | Skip all safety checks (DANGEROUS) |
|
||||
| `--auto` | | Run Auto Drive for non-interactive sessions |
|
||||
| `--demo <TEXT>` | | Developer message to prepend for demos |
|
||||
| `--debug` | `-d` | Enable debug logging of LLM requests |
|
||||
| `-c <KEY=VALUE>` | | Override configuration values |
|
||||
| `--version` | | Print version information |
|
||||
| `--help` | `-h` | Print help information |
|
||||
| Option | Short | Description |
|
||||
| -------------------------------------------- | ----- | --------------------------------------------------- |
|
||||
| `--model <MODEL>` | `-m` | Model the agent should use |
|
||||
| `--profile <PROFILE>` | `-p` | Configuration profile from config.toml |
|
||||
| `--sandbox <MODE>` | `-s` | Sandbox policy for shell commands |
|
||||
| `--ask-for-approval <POLICY>` | `-a` | When to require human approval |
|
||||
| `--cd <DIR>` | `-C` | Working directory for the agent |
|
||||
| `--image <FILE>` | `-i` | Attach image(s) to initial prompt (comma-separated) |
|
||||
| `--oss` | | Use local open source model provider (Ollama) |
|
||||
| `--full-auto` | | Enable low-friction sandboxed automatic execution |
|
||||
| `--dangerously-bypass-approvals-and-sandbox` | | Skip all safety checks (DANGEROUS) |
|
||||
| `--auto` | | Run Auto Drive for non-interactive sessions |
|
||||
| `--demo <TEXT>` | | Developer message to prepend for demos |
|
||||
| `--debug` | `-d` | Enable debug logging of LLM requests |
|
||||
| `-c <KEY=VALUE>` | | Override configuration values |
|
||||
| `--version` | | Print version information |
|
||||
| `--help` | `-h` | Print help information |
|
||||
|
||||
### Sandbox Modes
|
||||
|
||||
@@ -63,14 +63,14 @@ Launch the interactive TUI interface. If PROMPT is provided, it starts the conve
|
||||
|
||||
**TUI-Specific Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--search` | Enable web search (default: enabled) |
|
||||
| `--no-search` | Disable web search |
|
||||
| `--compact-prompt <TEXT>` | Override compaction prompt text |
|
||||
| `--compact-prompt-file <FILE>` | Read compaction prompt from file |
|
||||
| `--order` | Show ordering debug overlays |
|
||||
| `--timing` | Enable timing diagnostics |
|
||||
| Option | Description |
|
||||
| ------------------------------ | ------------------------------------ |
|
||||
| `--search` | Enable web search (default: enabled) |
|
||||
| `--no-search` | Disable web search |
|
||||
| `--compact-prompt <TEXT>` | Override compaction prompt text |
|
||||
| `--compact-prompt-file <FILE>` | Read compaction prompt from file |
|
||||
| `--order` | Show ordering debug overlays |
|
||||
| `--timing` | Enable timing diagnostics |
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -102,19 +102,19 @@ dev exec [OPTIONS] [PROMPT]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--auto` | Run Auto Drive instead of single turn |
|
||||
| `--auto-review` | Use Auto Review models and limits |
|
||||
| `--json` | Print events to stdout as JSONL |
|
||||
| `--color <MODE>` | Color output: `always`, `never`, `auto` |
|
||||
| `--max-seconds <N>` | Maximum wall-clock time budget |
|
||||
| `--turn-cap <N>` | Maximum Auto Drive coordinator turns |
|
||||
| `--include-plan-tool` | Include planning tool in conversation |
|
||||
| `--output-last-message <FILE>` | Write final message to file |
|
||||
| `--output-schema <FILE>` | JSON Schema for structured output |
|
||||
| `--review-output-json <FILE>` | Write review output JSON |
|
||||
| `--skip-git-repo-check` | Allow running outside Git repos |
|
||||
| Option | Description |
|
||||
| ------------------------------ | --------------------------------------- |
|
||||
| `--auto` | Run Auto Drive instead of single turn |
|
||||
| `--auto-review` | Use Auto Review models and limits |
|
||||
| `--json` | Print events to stdout as JSONL |
|
||||
| `--color <MODE>` | Color output: `always`, `never`, `auto` |
|
||||
| `--max-seconds <N>` | Maximum wall-clock time budget |
|
||||
| `--turn-cap <N>` | Maximum Auto Drive coordinator turns |
|
||||
| `--include-plan-tool` | Include planning tool in conversation |
|
||||
| `--output-last-message <FILE>` | Write final message to file |
|
||||
| `--output-schema <FILE>` | JSON Schema for structured output |
|
||||
| `--review-output-json <FILE>` | Write review output JSON |
|
||||
| `--skip-git-repo-check` | Allow running outside Git repos |
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -164,10 +164,10 @@ dev resume [SESSION_ID] [OPTIONS]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--last` | Continue the most recent session |
|
||||
| All TUI options | Inherited from interactive mode |
|
||||
| Option | Description |
|
||||
| --------------- | -------------------------------- |
|
||||
| `--last` | Continue the most recent session |
|
||||
| All TUI options | Inherited from interactive mode |
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -198,8 +198,8 @@ dev login status
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| Option | Description |
|
||||
| ---------------- | ----------------------- |
|
||||
| `--with-api-key` | Read API key from stdin |
|
||||
|
||||
**Examples:**
|
||||
@@ -284,11 +284,11 @@ dev mcp add <NAME> [OPTIONS] [-- COMMAND...]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--url <URL>` | URL of remote MCP server |
|
||||
| `--bearer-token <TOKEN>` | Bearer token for authentication |
|
||||
| `--env <KEY=VALUE>` | Environment variables (repeatable) |
|
||||
| Option | Description |
|
||||
| ------------------------ | ---------------------------------- |
|
||||
| `--url <URL>` | URL of remote MCP server |
|
||||
| `--bearer-token <TOKEN>` | Bearer token for authentication |
|
||||
| `--env <KEY=VALUE>` | Environment variables (repeatable) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -336,13 +336,13 @@ dev bridge subscription [OPTIONS]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--show` | Display current subscription |
|
||||
| `--levels <CSV>` | Set log levels: errors,warn,info,trace |
|
||||
| Option | Description |
|
||||
| ---------------------- | ----------------------------------------------------------- |
|
||||
| `--show` | Display current subscription |
|
||||
| `--levels <CSV>` | Set log levels: errors,warn,info,trace |
|
||||
| `--capabilities <CSV>` | Set capabilities: screenshot,pageview,control,console,error |
|
||||
| `--filter <MODE>` | LLM filter: off, minimal, aggressive |
|
||||
| `--clear` | Remove override and revert to defaults |
|
||||
| `--filter <MODE>` | LLM filter: off, minimal, aggressive |
|
||||
| `--clear` | Remove override and revert to defaults |
|
||||
|
||||
#### bridge list
|
||||
|
||||
@@ -364,11 +364,11 @@ dev bridge tail [OPTIONS]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--level <LEVEL>` | Minimum level: errors, warn, info, trace |
|
||||
| `--bridge <PATH\|INDEX>` | Select specific bridge target |
|
||||
| `--raw` | Print raw JSON frames |
|
||||
| Option | Description |
|
||||
| ------------------------ | ---------------------------------------- |
|
||||
| `--level <LEVEL>` | Minimum level: errors, warn, info, trace |
|
||||
| `--bridge <PATH\|INDEX>` | Select specific bridge target |
|
||||
| `--raw` | Print raw JSON frames |
|
||||
|
||||
#### bridge screenshot
|
||||
|
||||
@@ -380,9 +380,9 @@ dev bridge screenshot [OPTIONS]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--timeout <SECONDS>` | Wait timeout (default: 10) |
|
||||
| Option | Description |
|
||||
| ------------------------ | ----------------------------- |
|
||||
| `--timeout <SECONDS>` | Wait timeout (default: 10) |
|
||||
| `--bridge <PATH\|INDEX>` | Select specific bridge target |
|
||||
|
||||
#### bridge javascript
|
||||
@@ -397,9 +397,9 @@ dev bridge javascript <CODE> [OPTIONS]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--timeout <SECONDS>` | Wait timeout (default: 10) |
|
||||
| Option | Description |
|
||||
| ------------------------ | ----------------------------- |
|
||||
| `--timeout <SECONDS>` | Wait timeout (default: 10) |
|
||||
| `--bridge <PATH\|INDEX>` | Select specific bridge target |
|
||||
|
||||
---
|
||||
@@ -424,13 +424,13 @@ dev cloud submit <PROMPT> [OPTIONS]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--env <ENV_ID>` | Environment ID |
|
||||
| `--best-of <N>` | Best-of-N attempts (default: 1) |
|
||||
| `--qa` | Enable QA/review mode |
|
||||
| `--git-ref <REF>` | Git ref (default: main) |
|
||||
| `--wait` | Wait for completion |
|
||||
| Option | Description |
|
||||
| ----------------- | ------------------------------- |
|
||||
| `--env <ENV_ID>` | Environment ID |
|
||||
| `--best-of <N>` | Best-of-N attempts (default: 1) |
|
||||
| `--qa` | Enable QA/review mode |
|
||||
| `--git-ref <REF>` | Git ref (default: main) |
|
||||
| `--wait` | Wait for completion |
|
||||
|
||||
---
|
||||
|
||||
@@ -452,16 +452,16 @@ dev llm request [OPTIONS]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--developer <TEXT>` | Developer message |
|
||||
| `--message <TEXT>` | User message |
|
||||
| `--model <MODEL>` | Model override |
|
||||
| Option | Description |
|
||||
| ---------------------- | ----------------------------------------- |
|
||||
| `--developer <TEXT>` | Developer message |
|
||||
| `--message <TEXT>` | User message |
|
||||
| `--model <MODEL>` | Model override |
|
||||
| `--format-type <TYPE>` | Output format type (default: json_schema) |
|
||||
| `--format-name <NAME>` | Output format name |
|
||||
| `--format-strict` | Enable strict format (default: true) |
|
||||
| `--schema-json <JSON>` | Inline JSON schema |
|
||||
| `--schema-file <FILE>` | Path to JSON schema file |
|
||||
| `--format-name <NAME>` | Output format name |
|
||||
| `--format-strict` | Enable strict format (default: true) |
|
||||
| `--schema-json <JSON>` | Inline JSON schema |
|
||||
| `--schema-file <FILE>` | Path to JSON schema file |
|
||||
|
||||
---
|
||||
|
||||
@@ -499,6 +499,7 @@ dev doctor
|
||||
```
|
||||
|
||||
**Output includes:**
|
||||
|
||||
- Current executable path and version
|
||||
- PATH environment variable
|
||||
- All `dev` and `coder` binaries found
|
||||
@@ -517,10 +518,10 @@ dev preview <SLUG> [OPTIONS] [-- EXTRA_ARGS...]
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| Option | Description |
|
||||
| --------------------- | ------------------- |
|
||||
| `--repo <OWNER/REPO>` | Override repository |
|
||||
| `--out <DIR>` | Output directory |
|
||||
| `--out <DIR>` | Output directory |
|
||||
|
||||
---
|
||||
|
||||
@@ -563,12 +564,12 @@ dev exec -c model=gpt-4.1 "Your prompt"
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OPENAI_API_KEY` | OpenAI API key |
|
||||
| `CODE_HOME` | Override config directory (default: ~/.code) |
|
||||
| `CODEX_HOME` | Legacy config directory (fallback) |
|
||||
| `CODEX_SECURE_MODE` | Enable security hardening when set to "1" |
|
||||
| Variable | Description |
|
||||
| ------------------- | -------------------------------------------- |
|
||||
| `OPENAI_API_KEY` | OpenAI API key |
|
||||
| `CODE_HOME` | Override config directory (default: ~/.code) |
|
||||
| `CODEX_HOME` | Legacy config directory (fallback) |
|
||||
| `CODEX_SECURE_MODE` | Enable security hardening when set to "1" |
|
||||
|
||||
---
|
||||
|
||||
@@ -624,11 +625,11 @@ dev mcp remove my-search
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 0 | Success |
|
||||
| 1 | General error |
|
||||
| 130 | Interrupted (SIGINT) |
|
||||
| Code | Description |
|
||||
| ---- | -------------------- |
|
||||
| 0 | Success |
|
||||
| 1 | General error |
|
||||
| 130 | Interrupted (SIGINT) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+104
-88
@@ -43,6 +43,7 @@ The Hanzo compute marketplace (hanzo.network) requires a robust economic incenti
|
||||
### Current State
|
||||
|
||||
Hanzo already has foundational components:
|
||||
|
||||
- **HIP-004 (HMM)**: Hamiltonian Market Maker for compute pricing
|
||||
- **HIP-006 (AI Mining)**: NVTrust-based work attestation and chain-binding
|
||||
- **PAT-DEAI-004**: Compute reputation system design from Zoo Labs
|
||||
@@ -90,14 +91,14 @@ HCU = weighted_sum(
|
||||
|
||||
**Normalization Reference** (H100 = 1.0):
|
||||
|
||||
| Resource | Reference | HCU Weight |
|
||||
|----------|-----------|------------|
|
||||
| GPU FP16 | H100 (1.98 PFLOPS) | 0.50 |
|
||||
| GPU VRAM | 80GB | 0.15 |
|
||||
| CPU | 64 cores @ 3GHz | 0.10 |
|
||||
| Memory | 512GB @ 400GB/s | 0.10 |
|
||||
| Storage | 10TB NVMe @ 7GB/s | 0.10 |
|
||||
| Network | 100Gbps | 0.05 |
|
||||
| Resource | Reference | HCU Weight |
|
||||
| -------- | ------------------ | ---------- |
|
||||
| GPU FP16 | H100 (1.98 PFLOPS) | 0.50 |
|
||||
| GPU VRAM | 80GB | 0.15 |
|
||||
| CPU | 64 cores @ 3GHz | 0.10 |
|
||||
| Memory | 512GB @ 400GB/s | 0.10 |
|
||||
| Storage | 10TB NVMe @ 7GB/s | 0.10 |
|
||||
| Network | 100Gbps | 0.05 |
|
||||
|
||||
### 3.2 Contribution Metrics
|
||||
|
||||
@@ -157,12 +158,12 @@ pub struct AttestationProof {
|
||||
|
||||
**Trust Scores by GPU Type** (from HIP-006):
|
||||
|
||||
| GPU | NVTrust Support | Trust Score | Reward Multiplier |
|
||||
|-----|-----------------|-------------|-------------------|
|
||||
| B200/GB200 | Full + TEE-I/O | 100 | 1.00x |
|
||||
| H100/H200 | Full NVTrust | 95 | 0.95x |
|
||||
| RTX PRO 6000 | NVTrust | 85 | 0.85x |
|
||||
| RTX 5090/4090 | Software only | 60 | 0.60x |
|
||||
| GPU | NVTrust Support | Trust Score | Reward Multiplier |
|
||||
| ------------- | --------------- | ----------- | ----------------- |
|
||||
| B200/GB200 | Full + TEE-I/O | 100 | 1.00x |
|
||||
| H100/H200 | Full NVTrust | 95 | 0.95x |
|
||||
| RTX PRO 6000 | NVTrust | 85 | 0.85x |
|
||||
| RTX 5090/4090 | Software only | 60 | 0.60x |
|
||||
|
||||
### 3.4 On-Chain Contribution Records
|
||||
|
||||
@@ -207,6 +208,7 @@ contract ContributionRegistry {
|
||||
### 4.1 Epoch-Based Distribution
|
||||
|
||||
Rewards are distributed in **epochs** (24 hours) to:
|
||||
|
||||
- Smooth out variance in job availability
|
||||
- Allow time for quality verification
|
||||
- Prevent gaming through rapid state changes
|
||||
@@ -227,11 +229,11 @@ Where:
|
||||
**Emission Schedule**:
|
||||
|
||||
| Year | Daily Base Emission | Annual Inflation Cap |
|
||||
|------|---------------------|---------------------|
|
||||
| 1 | 1,000,000 AI | 10% |
|
||||
| 2 | 750,000 AI | 7.5% |
|
||||
| 3 | 500,000 AI | 5% |
|
||||
| 4+ | 250,000 AI | 3% |
|
||||
| ---- | ------------------- | -------------------- |
|
||||
| 1 | 1,000,000 AI | 10% |
|
||||
| 2 | 750,000 AI | 7.5% |
|
||||
| 3 | 500,000 AI | 5% |
|
||||
| 4+ | 250,000 AI | 3% |
|
||||
|
||||
### 4.3 Provider Reward Formula
|
||||
|
||||
@@ -279,6 +281,7 @@ UptimeRatio = uptime_seconds / total_registered_seconds
|
||||
```
|
||||
|
||||
**Bonus for consistent uptime**:
|
||||
|
||||
- 7+ consecutive days at 99%+ uptime: +5% bonus
|
||||
- 30+ consecutive days at 99%+ uptime: +10% bonus
|
||||
|
||||
@@ -293,6 +296,7 @@ MAX_STAKE = 1,000,000 AI (diminishing returns after this)
|
||||
### 4.4 Worked Example
|
||||
|
||||
**Provider Profile**:
|
||||
|
||||
- Contributed: 100 HCU-hours this epoch
|
||||
- Quality Score: 8500 (85%)
|
||||
- Trust Score: 95 (H100 with NVTrust)
|
||||
@@ -300,6 +304,7 @@ MAX_STAKE = 1,000,000 AI (diminishing returns after this)
|
||||
- Stake: 100,000 AI
|
||||
|
||||
**Calculation**:
|
||||
|
||||
```
|
||||
Quality_multiplier = 0.5 + 1.5 * (8500/10000) = 1.775
|
||||
Trust_multiplier = 95/100 = 0.95
|
||||
@@ -316,13 +321,13 @@ R_provider = 1,000,000 * (285.5 / 10,000) = 28,550 AI
|
||||
|
||||
To receive rewards, providers must meet:
|
||||
|
||||
| Metric | Minimum Threshold |
|
||||
|--------|-------------------|
|
||||
| Quality Score | 5000 (50%) |
|
||||
| Uptime Ratio | 9000 (90%) |
|
||||
| Stake | 1000 AI |
|
||||
| HCU-hours/epoch | 1.0 |
|
||||
| Hardware Attestation | Required |
|
||||
| Metric | Minimum Threshold |
|
||||
| -------------------- | ----------------- |
|
||||
| Quality Score | 5000 (50%) |
|
||||
| Uptime Ratio | 9000 (90%) |
|
||||
| Stake | 1000 AI |
|
||||
| HCU-hours/epoch | 1.0 |
|
||||
| Hardware Attestation | Required |
|
||||
|
||||
---
|
||||
|
||||
@@ -364,15 +369,16 @@ To receive rewards, providers must meet:
|
||||
|
||||
### 5.2 Reward Allocation
|
||||
|
||||
| Allocation | Percentage | Purpose |
|
||||
|------------|------------|---------|
|
||||
| Provider Rewards | 70% | Direct rewards to compute providers |
|
||||
| Treasury | 20% | Protocol development, grants, insurance |
|
||||
| Burn | 10% | Deflationary pressure |
|
||||
| Allocation | Percentage | Purpose |
|
||||
| ---------------- | ---------- | --------------------------------------- |
|
||||
| Provider Rewards | 70% | Direct rewards to compute providers |
|
||||
| Treasury | 20% | Protocol development, grants, insurance |
|
||||
| Burn | 10% | Deflationary pressure |
|
||||
|
||||
### 5.3 Vesting and Claiming
|
||||
|
||||
**Vesting Schedule**:
|
||||
|
||||
- 50% available immediately
|
||||
- 25% vests over 7 days
|
||||
- 25% vests over 30 days
|
||||
@@ -582,44 +588,44 @@ impl RewardDistributor {
|
||||
|
||||
**Year 1 Projection** (assuming 100 providers):
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Daily Emission | 1,000,000 AI |
|
||||
| Daily Burn (10%) | 100,000 AI |
|
||||
| Net Daily Inflation | 900,000 AI |
|
||||
| Annual Net Inflation | 328,500,000 AI |
|
||||
| Effective Inflation Rate | ~10% |
|
||||
| Metric | Value |
|
||||
| ------------------------ | -------------- |
|
||||
| Daily Emission | 1,000,000 AI |
|
||||
| Daily Burn (10%) | 100,000 AI |
|
||||
| Net Daily Inflation | 900,000 AI |
|
||||
| Annual Net Inflation | 328,500,000 AI |
|
||||
| Effective Inflation Rate | ~10% |
|
||||
|
||||
**Year 5 Projection** (assuming 1000 providers):
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Daily Emission | 250,000 AI |
|
||||
| Daily Burn (10%) | 25,000 AI |
|
||||
| Fee Recycling (estimated) | 50,000 AI |
|
||||
| Net Daily Inflation | 275,000 AI |
|
||||
| Effective Inflation Rate | ~3% |
|
||||
| Metric | Value |
|
||||
| ------------------------- | ---------- |
|
||||
| Daily Emission | 250,000 AI |
|
||||
| Daily Burn (10%) | 25,000 AI |
|
||||
| Fee Recycling (estimated) | 50,000 AI |
|
||||
| Net Daily Inflation | 275,000 AI |
|
||||
| Effective Inflation Rate | ~3% |
|
||||
|
||||
### 8.2 Provider Economics
|
||||
|
||||
**Break-Even Analysis** for H100 provider:
|
||||
|
||||
| Cost Item | Monthly |
|
||||
|-----------|---------|
|
||||
| Hardware Depreciation | $2,000 |
|
||||
| Electricity (0.7kW avg) | $400 |
|
||||
| Bandwidth (1TB) | $100 |
|
||||
| Maintenance | $200 |
|
||||
| **Total Cost** | **$2,700** |
|
||||
| Cost Item | Monthly |
|
||||
| ----------------------- | ---------- |
|
||||
| Hardware Depreciation | $2,000 |
|
||||
| Electricity (0.7kW avg) | $400 |
|
||||
| Bandwidth (1TB) | $100 |
|
||||
| Maintenance | $200 |
|
||||
| **Total Cost** | **$2,700** |
|
||||
|
||||
**Revenue at Various Utilization Rates**:
|
||||
|
||||
| Utilization | HCU-hours/month | Est. Reward (AI) | USD Value (@$0.10/AI) |
|
||||
|-------------|-----------------|------------------|----------------------|
|
||||
| 25% | 180 | 50,000 | $5,000 |
|
||||
| 50% | 360 | 100,000 | $10,000 |
|
||||
| 75% | 540 | 150,000 | $15,000 |
|
||||
| 100% | 720 | 200,000 | $20,000 |
|
||||
| ----------- | --------------- | ---------------- | --------------------- |
|
||||
| 25% | 180 | 50,000 | $5,000 |
|
||||
| 50% | 360 | 100,000 | $10,000 |
|
||||
| 75% | 540 | 150,000 | $15,000 |
|
||||
| 100% | 720 | 200,000 | $20,000 |
|
||||
|
||||
**Conclusion**: Profitable at >15% utilization at current prices.
|
||||
|
||||
@@ -628,6 +634,7 @@ impl RewardDistributor {
|
||||
**Dominant Strategy**: Provide high-quality compute honestly.
|
||||
|
||||
**Rationale**:
|
||||
|
||||
1. Gaming quality reduces Quality_multiplier (0.5x vs 2.0x)
|
||||
2. Low trust hardware reduces Trust_multiplier (0.6x vs 1.0x)
|
||||
3. Downtime reduces Uptime_multiplier (0.7x vs 1.2x)
|
||||
@@ -650,6 +657,7 @@ Sustainable when:
|
||||
```
|
||||
|
||||
**Self-Correcting Mechanisms**:
|
||||
|
||||
1. If rewards too high → new providers join → dilution → equilibrium
|
||||
2. If rewards too low → providers leave → scarcity → price increase → equilibrium
|
||||
3. Burn rate creates deflationary pressure balancing inflation
|
||||
@@ -661,12 +669,14 @@ Sustainable when:
|
||||
### Phase 1: Foundation (Q1 2026)
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] ContributionRegistry smart contract
|
||||
- [ ] Basic HCU calculation service
|
||||
- [ ] Integration with existing compute_usage table
|
||||
- [ ] Manual epoch distribution (admin-triggered)
|
||||
|
||||
**Success Criteria**:
|
||||
|
||||
- 10 providers onboarded
|
||||
- 100,000 AI distributed in first epoch
|
||||
- No critical bugs in 30-day audit
|
||||
@@ -674,12 +684,14 @@ Sustainable when:
|
||||
### Phase 2: Automation (Q2 2026)
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] Automated epoch distribution
|
||||
- [ ] Quality score integration (PoAI)
|
||||
- [ ] NVTrust attestation verification
|
||||
- [ ] Vesting contract deployment
|
||||
|
||||
**Success Criteria**:
|
||||
|
||||
- 50 providers onboarded
|
||||
- 99.9% uptime for distribution
|
||||
- <1% reward calculation variance
|
||||
@@ -687,12 +699,14 @@ Sustainable when:
|
||||
### Phase 3: Scaling (Q3 2026)
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] Cross-chain Teleport distribution
|
||||
- [ ] Auto-compound staking option
|
||||
- [ ] Advanced anti-gaming ML models
|
||||
- [ ] Public dashboard and analytics
|
||||
|
||||
**Success Criteria**:
|
||||
|
||||
- 200 providers onboarded
|
||||
- $10M+ monthly compute volume
|
||||
- <0.1% fraudulent claims
|
||||
@@ -700,12 +714,14 @@ Sustainable when:
|
||||
### Phase 4: Decentralization (Q4 2026)
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] DAO governance for parameters
|
||||
- [ ] Decentralized epoch triggers
|
||||
- [ ] Community-run validators
|
||||
- [ ] Open-source all components
|
||||
|
||||
**Success Criteria**:
|
||||
|
||||
- 500+ providers
|
||||
- 0 admin key dependencies
|
||||
- Community-approved parameter changes
|
||||
@@ -716,30 +732,30 @@ Sustainable when:
|
||||
|
||||
### 10.1 Smart Contract Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Reentrancy | Checks-effects-interactions, ReentrancyGuard |
|
||||
| Integer Overflow | SafeMath, Solidity 0.8+ |
|
||||
| Access Control | OpenZeppelin AccessControl |
|
||||
| Upgrade Risks | Transparent proxy with timelock |
|
||||
| Risk | Mitigation |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| Reentrancy | Checks-effects-interactions, ReentrancyGuard |
|
||||
| Integer Overflow | SafeMath, Solidity 0.8+ |
|
||||
| Access Control | OpenZeppelin AccessControl |
|
||||
| Upgrade Risks | Transparent proxy with timelock |
|
||||
|
||||
### 10.2 Economic Attacks
|
||||
|
||||
| Attack | Mitigation |
|
||||
|--------|------------|
|
||||
| Flash Loan Stake | Minimum stake duration (7 days) |
|
||||
| Attack | Mitigation |
|
||||
| ------------------- | ------------------------------------ |
|
||||
| Flash Loan Stake | Minimum stake duration (7 days) |
|
||||
| Reward Manipulation | Epoch-based distribution (24h delay) |
|
||||
| Front-running | Commit-reveal for claims |
|
||||
| MEV Extraction | Batch distribution transactions |
|
||||
| Front-running | Commit-reveal for claims |
|
||||
| MEV Extraction | Batch distribution transactions |
|
||||
|
||||
### 10.3 Operational Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Oracle Failure | On-chain contribution records |
|
||||
| Key Compromise | Multi-sig treasury, timelock |
|
||||
| Network Congestion | L2 distribution, batching |
|
||||
| Bug Discovery | Bug bounty program, insurance fund |
|
||||
| Risk | Mitigation |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| Oracle Failure | On-chain contribution records |
|
||||
| Key Compromise | Multi-sig treasury, timelock |
|
||||
| Network Congestion | L2 distribution, batching |
|
||||
| Bug Discovery | Bug bounty program, insurance fund |
|
||||
|
||||
### 10.4 Privacy Considerations
|
||||
|
||||
@@ -774,17 +790,17 @@ Sustainable when:
|
||||
|
||||
## Appendix A: Parameter Reference
|
||||
|
||||
| Parameter | Default | Range | Governance |
|
||||
|-----------|---------|-------|------------|
|
||||
| EPOCH_DURATION | 86400s (24h) | 3600-604800 | DAO |
|
||||
| MIN_STAKE | 1000 AI | 100-10000 | DAO |
|
||||
| PROVIDER_SHARE | 70% | 50-80% | DAO |
|
||||
| TREASURY_SHARE | 20% | 10-30% | DAO |
|
||||
| BURN_SHARE | 10% | 5-20% | DAO |
|
||||
| MIN_QUALITY_SCORE | 5000 | 3000-7000 | DAO |
|
||||
| MIN_UPTIME_RATIO | 9000 | 8000-9500 | DAO |
|
||||
| BASE_EMISSION_YEAR1 | 1M AI/day | Fixed | N/A |
|
||||
| MAX_STAKE_EFFECT | 1M AI | 100K-10M | DAO |
|
||||
| Parameter | Default | Range | Governance |
|
||||
| ------------------- | ------------ | ----------- | ---------- |
|
||||
| EPOCH_DURATION | 86400s (24h) | 3600-604800 | DAO |
|
||||
| MIN_STAKE | 1000 AI | 100-10000 | DAO |
|
||||
| PROVIDER_SHARE | 70% | 50-80% | DAO |
|
||||
| TREASURY_SHARE | 20% | 10-30% | DAO |
|
||||
| BURN_SHARE | 10% | 5-20% | DAO |
|
||||
| MIN_QUALITY_SCORE | 5000 | 3000-7000 | DAO |
|
||||
| MIN_UPTIME_RATIO | 9000 | 8000-9500 | DAO |
|
||||
| BASE_EMISSION_YEAR1 | 1M AI/day | Fixed | N/A |
|
||||
| MAX_STAKE_EFFECT | 1M AI | 100K-10M | DAO |
|
||||
|
||||
---
|
||||
|
||||
@@ -854,6 +870,6 @@ Copyright 2026 Hanzo AI Inc. Released under MIT License.
|
||||
|
||||
---
|
||||
|
||||
*HIP-XXX Created: January 24, 2026*
|
||||
*Status: Draft*
|
||||
*Contact: [email protected]*
|
||||
_HIP-XXX Created: January 24, 2026_
|
||||
_Status: Draft_
|
||||
_Contact: [email protected]_
|
||||
|
||||
@@ -277,7 +277,10 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { api } from "@/utils/api";
|
||||
import { formatBigInt, parseBigInt } from "@/lib/pools/format";
|
||||
import { calculateSlippage, calculatePriceImpact } from "@/lib/pools/calculations";
|
||||
import {
|
||||
calculateSlippage,
|
||||
calculatePriceImpact,
|
||||
} from "@/lib/pools/calculations";
|
||||
|
||||
interface SwapInterfaceProps {
|
||||
open: boolean;
|
||||
@@ -306,7 +309,7 @@ export function SwapInterface({ open, onClose, poolId }: SwapInterfaceProps) {
|
||||
{
|
||||
enabled: !!selectedPool && !!amountIn && parseFloat(amountIn) > 0,
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update output amount when quote changes
|
||||
@@ -345,7 +348,7 @@ export function SwapInterface({ open, onClose, poolId }: SwapInterfaceProps) {
|
||||
? calculatePriceImpact(
|
||||
parseBigInt(amountIn),
|
||||
BigInt(quote.expectedOutput),
|
||||
selectedPool?.lastPrice || 0n
|
||||
selectedPool?.lastPrice || 0n,
|
||||
)
|
||||
: 0;
|
||||
|
||||
@@ -359,7 +362,8 @@ export function SwapInterface({ open, onClose, poolId }: SwapInterfaceProps) {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Swap Resources</DialogTitle>
|
||||
<DialogDescription>
|
||||
Trade compute resources on the {selectedPool?.name || "selected"} pool
|
||||
Trade compute resources on the {selectedPool?.name || "selected"}{" "}
|
||||
pool
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -380,7 +384,8 @@ export function SwapInterface({ open, onClose, poolId }: SwapInterfaceProps) {
|
||||
<div className="flex justify-between">
|
||||
<Label>You Pay</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Balance: {formatBigInt(balance || "0")} {isBuying ? "HANZO" : "units"}
|
||||
Balance: {formatBigInt(balance || "0")}{" "}
|
||||
{isBuying ? "HANZO" : "units"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -394,9 +399,7 @@ export function SwapInterface({ open, onClose, poolId }: SwapInterfaceProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setAmountIn(formatBigInt(balance || "0"))
|
||||
}
|
||||
onClick={() => setAmountIn(formatBigInt(balance || "0"))}
|
||||
>
|
||||
MAX
|
||||
</Button>
|
||||
@@ -444,16 +447,16 @@ export function SwapInterface({ open, onClose, poolId }: SwapInterfaceProps) {
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Minimum Received</span>
|
||||
<span>
|
||||
{formatBigInt(calculateSlippage(quote.expectedOutput, slippage))}{" "}
|
||||
{formatBigInt(
|
||||
calculateSlippage(quote.expectedOutput, slippage),
|
||||
)}{" "}
|
||||
{isBuying ? "units" : "HANZO"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Trading Fee</span>
|
||||
<span>
|
||||
{formatBigInt(
|
||||
(parseBigInt(amountIn) * 3n) / 1000n
|
||||
)}{" "}
|
||||
{formatBigInt((parseBigInt(amountIn) * 3n) / 1000n)}{" "}
|
||||
{isBuying ? "HANZO" : "units"} (0.3%)
|
||||
</span>
|
||||
</div>
|
||||
@@ -555,7 +558,8 @@ export function AddLiquidityModal({
|
||||
const { data: pools } = api.pools.getAll.useQuery();
|
||||
const { data: tokenBalance } = api.user.getTokenBalance.useQuery();
|
||||
const { data: resourceBalance } = api.user.getResourceBalance.useQuery({
|
||||
resourceType: pools?.find((p) => p.id === selectedPoolId)?.resourceType || 0,
|
||||
resourceType:
|
||||
pools?.find((p) => p.id === selectedPoolId)?.resourceType || 0,
|
||||
});
|
||||
|
||||
const selectedPool = pools?.find((p) => p.id === selectedPoolId);
|
||||
@@ -623,10 +627,7 @@ export function AddLiquidityModal({
|
||||
{!poolId && (
|
||||
<div className="space-y-2">
|
||||
<Label>Select Pool</Label>
|
||||
<Select
|
||||
value={selectedPoolId}
|
||||
onValueChange={setSelectedPoolId}
|
||||
>
|
||||
<Select value={selectedPoolId} onValueChange={setSelectedPoolId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a pool" />
|
||||
</SelectTrigger>
|
||||
@@ -710,9 +711,7 @@ export function AddLiquidityModal({
|
||||
</AlertBlock>
|
||||
)}
|
||||
{insufficientResources && (
|
||||
<AlertBlock type="error">
|
||||
Insufficient resource balance.
|
||||
</AlertBlock>
|
||||
<AlertBlock type="error">Insufficient resource balance.</AlertBlock>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -777,7 +776,10 @@ import { api } from "@/utils/api";
|
||||
import { PoolSelector } from "./PoolSelector";
|
||||
import { ResourceEstimator } from "./ResourceEstimator";
|
||||
import { DeploymentBudget } from "./DeploymentBudget";
|
||||
import { HANZO_CLOUD_CONFIG, isHanzoCloudEnabled } from "@/lib/hanzo-cloud-config";
|
||||
import {
|
||||
HANZO_CLOUD_CONFIG,
|
||||
isHanzoCloudEnabled,
|
||||
} from "@/lib/hanzo-cloud-config";
|
||||
|
||||
interface DeploymentTarget {
|
||||
type: "local" | "cloud" | "pool";
|
||||
@@ -828,7 +830,8 @@ export function DeploymentTargetSelector({
|
||||
const { deploymentTargets } = HANZO_CLOUD_CONFIG;
|
||||
|
||||
// Fetch available pools
|
||||
const { data: pools, isLoading: isLoadingPools } = api.pools.getAll.useQuery();
|
||||
const { data: pools, isLoading: isLoadingPools } =
|
||||
api.pools.getAll.useQuery();
|
||||
|
||||
const handleTargetChange = (value: string) => {
|
||||
const target = value as "local" | "cloud" | "pool";
|
||||
@@ -927,28 +930,32 @@ export function DeploymentTargetSelector({
|
||||
</RadioGroup>
|
||||
|
||||
{/* Region Selection (Cloud) */}
|
||||
{selectedTarget === "cloud" && deploymentTargets.hanzoCloud.regions && (
|
||||
<div className="pt-4 border-t">
|
||||
<Label htmlFor="region" className="text-sm font-medium">
|
||||
Select Region
|
||||
</Label>
|
||||
<Select value={selectedRegion} onValueChange={handleRegionChange}>
|
||||
<SelectTrigger id="region" className="mt-2">
|
||||
<SelectValue placeholder="Select a region" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{deploymentTargets.hanzoCloud.regions.map((region) => (
|
||||
<SelectItem key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
<span className="text-xs text-muted-foreground ml-2">
|
||||
({region.id})
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{selectedTarget === "cloud" &&
|
||||
deploymentTargets.hanzoCloud.regions && (
|
||||
<div className="pt-4 border-t">
|
||||
<Label htmlFor="region" className="text-sm font-medium">
|
||||
Select Region
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedRegion}
|
||||
onValueChange={handleRegionChange}
|
||||
>
|
||||
<SelectTrigger id="region" className="mt-2">
|
||||
<SelectValue placeholder="Select a region" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{deploymentTargets.hanzoCloud.regions.map((region) => (
|
||||
<SelectItem key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
<span className="text-xs text-muted-foreground ml-2">
|
||||
({region.id})
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pool Selection */}
|
||||
{selectedTarget === "pool" && (
|
||||
@@ -999,7 +1006,15 @@ export function DeploymentTargetSelector({
|
||||
```tsx
|
||||
// /app/platform/components/deployment/PoolSelector.tsx
|
||||
|
||||
import { Check, Activity, Zap, HardDrive, Cpu, Container, Box } from "lucide-react";
|
||||
import {
|
||||
Check,
|
||||
Activity,
|
||||
Zap,
|
||||
HardDrive,
|
||||
Cpu,
|
||||
Container,
|
||||
Box,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Pool } from "@/lib/pools/types";
|
||||
import { formatBigInt } from "@/lib/pools/format";
|
||||
@@ -1040,7 +1055,7 @@ export function PoolSelector({
|
||||
"w-full flex items-center gap-4 p-4 rounded-lg border transition-all text-left",
|
||||
isSelected
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50 hover:bg-muted/50"
|
||||
: "border-border hover:border-primary/50 hover:bg-muted/50",
|
||||
)}
|
||||
>
|
||||
{/* Selection Indicator */}
|
||||
@@ -1049,7 +1064,7 @@ export function PoolSelector({
|
||||
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full border",
|
||||
isSelected
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-muted-foreground"
|
||||
: "border-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3" />}
|
||||
@@ -1059,7 +1074,7 @@ export function PoolSelector({
|
||||
<div
|
||||
className={cn(
|
||||
"p-2 rounded-lg",
|
||||
isSelected ? "bg-primary/10" : "bg-muted"
|
||||
isSelected ? "bg-primary/10" : "bg-muted",
|
||||
)}
|
||||
>
|
||||
{Icon}
|
||||
@@ -1073,7 +1088,9 @@ export function PoolSelector({
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-muted-foreground">
|
||||
<span>{formatBigInt(pool.lastPrice)} HANZO/unit/hr</span>
|
||||
<span>|</span>
|
||||
<span>{formatBigInt(pool.resourceAmount)} units available</span>
|
||||
<span>
|
||||
{formatBigInt(pool.resourceAmount)} units available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1140,7 +1157,7 @@ export function ResourceEstimator({
|
||||
},
|
||||
{
|
||||
enabled: !!pool,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const updateResource = (key: keyof ResourceRequirements, value: number) => {
|
||||
@@ -1262,7 +1279,7 @@ function calculateTotalResources(resources: ResourceRequirements): bigint {
|
||||
resources.cpu * 10 +
|
||||
Math.floor(resources.memory / 1024) * 5 +
|
||||
Math.floor(resources.storage / 10) +
|
||||
(resources.gpu || 0) * 100
|
||||
(resources.gpu || 0) * 100,
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1274,7 +1291,10 @@ function calculateTotalResources(resources: ResourceRequirements): bigint {
|
||||
```typescript
|
||||
// /lib/pools/format.ts
|
||||
|
||||
export function formatBigInt(value: string | bigint, decimals: number = 2): string {
|
||||
export function formatBigInt(
|
||||
value: string | bigint,
|
||||
decimals: number = 2,
|
||||
): string {
|
||||
const bn = typeof value === "string" ? BigInt(value) : value;
|
||||
const divisor = BigInt(10 ** 18);
|
||||
const whole = bn / divisor;
|
||||
@@ -1306,10 +1326,12 @@ export function formatUSD(value: bigint): string {
|
||||
}).format(usd);
|
||||
}
|
||||
|
||||
|
||||
// /lib/pools/calculations.ts
|
||||
|
||||
export function calculateSlippage(amount: string | bigint, slippagePercent: number): bigint {
|
||||
export function calculateSlippage(
|
||||
amount: string | bigint,
|
||||
slippagePercent: number,
|
||||
): bigint {
|
||||
const bn = typeof amount === "string" ? BigInt(amount) : amount;
|
||||
const slippageBps = BigInt(Math.floor(slippagePercent * 100));
|
||||
return bn - (bn * slippageBps) / 10000n;
|
||||
@@ -1318,7 +1340,7 @@ export function calculateSlippage(amount: string | bigint, slippagePercent: numb
|
||||
export function calculatePriceImpact(
|
||||
amountIn: bigint,
|
||||
amountOut: bigint,
|
||||
spotPrice: bigint
|
||||
spotPrice: bigint,
|
||||
): number {
|
||||
if (spotPrice === 0n || amountIn === 0n) return 0;
|
||||
|
||||
@@ -1330,7 +1352,7 @@ export function calculatePriceImpact(
|
||||
}
|
||||
|
||||
export function calculateImpermanentLoss(
|
||||
priceRatio: number // currentPrice / entryPrice
|
||||
priceRatio: number, // currentPrice / entryPrice
|
||||
): number {
|
||||
// IL = 2 * sqrt(priceRatio) / (1 + priceRatio) - 1
|
||||
const sqrtRatio = Math.sqrt(priceRatio);
|
||||
@@ -1340,7 +1362,7 @@ export function calculateImpermanentLoss(
|
||||
export function calculateAPR(
|
||||
volume24h: bigint,
|
||||
liquidity: bigint,
|
||||
feeRate: number = 0.003
|
||||
feeRate: number = 0.003,
|
||||
): number {
|
||||
if (liquidity === 0n) return 0;
|
||||
|
||||
@@ -1349,7 +1371,6 @@ export function calculateAPR(
|
||||
return dailyReturn * 365 * 100; // Annual percentage
|
||||
}
|
||||
|
||||
|
||||
// /lib/pools/types.ts
|
||||
|
||||
export enum ResourceType {
|
||||
@@ -1428,6 +1449,7 @@ This implementation reference provides:
|
||||
8. **Utility functions** - Formatting and calculations
|
||||
|
||||
All components follow the existing platform patterns using:
|
||||
|
||||
- Radix UI primitives
|
||||
- Tailwind CSS styling
|
||||
- tRPC for API calls
|
||||
|
||||
@@ -31,12 +31,12 @@ This document outlines the design for the Pools UI and Deployment Flow for the H
|
||||
|
||||
### 1.2 Integration Points
|
||||
|
||||
| Component | Protocol | Port | Purpose |
|
||||
|-----------|----------|------|---------|
|
||||
| Platform API | HTTP/tRPC | 3000 | UI backend |
|
||||
| hanzo-node | HTTP/gRPC | 8080/50051 | Compute operations |
|
||||
| ComputeDEX | Ethereum JSON-RPC | 8545 | Smart contract interactions |
|
||||
| Redis/BullMQ | Redis | 6379 | Job queue |
|
||||
| Component | Protocol | Port | Purpose |
|
||||
| ------------ | ----------------- | ---------- | --------------------------- |
|
||||
| Platform API | HTTP/tRPC | 3000 | UI backend |
|
||||
| hanzo-node | HTTP/gRPC | 8080/50051 | Compute operations |
|
||||
| ComputeDEX | Ethereum JSON-RPC | 8545 | Smart contract interactions |
|
||||
| Redis/BullMQ | Redis | 6379 | Job queue |
|
||||
|
||||
---
|
||||
|
||||
@@ -61,37 +61,37 @@ enum ResourceType {
|
||||
interface Pool {
|
||||
id: string;
|
||||
resourceType: ResourceType;
|
||||
resourceAmount: bigint; // Total resource units in pool
|
||||
tokenAmount: bigint; // HANZO tokens in pool
|
||||
totalLiquidity: bigint; // LP token supply
|
||||
feeRate: number; // Trading fee (e.g., 0.003 = 0.3%)
|
||||
utilization: number; // Current utilization percentage
|
||||
lastPrice: bigint; // Last traded price
|
||||
volume24h: bigint; // 24-hour trading volume
|
||||
resourceAmount: bigint; // Total resource units in pool
|
||||
tokenAmount: bigint; // HANZO tokens in pool
|
||||
totalLiquidity: bigint; // LP token supply
|
||||
feeRate: number; // Trading fee (e.g., 0.003 = 0.3%)
|
||||
utilization: number; // Current utilization percentage
|
||||
lastPrice: bigint; // Last traded price
|
||||
volume24h: bigint; // 24-hour trading volume
|
||||
}
|
||||
|
||||
// User's liquidity position
|
||||
interface LiquidityPosition {
|
||||
poolId: string;
|
||||
resourceType: ResourceType;
|
||||
liquidityShares: bigint; // LP tokens held
|
||||
resourceAmount: bigint; // Underlying resource
|
||||
tokenAmount: bigint; // Underlying HANZO tokens
|
||||
pendingRewards: bigint; // Unclaimed rewards
|
||||
entryPrice: bigint; // Price at entry
|
||||
impermanentLoss: number; // Current IL percentage
|
||||
liquidityShares: bigint; // LP tokens held
|
||||
resourceAmount: bigint; // Underlying resource
|
||||
tokenAmount: bigint; // Underlying HANZO tokens
|
||||
pendingRewards: bigint; // Unclaimed rewards
|
||||
entryPrice: bigint; // Price at entry
|
||||
impermanentLoss: number; // Current IL percentage
|
||||
}
|
||||
|
||||
// Order book entry
|
||||
interface OrderBookEntry {
|
||||
orderId: string;
|
||||
provider: string; // Provider address
|
||||
provider: string; // Provider address
|
||||
resourceType: ResourceType;
|
||||
amount: bigint;
|
||||
pricePerUnit: bigint;
|
||||
duration: number; // Seconds
|
||||
slaScore: number; // 0-100
|
||||
attestation?: string; // TEE attestation hash
|
||||
duration: number; // Seconds
|
||||
slaScore: number; // 0-100
|
||||
attestation?: string; // TEE attestation hash
|
||||
createdAt: Date;
|
||||
expiresAt: Date;
|
||||
}
|
||||
@@ -101,20 +101,20 @@ interface OrderBookEntry {
|
||||
|
||||
```typescript
|
||||
interface DeploymentTarget {
|
||||
type: 'local' | 'cloud' | 'pool';
|
||||
poolId?: string; // If type === 'pool'
|
||||
region?: string; // If type === 'cloud'
|
||||
serverId?: string; // If type === 'local'
|
||||
type: "local" | "cloud" | "pool";
|
||||
poolId?: string; // If type === 'pool'
|
||||
region?: string; // If type === 'cloud'
|
||||
serverId?: string; // If type === 'local'
|
||||
}
|
||||
|
||||
interface DeploymentConfig {
|
||||
applicationId: string;
|
||||
target: DeploymentTarget;
|
||||
resources: {
|
||||
cpu: number; // CPU units requested
|
||||
memory: number; // MB
|
||||
storage: number; // GB
|
||||
gpu?: number; // GPU units (optional)
|
||||
cpu: number; // CPU units requested
|
||||
memory: number; // MB
|
||||
storage: number; // GB
|
||||
gpu?: number; // GPU units (optional)
|
||||
};
|
||||
scaling: {
|
||||
minReplicas: number;
|
||||
@@ -380,53 +380,63 @@ export const poolsRouter = createTRPCRouter({
|
||||
|
||||
// Get pool price history
|
||||
getPriceHistory: protectedProcedure
|
||||
.input(z.object({
|
||||
poolId: z.string(),
|
||||
timeframe: z.enum(["1H", "24H", "7D", "30D", "ALL"]),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
poolId: z.string(),
|
||||
timeframe: z.enum(["1H", "24H", "7D", "30D", "ALL"]),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await ctx.hanzoNode.pools.getPriceHistory(input);
|
||||
}),
|
||||
|
||||
// Get order book depth
|
||||
getOrderBook: protectedProcedure
|
||||
.input(z.object({
|
||||
resourceType: z.number(),
|
||||
levels: z.number().default(10),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
resourceType: z.number(),
|
||||
levels: z.number().default(10),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await ctx.hanzoNode.pools.getOrderBookDepth(input);
|
||||
}),
|
||||
|
||||
// Swap resources
|
||||
swap: protectedProcedure
|
||||
.input(z.object({
|
||||
resourceType: z.number(),
|
||||
isBuying: z.boolean(),
|
||||
amountIn: z.string(), // BigInt as string
|
||||
minAmountOut: z.string(),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
resourceType: z.number(),
|
||||
isBuying: z.boolean(),
|
||||
amountIn: z.string(), // BigInt as string
|
||||
minAmountOut: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await ctx.hanzoNode.pools.swap(input);
|
||||
}),
|
||||
|
||||
// Add liquidity
|
||||
addLiquidity: protectedProcedure
|
||||
.input(z.object({
|
||||
resourceType: z.number(),
|
||||
resourceAmount: z.string(),
|
||||
tokenAmount: z.string(),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
resourceType: z.number(),
|
||||
resourceAmount: z.string(),
|
||||
tokenAmount: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await ctx.hanzoNode.pools.addLiquidity(input);
|
||||
}),
|
||||
|
||||
// Remove liquidity
|
||||
removeLiquidity: protectedProcedure
|
||||
.input(z.object({
|
||||
resourceType: z.number(),
|
||||
liquidityAmount: z.string(),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
resourceType: z.number(),
|
||||
liquidityAmount: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await ctx.hanzoNode.pools.removeLiquidity(input);
|
||||
}),
|
||||
@@ -445,11 +455,13 @@ export const poolsRouter = createTRPCRouter({
|
||||
|
||||
// Get best execution price
|
||||
getBestPrice: protectedProcedure
|
||||
.input(z.object({
|
||||
resourceType: z.number(),
|
||||
isBuying: z.boolean(),
|
||||
amount: z.string(),
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
resourceType: z.number(),
|
||||
isBuying: z.boolean(),
|
||||
amount: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await ctx.hanzoNode.pools.getBestPrice(input);
|
||||
}),
|
||||
@@ -609,23 +621,42 @@ User selects "Deploy"
|
||||
```typescript
|
||||
enum PoolDeploymentState {
|
||||
CREATED = "created",
|
||||
MATCHING = "matching", // Finding providers
|
||||
MATCHED = "matched", // Providers found
|
||||
MATCHING = "matching", // Finding providers
|
||||
MATCHED = "matched", // Providers found
|
||||
PROVISIONING = "provisioning", // Setting up resources
|
||||
RUNNING = "running", // Workload active
|
||||
SCALING = "scaling", // Auto-scaling in progress
|
||||
STOPPING = "stopping", // Graceful shutdown
|
||||
STOPPED = "stopped", // Deployment stopped
|
||||
FAILED = "failed", // Deployment failed
|
||||
RUNNING = "running", // Workload active
|
||||
SCALING = "scaling", // Auto-scaling in progress
|
||||
STOPPING = "stopping", // Graceful shutdown
|
||||
STOPPED = "stopped", // Deployment stopped
|
||||
FAILED = "failed", // Deployment failed
|
||||
}
|
||||
|
||||
const transitions: Record<PoolDeploymentState, PoolDeploymentState[]> = {
|
||||
[PoolDeploymentState.CREATED]: [PoolDeploymentState.MATCHING, PoolDeploymentState.FAILED],
|
||||
[PoolDeploymentState.MATCHING]: [PoolDeploymentState.MATCHED, PoolDeploymentState.FAILED],
|
||||
[PoolDeploymentState.MATCHED]: [PoolDeploymentState.PROVISIONING, PoolDeploymentState.FAILED],
|
||||
[PoolDeploymentState.PROVISIONING]: [PoolDeploymentState.RUNNING, PoolDeploymentState.FAILED],
|
||||
[PoolDeploymentState.RUNNING]: [PoolDeploymentState.SCALING, PoolDeploymentState.STOPPING, PoolDeploymentState.FAILED],
|
||||
[PoolDeploymentState.SCALING]: [PoolDeploymentState.RUNNING, PoolDeploymentState.FAILED],
|
||||
[PoolDeploymentState.CREATED]: [
|
||||
PoolDeploymentState.MATCHING,
|
||||
PoolDeploymentState.FAILED,
|
||||
],
|
||||
[PoolDeploymentState.MATCHING]: [
|
||||
PoolDeploymentState.MATCHED,
|
||||
PoolDeploymentState.FAILED,
|
||||
],
|
||||
[PoolDeploymentState.MATCHED]: [
|
||||
PoolDeploymentState.PROVISIONING,
|
||||
PoolDeploymentState.FAILED,
|
||||
],
|
||||
[PoolDeploymentState.PROVISIONING]: [
|
||||
PoolDeploymentState.RUNNING,
|
||||
PoolDeploymentState.FAILED,
|
||||
],
|
||||
[PoolDeploymentState.RUNNING]: [
|
||||
PoolDeploymentState.SCALING,
|
||||
PoolDeploymentState.STOPPING,
|
||||
PoolDeploymentState.FAILED,
|
||||
],
|
||||
[PoolDeploymentState.SCALING]: [
|
||||
PoolDeploymentState.RUNNING,
|
||||
PoolDeploymentState.FAILED,
|
||||
],
|
||||
[PoolDeploymentState.STOPPING]: [PoolDeploymentState.STOPPED],
|
||||
[PoolDeploymentState.STOPPED]: [],
|
||||
[PoolDeploymentState.FAILED]: [],
|
||||
@@ -688,7 +719,7 @@ export class HanzoNodeClient {
|
||||
constructor(config: HanzoNodeConfig) {
|
||||
this.poolsClient = new PoolsServiceClient(
|
||||
config.grpcEndpoint,
|
||||
credentials.createSsl()
|
||||
credentials.createSsl(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -723,7 +754,9 @@ export class HanzoNodeClient {
|
||||
return {
|
||||
async next() {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on("data", (update) => resolve({ value: update, done: false }));
|
||||
stream.on("data", (update) =>
|
||||
resolve({ value: update, done: false }),
|
||||
);
|
||||
stream.on("end", () => resolve({ value: undefined, done: true }));
|
||||
stream.on("error", reject);
|
||||
});
|
||||
@@ -841,7 +874,7 @@ export function handlePoolsWebSocket(wss: WebSocketServer) {
|
||||
// Transaction validation before submission
|
||||
async function validatePoolTransaction(
|
||||
tx: PoolTransaction,
|
||||
user: User
|
||||
user: User,
|
||||
): Promise<ValidationResult> {
|
||||
// 1. Verify user balance
|
||||
const balance = await getTokenBalance(user.walletAddress);
|
||||
@@ -853,7 +886,7 @@ async function validatePoolTransaction(
|
||||
const currentPrice = await getCurrentPrice(tx.poolId);
|
||||
const maxSlippage = 0.05; // 5%
|
||||
const priceDeviation = Math.abs(
|
||||
(Number(tx.expectedPrice) - Number(currentPrice)) / Number(currentPrice)
|
||||
(Number(tx.expectedPrice) - Number(currentPrice)) / Number(currentPrice),
|
||||
);
|
||||
if (priceDeviation > maxSlippage) {
|
||||
return { valid: false, error: "Price moved too much" };
|
||||
@@ -862,7 +895,11 @@ async function validatePoolTransaction(
|
||||
// 3. Verify allowance
|
||||
const allowance = await getTokenAllowance(user.walletAddress, POOL_CONTRACT);
|
||||
if (BigInt(allowance) < BigInt(tx.tokenAmount)) {
|
||||
return { valid: false, error: "Insufficient allowance", needsApproval: true };
|
||||
return {
|
||||
valid: false,
|
||||
error: "Insufficient allowance",
|
||||
needsApproval: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
@@ -928,9 +965,9 @@ describe("Pool Swap", () => {
|
||||
|
||||
it("rejects swap with excessive slippage", async () => {
|
||||
const pool = mockPool({ resourceAmount: "100", tokenAmount: "100000" });
|
||||
await expect(
|
||||
validateSwap(pool, "50000", "40", true)
|
||||
).rejects.toThrow("Price impact too high");
|
||||
await expect(validateSwap(pool, "50000", "40", true)).rejects.toThrow(
|
||||
"Price impact too high",
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -956,7 +993,9 @@ describe("Pool Deployment", () => {
|
||||
|
||||
// Wait for matching
|
||||
await waitFor(() => {
|
||||
const status = api.deployment.getPoolDeploymentStatus.query({ orderId: result.orderId });
|
||||
const status = api.deployment.getPoolDeploymentStatus.query({
|
||||
orderId: result.orderId,
|
||||
});
|
||||
return status.state === "matched";
|
||||
});
|
||||
});
|
||||
@@ -969,14 +1008,14 @@ describe("Pool Deployment", () => {
|
||||
|
||||
### 11.1 Key Metrics
|
||||
|
||||
| Metric | Description | Alert Threshold |
|
||||
|--------|-------------|-----------------|
|
||||
| pool_liquidity_total | Total liquidity across all pools | < $100K |
|
||||
| pool_utilization | Resource utilization per pool | > 90% |
|
||||
| swap_volume_24h | 24-hour trading volume | N/A |
|
||||
| deployment_success_rate | Pool deployment success rate | < 95% |
|
||||
| provider_sla_score | Average provider SLA score | < 97% |
|
||||
| transaction_latency_p99 | 99th percentile tx latency | > 5s |
|
||||
| Metric | Description | Alert Threshold |
|
||||
| ----------------------- | -------------------------------- | --------------- |
|
||||
| pool_liquidity_total | Total liquidity across all pools | < $100K |
|
||||
| pool_utilization | Resource utilization per pool | > 90% |
|
||||
| swap_volume_24h | 24-hour trading volume | N/A |
|
||||
| deployment_success_rate | Pool deployment success rate | < 95% |
|
||||
| provider_sla_score | Average provider SLA score | < 97% |
|
||||
| transaction_latency_p99 | 99th percentile tx latency | > 5s |
|
||||
|
||||
### 11.2 Grafana Dashboard
|
||||
|
||||
|
||||
+81
-66
@@ -270,6 +270,7 @@ code-rs/core/
|
||||
```
|
||||
|
||||
**Key Capabilities:**
|
||||
|
||||
- Multi-model conversation management
|
||||
- Streaming response handling
|
||||
- Token usage tracking and rate limiting
|
||||
@@ -293,6 +294,7 @@ code-rs/tui/
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Real-time streaming with syntax highlighting
|
||||
- Fuzzy file search with `@` mention
|
||||
- Image paste support (Ctrl+V / Cmd+V)
|
||||
@@ -378,11 +380,11 @@ flowchart TB
|
||||
|
||||
**Sandbox Policies:**
|
||||
|
||||
| Policy | Description | Use Case |
|
||||
|--------|-------------|----------|
|
||||
| `read-only` | No file modifications | Code review, analysis |
|
||||
| `workspace-write` | Write only to CWD + TMPDIR | Safe development |
|
||||
| `danger-full-access` | No restrictions | Trusted automation |
|
||||
| Policy | Description | Use Case |
|
||||
| -------------------- | -------------------------- | --------------------- |
|
||||
| `read-only` | No file modifications | Code review, analysis |
|
||||
| `workspace-write` | Write only to CWD + TMPDIR | Safe development |
|
||||
| `danger-full-access` | No restrictions | Trusted automation |
|
||||
|
||||
### 5. Auto Drive (`code-auto-drive-core`)
|
||||
|
||||
@@ -540,16 +542,16 @@ agent-instructions = "Write minimal, focused changes with clear rationale."
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `CODE_HOME` | Config directory override | `~/.code` |
|
||||
| `OPENAI_API_KEY` | OpenAI API key | - |
|
||||
| `OPENAI_BASE_URL` | Custom OpenAI-compatible endpoint | - |
|
||||
| `OPENAI_WIRE_API` | Force `chat` or `responses` API | auto |
|
||||
| `ANTHROPIC_API_KEY` | Anthropic API key | - |
|
||||
| `GOOGLE_API_KEY` | Google AI API key | - |
|
||||
| `DASHSCOPE_API_KEY` | Alibaba DashScope API key | - |
|
||||
| `CODE_ENABLE_CLOUD_AGENT_MODEL` | Enable cloud agent variants | false |
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------- | --------------------------------- | --------- |
|
||||
| `CODE_HOME` | Config directory override | `~/.code` |
|
||||
| `OPENAI_API_KEY` | OpenAI API key | - |
|
||||
| `OPENAI_BASE_URL` | Custom OpenAI-compatible endpoint | - |
|
||||
| `OPENAI_WIRE_API` | Force `chat` or `responses` API | auto |
|
||||
| `ANTHROPIC_API_KEY` | Anthropic API key | - |
|
||||
| `GOOGLE_API_KEY` | Google AI API key | - |
|
||||
| `DASHSCOPE_API_KEY` | Alibaba DashScope API key | - |
|
||||
| `CODE_ENABLE_CLOUD_AGENT_MODEL` | Enable cloud agent variants | false |
|
||||
|
||||
---
|
||||
|
||||
@@ -557,67 +559,72 @@ agent-instructions = "Write minimal, focused changes with clear rationale."
|
||||
|
||||
### Protocol Operations (Op)
|
||||
|
||||
| Operation | Description | Response Event |
|
||||
|-----------|-------------|----------------|
|
||||
| `Interrupt` | Abort current task | `TurnAborted` |
|
||||
| `UserInput` | Send user message | `AgentMessage*` |
|
||||
| `UserTurn` | Full turn with context | `AgentMessage*` |
|
||||
| `ExecApproval` | Approve/deny command | `ExecCommandEnd` |
|
||||
| `PatchApproval` | Approve/deny patch | `PatchApplyEnd` |
|
||||
| `ListMcpTools` | List available MCP tools | `McpListToolsResponse` |
|
||||
| `ListSkills` | List available skills | `ListSkillsResponse` |
|
||||
| `Compact` | Summarize conversation | `AgentMessage` |
|
||||
| `Review` | Request code review | `ReviewOutput` |
|
||||
| `Shutdown` | Graceful shutdown | `ShutdownComplete` |
|
||||
| Operation | Description | Response Event |
|
||||
| --------------- | ------------------------ | ---------------------- |
|
||||
| `Interrupt` | Abort current task | `TurnAborted` |
|
||||
| `UserInput` | Send user message | `AgentMessage*` |
|
||||
| `UserTurn` | Full turn with context | `AgentMessage*` |
|
||||
| `ExecApproval` | Approve/deny command | `ExecCommandEnd` |
|
||||
| `PatchApproval` | Approve/deny patch | `PatchApplyEnd` |
|
||||
| `ListMcpTools` | List available MCP tools | `McpListToolsResponse` |
|
||||
| `ListSkills` | List available skills | `ListSkillsResponse` |
|
||||
| `Compact` | Summarize conversation | `AgentMessage` |
|
||||
| `Review` | Request code review | `ReviewOutput` |
|
||||
| `Shutdown` | Graceful shutdown | `ShutdownComplete` |
|
||||
|
||||
### Event Types (EventMsg)
|
||||
|
||||
**Lifecycle Events:**
|
||||
|
||||
- `TaskStarted` - Agent began processing
|
||||
- `TaskComplete` - Agent finished all actions
|
||||
- `SessionConfigured` - Session initialized
|
||||
- `ShutdownComplete` - Clean shutdown
|
||||
|
||||
**Message Events:**
|
||||
|
||||
- `AgentMessage` - Complete agent response
|
||||
- `AgentMessageDelta` - Streaming response chunk
|
||||
- `AgentReasoning` - Reasoning output
|
||||
- `UserMessage` - Echo of user input
|
||||
|
||||
**Execution Events:**
|
||||
|
||||
- `ExecCommandBegin` - Command starting
|
||||
- `ExecCommandOutputDelta` - Streaming command output
|
||||
- `ExecCommandEnd` - Command completed
|
||||
- `ExecApprovalRequest` - Approval needed
|
||||
|
||||
**Patch Events:**
|
||||
|
||||
- `PatchApplyBegin` - Patch starting
|
||||
- `PatchApplyEnd` - Patch completed
|
||||
- `ApplyPatchApprovalRequest` - Patch approval needed
|
||||
|
||||
**MCP Events:**
|
||||
|
||||
- `McpToolCallBegin` - Tool call starting
|
||||
- `McpToolCallEnd` - Tool call completed
|
||||
- `McpListToolsResponse` - Available tools list
|
||||
|
||||
### Slash Commands
|
||||
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `/new` | Start new conversation | `/new` |
|
||||
| `/resume` | Resume past session | `/resume` |
|
||||
| `/auto [goal]` | Start Auto Drive | `/auto refactor auth` |
|
||||
| `/plan <task>` | Multi-agent planning | `/plan new feature` |
|
||||
| `/solve <problem>` | Multi-agent problem solving | `/solve this bug` |
|
||||
| `/code <task>` | Multi-agent coding | `/code add tests` |
|
||||
| `/review [focus]` | Code review | `/review security` |
|
||||
| `/diff` | Show git diff | `/diff` |
|
||||
| `/undo` | Restore previous state | `/undo` |
|
||||
| `/settings` | Open settings panel | `/settings model` |
|
||||
| `/model` | Change model | `/model` |
|
||||
| `/reasoning` | Change reasoning level | `/reasoning high` |
|
||||
| `/theme` | Change theme | `/theme` |
|
||||
| `/mcp` | Manage MCP servers | `/mcp status` |
|
||||
| Command | Description | Example |
|
||||
| ------------------ | --------------------------- | --------------------- |
|
||||
| `/new` | Start new conversation | `/new` |
|
||||
| `/resume` | Resume past session | `/resume` |
|
||||
| `/auto [goal]` | Start Auto Drive | `/auto refactor auth` |
|
||||
| `/plan <task>` | Multi-agent planning | `/plan new feature` |
|
||||
| `/solve <problem>` | Multi-agent problem solving | `/solve this bug` |
|
||||
| `/code <task>` | Multi-agent coding | `/code add tests` |
|
||||
| `/review [focus]` | Code review | `/review security` |
|
||||
| `/diff` | Show git diff | `/diff` |
|
||||
| `/undo` | Restore previous state | `/undo` |
|
||||
| `/settings` | Open settings panel | `/settings model` |
|
||||
| `/model` | Change model | `/model` |
|
||||
| `/reasoning` | Change reasoning level | `/reasoning high` |
|
||||
| `/theme` | Change theme | `/theme` |
|
||||
| `/mcp` | Manage MCP servers | `/mcp status` |
|
||||
|
||||
---
|
||||
|
||||
@@ -680,6 +687,7 @@ hanzo-dev --help
|
||||
```
|
||||
|
||||
**Platform packages:**
|
||||
|
||||
- `hanzo-node-darwin-arm64` - macOS Apple Silicon
|
||||
- `hanzo-node-darwin-x64` - macOS Intel
|
||||
- `hanzo-node-linux-x64-musl` - Linux x64
|
||||
@@ -784,23 +792,25 @@ flowchart TB
|
||||
### Performance Optimization
|
||||
|
||||
**Token Management:**
|
||||
|
||||
- Automatic context compaction when approaching limits
|
||||
- Cached input token tracking
|
||||
- Reasoning token separation
|
||||
|
||||
**Streaming:**
|
||||
|
||||
- Real-time delta streaming for responsive UI
|
||||
- Buffered command output
|
||||
- Async event processing
|
||||
|
||||
**Build Profiles:**
|
||||
|
||||
| Profile | Use Case | Optimization |
|
||||
|---------|----------|--------------|
|
||||
| `dev` | Development | Incremental |
|
||||
| `dev-fast` | Fast iteration | opt-level=1 |
|
||||
| `perf` | Profiling | debug symbols |
|
||||
| `release` | Production | LTO + strip |
|
||||
| Profile | Use Case | Optimization |
|
||||
| ---------- | -------------- | ------------- |
|
||||
| `dev` | Development | Incremental |
|
||||
| `dev-fast` | Fast iteration | opt-level=1 |
|
||||
| `perf` | Profiling | debug symbols |
|
||||
| `release` | Production | LTO + strip |
|
||||
|
||||
### Extending with MCP Tools
|
||||
|
||||
@@ -809,24 +819,29 @@ Create custom MCP servers to extend Hanzo Dev:
|
||||
```typescript
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
|
||||
const server = new Server({
|
||||
name: "my-custom-tools",
|
||||
version: "1.0.0",
|
||||
}, {
|
||||
capabilities: { tools: {} }
|
||||
});
|
||||
const server = new Server(
|
||||
{
|
||||
name: "my-custom-tools",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: { tools: {} },
|
||||
},
|
||||
);
|
||||
|
||||
server.setRequestHandler("tools/list", async () => ({
|
||||
tools: [{
|
||||
name: "my_tool",
|
||||
description: "Does something useful",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
input: { type: "string" }
|
||||
}
|
||||
}
|
||||
}]
|
||||
tools: [
|
||||
{
|
||||
name: "my_tool",
|
||||
description: "Does something useful",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
input: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler("tools/call", async (request) => {
|
||||
@@ -888,4 +903,4 @@ Hanzo Dev is a community fork of the original Codex CLI, maintaining compatibili
|
||||
|
||||
---
|
||||
|
||||
*Built with love by [Hanzo AI](https://hanzo.ai) - Frontier AI for Developers*
|
||||
_Built with love by [Hanzo AI](https://hanzo.ai) - Frontier AI for Developers_
|
||||
|
||||
+2
-1
@@ -94,5 +94,6 @@ env = { "API_KEY" = "value" }
|
||||
```
|
||||
|
||||
## Using Code as an MCP Server
|
||||
|
||||
> [!TIP]
|
||||
> It is somewhat experimental, but the Code CLI can also be run as an MCP _server_ via `code mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector code mcp` and send it a `tools/list` request, you will see that there is only one tool, `code`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues.
|
||||
> It is somewhat experimental, but the Code CLI can also be run as an MCP _server_ via `code mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector code mcp` and send it a `tools/list` request, you will see that there is only one tool, `code`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Every Code can launch external CLI “agents” and orchestrate them in multi-agent “subagent” flows such as `/plan`, `/solve`, and `/code`.
|
||||
|
||||
## Agent configuration (`[[agents]]` in `config.toml`)
|
||||
|
||||
```toml
|
||||
[[agents]]
|
||||
name = "code-gpt-5.2-codex" # slug or alias shown in pickers
|
||||
@@ -16,14 +17,17 @@ enabled = true # hide from pickers when false
|
||||
description = "Frontline coding agent"
|
||||
instructions = "Preamble added to this agent’s prompt"
|
||||
```
|
||||
|
||||
Field recap: `name` (slug/alias), `command` (absolute paths ok), `args*` (RO/RW lists override base), `env`, `read_only`, `enabled`, optional `description` and `instructions`.
|
||||
|
||||
### Built-in defaults
|
||||
|
||||
If no `[[agents]]` are configured, Code advertises built-ins (gated by env `CODE_ENABLE_CLOUD_AGENT_MODEL` for cloud variants): `code-gpt-5.2`, `code-gpt-5.2-codex`, `claude-opus-4.5`, `gemini-3-pro`, `code-gpt-5.1-codex-mini`, `claude-sonnet-4.5`, `gemini-3-flash`, `claude-haiku-4.5`, `qwen-3-coder`, `cloud-gpt-5.1-codex-max`. Built-ins strip any user `--model/-m` flags to avoid conflicts and inject their own.
|
||||
|
||||
Tip: `gemini` resolves to `gemini-3-flash` (fast/cheap). Use `gemini-3-pro` when you want the higher-capacity Gemini option.
|
||||
|
||||
## Subagents (`[[subagents.commands]]`)
|
||||
|
||||
```toml
|
||||
[[subagents.commands]]
|
||||
name = "plan" # slash name (/plan, /solve, /code, or custom)
|
||||
@@ -32,6 +36,7 @@ agents = ["code-gpt-5.2-codex", "claude-opus-4.5"] # falls back to enabled agen
|
||||
orchestrator_instructions = "Guidance for Code before spawning agents"
|
||||
agent_instructions = "Preamble added to each spawned agent"
|
||||
```
|
||||
|
||||
- `name`: slash command created/overridden.
|
||||
- `read_only`: forces spawned agents to RO when true.
|
||||
- `agents`: explicit list; empty → enabled `[[agents]]`; none configured → built-in roster.
|
||||
@@ -41,37 +46,45 @@ agent_instructions = "Preamble added to each spawned agent"
|
||||
The orchestrator fans out agents, waits for results, and merges reasoning according to your `hide_agent_reasoning` / `show_raw_agent_reasoning` settings.
|
||||
|
||||
## TUI controls
|
||||
|
||||
- `/agents` opens the settings overlay to the Agents section: toggle enabled/read-only, view defaults, and open editors.
|
||||
- Agent editor: create or edit a single agent (enable/disable, read-only, instructions). Args/env come from `config.toml`.
|
||||
- Subagent editor: configure per-command agent lists, read-only flag, and instructions. Built-in `/plan` `/solve` `/code` can be overridden the same way.
|
||||
- Model pickers are modal and return to the invoking section after selection.
|
||||
|
||||
## Auto Drive interaction
|
||||
|
||||
- Auto Drive uses the `agents_enabled` toggle in its settings pane; when off, the coordinator skips agent batches.
|
||||
- If no git repo exists, Auto Drive instructs all agents to run read-only.
|
||||
- `AUTO_AGENTS.md` is read alongside `AGENTS.md` for Auto Drive–specific guidance.
|
||||
|
||||
## AGENTS.md and project memory
|
||||
|
||||
- Code loads AGENTS.md files along the path (global, repo root, cwd) up to 32 KiB total; deeper files override higher-level ones.
|
||||
- Contents become system/developer instructions on the first turn; direct user/developer prompts still take precedence.
|
||||
|
||||
## Windows discovery tips
|
||||
|
||||
- On Windows, include extensions in `command` (`.exe`, `.cmd`, `.bat`, `.com`).
|
||||
- NPM globals often live under `C:\\Users\\<you>\\AppData\\Roaming\\npm\\`.
|
||||
- If PATH is unreliable, use absolute `command` paths in `[[agents]]`.
|
||||
|
||||
## Notifications and reasoning visibility
|
||||
|
||||
- `hide_agent_reasoning = true` removes agent reasoning streams in both the TUI and `code exec`.
|
||||
- `show_raw_agent_reasoning = true` surfaces raw chains-of-thought when provided by the model.
|
||||
- Notification filtering is controlled via `/notifications` or `config.toml` `notify` / `tui.notifications`.
|
||||
|
||||
## Headless `code exec`
|
||||
|
||||
- `code exec --json` streams JSONL events (agent turns included).
|
||||
- `--output-schema <schema.json>` enforces structured JSON output; combine with `--output-last-message` to capture only the final payload.
|
||||
- `code exec` defaults to read-only; add `--full-auto` plus a writable sandbox to permit edits.
|
||||
|
||||
## Quick examples
|
||||
|
||||
- Custom agent:
|
||||
|
||||
```toml
|
||||
[[agents]]
|
||||
name = "my-coder"
|
||||
@@ -79,7 +92,9 @@ command = "/usr/local/bin/coder"
|
||||
args_write = ["-s", "workspace-write", "--dangerously-bypass-approvals-and-sandbox", "exec", "--skip-git-repo-check"]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
- Custom context sweep command:
|
||||
|
||||
```toml
|
||||
[[subagents.commands]]
|
||||
name = "context"
|
||||
|
||||
@@ -3,63 +3,77 @@
|
||||
What Auto Drive is, how to start it, and how it behaves in Every Code.
|
||||
|
||||
## Start points
|
||||
|
||||
- TUI: `/auto <goal>`. If you omit the goal and there is recent history, Code proposes one for you. `/auto settings` jumps straight to the Auto Drive pane.
|
||||
- CLI: `code exec --auto "<goal>"` or `code exec "/auto <goal>"`. A goal is required when launching headless.
|
||||
- Precondition: Full Auto mode (danger-full-access + approval=never) must be selected in the TUI; otherwise you’ll see a warning and Auto Drive will not start.
|
||||
|
||||
## Goal handling
|
||||
|
||||
- Images passed on the CLI are attached before the first turn.
|
||||
- If you supply no goal and there is nothing in history to derive from, Auto Drive will not start.
|
||||
|
||||
## How it runs
|
||||
|
||||
- Each turn Auto Drive drafts a plan, prepares commands, optionally assigns agents, and waits for your confirmation (or the countdown) before running.
|
||||
- The transcript is kept in memory and compacted automatically; you’ll see a notice if history was trimmed.
|
||||
- If an `AUTO_AGENTS.md` exists, its guidance is applied to the run alongside any AGENTS.md rules.
|
||||
|
||||
## Agents
|
||||
|
||||
- Auto Drive can spawn helper agents during a turn. Toggle this with `agents_enabled` in Auto Drive settings.
|
||||
- Outside a git repo, Auto Drive forces those agents to run read-only to avoid unintended writes.
|
||||
|
||||
## Observer
|
||||
|
||||
- A lightweight watchdog reviews the run every `auto_drive_observer_cadence` turns (default 5). If it spots trouble it surfaces guidance in the banner. Set the cadence to `0` to disable.
|
||||
|
||||
## Sandbox and approvals
|
||||
|
||||
- TUI: needs `danger-full-access` plus `approval_policy=never` so turns aren’t blocked by prompts.
|
||||
- CLI: `--auto` forces approvals off; add `--full-auto` (or `--dangerously-bypass-approvals-and-sandbox`) if you want Auto Drive to make edits and run commands.
|
||||
|
||||
## Continue and countdown modes
|
||||
|
||||
- `continue_mode`: `immediate`, `ten-seconds` (default), `sixty-seconds`, `manual`.
|
||||
- In countdown modes, the Auto Drive card shows a timer; Enter proceeds early, Esc reopens the draft, 0 auto-submits.
|
||||
- Manual mode pauses after each prepared prompt until you confirm.
|
||||
|
||||
## Stop and pause
|
||||
|
||||
- Press Esc while Auto Drive is active to pause or stop (context-dependent). Countdown modes show this hint in the footer.
|
||||
- Approval dialogs never capture Esc; it always reaches Auto Drive.
|
||||
|
||||
## Review, QA, diagnostics
|
||||
|
||||
- `review_enabled` (default true) can insert a review gate; the card shows “Awaiting review.”
|
||||
- `qa_automation_enabled` and `cross_check_enabled` (default true) allow diagnostics and cross-check turns before continuing.
|
||||
- `auto_resolve_review_attempts` limits how many times Auto Drive will auto-resolve review feedback (default 5).
|
||||
|
||||
## Models
|
||||
|
||||
- Defaults: model `gpt-5.1`, reasoning effort `high`.
|
||||
- Toggle “use chat model” in settings to reuse your current chat model/effort instead of the dedicated Auto Drive model.
|
||||
|
||||
## UI surfaces
|
||||
|
||||
- Auto Drive card shows status (Ready, Waiting, Thinking, Running, Awaiting review, Failed/Stopped), the goal, action log, token/time counters, countdown, and a celebration on success.
|
||||
- Bottom pane header mirrors status and shows hints (Ctrl+S settings, Esc stop, whether agents/diagnostics are on).
|
||||
|
||||
## Resume and persistence
|
||||
|
||||
- History is kept in memory; there’s no Auto Drive–specific history file. If trimmed, you’ll see a note.
|
||||
- You can resume a session as usual; Auto Drive can derive a goal from restored history.
|
||||
- CLI `--output-last-message` still works here if you only need the final reply.
|
||||
|
||||
## Settings (config.toml)
|
||||
|
||||
- Top-level keys: `auto_drive_use_chat_model` (default false), `auto_drive_observer_cadence` (default 5).
|
||||
- `[auto_drive]` defaults: `review_enabled=true`, `agents_enabled=true`, `qa_automation_enabled=true`, `cross_check_enabled=true`, `observer_enabled=true`, `coordinator_routing=true`, `continue_mode="ten-seconds"`, `model="gpt-5.1"`, `model_reasoning_effort="high"`, `auto_resolve_review_attempts=5`.
|
||||
- All of these can be changed from `/auto settings` in the TUI or directly in `config.toml`.
|
||||
|
||||
## Tips
|
||||
|
||||
- Stay in the TUI if you want countdowns and visual status; use `code exec --auto` for CI or scripted flows.
|
||||
- If Auto Drive stops because it couldn’t derive a goal, rerun `/auto <goal>` with a short, specific instruction.
|
||||
- Turn off agents in `/auto settings` if you want a single-model run.
|
||||
|
||||
@@ -5,31 +5,37 @@ The Hanzo Dev CLI now includes comprehensive GitHub Copilot integration, making
|
||||
## Features
|
||||
|
||||
### 🤖 Interactive Chat
|
||||
|
||||
- Real-time conversations with GitHub Copilot
|
||||
- Context-aware responses with file integration
|
||||
- Continuous session support
|
||||
|
||||
### 💡 Code Suggestions
|
||||
|
||||
- AI-powered code completions and suggestions
|
||||
- Language-aware context understanding
|
||||
- Multiple suggestion alternatives
|
||||
|
||||
### 🔍 Code Review & Analysis
|
||||
|
||||
- Automated code review and suggestions
|
||||
- Git diff analysis and feedback
|
||||
- Quality improvement recommendations
|
||||
|
||||
### 📚 Documentation Generation
|
||||
|
||||
- Auto-generate comments, README files, and API docs
|
||||
- Multiple documentation formats
|
||||
- Context-aware explanations
|
||||
|
||||
### 🐚 Shell Command Assistance
|
||||
|
||||
- Natural language to shell commands
|
||||
- OS-specific command suggestions
|
||||
- Command explanations and safety tips
|
||||
|
||||
### 🎯 Git Integration
|
||||
|
||||
- Auto-generate commit messages from diffs
|
||||
- Conventional commit format support
|
||||
- Change analysis and categorization
|
||||
@@ -196,6 +202,7 @@ dev copilot complete main.rs --line 42 --column 15 --context 20
|
||||
The Copilot CLI commands can be integrated into your favorite editor:
|
||||
|
||||
#### Vim/Neovim
|
||||
|
||||
```vim
|
||||
" Add to your .vimrc or init.vim
|
||||
command! CopilotChat !dev copilot chat
|
||||
@@ -204,18 +211,17 @@ command! CopilotExplain !dev copilot explain --file %
|
||||
```
|
||||
|
||||
#### VS Code
|
||||
|
||||
```json
|
||||
{
|
||||
"terminal.integrated.shellArgs.osx": [
|
||||
"-c",
|
||||
"alias cop='dev copilot'"
|
||||
]
|
||||
"terminal.integrated.shellArgs.osx": ["-c", "alias cop='dev copilot'"]
|
||||
}
|
||||
```
|
||||
|
||||
### Git Hooks
|
||||
|
||||
#### Pre-commit Hook
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# .git/hooks/pre-commit
|
||||
@@ -223,6 +229,7 @@ dev copilot review --diff $(git diff --cached --name-only)
|
||||
```
|
||||
|
||||
#### Commit Message Hook
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# .git/hooks/prepare-commit-msg
|
||||
@@ -234,6 +241,7 @@ fi
|
||||
### CI/CD Integration
|
||||
|
||||
#### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: AI Code Review
|
||||
on: [pull_request]
|
||||
@@ -285,6 +293,7 @@ context_window = 20000
|
||||
### Custom Workflows
|
||||
|
||||
#### Code Refactoring Workflow
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# refactor.sh - AI-assisted refactoring workflow
|
||||
@@ -310,6 +319,7 @@ dev copilot docs "$FILE" --type comment
|
||||
```
|
||||
|
||||
#### Documentation Generation Pipeline
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# docs-gen.sh - Comprehensive documentation generation
|
||||
@@ -327,21 +337,25 @@ find src/ -name "*.rs" -exec dev copilot docs {} --type comment \;
|
||||
## Tips and Best Practices
|
||||
|
||||
### 1. Effective Prompting
|
||||
|
||||
- **Be specific**: Instead of "help with code", say "optimize this sorting algorithm for large datasets"
|
||||
- **Provide context**: Include relevant file paths and project information
|
||||
- **Ask follow-up questions**: Build on previous responses for deeper insights
|
||||
|
||||
### 2. Code Review Integration
|
||||
|
||||
- Use `--diff` flag for reviewing changes before committing
|
||||
- Combine with your existing code review process
|
||||
- Focus on security, performance, and maintainability suggestions
|
||||
|
||||
### 3. Documentation Workflow
|
||||
|
||||
- Generate docs incrementally as you write code
|
||||
- Use different doc types for different audiences
|
||||
- Review and edit AI-generated documentation for accuracy
|
||||
|
||||
### 4. Shell Command Safety
|
||||
|
||||
- Always review suggested commands before execution
|
||||
- Use `--explain` flag to understand what commands do
|
||||
- Test in safe environments first
|
||||
@@ -351,6 +365,7 @@ find src/ -name "*.rs" -exec dev copilot docs {} --type comment \;
|
||||
### Common Issues
|
||||
|
||||
#### "Copilot is not available"
|
||||
|
||||
```bash
|
||||
# Check installation status
|
||||
dev copilot setup --check
|
||||
@@ -360,6 +375,7 @@ dev copilot setup --install --auth
|
||||
```
|
||||
|
||||
#### Authentication Issues
|
||||
|
||||
```bash
|
||||
# Re-authenticate with GitHub
|
||||
gh auth login
|
||||
@@ -369,11 +385,13 @@ gh auth refresh -s copilot
|
||||
```
|
||||
|
||||
#### Performance Issues
|
||||
|
||||
- Reduce context window size for faster responses
|
||||
- Use shorter prompts for simple questions
|
||||
- Check internet connectivity
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
# Enable verbose logging
|
||||
RUST_LOG=debug dev copilot chat "debug prompt"
|
||||
@@ -384,6 +402,7 @@ RUST_LOG=debug dev copilot chat "debug prompt"
|
||||
### Real-world Scenarios
|
||||
|
||||
#### 1. Bug Investigation
|
||||
|
||||
```bash
|
||||
# Get suggestions for fixing a bug
|
||||
dev copilot chat --file buggy_function.rs "This function sometimes returns None unexpectedly. What could be wrong?"
|
||||
@@ -393,6 +412,7 @@ dev copilot review buggy_function.rs --diff
|
||||
```
|
||||
|
||||
#### 2. Performance Optimization
|
||||
|
||||
```bash
|
||||
# Get optimization suggestions
|
||||
dev copilot suggest --file slow_algorithm.rs "optimize for performance"
|
||||
@@ -402,6 +422,7 @@ dev copilot explain --file slow_algorithm.rs --level expert
|
||||
```
|
||||
|
||||
#### 3. Testing Strategy
|
||||
|
||||
```bash
|
||||
# Generate test suggestions
|
||||
dev copilot chat "What tests should I write for this authentication module?" --file auth.rs
|
||||
@@ -411,6 +432,7 @@ dev copilot review tests/ --format markdown
|
||||
```
|
||||
|
||||
#### 4. Documentation Sprint
|
||||
|
||||
```bash
|
||||
# Document entire project
|
||||
find src/ -name "*.rs" | xargs -I {} dev copilot docs {} --type comment
|
||||
@@ -424,6 +446,7 @@ dev copilot docs . --type readme --output README.md
|
||||
The Copilot integration is part of the open-source Hanzo Dev project. Contributions are welcome!
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Build from source
|
||||
git clone https://github.com/hanzoai/dev
|
||||
@@ -435,6 +458,7 @@ cargo test copilot
|
||||
```
|
||||
|
||||
### Feature Requests
|
||||
|
||||
- File issues on the [Hanzo Dev repository](https://github.com/hanzoai/dev/issues)
|
||||
- Use the "enhancement" label for new feature requests
|
||||
- Provide use cases and examples
|
||||
@@ -446,7 +470,8 @@ This integration is part of Hanzo Dev and follows the same licensing terms as th
|
||||
---
|
||||
|
||||
**Need Help?**
|
||||
|
||||
- 📖 [Full Documentation](https://hanzo.ai/dev/docs)
|
||||
- 💬 [Community Discord](https://discord.gg/hanzo)
|
||||
- 🐛 [Report Issues](https://github.com/hanzoai/dev/issues)
|
||||
- ✨ [Feature Requests](https://github.com/hanzoai/dev/discussions)
|
||||
- ✨ [Feature Requests](https://github.com/hanzoai/dev/discussions)
|
||||
|
||||
@@ -47,11 +47,13 @@ Replace `YourUser` with your actual Windows username.
|
||||
**2. Find your npm global install location:**
|
||||
|
||||
Run this command to find where npm installs global packages:
|
||||
|
||||
```cmd
|
||||
npm config get prefix
|
||||
```
|
||||
|
||||
The executables will be in the returned directory. For example, if it returns `C:\Users\YourUser\AppData\Roaming\npm`, your agent commands will be at:
|
||||
|
||||
- `C:\Users\YourUser\AppData\Roaming\npm\claude.cmd`
|
||||
- `C:\Users\YourUser\AppData\Roaming\npm\gemini.cmd`
|
||||
- `C:\Users\YourUser\AppData\Roaming\npm\coder.cmd`
|
||||
@@ -59,16 +61,19 @@ The executables will be in the returned directory. For example, if it returns `C
|
||||
**3. Verify PATH includes npm directory:**
|
||||
|
||||
In PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PATH -split ';' | Select-String "npm"
|
||||
```
|
||||
|
||||
In Command Prompt:
|
||||
|
||||
```cmd
|
||||
echo %PATH% | findstr npm
|
||||
```
|
||||
|
||||
If npm's directory isn't in your PATH, you can either:
|
||||
|
||||
- Add it to your system PATH (requires restart)
|
||||
- Use absolute paths in your config (recommended)
|
||||
|
||||
|
||||
+4
-5
@@ -5,16 +5,16 @@ from the latest GitHub release artifacts. Publishing to Homebrew requires a
|
||||
tap repository (for example, `just-every/homebrew-tap`). Once a tap exists,
|
||||
you can generate and publish the formula like so:
|
||||
|
||||
1) Generate the formula for the latest version:
|
||||
1. Generate the formula for the latest version:
|
||||
|
||||
```
|
||||
scripts/generate-homebrew-formula.sh
|
||||
```
|
||||
|
||||
2) Copy the generated `Code.rb` into your tap repo under `Formula/Code.rb`
|
||||
and update the `url`/`sha256` if needed.
|
||||
2. Copy the generated `Code.rb` into your tap repo under `Formula/Code.rb`
|
||||
and update the `url`/`sha256` if needed.
|
||||
|
||||
3) Users can then install with:
|
||||
3. Users can then install with:
|
||||
|
||||
```
|
||||
brew tap just-every/tap
|
||||
@@ -27,4 +27,3 @@ Notes
|
||||
- `code-aarch64-apple-darwin.tar.gz`
|
||||
- `code-x86_64-apple-darwin.tar.gz`
|
||||
- The CLI is installed as `code` and `coder` shims for compatibility.
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
Use these docs to install, configure, and operate Every Code (Code).
|
||||
|
||||
## Start Here
|
||||
|
||||
- [Getting started](getting-started.md)
|
||||
- [Install](install.md) · [Homebrew](homebrew.md)
|
||||
- [Authentication](authentication.md)
|
||||
- [FAQ](faq.md)
|
||||
|
||||
## Using Code
|
||||
|
||||
- [Executing commands](exec.md)
|
||||
- [Execution policy](execpolicy.md)
|
||||
- [Slash commands](slash-commands.md)
|
||||
@@ -20,6 +22,7 @@ Use these docs to install, configure, and operate Every Code (Code).
|
||||
- [Zed integration](integration-zed.md)
|
||||
|
||||
## Configuration & Security
|
||||
|
||||
- [Configuration](config.md)
|
||||
- [Sandboxing overview](sandbox.md)
|
||||
- [Platform sandboxing](platform-sandboxing.md)
|
||||
|
||||
@@ -7,9 +7,9 @@ To point Zed at Every Code's (Code) ACP server, add this block to `settings.json
|
||||
"agent_servers": {
|
||||
"Code": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@just-every/code", "acp"]
|
||||
}
|
||||
}
|
||||
"args": ["-y", "@just-every/code", "acp"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -35,6 +35,7 @@ write_text_file = { mcp_server = "zed", tool_name = "writeTextFile" }
|
||||
```
|
||||
|
||||
Zed wires these tools automatically when you add the Code (Every Code) agent, so the identifiers above match the defaults.
|
||||
|
||||
- The CLI entry point (`npx @just-every/code acp`) is a thin wrapper over the Rust binary (`cargo run -p code-mcp-server -- --stdio`) that ships alongside the rest of Every Code. Build-from-source workflows plug in by swapping `command` for an absolute path to that binary.
|
||||
|
||||
## Tips and troubleshooting
|
||||
|
||||
@@ -3,24 +3,29 @@
|
||||
The mechanism Code uses to enforce sandboxing varies by OS.
|
||||
|
||||
## macOS 12+
|
||||
|
||||
- Uses Apple Seatbelt via `sandbox-exec` with a profile that matches the selected `--sandbox` mode.
|
||||
|
||||
## Linux
|
||||
|
||||
- Uses Landlock plus seccomp to apply the configured sandbox policy.
|
||||
- In containerized environments (e.g., Docker) the host must support these APIs. If it does not, configure the container to provide the isolation you need and run Code with `--sandbox danger-full-access` (or `--dangerously-bypass-approvals-and-sandbox`) inside that container instead.
|
||||
|
||||
## Windows
|
||||
|
||||
Code launches commands with a restricted Windows token and an allowlist tied to declared workspace roots. Writes are blocked outside those roots (and `%TEMP%` when workspace-write is requested); common escape vectors like alternate data streams, UNC paths, and device handles are proactively denied. The CLI also inserts stub executables (for example, wrapping `ssh`) ahead of the host `PATH` to intercept risky tools before they escape the sandbox.
|
||||
|
||||
### Known limitations (smoketests)
|
||||
|
||||
Running `python windows-sandbox-rs/sandbox_smoketests.py` with full filesystem and network access currently passes **37/41** cases. The remaining high-value gaps are:
|
||||
|
||||
| Test | Purpose |
|
||||
| --- | --- |
|
||||
| ADS write denied (#32) | Alternate data streams can still be written inside the workspace (should be blocked). |
|
||||
| Protected path case-variation denied (#33) | `.GiT` bypasses protections meant for `.git`. Case variants should be rejected. |
|
||||
| PATH stub bypass denied (#35) | A workspace `ssh.bat` shim placed first on `PATH` is not reliably executed, so interception cannot be proven. |
|
||||
| Start-Process https denied (#41) | `Start-Process 'https://…'` succeeds in read-only runs because Explorer handles the ShellExecute outside the sandbox. |
|
||||
| Test | Purpose |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| ADS write denied (#32) | Alternate data streams can still be written inside the workspace (should be blocked). |
|
||||
| Protected path case-variation denied (#33) | `.GiT` bypasses protections meant for `.git`. Case variants should be rejected. |
|
||||
| PATH stub bypass denied (#35) | A workspace `ssh.bat` shim placed first on `PATH` is not reliably executed, so interception cannot be proven. |
|
||||
| Start-Process https denied (#41) | `Start-Process 'https://…'` succeeds in read-only runs because Explorer handles the ShellExecute outside the sandbox. |
|
||||
|
||||
### How to help
|
||||
|
||||
If you can iterate on Windows sandboxing, aim to close the four smoketest failures above and rerun `python windows-sandbox-rs/sandbox_smoketests.py` until all **41/41** pass.
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
Full-screen settings panel for Every Code’s TUI. Use it to change models, themes, Auto Drive defaults, agents, notifications, and more without leaving the chat.
|
||||
|
||||
## Open & navigate
|
||||
|
||||
- `/settings` opens the overview; `/settings <section>` jumps directly (section names below). `/auto settings` and `/update` route into their sections.
|
||||
- Keys: `↑/↓` or `j/k` to move, `Tab/Shift+Tab` to cycle sections, `Home/End` jump list ends. Enter opens/activates; Esc closes the current section, Esc again closes the overlay. `?` toggles inline help. Paste is forwarded to the active section when allowed.
|
||||
- Overlay is modal: chat input is blocked while it is visible. It remembers the last active section on reopen (`pending_settings_return`).
|
||||
|
||||
## Persistence
|
||||
|
||||
- Changes write to `CODE_HOME/config.toml` when available; if that directory is missing you’ll see a warning and the changes remain session-only.
|
||||
- Access mode can be stored per workspace; other settings apply globally unless your config file overrides them per project.
|
||||
- Agent and MCP edits also live in the same config directory.
|
||||
|
||||
## Sections
|
||||
|
||||
- **Model**: pick the default chat model and reasoning effort.
|
||||
- **Theme**: choose a theme and spinner; applies immediately.
|
||||
- **Updates**: view upgrade channel/status. `/update` opens here before running installers.
|
||||
@@ -28,16 +31,19 @@ Full-screen settings panel for Every Code’s TUI. Use it to change models, them
|
||||
- **Notifications**: toggle all or set filters. `/notifications on|off|status` also lands here.
|
||||
|
||||
## Overlay lifecycle
|
||||
|
||||
- All keys route through `handle_settings_key` while open; composer/history ignore input until closed.
|
||||
- Help overlay (`?`) closes before the main overlay when Esc is pressed.
|
||||
- Sections mark completion via their content structs; the overlay closes when a section reports `is_complete` (e.g., Chrome option chosen).
|
||||
|
||||
## Scope reminders
|
||||
|
||||
- Global defaults live in `CODE_HOME/config.toml`.
|
||||
- Workspace overrides are honored where setters accept `cwd` (access mode) or when project-level config files exist. The UI always renders merged effective values.
|
||||
- Agent commands and MCP servers are stored under `CODE_HOME` and apply to all workspaces unless overridden by project config.
|
||||
|
||||
## Commands
|
||||
|
||||
- `/settings [section]`
|
||||
- `/auto settings`
|
||||
- `/update` (or `/update settings`)
|
||||
|
||||
+77
-75
@@ -7,15 +7,15 @@
|
||||
* and additional AI-powered features.
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { spawn } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
// Configuration
|
||||
const HANZO_VERSION = '0.3.0';
|
||||
const HANZO_CONFIG_DIR = path.join(os.homedir(), '.hanzo');
|
||||
const LEGACY_CODEX_CONFIG_DIR = path.join(os.homedir(), '.codex');
|
||||
const HANZO_VERSION = "0.3.0";
|
||||
const HANZO_CONFIG_DIR = path.join(os.homedir(), ".hanzo");
|
||||
const LEGACY_CODEX_CONFIG_DIR = path.join(os.homedir(), ".codex");
|
||||
|
||||
// ASCII art logo
|
||||
const HANZO_LOGO = `
|
||||
@@ -31,16 +31,16 @@ const HANZO_LOGO = `
|
||||
|
||||
// Command mappings
|
||||
const COMMAND_ALIASES = {
|
||||
'chat': 'Start an AI chat session',
|
||||
'exec': 'Execute commands with AI assistance',
|
||||
'plan': 'Create a development plan',
|
||||
'solve': 'Solve a coding problem',
|
||||
'review': 'Review code changes',
|
||||
'test': 'Run tests with AI analysis',
|
||||
'debug': 'Debug with AI assistance',
|
||||
'refactor': 'AI-assisted refactoring',
|
||||
'explain': 'Explain code or concepts',
|
||||
'optimize': 'Optimize code performance',
|
||||
chat: "Start an AI chat session",
|
||||
exec: "Execute commands with AI assistance",
|
||||
plan: "Create a development plan",
|
||||
solve: "Solve a coding problem",
|
||||
review: "Review code changes",
|
||||
test: "Run tests with AI analysis",
|
||||
debug: "Debug with AI assistance",
|
||||
refactor: "AI-assisted refactoring",
|
||||
explain: "Explain code or concepts",
|
||||
optimize: "Optimize code performance",
|
||||
};
|
||||
|
||||
// Parse command line arguments
|
||||
@@ -49,31 +49,31 @@ const args = process.argv.slice(2);
|
||||
// Show help
|
||||
function showHelp() {
|
||||
console.log(HANZO_LOGO);
|
||||
console.log('Usage: hanzo [command] [options]\n');
|
||||
console.log('Commands:');
|
||||
console.log("Usage: hanzo [command] [options]\n");
|
||||
console.log("Commands:");
|
||||
for (const [cmd, desc] of Object.entries(COMMAND_ALIASES)) {
|
||||
console.log(` ${cmd.padEnd(12)} ${desc}`);
|
||||
}
|
||||
console.log('\nOptions:');
|
||||
console.log(' --version Show version information');
|
||||
console.log(' --help Show this help message');
|
||||
console.log(' --config Show configuration');
|
||||
console.log(' --model Set AI model (gpt-4, claude-3, etc)');
|
||||
console.log(' --verbose Enable verbose output');
|
||||
console.log('\nExamples:');
|
||||
console.log(' hanzo chat Start interactive AI chat');
|
||||
console.log("\nOptions:");
|
||||
console.log(" --version Show version information");
|
||||
console.log(" --help Show this help message");
|
||||
console.log(" --config Show configuration");
|
||||
console.log(" --model Set AI model (gpt-4, claude-3, etc)");
|
||||
console.log(" --verbose Enable verbose output");
|
||||
console.log("\nExamples:");
|
||||
console.log(" hanzo chat Start interactive AI chat");
|
||||
console.log(' hanzo exec "build project" Execute with AI assistance');
|
||||
console.log(' hanzo plan "new feature" Create development plan');
|
||||
console.log(' hanzo solve "fix bug #123" AI-powered problem solving');
|
||||
console.log('\nFor more information: https://hanzo.ai/cli');
|
||||
console.log("\nFor more information: https://hanzo.ai/cli");
|
||||
}
|
||||
|
||||
// Show version
|
||||
function showVersion() {
|
||||
console.log(`Hanzo CLI v${HANZO_VERSION}`);
|
||||
console.log('Node.js:', process.version);
|
||||
console.log('Platform:', process.platform, process.arch);
|
||||
console.log('Home:', HANZO_CONFIG_DIR);
|
||||
console.log("Node.js:", process.version);
|
||||
console.log("Platform:", process.platform, process.arch);
|
||||
console.log("Home:", HANZO_CONFIG_DIR);
|
||||
}
|
||||
|
||||
// Ensure config directory exists
|
||||
@@ -81,23 +81,23 @@ function ensureConfigDir() {
|
||||
if (!fs.existsSync(HANZO_CONFIG_DIR)) {
|
||||
fs.mkdirSync(HANZO_CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
// Create default config if not exists
|
||||
const configFile = path.join(HANZO_CONFIG_DIR, 'config.json');
|
||||
const configFile = path.join(HANZO_CONFIG_DIR, "config.json");
|
||||
if (!fs.existsSync(configFile)) {
|
||||
const defaultConfig = {
|
||||
model: 'gpt-4',
|
||||
model: "gpt-4",
|
||||
apiKeys: {},
|
||||
theme: 'dark',
|
||||
verbosity: 'normal',
|
||||
safety: 'balanced',
|
||||
theme: "dark",
|
||||
verbosity: "normal",
|
||||
safety: "balanced",
|
||||
features: {
|
||||
autoComplete: true,
|
||||
syntaxHighlight: true,
|
||||
aiSuggestions: true,
|
||||
quantumStaking: false,
|
||||
luxConsensus: false,
|
||||
}
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(configFile, JSON.stringify(defaultConfig, null, 2));
|
||||
}
|
||||
@@ -107,12 +107,12 @@ function ensureConfigDir() {
|
||||
function findExecutable() {
|
||||
// Check for Rust binary first
|
||||
const possiblePaths = [
|
||||
path.join(__dirname, '..', 'bin', 'dev'),
|
||||
path.join(__dirname, '..', 'code-rs', 'target', 'release', 'dev'),
|
||||
path.join(__dirname, '..', 'code-rs', 'target', 'debug', 'dev'),
|
||||
path.join(__dirname, "..", "bin", "dev"),
|
||||
path.join(__dirname, "..", "code-rs", "target", "release", "dev"),
|
||||
path.join(__dirname, "..", "code-rs", "target", "debug", "dev"),
|
||||
// Fallback to system paths
|
||||
'/usr/local/bin/dev',
|
||||
'/opt/homebrew/bin/dev',
|
||||
"/usr/local/bin/dev",
|
||||
"/opt/homebrew/bin/dev",
|
||||
];
|
||||
|
||||
for (const p of possiblePaths) {
|
||||
@@ -120,36 +120,36 @@ function findExecutable() {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try to find in PATH
|
||||
const pathDirs = process.env.PATH?.split(':') || [];
|
||||
const pathDirs = process.env.PATH?.split(":") || [];
|
||||
for (const dir of pathDirs) {
|
||||
const devPath = path.join(dir, 'dev');
|
||||
const devPath = path.join(dir, "dev");
|
||||
if (fs.existsSync(devPath)) {
|
||||
return devPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Execute underlying command
|
||||
function executeCommand(executable, args) {
|
||||
const child = spawn(executable, args, {
|
||||
stdio: 'inherit',
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
HANZO_CLI: 'true',
|
||||
HANZO_CLI: "true",
|
||||
HANZO_VERSION: HANZO_VERSION,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
child.on("exit", (code) => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error('Error executing command:', err.message);
|
||||
child.on("error", (err) => {
|
||||
console.error("Error executing command:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -159,51 +159,53 @@ async function main() {
|
||||
ensureConfigDir();
|
||||
|
||||
// Handle special flags
|
||||
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
||||
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.includes('--version') || args.includes('-v')) {
|
||||
if (args.includes("--version") || args.includes("-v")) {
|
||||
showVersion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special Hanzo commands
|
||||
const command = args[0];
|
||||
|
||||
if (command === 'init') {
|
||||
|
||||
if (command === "init") {
|
||||
console.log(HANZO_LOGO);
|
||||
console.log('Initializing Hanzo AI assistant in current directory...');
|
||||
console.log('✓ Created .hanzo directory');
|
||||
console.log('✓ Initialized AI context');
|
||||
console.log('✓ Ready for development!');
|
||||
console.log('\nTry: hanzo chat');
|
||||
console.log("Initializing Hanzo AI assistant in current directory...");
|
||||
console.log("✓ Created .hanzo directory");
|
||||
console.log("✓ Initialized AI context");
|
||||
console.log("✓ Ready for development!");
|
||||
console.log("\nTry: hanzo chat");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'config') {
|
||||
const configFile = path.join(HANZO_CONFIG_DIR, 'config.json');
|
||||
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||
console.log('Hanzo Configuration:');
|
||||
if (command === "config") {
|
||||
const configFile = path.join(HANZO_CONFIG_DIR, "config.json");
|
||||
const config = JSON.parse(fs.readFileSync(configFile, "utf8"));
|
||||
console.log("Hanzo Configuration:");
|
||||
console.log(JSON.stringify(config, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Find underlying executable
|
||||
const executable = findExecutable();
|
||||
|
||||
|
||||
if (!executable) {
|
||||
console.log(HANZO_LOGO);
|
||||
console.log('Welcome to Hanzo CLI!\n');
|
||||
console.log('The underlying Rust binary is not yet built.');
|
||||
console.log('For now, Hanzo provides a preview of available commands.\n');
|
||||
|
||||
console.log("Welcome to Hanzo CLI!\n");
|
||||
console.log("The underlying Rust binary is not yet built.");
|
||||
console.log("For now, Hanzo provides a preview of available commands.\n");
|
||||
|
||||
if (command && COMMAND_ALIASES[command]) {
|
||||
console.log(`Command: ${command}`);
|
||||
console.log(`Description: ${COMMAND_ALIASES[command]}`);
|
||||
console.log('\nThis command will be available once the Rust binary is built.');
|
||||
console.log('Run: ./build-fast.sh');
|
||||
console.log(
|
||||
"\nThis command will be available once the Rust binary is built.",
|
||||
);
|
||||
console.log("Run: ./build-fast.sh");
|
||||
} else {
|
||||
showHelp();
|
||||
}
|
||||
@@ -216,7 +218,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// Run the CLI
|
||||
main().catch(err => {
|
||||
console.error('Fatal error:', err);
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1889,7 +1889,7 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
|
||||
json!({
|
||||
"type": ["string", "null"],
|
||||
"minLength": CLI_PROMPT_MIN_CHARS,
|
||||
"description": "Instruction sent to the CLI to push it forward with the task (4-600 chars). Write this like a human maintainer pushing the CLI forwards, without digging too deep into the technical side. Provide when finish_status is 'continue'. Keep it high-level; the CLI has more context and tools than you do. e.g. 'Execute the first two steps of the plan you provided in parellel using agents.' NEVER ask the CLI to show you files so you solve problems directly. ALWAYS allow the CLI to take control. You are the COORDINATOR not the WORKER. Prompts over 600 characters will be rejected as this indicates the CLI is not being given sufficient autonomy."
|
||||
"description": "Instruction sent to the CLI to push it forward with the task (4-600 chars). Write this like a human maintainer pushing the CLI forwards, without digging too deep into the technical side. Provide when finish_status is 'continue'. Keep it high-level; the CLI has more context and tools than you do. e.g. 'Execute the first two steps of the plan you provided in parallel using agents.' NEVER ask the CLI to show you files so you solve problems directly. ALWAYS allow the CLI to take control. You are the COORDINATOR not the WORKER. Prompts over 600 characters will be rejected as this indicates the CLI is not being given sufficient autonomy."
|
||||
}),
|
||||
);
|
||||
required.push(Value::String("prompt_sent_to_cli".to_string()));
|
||||
@@ -1921,7 +1921,7 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
|
||||
"context": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 1500,
|
||||
"description": "Background details (agents can not see the conversation - you must provide ALL neccessary information here). You might want to include parts of the plan or conversation history relevant to the work given to the agent."
|
||||
"description": "Background details (agents can not see the conversation - you must provide ALL necessary information here). You might want to include parts of the plan or conversation history relevant to the work given to the agent."
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
@@ -1933,7 +1933,7 @@ fn build_schema(active_agents: &[String], features: SchemaFeatures) -> Value {
|
||||
},
|
||||
"required": ["prompt", "context", "write", "models"]
|
||||
},
|
||||
"description": "Up to 3 batches per turn with up to 4 agents in each. Use agents whenever it will help to source a variety of opinions when planning/researching or when there a mulitple workstreams which can be extecuted at once. Instruct the agent to carefully merge in the results of the agents work. Another great reason to use agents is that it helps to split the work up in small batches with a new context history - this speeds up work and dramatically improve focus. Having said that, the CLI has to be responible for merging in the results and producing the final product, so you need to balance the work given to the agents vs work given to the CLI at each step."
|
||||
"description": "Up to 3 batches per turn with up to 4 agents in each. Use agents whenever it will help to source a variety of opinions when planning/researching or when there are multiple workstreams which can be executed at once. Instruct the agent to carefully merge in the results of the agents work. Another great reason to use agents is that it helps to split the work up in small batches with a new context history - this speeds up work and dramatically improve focus. Having said that, the CLI has to be responsible for merging in the results and producing the final product, so you need to balance the work given to the agents vs work given to the CLI at each step."
|
||||
},
|
||||
},
|
||||
"required": ["timing", "list"]
|
||||
|
||||
@@ -35250,7 +35250,7 @@ impl ChatWidget<'_> {
|
||||
|
||||
let prompt = if let Some(current) = current_trimmed.as_ref() {
|
||||
format!(
|
||||
"Review the code changes between the current branch '{current}' and '{branch_trimmed}'. Identify the intent of the changes in '{current}' and ensure no obvious gaps remain. Find all geniune bugs or regressions which need to be addressed before merging. Return ALL issues which need to be addressed, not just the first one you find."
|
||||
"Review the code changes between the current branch '{current}' and '{branch_trimmed}'. Identify the intent of the changes in '{current}' and ensure no obvious gaps remain. Find all genuine bugs or regressions which need to be addressed before merging. Return ALL issues which need to be addressed, not just the first one you find."
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
@@ -35371,7 +35371,7 @@ impl ChatWidget<'_> {
|
||||
{
|
||||
if base_branch != current_branch {
|
||||
let prompt = format!(
|
||||
"Review the code changes between the current branch '{current_branch}' and '{base_branch}'. Identify the intent of the changes in '{current_branch}' and ensure no obvious gaps remain. Find all geniune bugs or regressions which need to be addressed before merging. Return ALL issues which need to be addressed, not just the first one you find."
|
||||
"Review the code changes between the current branch '{current_branch}' and '{base_branch}'. Identify the intent of the changes in '{current_branch}' and ensure no obvious gaps remain. Find all genuine bugs or regressions which need to be addressed before merging. Return ALL issues which need to be addressed, not just the first one you find."
|
||||
);
|
||||
let hint = format!("against {base_branch}");
|
||||
let preparation_label =
|
||||
@@ -36626,7 +36626,7 @@ impl ChatWidget<'_> {
|
||||
let mut steps = Vec::new();
|
||||
if has_dirty_changes {
|
||||
steps.push(
|
||||
"Briefly clean this repo (add working files/secrets to .gitignore, delete any temporary files) if neccessary, then commit all remaining dirty files."
|
||||
"Briefly clean this repo (add working files/secrets to .gitignore, delete any temporary files) if necessary, then commit all remaining dirty files."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -36654,7 +36654,7 @@ impl ChatWidget<'_> {
|
||||
let _ = writeln!(message, "{}. {}", idx + 1, step);
|
||||
}
|
||||
message.push_str(
|
||||
"You must ensure all workflows complete successfully. Do not yeild or respond until this has been completed.",
|
||||
"You must ensure all workflows complete successfully. Do not yield or respond until this has been completed.",
|
||||
);
|
||||
|
||||
tx.send(AppEvent::SubmitHiddenTextWithPreface {
|
||||
|
||||
+117
-41
@@ -3,7 +3,15 @@
|
||||
import { platform as nodePlatform, arch as nodeArch } from "os";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { existsSync, mkdirSync, copyFileSync, chmodSync, readFileSync, unlinkSync, createWriteStream } from "fs";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
copyFileSync,
|
||||
chmodSync,
|
||||
readFileSync,
|
||||
unlinkSync,
|
||||
createWriteStream,
|
||||
} from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import { get as httpsGet } from "https";
|
||||
|
||||
@@ -79,38 +87,45 @@ const getCacheDir = (version) => {
|
||||
return dir;
|
||||
};
|
||||
|
||||
const httpsDownload = (url, dest) => new Promise((resolve, reject) => {
|
||||
const req = httpsGet(url, (res) => {
|
||||
const status = res.statusCode || 0;
|
||||
if (status >= 300 && status < 400 && res.headers.location) {
|
||||
return resolve(httpsDownload(res.headers.location, dest));
|
||||
}
|
||||
if (status !== 200) {
|
||||
return reject(new Error(`HTTP ${status}`));
|
||||
}
|
||||
const out = createWriteStream(dest);
|
||||
res.pipe(out);
|
||||
out.on("finish", () => out.close(resolve));
|
||||
out.on("error", (e) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
const httpsDownload = (url, dest) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const req = httpsGet(url, (res) => {
|
||||
const status = res.statusCode || 0;
|
||||
if (status >= 300 && status < 400 && res.headers.location) {
|
||||
return resolve(httpsDownload(res.headers.location, dest));
|
||||
}
|
||||
if (status !== 200) {
|
||||
return reject(new Error(`HTTP ${status}`));
|
||||
}
|
||||
const out = createWriteStream(dest);
|
||||
res.pipe(out);
|
||||
out.on("finish", () => out.close(resolve));
|
||||
out.on("error", (e) => {
|
||||
try {
|
||||
unlinkSync(dest);
|
||||
} catch {}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
req.on("error", (e) => {
|
||||
try {
|
||||
unlinkSync(dest);
|
||||
} catch {}
|
||||
reject(e);
|
||||
});
|
||||
req.setTimeout(120000, () => {
|
||||
req.destroy(new Error("download timed out"));
|
||||
});
|
||||
});
|
||||
req.on("error", (e) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(e);
|
||||
});
|
||||
req.setTimeout(120000, () => {
|
||||
req.destroy(new Error("download timed out"));
|
||||
});
|
||||
});
|
||||
|
||||
export async function runPostinstall(opts = {}) {
|
||||
const { invokedByRuntime = false, skipGlobalAlias = false } = opts;
|
||||
|
||||
if (!targetTriple) {
|
||||
if (!invokedByRuntime) {
|
||||
console.log(`hanzo-node: unsupported platform ${platform}/${arch}, skipping postinstall`);
|
||||
console.log(
|
||||
`hanzo-node: unsupported platform ${platform}/${arch}, skipping postinstall`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -140,7 +155,9 @@ export async function runPostinstall(opts = {}) {
|
||||
if (existsSync(src)) {
|
||||
copyFileSync(src, binaryPath);
|
||||
if (platform !== "win32") {
|
||||
try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
try {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
} catch {}
|
||||
}
|
||||
if (!invokedByRuntime) {
|
||||
console.log(`hanzo-node: installed from ${pkgName}`);
|
||||
@@ -154,14 +171,23 @@ export async function runPostinstall(opts = {}) {
|
||||
|
||||
// Download from GitHub release
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(path.join(__dirname, "package.json"), "utf8"));
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
||||
);
|
||||
const version = pkg.version;
|
||||
|
||||
const isWin = platform === "win32";
|
||||
const binaryName = `code-${targetTriple}`;
|
||||
const archiveName = isWin
|
||||
? `${binaryName}.zip`
|
||||
: (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `${binaryName}.zst`; } catch { return `${binaryName}.tar.gz`; } })();
|
||||
: (() => {
|
||||
try {
|
||||
execSync("zstd --version", { stdio: "ignore", shell: true });
|
||||
return `${binaryName}.zst`;
|
||||
} catch {
|
||||
return `${binaryName}.tar.gz`;
|
||||
}
|
||||
})();
|
||||
const url = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
|
||||
|
||||
if (!invokedByRuntime) {
|
||||
@@ -172,30 +198,78 @@ export async function runPostinstall(opts = {}) {
|
||||
await httpsDownload(url, tmp);
|
||||
|
||||
if (isWin) {
|
||||
const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
|
||||
const psFull = path.join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
||||
const sysRoot =
|
||||
process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
||||
const psFull = path.join(
|
||||
sysRoot,
|
||||
"System32",
|
||||
"WindowsPowerShell",
|
||||
"v1.0",
|
||||
"powershell.exe",
|
||||
);
|
||||
const psCmd = `Expand-Archive -Path '${tmp}' -DestinationPath '${binDir}' -Force`;
|
||||
let ok = false;
|
||||
try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
|
||||
if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
if (!ok) { execSync(`tar -xf "${tmp}" -C "${binDir}"`, { stdio: 'ignore', shell: true }); }
|
||||
try { unlinkSync(tmp); } catch {}
|
||||
try {
|
||||
execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, {
|
||||
stdio: "ignore",
|
||||
});
|
||||
ok = true;
|
||||
} catch {}
|
||||
if (!ok) {
|
||||
try {
|
||||
execSync(
|
||||
`powershell -NoProfile -NonInteractive -Command "${psCmd}"`,
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
ok = true;
|
||||
} catch {}
|
||||
}
|
||||
if (!ok) {
|
||||
try {
|
||||
execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, {
|
||||
stdio: "ignore",
|
||||
});
|
||||
ok = true;
|
||||
} catch {}
|
||||
}
|
||||
if (!ok) {
|
||||
execSync(`tar -xf "${tmp}" -C "${binDir}"`, {
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
});
|
||||
}
|
||||
try {
|
||||
unlinkSync(tmp);
|
||||
} catch {}
|
||||
} else {
|
||||
if (archiveName.endsWith(".zst")) {
|
||||
execSync(`zstd -d '${tmp}' -o '${binaryPath}'`, { stdio: 'ignore', shell: true });
|
||||
try { unlinkSync(tmp); } catch {}
|
||||
execSync(`zstd -d '${tmp}' -o '${binaryPath}'`, {
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
});
|
||||
try {
|
||||
unlinkSync(tmp);
|
||||
} catch {}
|
||||
} else {
|
||||
execSync(`tar -xzf '${tmp}' -C '${binDir}'`, { stdio: 'ignore', shell: true });
|
||||
try { unlinkSync(tmp); } catch {}
|
||||
execSync(`tar -xzf '${tmp}' -C '${binDir}'`, {
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
});
|
||||
try {
|
||||
unlinkSync(tmp);
|
||||
} catch {}
|
||||
}
|
||||
try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
try {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Cache for future use
|
||||
const cacheDir = getCacheDir(version);
|
||||
const cachePath = path.join(cacheDir, `hanzo-${targetTriple}`);
|
||||
try { copyFileSync(binaryPath, cachePath); } catch {}
|
||||
try {
|
||||
copyFileSync(binaryPath, cachePath);
|
||||
} catch {}
|
||||
|
||||
if (!invokedByRuntime) {
|
||||
console.log("hanzo-node: installation complete");
|
||||
@@ -203,7 +277,9 @@ export async function runPostinstall(opts = {}) {
|
||||
} catch (e) {
|
||||
if (!invokedByRuntime) {
|
||||
console.error(`hanzo-node: postinstall failed: ${e.message}`);
|
||||
console.error("You can try running the CLI directly, which will attempt to bootstrap the binary.");
|
||||
console.error(
|
||||
"You can try running the CLI directly, which will attempt to bootstrap the binary.",
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
+166
-89
@@ -29,15 +29,17 @@
|
||||
//
|
||||
// # Or run your usual `code` subcommand (Exec/TUI) with the same env vars.
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const crypto = require("crypto");
|
||||
const { URL } = require("url");
|
||||
|
||||
const PORT = Number(process.env.PORT || 5055);
|
||||
const API_KEY = process.env.OPENAI_API_KEY || '';
|
||||
const UPSTREAM = new URL(process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1');
|
||||
const ALLOWED = ['/v1/chat/completions', '/v1/responses'];
|
||||
const API_KEY = process.env.OPENAI_API_KEY || "";
|
||||
const UPSTREAM = new URL(
|
||||
process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
|
||||
);
|
||||
const ALLOWED = ["/v1/chat/completions", "/v1/responses"];
|
||||
|
||||
// CI options:
|
||||
// EXIT_ON_5XX=1 -> Exit process non‑zero if a 5xx response head is seen
|
||||
@@ -53,26 +55,26 @@ const ALLOWED = ['/v1/chat/completions', '/v1/responses'];
|
||||
// LOG_ERROR_BODY_BYTES=1024 -> Max bytes to log from error body
|
||||
// STRICT_HEADERS=1 -> Rebuild upstream headers from a minimal allowlist
|
||||
// RESPONSES_BETA="responses=v1" (override beta header)
|
||||
const EXIT_ON_5XX = process.env.EXIT_ON_5XX === '1' || false;
|
||||
const READY_FILE = process.env.READY_FILE || '';
|
||||
const LOG_DEST = (process.env.LOG_DEST || 'stdout').toLowerCase();
|
||||
const DISABLE_KEEPALIVE = process.env.DISABLE_KEEPALIVE === '1' || false;
|
||||
const FORCE_CLOSE = process.env.FORCE_CLOSE === '1' || false;
|
||||
const NO_GZIP = process.env.NO_GZIP === '1' || false;
|
||||
const STRIP_SESSION_ID = process.env.STRIP_SESSION_ID === '1' || false;
|
||||
const IDEMPOTENCY_KEY = (process.env.IDEMPOTENCY_KEY || '').toLowerCase();
|
||||
const LOG_ERROR_BODY = process.env.LOG_ERROR_BODY === '1' || false;
|
||||
const EXIT_ON_5XX = process.env.EXIT_ON_5XX === "1" || false;
|
||||
const READY_FILE = process.env.READY_FILE || "";
|
||||
const LOG_DEST = (process.env.LOG_DEST || "stdout").toLowerCase();
|
||||
const DISABLE_KEEPALIVE = process.env.DISABLE_KEEPALIVE === "1" || false;
|
||||
const FORCE_CLOSE = process.env.FORCE_CLOSE === "1" || false;
|
||||
const NO_GZIP = process.env.NO_GZIP === "1" || false;
|
||||
const STRIP_SESSION_ID = process.env.STRIP_SESSION_ID === "1" || false;
|
||||
const IDEMPOTENCY_KEY = (process.env.IDEMPOTENCY_KEY || "").toLowerCase();
|
||||
const LOG_ERROR_BODY = process.env.LOG_ERROR_BODY === "1" || false;
|
||||
const LOG_ERROR_BODY_BYTES = Number(process.env.LOG_ERROR_BODY_BYTES || 2048);
|
||||
const STRICT_HEADERS = process.env.STRICT_HEADERS === '1' || false;
|
||||
const RESPONSES_BETA = process.env.RESPONSES_BETA || 'responses=v1';
|
||||
const STRICT_HEADERS = process.env.STRICT_HEADERS === "1" || false;
|
||||
const RESPONSES_BETA = process.env.RESPONSES_BETA || "responses=v1";
|
||||
|
||||
function outWrite(s) {
|
||||
if (LOG_DEST === 'stderr') process.stderr.write(s + '\n');
|
||||
else process.stdout.write(s + '\n');
|
||||
if (LOG_DEST === "stderr") process.stderr.write(s + "\n");
|
||||
else process.stdout.write(s + "\n");
|
||||
}
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error('[fatal] OPENAI_API_KEY missing');
|
||||
console.error("[fatal] OPENAI_API_KEY missing");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -80,119 +82,194 @@ function redactHeaders(h) {
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(h || {})) {
|
||||
const key = k.toLowerCase();
|
||||
if (key === 'authorization' || key === 'proxy-authorization' || key === 'cookie' || key === 'set-cookie') {
|
||||
out[key] = 'REDACTED';
|
||||
if (
|
||||
key === "authorization" ||
|
||||
key === "proxy-authorization" ||
|
||||
key === "cookie" ||
|
||||
key === "set-cookie"
|
||||
) {
|
||||
out[key] = "REDACTED";
|
||||
} else {
|
||||
out[key] = Array.isArray(v) ? v.map(() => '<omitted>') : String(v);
|
||||
out[key] = Array.isArray(v) ? v.map(() => "<omitted>") : String(v);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function log(ev) {
|
||||
try { outWrite(JSON.stringify({ ts: new Date().toISOString(), ...ev })); } catch {}
|
||||
try {
|
||||
outWrite(JSON.stringify({ ts: new Date().toISOString(), ...ev }));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const rid = crypto.randomUUID();
|
||||
if (!ALLOWED.some(p => req.url.startsWith(p))) {
|
||||
res.writeHead(403, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'blocked', path: req.url }));
|
||||
if (!ALLOWED.some((p) => req.url.startsWith(p))) {
|
||||
res.writeHead(403, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "blocked", path: req.url }));
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
req.on('data', c => { chunks.push(c); size += c.length; if (size > 2 * 1024 * 1024) { req.destroy(new Error('body too large')); } });
|
||||
req.on('error', err => {
|
||||
log({ level: 'error', rid, msg: 'client request error', err: String(err) });
|
||||
if (!res.headersSent) res.writeHead(400, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'bad_request' }));
|
||||
req.on("data", (c) => {
|
||||
chunks.push(c);
|
||||
size += c.length;
|
||||
if (size > 2 * 1024 * 1024) {
|
||||
req.destroy(new Error("body too large"));
|
||||
}
|
||||
});
|
||||
req.on('end', () => {
|
||||
req.on("error", (err) => {
|
||||
log({ level: "error", rid, msg: "client request error", err: String(err) });
|
||||
if (!res.headersSent)
|
||||
res.writeHead(400, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "bad_request" }));
|
||||
});
|
||||
req.on("end", () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
const up = new URL(req.url, UPSTREAM);
|
||||
let incoming;
|
||||
if (STRICT_HEADERS) {
|
||||
incoming = {};
|
||||
incoming['authorization'] = `Bearer ${API_KEY}`;
|
||||
incoming['content-type'] = 'application/json';
|
||||
incoming['accept'] = 'text/event-stream';
|
||||
if (NO_GZIP) incoming['accept-encoding'] = 'identity';
|
||||
if (up.pathname.startsWith('/v1/responses')) incoming['openai-beta'] = RESPONSES_BETA;
|
||||
incoming['user-agent'] = 'code-proxy/1.0';
|
||||
incoming['originator'] = 'codex_cli_rs';
|
||||
if (IDEMPOTENCY_KEY === 'auto') incoming['idempotency-key'] = crypto.randomUUID();
|
||||
incoming["authorization"] = `Bearer ${API_KEY}`;
|
||||
incoming["content-type"] = "application/json";
|
||||
incoming["accept"] = "text/event-stream";
|
||||
if (NO_GZIP) incoming["accept-encoding"] = "identity";
|
||||
if (up.pathname.startsWith("/v1/responses"))
|
||||
incoming["openai-beta"] = RESPONSES_BETA;
|
||||
incoming["user-agent"] = "code-proxy/1.0";
|
||||
incoming["originator"] = "codex_cli_rs";
|
||||
if (IDEMPOTENCY_KEY === "auto")
|
||||
incoming["idempotency-key"] = crypto.randomUUID();
|
||||
} else {
|
||||
incoming = { ...req.headers };
|
||||
if (!incoming['content-type']) incoming['content-type'] = 'application/json';
|
||||
if (!incoming['accept']) incoming['accept'] = 'text/event-stream';
|
||||
if (NO_GZIP) incoming['accept-encoding'] = 'identity';
|
||||
incoming['authorization'] = `Bearer ${API_KEY}`; // replace with real key
|
||||
if (up.pathname.startsWith('/v1/responses')) {
|
||||
if (!incoming['openai-beta']) incoming['openai-beta'] = RESPONSES_BETA;
|
||||
if (!incoming["content-type"])
|
||||
incoming["content-type"] = "application/json";
|
||||
if (!incoming["accept"]) incoming["accept"] = "text/event-stream";
|
||||
if (NO_GZIP) incoming["accept-encoding"] = "identity";
|
||||
incoming["authorization"] = `Bearer ${API_KEY}`; // replace with real key
|
||||
if (up.pathname.startsWith("/v1/responses")) {
|
||||
if (!incoming["openai-beta"]) incoming["openai-beta"] = RESPONSES_BETA;
|
||||
}
|
||||
if (!incoming['originator']) incoming['originator'] = 'codex_cli_rs';
|
||||
if (STRIP_SESSION_ID && 'session_id' in incoming) delete incoming['session_id'];
|
||||
if (IDEMPOTENCY_KEY === 'auto') incoming['idempotency-key'] = crypto.randomUUID();
|
||||
if (!incoming["originator"]) incoming["originator"] = "codex_cli_rs";
|
||||
if (STRIP_SESSION_ID && "session_id" in incoming)
|
||||
delete incoming["session_id"];
|
||||
if (IDEMPOTENCY_KEY === "auto")
|
||||
incoming["idempotency-key"] = crypto.randomUUID();
|
||||
}
|
||||
delete incoming['host']; incoming['host'] = up.host;
|
||||
incoming['connection'] = FORCE_CLOSE ? 'close' : 'keep-alive';
|
||||
if (incoming['content-length']) incoming['content-length'] = String(body.length);
|
||||
delete incoming['proxy-connection'];
|
||||
delete incoming['proxy-authorization'];
|
||||
delete incoming["host"];
|
||||
incoming["host"] = up.host;
|
||||
incoming["connection"] = FORCE_CLOSE ? "close" : "keep-alive";
|
||||
if (incoming["content-length"])
|
||||
incoming["content-length"] = String(body.length);
|
||||
delete incoming["proxy-connection"];
|
||||
delete incoming["proxy-authorization"];
|
||||
|
||||
log({ level: 'info', rid, phase: 'request', method: req.method, url: up.toString(), headers: redactHeaders(incoming), body_bytes: body.length });
|
||||
log({
|
||||
level: "info",
|
||||
rid,
|
||||
phase: "request",
|
||||
method: req.method,
|
||||
url: up.toString(),
|
||||
headers: redactHeaders(incoming),
|
||||
body_bytes: body.length,
|
||||
});
|
||||
|
||||
const opts = {
|
||||
protocol: up.protocol,
|
||||
hostname: up.hostname,
|
||||
port: up.port || (up.protocol === 'https:' ? 443 : 80),
|
||||
port: up.port || (up.protocol === "https:" ? 443 : 80),
|
||||
path: up.pathname + up.search,
|
||||
method: req.method,
|
||||
headers: incoming,
|
||||
servername: up.hostname,
|
||||
agent: (up.protocol === 'https:'
|
||||
? new https.Agent({ keepAlive: !DISABLE_KEEPALIVE, maxSockets: 64, servername: up.hostname })
|
||||
: new http.Agent({ keepAlive: !DISABLE_KEEPALIVE, maxSockets: 64 }))
|
||||
agent:
|
||||
up.protocol === "https:"
|
||||
? new https.Agent({
|
||||
keepAlive: !DISABLE_KEEPALIVE,
|
||||
maxSockets: 64,
|
||||
servername: up.hostname,
|
||||
})
|
||||
: new http.Agent({ keepAlive: !DISABLE_KEEPALIVE, maxSockets: 64 }),
|
||||
};
|
||||
|
||||
const upstream = (up.protocol === 'https:' ? https : http).request(opts, (upr) => {
|
||||
const resHeaders = { ...upr.headers };
|
||||
res.writeHead(upr.statusCode || 500, resHeaders);
|
||||
log({ level: 'info', rid, phase: 'response_head', status: upr.statusCode, headers: redactHeaders(resHeaders) });
|
||||
if (EXIT_ON_5XX && upr.statusCode && upr.statusCode >= 500) {
|
||||
// Propagate response then schedule exit(2) once stream completes
|
||||
upr.on('end', () => { try { process.exitCode = 2; } catch {} });
|
||||
}
|
||||
let total = 0;
|
||||
const chunks = [];
|
||||
upr.on('data', (chunk) => { total += chunk.length; if (LOG_ERROR_BODY && upr.statusCode && upr.statusCode >= 400) { if (chunks.length < 32 && total <= LOG_ERROR_BODY_BYTES) chunks.push(chunk); } });
|
||||
upr.on('end', () => {
|
||||
if (LOG_ERROR_BODY && upr.statusCode && upr.statusCode >= 400) {
|
||||
let bodyStr = '';
|
||||
try { bodyStr = Buffer.concat(chunks).toString('utf8'); } catch {}
|
||||
log({ level: 'error', rid, phase: 'response_error_body', status: upr.statusCode, body: bodyStr.slice(0, LOG_ERROR_BODY_BYTES) });
|
||||
const upstream = (up.protocol === "https:" ? https : http).request(
|
||||
opts,
|
||||
(upr) => {
|
||||
const resHeaders = { ...upr.headers };
|
||||
res.writeHead(upr.statusCode || 500, resHeaders);
|
||||
log({
|
||||
level: "info",
|
||||
rid,
|
||||
phase: "response_head",
|
||||
status: upr.statusCode,
|
||||
headers: redactHeaders(resHeaders),
|
||||
});
|
||||
if (EXIT_ON_5XX && upr.statusCode && upr.statusCode >= 500) {
|
||||
// Propagate response then schedule exit(2) once stream completes
|
||||
upr.on("end", () => {
|
||||
try {
|
||||
process.exitCode = 2;
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
log({ level: 'info', rid, phase: 'response_end', status: upr.statusCode, bytes: total, request_id: resHeaders['x-request-id'] || null });
|
||||
});
|
||||
upr.on('error', (e) => { log({ level: 'error', rid, phase: 'response_error', err: String(e) }); });
|
||||
upr.pipe(res);
|
||||
let total = 0;
|
||||
const chunks = [];
|
||||
upr.on("data", (chunk) => {
|
||||
total += chunk.length;
|
||||
if (LOG_ERROR_BODY && upr.statusCode && upr.statusCode >= 400) {
|
||||
if (chunks.length < 32 && total <= LOG_ERROR_BODY_BYTES)
|
||||
chunks.push(chunk);
|
||||
}
|
||||
});
|
||||
upr.on("end", () => {
|
||||
if (LOG_ERROR_BODY && upr.statusCode && upr.statusCode >= 400) {
|
||||
let bodyStr = "";
|
||||
try {
|
||||
bodyStr = Buffer.concat(chunks).toString("utf8");
|
||||
} catch {}
|
||||
log({
|
||||
level: "error",
|
||||
rid,
|
||||
phase: "response_error_body",
|
||||
status: upr.statusCode,
|
||||
body: bodyStr.slice(0, LOG_ERROR_BODY_BYTES),
|
||||
});
|
||||
}
|
||||
log({
|
||||
level: "info",
|
||||
rid,
|
||||
phase: "response_end",
|
||||
status: upr.statusCode,
|
||||
bytes: total,
|
||||
request_id: resHeaders["x-request-id"] || null,
|
||||
});
|
||||
});
|
||||
upr.on("error", (e) => {
|
||||
log({ level: "error", rid, phase: "response_error", err: String(e) });
|
||||
});
|
||||
upr.pipe(res);
|
||||
},
|
||||
);
|
||||
upstream.setTimeout(600000, () => {
|
||||
upstream.destroy(new Error("upstream timeout"));
|
||||
});
|
||||
upstream.setTimeout(600000, () => { upstream.destroy(new Error('upstream timeout')); });
|
||||
upstream.on('error', (e) => {
|
||||
log({ level: 'error', rid, phase: 'upstream_error', err: String(e) });
|
||||
if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'upstream_error' }));
|
||||
upstream.on("error", (e) => {
|
||||
log({ level: "error", rid, phase: "upstream_error", err: String(e) });
|
||||
if (!res.headersSent)
|
||||
res.writeHead(502, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "upstream_error" }));
|
||||
});
|
||||
upstream.end(body);
|
||||
});
|
||||
});
|
||||
server.headersTimeout = 650000;
|
||||
server.keepAliveTimeout = 650000;
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
log({ level: 'info', msg: 'proxy listening', addr: '127.0.0.1', port: PORT });
|
||||
server.listen(PORT, "127.0.0.1", () => {
|
||||
log({ level: "info", msg: "proxy listening", addr: "127.0.0.1", port: PORT });
|
||||
if (READY_FILE) {
|
||||
try { require('fs').writeFileSync(READY_FILE, String(Date.now())); } catch {}
|
||||
try {
|
||||
require("fs").writeFileSync(READY_FILE, String(Date.now()));
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user