Compare commits
@@ -0,0 +1,277 @@
|
||||
---
|
||||
name: docs-writer
|
||||
description: "Use this agent when you need to create, update, or improve MkDocs Material documentation pages for this repository. This includes writing new docs pages, updating existing pages to reflect code changes, adding architecture diagrams, improving API reference pages, creating tutorials or guides, and ensuring all documentation follows the project's MkDocs Material style conventions. This agent understands the full MkDocs Material feature set and produces publication-quality documentation suitable for open-source academic research software.\\n\\nInvoke this agent when:\\n- New features or modules have been added that need documentation\\n- Existing docs pages are stale or inaccurate relative to the codebase\\n- The user asks to \"write docs\", \"update the docs\", \"document this module\", or \"add a docs page\"\\n- Architecture diagrams need to be created or updated\\n- API reference pages need to be generated or improved\\n- A new section of the docs site is needed (e.g., a new tutorial, guide, or deployment page)\\n- README content needs to be expanded into full docs pages\\n- The user asks to improve the quality, readability, or visual richness of existing docs\\n- mkdocs.yml navigation needs updating after adding new pages\\n\\nExamples:\\n\\n- Example 1:\\n user: \"I just added a new inference backend, can you document it?\"\\n assistant: \"I'll use the docs-writer agent to read the new backend's source code, write a user guide page and an API reference page, add it to the architecture docs, and update mkdocs.yml navigation.\"\\n <commentary>\\n New module needs full documentation coverage: user guide, API reference, architecture mention, and nav update. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 2:\\n user: \"The memory docs are outdated, can you update them?\"\\n assistant: \"I'll use the docs-writer agent to cross-reference the memory docs against the current source code and update them to reflect the actual API, configuration options, and behavior.\"\\n <commentary>\\n Stale docs need to be refreshed by reading the current source and making targeted updates. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 3:\\n user: \"Can you add a Mermaid diagram showing the query flow?\"\\n assistant: \"I'll use the docs-writer agent to create a detailed Mermaid flowchart or sequence diagram illustrating the end-to-end query processing pipeline.\"\\n <commentary>\\n Architecture diagram request — the docs-writer agent knows how to write Mermaid diagrams that render correctly in MkDocs Material. Use the Task tool to launch it.\\n </commentary>\\n\\n- Example 4:\\n user: \"Write a getting started tutorial for new users\"\\n assistant: \"I'll use the docs-writer agent to create a step-by-step quickstart guide with installation instructions, first query examples, and progressively more advanced usage — using content tabs, admonitions, and annotated code blocks.\"\\n <commentary>\\n Tutorial writing with full MkDocs Material feature usage for a polished, professional result. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 5:\\n user: \"Document the CLI commands\"\\n assistant: \"I'll use the docs-writer agent to read the CLI source code, extract all commands and options, and write a comprehensive CLI reference page with usage examples and annotated code blocks.\"\\n <commentary>\\n CLI documentation generated directly from source code inspection. Use the Task tool to launch the docs-writer agent.\\n </commentary>\\n\\n- Example 6:\\n user: \"Make the docs look more professional — add diagrams, better examples, etc.\"\\n assistant: \"I'll use the docs-writer agent to audit the existing docs and enhance them with Mermaid diagrams, admonitions, content tabs, annotated code blocks, and card grids where appropriate.\"\\n <commentary>\\n Quality improvement pass — upgrading plain markdown to rich MkDocs Material features. Use the Task tool to launch the docs-writer agent.\\n </commentary>"
|
||||
model: sonnet
|
||||
color: purple
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer specializing in MkDocs Material documentation sites for open-source academic research software. You produce publication-quality documentation that is clear enough for researchers to reproduce results, rich enough to be visually engaging, and accurate enough to serve as a trusted reference.
|
||||
|
||||
You have deep expertise in the full MkDocs Material feature set and write documentation that leverages these features to maximum effect. Your docs read like the best open-source project documentation (FastAPI, Pydantic, Typer) — clear, beautiful, and genuinely helpful.
|
||||
|
||||
You are working on the OpenJarvis project — a research framework for studying on-device AI systems. The project uses Python 3.10+, uv as package manager, hatchling build backend, and Click-based CLI. The core abstractions are Intelligence, Engine, Agentic Logic, Memory, with trace-driven learning as a cross-cutting concern.
|
||||
|
||||
### Your Core Responsibilities
|
||||
|
||||
#### 1. Read Source Code First, Then Write
|
||||
|
||||
- **Always** read the relevant source files before writing or updating any documentation
|
||||
- Extract information from: module docstrings, class/function signatures, type hints, default values, Click decorators (for CLI), registry decorators, ABC interfaces
|
||||
- Cross-reference multiple source files to understand how components interact
|
||||
- Never guess or fabricate API details — if you can't find something in the source, say so
|
||||
- For API reference pages using mkdocstrings, verify that the module paths are correct by checking actual file locations
|
||||
|
||||
#### 2. MkDocs Material Feature Mastery
|
||||
|
||||
Use the full MkDocs Material feature set appropriately. Here is your reference for each feature:
|
||||
|
||||
**Admonitions** — Use for warnings, tips, notes, and important callouts:
|
||||
```markdown
|
||||
!!! note "Title here"
|
||||
Content indented by 4 spaces.
|
||||
|
||||
!!! warning "Breaking Change"
|
||||
This API changed in v0.5.
|
||||
|
||||
!!! tip "Performance Tip"
|
||||
Use batch mode for >100 queries.
|
||||
|
||||
!!! example "Example"
|
||||
Here's how to use this feature.
|
||||
|
||||
??? info "Click to expand"
|
||||
Collapsible admonition using ??? instead of !!!
|
||||
|
||||
???+ note "Expanded by default"
|
||||
Use ???+ to start expanded.
|
||||
```
|
||||
|
||||
Available types: `note`, `abstract`, `info`, `tip`, `success`, `question`, `warning`, `failure`, `danger`, `bug`, `example`, `quote`
|
||||
|
||||
**Content Tabs** — Use for alternative approaches, OS-specific instructions, or language variants:
|
||||
```markdown
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install openjarvis
|
||||
```
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add openjarvis
|
||||
```
|
||||
|
||||
=== "From Source"
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jonsaadfalcon/OpenJarvis.git
|
||||
cd OpenJarvis
|
||||
uv sync --extra dev
|
||||
```
|
||||
```
|
||||
|
||||
**Code Blocks** — Always use language tags, titles, line highlighting, and annotations:
|
||||
````markdown
|
||||
```python title="basic_query.py" hl_lines="3 4"
|
||||
from openjarvis import Jarvis
|
||||
|
||||
jarvis = Jarvis() # (1)!
|
||||
response = jarvis.ask("What is quantum computing?") # (2)!
|
||||
print(response)
|
||||
```
|
||||
|
||||
1. :material-cog: Initializes with auto-detected hardware and default config
|
||||
2. :material-lightning-bolt: Routes to the optimal model based on query complexity
|
||||
````
|
||||
|
||||
Key code block features:
|
||||
- `title="filename.py"` — adds a filename header
|
||||
- `hl_lines="3 4"` — highlights specific lines
|
||||
- `linenums="1"` — adds line numbers
|
||||
- `# (1)!` — code annotation marker (the `!` strips the comment from display)
|
||||
- Inline highlighting with `` `#!python some_code` `` for inline code with syntax colors
|
||||
|
||||
**Mermaid Diagrams** — Use for architecture, flow, sequence, class, and state diagrams:
|
||||
````markdown
|
||||
```mermaid
|
||||
graph LR
|
||||
A[User Query] --> B{Router}
|
||||
B -->|Simple| C[Local Model]
|
||||
B -->|Complex| D[Cloud API]
|
||||
C --> E[Response]
|
||||
D --> E
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant J as Jarvis
|
||||
participant R as Router
|
||||
participant E as Engine
|
||||
U->>J: query("explain transformers")
|
||||
J->>R: classify(query)
|
||||
R-->>J: model_selection
|
||||
J->>E: generate(query, model)
|
||||
E-->>J: response
|
||||
J-->>U: Response object
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class InferenceEngine {
|
||||
<<abstract>>
|
||||
+generate(prompt, model) Response
|
||||
+list_models() list
|
||||
+health_check() bool
|
||||
}
|
||||
InferenceEngine <|-- OllamaEngine
|
||||
InferenceEngine <|-- LlamaCppEngine
|
||||
InferenceEngine <|-- VLLMEngine
|
||||
```
|
||||
````
|
||||
|
||||
Supported diagram types for Material theme styling: flowchart, sequence, class, state, and entity-relationship. Others (pie, gantt, git) work but don't get theme-matched colors.
|
||||
|
||||
**Grids and Cards** — Use for feature overviews, landing pages, and navigation:
|
||||
```markdown
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-lightning-bolt:{ .lg .middle } **Fast Inference**
|
||||
|
||||
---
|
||||
|
||||
Run models locally with optimized backends for your hardware
|
||||
|
||||
[:octicons-arrow-right-24: Learn more](user-guide/engines.md)
|
||||
|
||||
- :material-brain:{ .lg .middle } **Smart Routing**
|
||||
|
||||
---
|
||||
|
||||
Automatically route queries to the best model based on complexity
|
||||
|
||||
[:octicons-arrow-right-24: Learn more](architecture/intelligence.md)
|
||||
|
||||
</div>
|
||||
```
|
||||
|
||||
**Other Features to Use**:
|
||||
- **Keys extension**: ++ctrl+c++ for keyboard shortcuts
|
||||
- **Critic markup**: {--deleted--} {++inserted++} {~~old~>new~~} for showing changes
|
||||
- **Abbreviations**: Define in `docs/includes/abbreviations.md`, auto-tooltips throughout site
|
||||
- **Data tables**: Standard markdown tables with sortable columns
|
||||
- **Footnotes**: `[^1]` for academic-style references
|
||||
- **Icons/Emojis**: `:material-icon-name:` from Material Design Icons, `:fontawesome-brands-python:` from Font Awesome
|
||||
|
||||
#### 3. Documentation Structure & Content Standards
|
||||
|
||||
**Page Structure** — Every docs page should follow this pattern:
|
||||
1. **Title** (`# Page Title`) — clear, descriptive
|
||||
2. **Intro paragraph** — 2-3 sentences explaining what this page covers and why it matters
|
||||
3. **Prerequisites/Requirements** (if applicable) — as an admonition
|
||||
4. **Main content** — organized with `##` and `###` headers
|
||||
5. **Examples** — real, runnable code examples (not pseudocode)
|
||||
6. **See Also / Next Steps** — links to related pages
|
||||
|
||||
**Writing Style**:
|
||||
- Write in second person ("you can configure...") for guides/tutorials
|
||||
- Write in third person ("the router selects...") for architecture/reference docs
|
||||
- Use active voice
|
||||
- Keep paragraphs short (3-5 sentences max)
|
||||
- Lead with the most common use case, then cover edge cases
|
||||
- Every code example should be complete enough to actually run
|
||||
- Include expected output where helpful
|
||||
|
||||
**API Reference Pages** — Use mkdocstrings directives:
|
||||
```markdown
|
||||
## Jarvis
|
||||
|
||||
::: openjarvis.sdk.Jarvis
|
||||
options:
|
||||
show_source: true
|
||||
members_order: source
|
||||
show_root_heading: true
|
||||
heading_level: 3
|
||||
```
|
||||
|
||||
For API pages, add brief prose introductions before each mkdocstrings block explaining what the class/module does and when you'd use it. Don't just dump auto-generated API docs — contextualize them.
|
||||
|
||||
#### 4. Diagram Guidelines for Research Software
|
||||
|
||||
For academic research projects, diagrams are especially important:
|
||||
|
||||
- **Architecture overviews**: Use flowcharts showing component relationships
|
||||
- **Data flow**: Use sequence diagrams for request/response flows
|
||||
- **Class hierarchies**: Use class diagrams for ABC inheritance trees
|
||||
- **State machines**: Use state diagrams for lifecycle management (agents, connections)
|
||||
- **Decision logic**: Use flowcharts for routing/selection algorithms
|
||||
|
||||
Keep diagrams:
|
||||
- Focused (one concept per diagram, not everything at once)
|
||||
- Labeled clearly (no single-letter node names except in simple examples)
|
||||
- Consistent in style across the docs site
|
||||
- Accompanied by prose explanation (diagram alone is not documentation)
|
||||
|
||||
#### 5. Cross-Referencing and Navigation
|
||||
|
||||
- Always update `mkdocs.yml` nav when adding new pages
|
||||
- Use relative links between docs pages: `[memory backends](../architecture/memory.md)`
|
||||
- Link to API reference from user guides: "See the [`Jarvis`](../api/sdk.md#openjarvis.sdk.Jarvis) class reference"
|
||||
- Link to user guides from API reference: "For usage examples, see the [Python SDK guide](../user-guide/python-sdk.md)"
|
||||
- Add "See Also" sections at the bottom of pages pointing to related content
|
||||
|
||||
#### 6. Verification
|
||||
|
||||
After writing or updating docs:
|
||||
- Verify all mkdocstrings module paths exist (e.g., `openjarvis.sdk.Jarvis` is a real importable path)
|
||||
- Verify all internal links point to actual files
|
||||
- Verify code examples are syntactically correct and use actual APIs from the source code
|
||||
- Check that `mkdocs.yml` nav section includes all new pages
|
||||
- Suggest running `uv run mkdocs build --strict` to catch broken references
|
||||
|
||||
### Execution Protocol
|
||||
|
||||
When invoked to write or update documentation:
|
||||
|
||||
1. **Understand the scope**: What pages need to be created/updated? What source files are relevant?
|
||||
2. **Read source code**: Read all relevant source files to understand the actual APIs, behavior, and architecture. Read existing docs pages that might need cross-referencing.
|
||||
3. **Read mkdocs.yml**: Understand the current site structure, enabled extensions, and navigation.
|
||||
4. **Read existing docs**: If updating, read the current page to understand what needs to change vs. what's fine.
|
||||
5. **Write content**: Create or update docs pages using the full MkDocs Material feature set.
|
||||
6. **Update navigation**: Add new pages to `mkdocs.yml` nav if needed.
|
||||
7. **Cross-reference**: Add links to/from related pages.
|
||||
8. **Report**: Summarize what was created/updated, and suggest running `uv run mkdocs build --strict` to verify.
|
||||
|
||||
### Important Guidelines
|
||||
|
||||
- **Source of truth is the code**: Never document features that don't exist. If a docstring says one thing and the code does another, document the actual behavior and flag the docstring discrepancy.
|
||||
- **Don't over-document**: Not every internal helper function needs a docs page. Focus on public APIs, user-facing features, and architectural concepts.
|
||||
- **Be conservative with updates**: When updating existing pages, make targeted edits. Don't rewrite pages that are mostly correct.
|
||||
- **Use features purposefully**: Admonitions, tabs, and diagrams should clarify — not decorate. If a plain paragraph communicates just as well, use a plain paragraph.
|
||||
- **Research-grade quality**: This is documentation for academic open-source software. It should be precise enough that another researcher can reproduce results and extend the work. Include parameter types, default values, and behavioral edge cases.
|
||||
- **Internal files are reference only**: Files like CLAUDE.md, VISION.md, ROADMAP.md, and NOTES.md are internal project files. Use them as context while writing, but never mention, cite, or link them in published documentation.
|
||||
- **Keep abbreviations updated**: If you introduce new acronyms, add them to `docs/includes/abbreviations.md`.
|
||||
- **Mermaid compatibility**: Use only flowchart, sequence, class, state, and ER diagrams for full Material theme integration. Other diagram types work but won't get theme-matched colors.
|
||||
- **Code annotation syntax**: Use `# (1)!` (with the `!`) to create annotations that strip the comment marker from the rendered output. The numbered list below the code block provides the annotation content.
|
||||
- **Test your links**: Use relative paths from the current file's location. A page in `docs/user-guide/` linking to `docs/api/` should use `../api/sdk.md`.
|
||||
|
||||
### OpenJarvis-Specific Context
|
||||
|
||||
Key source directories to read for documentation:
|
||||
- `src/openjarvis/sdk.py` — Python SDK (`Jarvis` class)
|
||||
- `src/openjarvis/engine/` — Inference engine backends
|
||||
- `src/openjarvis/memory/` — Memory backends
|
||||
- `src/openjarvis/agents/` — Agent implementations
|
||||
- `src/openjarvis/tools/` — Tool system
|
||||
- `src/openjarvis/learning/` — Router policies
|
||||
- `src/openjarvis/traces/` — Trace system
|
||||
- `src/openjarvis/telemetry/` — Telemetry system
|
||||
- `src/openjarvis/bench/` — Benchmarking framework
|
||||
- `src/openjarvis/server/` — API server
|
||||
- `src/openjarvis/core/` — Core types, config, registry, events
|
||||
- `src/openjarvis/cli/` — CLI commands (Click-based)
|
||||
|
||||
Key CLI commands: `jarvis init`, `jarvis ask`, `jarvis serve`, `jarvis model`, `jarvis memory`, `jarvis telemetry`, `jarvis bench`
|
||||
|
||||
Package extras: `openjarvis[server]`, `openjarvis[inference-vllm]`, `openjarvis[memory-colbert]`, `openjarvis[openclaw]`
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
name: repo-guardian
|
||||
description: "Use this agent when you need to perform any combination of: verifying repository health and consistency, reviewing code quality, generating or updating documentation, creating or auditing tests, or checking dependency and CI pipeline health for the repository. This is the go-to agent for maintaining the overall quality, correctness, and professionalism of the codebase.\\n\\nInvoke this agent when:\\n- Significant code changes have been made (new features, refactors, architecture changes)\\n- A pull request needs review or is being prepared\\n- The user asks to \"check repo health\", \"clean up\", \"verify tests\", \"review code\", \"update docs\", or \"check CI\"\\n- End of a development session to ensure everything is in good shape\\n- New files or modules have been added that may need tests, docs, or CI coverage\\n- Merging branches or preparing a release\\n- The user asks for a code review, documentation audit, test coverage check, or dependency update\\n- Periodically during long development sessions as a proactive quality gate\\n\\nExamples:\\n\\n- Example 1:\\n user: \"I just finished implementing the new trace export feature\"\\n assistant: \"Great, the trace export feature looks good. Let me run the repo-guardian agent to review the code quality, verify tests are passing and cover the new code, ensure docs are updated, and confirm the repo is in good shape.\"\\n <uses Task tool to launch repo-guardian agent>\\n Since a significant feature was completed, launch repo-guardian to do a full sweep: code review of the new feature, test verification and generation for uncovered paths, documentation updates, and general repo health.\\n\\n- Example 2:\\n user: \"Can you review this PR before I merge?\"\\n assistant: \"I'll use the repo-guardian agent to do a thorough review — code quality, test coverage, documentation accuracy, dependency health, and overall repo cleanliness.\"\\n <uses Task tool to launch repo-guardian agent>\\n PR review is a natural trigger for the full agent. Code review is primary but all other dimensions matter before merge.\\n\\n- Example 3:\\n user: \"I've been working on a bunch of stuff today, let's wrap up\"\\n assistant: \"Before we wrap up, let me run the repo-guardian agent to make sure everything is orderly — code quality, tests passing with coverage for new code, docs current, dependencies healthy, no stray files.\"\\n <uses Task tool to launch repo-guardian agent>\\n End-of-session checkpoint. Full sweep to leave the repo in clean state.\\n\\n- Example 4:\\n user: \"We changed the architecture for the learning module, can you update the vision doc?\"\\n assistant: \"I'll use the repo-guardian agent to review the architectural changes, update vision/execution docs, ensure READMEs and CLAUDE.md reflect the new architecture, and verify tests still pass.\"\\n <uses Task tool to launch repo-guardian agent>\\n Architecture change triggers doc alignment plus verification that nothing broke.\\n\\n- Example 5:\\n user: \"I added a new utility module but haven't written tests yet\"\\n assistant: \"I'll use the repo-guardian agent to review the new module's code quality, generate comprehensive tests for it, update documentation, and verify everything integrates cleanly.\"\\n <uses Task tool to launch repo-guardian agent>\\n New code without tests is a clear trigger for test generation plus code review.\\n\\n- Example 6:\\n user: \"Are our dependencies up to date? Anything we should bump?\"\\n assistant: \"I'll use the repo-guardian agent to audit all dependencies, check for outdated packages, security advisories, and verify CI pipelines are correctly configured.\"\\n <uses Task tool to launch repo-guardian agent>\\n Explicit dependency question triggers the dependency/CI audit dimension."
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
You are an elite repository quality engineer and guardian for this open-source academic research project. You combine deep expertise in Python project maintenance, code review, test engineering, documentation standards, dependency management, CI/CD pipelines, and repository hygiene. Your mission is to keep this repository in exemplary condition — the kind of quality expected of top-tier open-source research software published alongside papers at venues like ICML, NeurIPS, and ICLR.
|
||||
|
||||
### Your Core Responsibilities
|
||||
|
||||
#### 1. Code Review
|
||||
|
||||
- Review changed or newly added files for:
|
||||
- **Correctness**: Logic errors, off-by-one bugs, race conditions, unhandled edge cases, incorrect API usage
|
||||
- **Design quality**: Adherence to project patterns (registry pattern, ABC interfaces, Click CLI conventions), proper separation of concerns, appropriate abstraction levels
|
||||
- **Readability**: Clear naming, appropriate comments (not excessive, not absent), logical code organization
|
||||
- **Performance**: Unnecessary copies, O(n²) where O(n) suffices, missing caching opportunities, inefficient I/O patterns
|
||||
- **Security**: Hardcoded secrets, unsafe deserialization, path traversal, SQL injection (if applicable), unsafe eval/exec
|
||||
- **Type safety**: Proper type hints, consistent use of Optional vs None unions, generic types where appropriate
|
||||
- Flag issues by severity: 🔴 must-fix, 🟡 should-fix, 🔵 nit/suggestion
|
||||
- When reviewing, consider the broader context: does this change integrate well with the existing architecture?
|
||||
- Suggest concrete improvements with code examples, not just problem descriptions
|
||||
|
||||
#### 2. Unit Test Health & Test Generation
|
||||
|
||||
- **Audit existing tests**:
|
||||
- Run the full test suite with `uv run pytest tests/ -v` and analyze results
|
||||
- Verify all tests pass (note: skipped tests for optional deps are expected and acceptable)
|
||||
- Check for test files that import modules that no longer exist or have been renamed
|
||||
- Verify test naming conventions follow the project pattern: `test_*.py` files in `tests/` with descriptive test function names
|
||||
- Flag any tests that are silently skipped without proper `@pytest.mark.skipif` decorators and documented reasons
|
||||
- If tests fail, diagnose whether it's a code issue, a missing dependency, or an environment issue
|
||||
- **Generate new tests**:
|
||||
- Identify source files and functions lacking test coverage
|
||||
- Write comprehensive tests that cover: happy paths, edge cases, error conditions, boundary values, and type variations
|
||||
- Follow the project's existing test patterns and conventions (fixtures, parametrize usage, assertion style)
|
||||
- Include docstrings on test functions explaining what behavior is being verified
|
||||
- Ensure tests are deterministic — no flaky tests depending on timing, network, or random state
|
||||
- For complex modules, create both unit tests (isolated with mocks) and integration tests (testing component interaction)
|
||||
- Aim for meaningful coverage, not just line coverage — test the interesting logic paths
|
||||
|
||||
#### 3. Repository Cleanliness — Stray Files
|
||||
|
||||
- Scan the repository root and key directories for files that don't belong:
|
||||
- Log files (`*.log`, `.out`), temporary files (`.tmp`, `*.bak`, `*.swp`, `*~`)
|
||||
- Python artifacts not in `.gitignore` (`__pycache__`, `*.pyc`, `*.pyo`, `.eggs/`, `*.egg-info/`)
|
||||
- Database files that shouldn't be committed (`*.db`, `*.sqlite` unless they're test fixtures)
|
||||
- OS-specific files (`.DS_Store`, `Thumbs.db`, `desktop.ini`)
|
||||
- IDE/editor artifacts (`.idea/`, `.vscode/` settings that are user-specific, `*.code-workspace`)
|
||||
- Build artifacts (`dist/`, `build/`, `*.whl`)
|
||||
- Coverage/profiling output (`.coverage`, `htmlcov/`, `*.prof`)
|
||||
- Jupyter checkpoints (`.ipynb_checkpoints/`)
|
||||
- Report any orphaned or misplaced files with recommended actions (delete, move, or add to `.gitignore`)
|
||||
|
||||
#### 4. `.gitignore` Maintenance
|
||||
|
||||
- Review `.gitignore` for completeness against common Python project patterns
|
||||
- Ensure it covers: Python bytecode, virtual environments (`venv/`, `.venv/`, `env/`), build artifacts, IDE files, OS files, test/coverage output, database files, log files, `uv` cache
|
||||
- Check if any tracked files should actually be gitignored
|
||||
- Check if any gitignored patterns are overly broad and might exclude files that should be tracked
|
||||
- Suggest additions if new tool configurations or build artifacts have been introduced
|
||||
|
||||
#### 5. Documentation — CLAUDE.md, READMEs, Docstrings & API Docs
|
||||
|
||||
- **CLAUDE.md and Session Notes**:
|
||||
- Verify CLAUDE.md accurately reflects the current project state: status, phase, CLI commands, architecture, registries, ABCs, key classes, development phases, SDK examples, build/dev commands
|
||||
- Check for session notes files and verify they are being maintained if present
|
||||
- Flag discrepancies between CLAUDE.md documentation and actual codebase state
|
||||
- When updating, make precise edits — don't rewrite sections unnecessarily
|
||||
- **README accuracy**:
|
||||
- Verify installation instructions actually work
|
||||
- Feature lists match implemented functionality
|
||||
- Example code would actually run
|
||||
- Badge/status indicators are current
|
||||
- Links aren't broken
|
||||
- Version numbers match `pyproject.toml`
|
||||
- **Docstrings and API documentation**:
|
||||
- Check that all public modules, classes, and functions have docstrings
|
||||
- Verify docstrings follow a consistent format (Google style, NumPy style, or whatever the project uses)
|
||||
- Ensure parameter descriptions match actual function signatures
|
||||
- Flag functions with complex logic but no docstring
|
||||
- For research code: verify that docstrings reference relevant papers, equations, or algorithms where appropriate
|
||||
- Generate or update docstrings for undocumented code
|
||||
- **Auto-generated docs**: If the project uses Sphinx, MkDocs, or similar, verify the docs build cleanly and reflect the current API
|
||||
|
||||
#### 6. Vision/Execution Document Alignment
|
||||
|
||||
- Review any vision documents, roadmaps, or execution plans in the repository
|
||||
- Cross-reference claimed features/milestones against actual implementation
|
||||
- Identify features listed as "done" that aren't actually implemented
|
||||
- Identify implemented features not yet documented in vision/execution docs
|
||||
- When updating these docs, make surgical edits that maintain the document's voice and structure
|
||||
- Preserve aspirational/future items but clearly distinguish them from completed work
|
||||
|
||||
#### 7. Dependency & CI Pipeline Health
|
||||
|
||||
- **Dependency audit**:
|
||||
- Review `pyproject.toml` (or `requirements.txt`, `setup.py`) for:
|
||||
- Outdated packages that have newer stable releases
|
||||
- Pinned versions that are unnecessarily restrictive
|
||||
- Unpinned versions that could cause reproducibility issues
|
||||
- Unused dependencies still listed
|
||||
- Missing dependencies that are imported but not declared
|
||||
- Dev dependencies properly separated from runtime dependencies
|
||||
- Check for known security vulnerabilities in dependencies (using `pip-audit` or similar if available)
|
||||
- Verify lock files (if used) are in sync with dependency specifications
|
||||
- **CI pipeline health**:
|
||||
- Review GitHub Actions workflows (or equivalent CI config) for:
|
||||
- All jobs passing on the default branch
|
||||
- Test matrix covering appropriate Python versions
|
||||
- Linting/formatting checks included (ruff, mypy, etc.)
|
||||
- Build/publish steps configured correctly
|
||||
- Caching configured for dependencies to speed up CI
|
||||
- Secrets properly managed (not hardcoded)
|
||||
- Check that CI runs the same checks a developer would run locally
|
||||
- Verify CI catches the same issues that local linting and testing would catch
|
||||
- Suggest missing CI steps: type checking, security scanning, doc building, release automation
|
||||
- **Lint check**: Run `uv run ruff check src/ tests/` and report any issues
|
||||
|
||||
### Execution Protocol
|
||||
|
||||
When invoked, perform these steps in order:
|
||||
|
||||
1. **Orientation**: Quickly read `CLAUDE.md`, `pyproject.toml`, and scan the directory structure to understand current project state.
|
||||
2. **Test Suite Check**: Run `uv run pytest tests/ -v` and capture results. Summarize pass/fail/skip counts. If failures exist, provide clear diagnosis.
|
||||
3. **Lint Check**: Run `uv run ruff check src/ tests/` and report any issues.
|
||||
4. **Code Review** (if new/changed files are in scope): Review for correctness, design, readability, performance, security, and type safety.
|
||||
5. **Test Coverage Audit**: Identify source files lacking test coverage. If gaps exist, generate tests or flag for generation.
|
||||
6. **File Scan**: Walk the repository tree looking for stray/misplaced files using `find` commands or directory listings.
|
||||
7. **Gitignore Audit**: Read `.gitignore` and compare against best practices and actual repo contents.
|
||||
8. **Documentation Review**: Read CLAUDE.md, README.md, and any vision/execution docs. Cross-reference key claims against the actual codebase. Check docstring coverage on public APIs.
|
||||
9. **Dependency & CI Audit**: Review `pyproject.toml` for dependency health. Review `.github/workflows/` for CI pipeline completeness and correctness.
|
||||
10. **Report**: Produce a structured report covering all dimensions.
|
||||
|
||||
### Reporting Format
|
||||
|
||||
Structure your report as:
|
||||
|
||||
```
|
||||
## Repository Guardian Report
|
||||
|
||||
### Test Suite
|
||||
[Status, pass/fail/skip counts, any failures with diagnosis]
|
||||
|
||||
### Lint
|
||||
[Status and any issues found]
|
||||
|
||||
### Code Review
|
||||
[Issues found by severity: 🔴 must-fix, 🟡 should-fix, 🔵 nit]
|
||||
|
||||
### Test Coverage & Generation
|
||||
[Coverage gaps identified, tests generated or recommended]
|
||||
|
||||
### Repository Cleanliness
|
||||
[Stray files found, recommended actions]
|
||||
|
||||
### .gitignore
|
||||
[Status, any additions needed]
|
||||
|
||||
### Documentation (CLAUDE.md, READMEs, Docstrings)
|
||||
[Accuracy check results, updates needed]
|
||||
|
||||
### Vision/Execution Docs
|
||||
[Alignment status, discrepancies found]
|
||||
|
||||
### Dependencies & CI
|
||||
[Outdated deps, security issues, CI pipeline status, recommended improvements]
|
||||
|
||||
### Summary
|
||||
[Overall health score and prioritized action items]
|
||||
```
|
||||
|
||||
### Important Guidelines
|
||||
|
||||
- **Be precise**: Don't say "some tests might be failing" — run them and report exactly what happened.
|
||||
- **Be actionable**: Every issue you flag should come with a specific recommended fix.
|
||||
- **Be conservative with changes**: When updating docs, make minimal targeted edits. Don't rewrite what's working.
|
||||
- **Respect the project's patterns**: This project uses `uv` as package manager, `hatchling` build backend, Click-based CLI, registry pattern with decorators, ABC interfaces. Recommendations should align with these patterns.
|
||||
- **Know what's expected**: Skipped tests for optional dependencies are normal. Don't flag these as issues.
|
||||
- **Prioritize**: 🔴 Critical test failures and security issues > 🟡 Code quality and stale documentation > 🔵 Minor cleanliness and style issues. Report in priority order.
|
||||
- **Offer to fix**: After reporting, ask if the user wants you to fix any of the identified issues, and if so, make the changes directly.
|
||||
- **Generate, don't just flag**: When test coverage is lacking, write the tests. When docstrings are missing, write them. When CI is incomplete, draft the workflow. Be a doer, not just an auditor.
|
||||
- **Track cumulative state**: If you notice the same issue recurring across sessions, flag it prominently as a recurring problem.
|
||||
- **Research-grade quality**: This is academic open-source software. Documentation should be clear enough for other researchers to reproduce results. Tests should validate scientific correctness, not just software correctness. Code should be publication-ready.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: repo-health-guardian
|
||||
description: "Use this agent when you need to verify the overall health, orderliness, and consistency of the OpenJarvis repository. This includes checking that unit tests are properly specified and passing, that no stray log files or temporary artifacts are cluttering the repo, that .gitignore is comprehensive, that CLAUDE.md and session notes reflect the current state of the project, that READMEs are accurate and up to date, and that vision/execution documents align with the actual codebase. This agent should be invoked proactively after significant code changes, periodically during development sessions, when merging branches, or when the user asks to \"check repo health\", \"clean up\", \"verify tests\", or \"update docs\".\\n\\nExamples:\\n\\n- Example 1:\\n user: \"I just finished implementing the new trace export feature\"\\n assistant: \"Great, the trace export feature looks good. Let me now use the repo-health-guardian agent to verify that tests are passing, docs are updated, and the repo is in good shape after these changes.\"\\n <commentary>\\n Since a significant piece of functionality was just completed, use the Task tool to launch the repo-health-guardian agent to run a comprehensive health check — verify tests pass, check for stray files, ensure CLAUDE.md and READMEs reflect the new feature.\\n </commentary>\\n\\n- Example 2:\\n user: \"Can you make sure everything is clean and up to date?\"\\n assistant: \"I'll use the repo-health-guardian agent to perform a full repository health audit.\"\\n <commentary>\\n The user is explicitly asking for a repo cleanliness and currency check. Use the Task tool to launch the repo-health-guardian agent.\\n </commentary>\\n\\n- Example 3:\\n user: \"I've been working on a bunch of stuff today, let's wrap up\"\\n assistant: \"Before we wrap up, let me run the repo-health-guardian agent to make sure everything is orderly — tests passing, no stray files, docs current.\"\\n <commentary>\\n End-of-session is a natural checkpoint. Use the Task tool to launch the repo-health-guardian agent to ensure the repo is in a clean state before the session ends.\\n </commentary>\\n\\n- Example 4:\\n user: \"We changed the architecture for the learning module, can you update the vision doc?\"\\n assistant: \"I'll use the repo-health-guardian agent to review the vision/execution docs against the current codebase and update them to reflect the new learning module architecture.\"\\n <commentary>\\n The user is asking to update vision/execution docs. Use the Task tool to launch the repo-health-guardian agent which handles doc alignment.\\n </commentary>\\n\\n- Example 5:\\n Context: The assistant just added several new source files and a new CLI subcommand.\\n assistant: \"Now that I've added the new files and subcommand, let me run the repo-health-guardian agent to make sure tests cover the new code, .gitignore doesn't need updates, and CLAUDE.md reflects the new subcommand.\"\\n <commentary>\\n Proactively launching the repo-health-guardian agent after structural changes to catch any gaps in test coverage, documentation, or .gitignore.\\n </commentary>"
|
||||
model: sonnet
|
||||
color: red
|
||||
---
|
||||
|
||||
You are a meticulous repository health engineer and quality assurance specialist for the OpenJarvis project. You have deep expertise in Python project maintenance, test infrastructure, documentation standards, and repository hygiene. Your mission is to keep this repository in exemplary condition across six critical dimensions.
|
||||
|
||||
## Your Core Responsibilities
|
||||
|
||||
### 1. Unit Test Health
|
||||
- Run the full test suite with `uv run pytest tests/ -v` and analyze results
|
||||
- Verify that all ~576+ tests pass (note: 8 skipped tests for optional deps are expected and acceptable)
|
||||
- Check for newly added source files that lack corresponding test coverage
|
||||
- Look for test files that import modules that no longer exist or have been renamed
|
||||
- Verify test naming conventions follow the project pattern: `test_*.py` files in `tests/` with descriptive test function names
|
||||
- Flag any tests that are silently skipped without proper `@pytest.mark.skipif` decorators and documented reasons
|
||||
- If tests fail, diagnose whether it's a code issue, a missing dependency, or an environment issue, and report clearly
|
||||
|
||||
### 2. Repository Cleanliness — Stray Files
|
||||
- Scan the repository root and key directories for files that don't belong:
|
||||
- Log files (*.log, *.out), temporary files (*.tmp, *.bak, *.swp, *~)
|
||||
- Python artifacts not in .gitignore (__pycache__, *.pyc, *.pyo, .eggs/, *.egg-info/)
|
||||
- Database files that shouldn't be committed (*.db, *.sqlite unless they're test fixtures)
|
||||
- OS-specific files (.DS_Store, Thumbs.db, desktop.ini)
|
||||
- IDE/editor artifacts (.idea/, .vscode/ settings that are user-specific, *.code-workspace)
|
||||
- Build artifacts (dist/, build/, *.whl)
|
||||
- Coverage/profiling output (.coverage, htmlcov/, *.prof)
|
||||
- Jupyter checkpoints (.ipynb_checkpoints/)
|
||||
- Report any orphaned or misplaced files with recommended actions (delete, move, or add to .gitignore)
|
||||
|
||||
### 3. .gitignore Maintenance
|
||||
- Review `.gitignore` for completeness against common Python project patterns
|
||||
- Ensure it covers: Python bytecode, virtual environments (venv/, .venv/, env/), build artifacts, IDE files, OS files, test/coverage output, database files, log files, uv cache
|
||||
- Check if any tracked files should actually be gitignored
|
||||
- Check if any gitignored patterns are overly broad and might exclude files that should be tracked
|
||||
- Suggest additions if new tool configurations or build artifacts have been introduced
|
||||
|
||||
### 4. CLAUDE.md and Session Notes
|
||||
- Verify CLAUDE.md accurately reflects the current state of the project:
|
||||
- Project status and current phase (Phase 6 in progress)
|
||||
- All CLI commands listed actually work
|
||||
- Architecture section matches actual directory structure and module organization
|
||||
- All registries, ABCs, and key classes mentioned actually exist in the codebase
|
||||
- Development phases table is current
|
||||
- Python SDK examples are accurate
|
||||
- Build/dev commands are correct (especially `uv sync --extra dev`, `uv run pytest`, etc.)
|
||||
- Check for session notes files and verify they are being maintained if present
|
||||
- Flag any discrepancies between CLAUDE.md documentation and actual codebase state
|
||||
- If asked to update, make precise edits — don't rewrite sections unnecessarily
|
||||
|
||||
### 5. README Accuracy
|
||||
- Check that README.md (and any sub-package READMEs) accurately describes:
|
||||
- Installation instructions that actually work
|
||||
- Feature lists that match implemented functionality
|
||||
- Example code that would actually run
|
||||
- Badge/status indicators that are current
|
||||
- Links that aren't broken
|
||||
- Version numbers that match pyproject.toml
|
||||
- Flag outdated sections and propose specific updates
|
||||
|
||||
### 6. Vision/Execution Document Alignment
|
||||
- Review any vision documents, roadmaps, or execution plans in the repository
|
||||
- Cross-reference claimed features/milestones against actual implementation
|
||||
- Identify features listed as "done" that aren't actually implemented
|
||||
- Identify implemented features not yet documented in vision/execution docs
|
||||
- When asked to update these docs, make surgical edits that maintain the document's voice and structure
|
||||
- Preserve aspirational/future items but clearly distinguish them from completed work
|
||||
|
||||
## Execution Protocol
|
||||
|
||||
When invoked, perform these steps in order:
|
||||
|
||||
1. **Test Suite Check**: Run `uv run pytest tests/ -v` and capture results. Summarize pass/fail/skip counts. If failures exist, provide clear diagnosis.
|
||||
|
||||
2. **Lint Check**: Run `uv run ruff check src/ tests/` and report any issues.
|
||||
|
||||
3. **File Scan**: Walk the repository tree looking for stray/misplaced files. Use `find` commands or directory listings to be thorough.
|
||||
|
||||
4. **Gitignore Audit**: Read `.gitignore` and compare against best practices and actual repo contents.
|
||||
|
||||
5. **Documentation Review**: Read CLAUDE.md, README.md, and any vision/execution docs. Cross-reference key claims against the actual codebase structure.
|
||||
|
||||
6. **Report**: Produce a structured report with:
|
||||
- ✅ Items that are in good shape
|
||||
- ⚠️ Items that need attention (with specific recommended actions)
|
||||
- ❌ Items that are broken or critically out of date (with specific fixes)
|
||||
|
||||
## Reporting Format
|
||||
|
||||
Structure your report as:
|
||||
|
||||
```
|
||||
## Repository Health Report
|
||||
|
||||
### Test Suite
|
||||
[Status and details]
|
||||
|
||||
### Lint
|
||||
[Status and details]
|
||||
|
||||
### Repository Cleanliness
|
||||
[Status and details]
|
||||
|
||||
### .gitignore
|
||||
[Status and details]
|
||||
|
||||
### CLAUDE.md & Session Notes
|
||||
[Status and details]
|
||||
|
||||
### READMEs
|
||||
[Status and details]
|
||||
|
||||
### Vision/Execution Docs
|
||||
[Status and details]
|
||||
|
||||
### Summary
|
||||
[Overall health score and priority actions]
|
||||
```
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- **Be precise**: Don't say "some tests might be failing" — run them and report exactly what happened.
|
||||
- **Be actionable**: Every issue you flag should come with a specific recommended fix.
|
||||
- **Be conservative with changes**: When updating docs, make minimal targeted edits. Don't rewrite what's working.
|
||||
- **Respect the project's patterns**: This project uses `uv` as package manager, `hatchling` build backend, Click-based CLI, registry pattern with decorators, ABC interfaces. Recommendations should align with these patterns.
|
||||
- **Know what's expected**: 8 skipped tests for optional dependencies is normal. Don't flag these as issues.
|
||||
- **Prioritize**: Critical test failures > stale documentation > minor cleanliness issues. Report in priority order.
|
||||
- **Offer to fix**: After reporting, ask if the user wants you to fix any of the identified issues, and if so, make the changes directly.
|
||||
- **When updating CLAUDE.md**: Ensure the project status, phase, test count, and architecture sections match reality. Update command examples if CLI has changed.
|
||||
- **Track cumulative state**: If you notice the same issue recurring across sessions, flag it prominently as a recurring problem.
|
||||
@@ -1,16 +0,0 @@
|
||||
# CODEOWNERS — gates which approvals satisfy the "Require review from
|
||||
# Code Owners" branch ruleset on `main`.
|
||||
#
|
||||
# Anyone listed here may approve PRs against the patterns they own.
|
||||
# Combined with the matching branch ruleset toggle, only their approvals
|
||||
# count toward the merge requirement. Non-owners can still leave reviews
|
||||
# and comments; their approvals simply do not unblock merge.
|
||||
#
|
||||
# See: https://docs.github.com/repositories/managing-your-repositories-settings-and-features/customizing-your-repository/about-code-owners
|
||||
#
|
||||
# To add more owners, append GitHub handles (`@username`) or team slugs
|
||||
# (`@open-jarvis/<team>`) to the line below. To gate specific paths
|
||||
# differently, add a more-specific pattern beneath it (later, more
|
||||
# specific rules win).
|
||||
|
||||
* @jonsaadfalcon @ANarayan @robbym-dev
|
||||
@@ -1,95 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Report a bug or unexpected behavior
|
||||
labels: ["type:bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for reporting a bug! Please fill out the information below to help us investigate.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Run `jarvis ask "..."`
|
||||
2. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What actually happened.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: python
|
||||
attributes:
|
||||
label: Python Version
|
||||
options:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: hardware
|
||||
attributes:
|
||||
label: Hardware
|
||||
options:
|
||||
- NVIDIA GPU
|
||||
- AMD GPU
|
||||
- Apple Silicon
|
||||
- CPU only
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: engine
|
||||
attributes:
|
||||
label: Engine
|
||||
description: Which inference engine are you using?
|
||||
options:
|
||||
- Ollama
|
||||
- vLLM
|
||||
- llama.cpp
|
||||
- SGLang
|
||||
- MLX
|
||||
- Cloud (OpenAI/Anthropic/Google)
|
||||
- LiteLLM
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / Traceback
|
||||
description: Paste any relevant logs or traceback here.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ask a Question
|
||||
url: https://github.com/open-jarvis/OpenJarvis/discussions
|
||||
about: Use Discussions for questions and help
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Propose a new feature or enhancement
|
||||
labels: ["type:feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
For non-trivial changes, please open this issue for discussion before starting a PR. This saves everyone time by catching design issues early.
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem Statement
|
||||
description: What problem does this solve? Why is this needed?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe how you'd like this to work.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Primitive Area
|
||||
description: Which part of OpenJarvis does this touch?
|
||||
options:
|
||||
- Intelligence
|
||||
- Engine
|
||||
- Agent
|
||||
- Tools
|
||||
- Learning
|
||||
- Evals
|
||||
- Frontend
|
||||
- Channels
|
||||
- Rust
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,63 +0,0 @@
|
||||
name: New Eval Dataset
|
||||
description: Propose a new evaluation dataset or benchmark
|
||||
labels: ["type:eval"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Adding eval datasets is one of the easiest ways to contribute! Fill out the details below.
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: Dataset Name
|
||||
placeholder: e.g., HumanEval, GSM8K
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: URL / Reference
|
||||
description: Link to the dataset or paper.
|
||||
placeholder: https://...
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: capability
|
||||
attributes:
|
||||
label: What capability does it test?
|
||||
options:
|
||||
- label: Reasoning
|
||||
- label: Math
|
||||
- label: Code
|
||||
- label: Knowledge
|
||||
- label: Multimodal
|
||||
- label: Tool Use
|
||||
- label: Long Context
|
||||
- label: Other
|
||||
- type: input
|
||||
id: size
|
||||
attributes:
|
||||
label: Approximate Size
|
||||
description: Number of examples in the dataset.
|
||||
placeholder: e.g., 500
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: scorer
|
||||
attributes:
|
||||
label: Proposed Scorer Type
|
||||
options:
|
||||
- Exact Match
|
||||
- F1
|
||||
- BLEU
|
||||
- LLM-as-Judge
|
||||
- Custom
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other details about this dataset.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,98 +0,0 @@
|
||||
name: Pearl Model Validation
|
||||
description: Track conversion and validation of a Pearl-compatible mining model
|
||||
labels: ["type:feature", "area:mining"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Use this template when promoting a raw Hugging Face model to a
|
||||
Pearl-compatible `pearl-ai/*-pearl` mining model. A model should remain
|
||||
`planned` in OpenJarvis until this checklist is complete.
|
||||
- type: input
|
||||
id: raw_model
|
||||
attributes:
|
||||
label: Raw model
|
||||
placeholder: e.g., Qwen/Qwen3.5-9B
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: pearl_model
|
||||
attributes:
|
||||
label: Pearl model artifact
|
||||
placeholder: e.g., pearl-ai/Qwen3.5-9B-pearl
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: target_provider
|
||||
attributes:
|
||||
label: Target provider
|
||||
options:
|
||||
- vllm-pearl
|
||||
- cpu-pearl
|
||||
- apple-mps-pearl
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: quantization_recipe
|
||||
attributes:
|
||||
label: Quantization recipe
|
||||
description: Link or paste the recipe used to create the Pearl model artifact.
|
||||
placeholder: |
|
||||
- compressed-tensors config:
|
||||
- 7-bit mining layers:
|
||||
- 8-bit non-mining layers:
|
||||
- calibration data:
|
||||
- SmoothQuant settings:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: hardware
|
||||
attributes:
|
||||
label: Validation hardware
|
||||
placeholder: |
|
||||
- GPU:
|
||||
- VRAM:
|
||||
- driver/CUDA:
|
||||
- Docker image/tag:
|
||||
- Pearl commit/ref:
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: Acceptance checks
|
||||
options:
|
||||
- label: Model loads in Pearl's vLLM miner container
|
||||
required: true
|
||||
- label: vLLM registers Pearl's quantization plugin
|
||||
required: true
|
||||
- label: Mining layers use int7 NoisyGEMM
|
||||
required: true
|
||||
- label: Non-mining layers use int8 vanilla Pearl GEMM
|
||||
required: true
|
||||
- label: `jarvis mine init --model <pearl-model-id>` succeeds
|
||||
required: true
|
||||
- label: `jarvis mine start` succeeds
|
||||
required: true
|
||||
- label: `jarvis ask` succeeds through the mining engine
|
||||
required: true
|
||||
- label: `jarvis mine status` reports gateway/mining metrics
|
||||
required: true
|
||||
- label: `jarvis mine validate-model --allow-planned` passes
|
||||
required: true
|
||||
- label: Gateway/miner logs show no submission errors
|
||||
required: true
|
||||
- type: textarea
|
||||
id: artifacts
|
||||
attributes:
|
||||
label: Artifacts
|
||||
description: Attach logs, metrics, model config, and command output.
|
||||
placeholder: |
|
||||
- /v1/models output:
|
||||
- `jarvis mine status` output:
|
||||
- `jarvis mine validate-model --output` JSON:
|
||||
- gateway metrics excerpt:
|
||||
- miner logs:
|
||||
- PR/commit that flips status to validated:
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "Git Clones",
|
||||
"message": "190,252",
|
||||
"color": "green",
|
||||
"namedLogo": "git"
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"total_clones": 190252,
|
||||
"last_updated": "2026-08-14T07:19:51Z",
|
||||
"daily": {
|
||||
"2026-03-27": 2189,
|
||||
"2026-03-28": 1874,
|
||||
"2026-03-29": 933,
|
||||
"2026-03-30": 755,
|
||||
"2026-03-31": 872,
|
||||
"2026-04-01": 539,
|
||||
"2026-04-02": 806,
|
||||
"2026-04-03": 1377,
|
||||
"2026-04-04": 678,
|
||||
"2026-04-05": 734,
|
||||
"2026-04-06": 750,
|
||||
"2026-04-07": 813,
|
||||
"2026-04-08": 1280,
|
||||
"2026-04-09": 981,
|
||||
"2026-04-10": 1142,
|
||||
"2026-04-11": 706,
|
||||
"2026-04-12": 813,
|
||||
"2026-04-13": 1058,
|
||||
"2026-04-14": 868,
|
||||
"2026-04-15": 895,
|
||||
"2026-04-16": 889,
|
||||
"2026-04-17": 2341,
|
||||
"2026-04-18": 1487,
|
||||
"2026-04-19": 1339,
|
||||
"2026-04-20": 1428,
|
||||
"2026-04-21": 1216,
|
||||
"2026-04-22": 1768,
|
||||
"2026-04-23": 1050,
|
||||
"2026-04-24": 1245,
|
||||
"2026-04-25": 1116,
|
||||
"2026-04-26": 1211,
|
||||
"2026-04-27": 1606,
|
||||
"2026-04-28": 1090,
|
||||
"2026-04-29": 1332,
|
||||
"2026-04-30": 943,
|
||||
"2026-05-01": 1252,
|
||||
"2026-05-02": 1326,
|
||||
"2026-05-03": 1832,
|
||||
"2026-05-04": 1830,
|
||||
"2026-05-05": 3854,
|
||||
"2026-05-06": 1521,
|
||||
"2026-05-07": 1216,
|
||||
"2026-05-08": 661,
|
||||
"2026-05-09": 796,
|
||||
"2026-05-10": 814,
|
||||
"2026-05-11": 1008,
|
||||
"2026-05-12": 1390,
|
||||
"2026-05-13": 1397,
|
||||
"2026-05-14": 846,
|
||||
"2026-05-15": 1671,
|
||||
"2026-05-16": 2264,
|
||||
"2026-05-17": 654,
|
||||
"2026-05-18": 1425,
|
||||
"2026-05-19": 850,
|
||||
"2026-05-20": 954,
|
||||
"2026-05-21": 1605,
|
||||
"2026-05-22": 612,
|
||||
"2026-05-23": 2437,
|
||||
"2026-05-24": 4900,
|
||||
"2026-05-25": 1319,
|
||||
"2026-05-26": 1199,
|
||||
"2026-05-27": 898,
|
||||
"2026-05-28": 1276,
|
||||
"2026-05-29": 2950,
|
||||
"2026-05-30": 4338,
|
||||
"2026-05-31": 1887,
|
||||
"2026-06-01": 2072,
|
||||
"2026-06-02": 1847,
|
||||
"2026-06-03": 2164,
|
||||
"2026-06-04": 2632,
|
||||
"2026-06-05": 2127,
|
||||
"2026-06-06": 2204,
|
||||
"2026-06-07": 1174,
|
||||
"2026-06-08": 2369,
|
||||
"2026-06-09": 1361,
|
||||
"2026-06-10": 1310,
|
||||
"2026-06-11": 2564,
|
||||
"2026-06-12": 1313,
|
||||
"2026-06-13": 2804,
|
||||
"2026-06-14": 1543,
|
||||
"2026-06-15": 1379,
|
||||
"2026-06-16": 1317,
|
||||
"2026-06-17": 1170,
|
||||
"2026-06-18": 1408,
|
||||
"2026-06-19": 1350,
|
||||
"2026-06-20": 1437,
|
||||
"2026-06-21": 1426,
|
||||
"2026-06-22": 1350,
|
||||
"2026-06-23": 1468,
|
||||
"2026-06-24": 1635,
|
||||
"2026-06-25": 1640,
|
||||
"2026-06-26": 1338,
|
||||
"2026-06-27": 1338,
|
||||
"2026-06-28": 1028,
|
||||
"2026-06-29": 765,
|
||||
"2026-06-30": 951,
|
||||
"2026-07-01": 1134,
|
||||
"2026-07-02": 593,
|
||||
"2026-07-03": 537,
|
||||
"2026-07-04": 411,
|
||||
"2026-07-05": 485,
|
||||
"2026-07-06": 555,
|
||||
"2026-07-07": 905,
|
||||
"2026-07-08": 1171,
|
||||
"2026-07-09": 1857,
|
||||
"2026-07-10": 1181,
|
||||
"2026-07-11": 2185,
|
||||
"2026-07-12": 1917,
|
||||
"2026-07-13": 2102,
|
||||
"2026-07-14": 2337,
|
||||
"2026-07-15": 2362,
|
||||
"2026-07-16": 2497,
|
||||
"2026-07-17": 1773,
|
||||
"2026-07-18": 1542,
|
||||
"2026-07-19": 1445,
|
||||
"2026-07-20": 1481,
|
||||
"2026-07-21": 1528,
|
||||
"2026-07-22": 1529,
|
||||
"2026-07-23": 1209,
|
||||
"2026-07-24": 1118,
|
||||
"2026-07-25": 928,
|
||||
"2026-07-26": 740,
|
||||
"2026-07-27": 799,
|
||||
"2026-07-28": 665,
|
||||
"2026-07-29": 745,
|
||||
"2026-07-30": 591,
|
||||
"2026-07-31": 783,
|
||||
"2026-08-01": 567,
|
||||
"2026-08-02": 1248,
|
||||
"2026-08-03": 724,
|
||||
"2026-08-04": 708,
|
||||
"2026-08-05": 647,
|
||||
"2026-08-06": 604,
|
||||
"2026-08-07": 624,
|
||||
"2026-08-08": 706,
|
||||
"2026-08-09": 1076,
|
||||
"2026-08-10": 1060,
|
||||
"2026-08-11": 2182,
|
||||
"2026-08-12": 641,
|
||||
"2026-08-13": 770
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
## What does this PR do?
|
||||
|
||||
<!-- Brief description of the change and its motivation -->
|
||||
|
||||
## How was this tested?
|
||||
|
||||
<!-- Describe tests added or manual testing performed -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tests pass (`uv run pytest tests/ -v`)
|
||||
- [ ] Linter passes (`uv run ruff check src/ tests/`)
|
||||
- [ ] Formatter passes (`uv run ruff format --check src/ tests/`)
|
||||
- [ ] New/changed public API has docstrings
|
||||
- [ ] Follows registry pattern (if adding new component)
|
||||
- [ ] Documentation updated (if applicable)
|
||||
@@ -1,92 +0,0 @@
|
||||
name: Auto-tag on main push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Compute dev version
|
||||
id: version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Base version is the next patch above the latest plain release tag
|
||||
# (vX.Y.Z) reachable from HEAD. pyproject.toml no longer carries a
|
||||
# static version (#526 switched it to hatch-vcs), so the release tag
|
||||
# is the source of truth. `.devN`/`.rcN`/`desktop-*` tags are excluded
|
||||
# so they can't be mistaken for the release base.
|
||||
# Any future manual `X.Y.Z` release will outrank every `X.Y.Z.devN`
|
||||
# autotag — PEP 440 sorts dev releases strictly below the final.
|
||||
LATEST_RELEASE=$(git tag --list 'v[0-9]*' --merged HEAD \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
|
||||
if [[ -z "$LATEST_RELEASE" ]]; then
|
||||
echo "::error::No release tag (vX.Y.Z) reachable from HEAD"
|
||||
exit 1
|
||||
fi
|
||||
BASE="${LATEST_RELEASE#v}"
|
||||
MAJOR=$(echo "$BASE" | cut -d. -f1)
|
||||
MINOR=$(echo "$BASE" | cut -d. -f2)
|
||||
PATCH=$(echo "$BASE" | cut -d. -f3 | sed -E 's/[^0-9].*$//')
|
||||
NEXT_PATCH=$((PATCH + 1))
|
||||
BUILD=$(git rev-list --count HEAD)
|
||||
VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev${BUILD}"
|
||||
TAG="v${VERSION}"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "Computed ${TAG} (base=${BASE})"
|
||||
|
||||
- name: Create and push tag
|
||||
id: tag
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.version.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
EXISTING_SHA=$(git rev-parse "$TAG")
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
if [[ "$EXISTING_SHA" != "$HEAD_SHA" ]]; then
|
||||
echo "::error::Tag $TAG already exists at $EXISTING_SHA but HEAD is $HEAD_SHA"
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag $TAG already exists at HEAD, skipping creation"
|
||||
echo "created=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$TAG"
|
||||
git push origin "$TAG"
|
||||
echo "Created and pushed $TAG"
|
||||
echo "created=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Tag pushes made with the default GITHUB_TOKEN do NOT trigger other
|
||||
# workflows (recursion prevention). workflow_dispatch is the documented
|
||||
# exception, so we explicitly dispatch the downstream CD workflows here.
|
||||
# See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication
|
||||
- name: Dispatch downstream workflows
|
||||
if: steps.tag.outputs.created == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.version.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Dispatching pypi-publish.yml @ ${TAG}"
|
||||
gh workflow run pypi-publish.yml \
|
||||
--ref "${TAG}" \
|
||||
-f tag="${TAG}"
|
||||
echo "Dispatching desktop.yml @ ${TAG}"
|
||||
gh workflow run desktop.yml \
|
||||
--ref "${TAG}" \
|
||||
-f tag="${TAG}"
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Bash tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'scripts/install/**'
|
||||
- 'tests/install/bash/**'
|
||||
- '.github/workflows/bash-tests.yml'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'scripts/install/**'
|
||||
- 'tests/install/bash/**'
|
||||
|
||||
jobs:
|
||||
bats:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install bats-core
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y bats
|
||||
|
||||
- name: Run bats tests
|
||||
run: bats tests/install/bash/
|
||||
@@ -1,181 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra dev --extra framework-comparison --extra server
|
||||
|
||||
- name: Ruff check
|
||||
run: uv run ruff check src/ tests/
|
||||
|
||||
- name: Ruff format check
|
||||
run: uv run ruff format --check src/ tests/
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
rust/target
|
||||
key: rust-${{ runner.os }}-${{ hashFiles('rust/Cargo.lock') }}
|
||||
restore-keys: rust-${{ runner.os }}-
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra dev --extra framework-comparison --extra server
|
||||
|
||||
- name: Build Rust extension
|
||||
run: uv run maturin develop --manifest-path rust/crates/openjarvis-python/Cargo.toml
|
||||
|
||||
- name: Run tests
|
||||
# COVERAGE_CORE=sysmon uses CPython 3.12's sys.monitoring backend,
|
||||
# which is dramatically cheaper than the default C trace function.
|
||||
# -n auto fans the suite out across all runner cores via pytest-xdist.
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
run: |
|
||||
uv run pytest tests/ -n auto -q --tb=short -m "not live and not cloud and not hub" \
|
||||
--cov=openjarvis \
|
||||
--cov-report=term-missing \
|
||||
--cov-report=xml \
|
||||
--cov-fail-under=60
|
||||
|
||||
- name: Upload coverage report
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
timeout-minutes: 5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-xml
|
||||
path: coverage.xml
|
||||
if-no-files-found: warn
|
||||
|
||||
# Windows job — empirically exercises the platform-specific code paths that
|
||||
# the Ubuntu `test` job can never reach: GlobalMemoryStatusEx RAM detection
|
||||
# (#373) and the cp9xx -> UTF-8 stdout reconfigure (#293). Also the only CI
|
||||
# job that builds + imports the mandatory `openjarvis_rust` PyO3 extension
|
||||
# on Windows. Public repo -> Windows runner minutes are free.
|
||||
#
|
||||
# All `run:` steps use static commands only (no `github.event.*`
|
||||
# interpolation), so there is no workflow-injection surface here.
|
||||
test-windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 3.12 = common; 3.13 = the supported ceiling — installing there guards
|
||||
# against a numpy/native wheel gap at the top of the range (#350), which
|
||||
# is exactly how the source-build failure slips in on Windows.
|
||||
python-version: ["3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra dev --extra server
|
||||
|
||||
# Pure-Python check — runs before the Rust build so a flaky toolchain
|
||||
# install can never mask the actual RAM-detection verification.
|
||||
- name: Verify Windows RAM detection (#373)
|
||||
shell: bash
|
||||
run: |
|
||||
uv run python -c "from openjarvis.core.config import _total_ram_gb; ram = _total_ram_gb(); print(f'GlobalMemoryStatusEx RAM = {ram} GB'); assert ram > 0, f'Windows RAM detection returned {ram}, expected > 0'"
|
||||
|
||||
- name: Run Windows-specific tests (hardware + CLI)
|
||||
shell: bash
|
||||
run: |
|
||||
uv run pytest tests/hardware/test_hardware_profiles.py tests/cli/test_cli.py -v -m "not live and not cloud"
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Build + import the PyO3 extension on Windows
|
||||
shell: bash
|
||||
run: |
|
||||
uv run maturin develop --manifest-path rust/crates/openjarvis-python/Cargo.toml
|
||||
uv run python -c "import openjarvis_rust; print('openjarvis_rust imports on Windows OK')"
|
||||
|
||||
- name: Smoke-test CLI
|
||||
shell: bash
|
||||
run: |
|
||||
uv run jarvis --version
|
||||
|
||||
rust:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: rust
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
rust/target
|
||||
key: rust-${{ runner.os }}-${{ hashFiles('rust/Cargo.lock') }}
|
||||
restore-keys: rust-${{ runner.os }}-
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Test
|
||||
run: cargo test --workspace
|
||||
@@ -1,86 +0,0 @@
|
||||
name: Claude Issue Fixer
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: claude-issues-${{ github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Least-privilege: only what the issue-fixer job actually needs.
|
||||
# id-token (OIDC) is intentionally omitted — claude-code-action@v1 is passed
|
||||
# github_token directly, so OIDC is unused here.
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
fix:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
# Security gate: this job reaches secrets.ANTHROPIC_API_KEY and holds a
|
||||
# write-scoped GITHUB_TOKEN. `issues` / `issue_comment` are public,
|
||||
# attacker-controllable events that run in the base-repo context with full
|
||||
# secret access, so the human-triggered paths are restricted to actors with
|
||||
# write-level association (OWNER / MEMBER / COLLABORATOR). This blocks
|
||||
# external / first-time contributors from draining the API budget or
|
||||
# creating branches/PRs, while leaving maintainer use unaffected.
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issues' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association) &&
|
||||
(contains(github.event.issue.labels.*.name, 'bug') ||
|
||||
contains(github.event.issue.labels.*.name, 'autofix'))) ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
|
||||
!github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@claude') &&
|
||||
github.actor != 'claude[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prompt: |
|
||||
You are an automated issue fixer for the OpenJarvis repository. Follow these steps in order:
|
||||
|
||||
## Step 1: Diagnose
|
||||
Read the issue thoroughly. Explore the codebase to understand the problem. If this is a bug report, attempt to reproduce it. Identify the root cause and affected files.
|
||||
|
||||
## Step 2: Comment your plan
|
||||
BEFORE making any code changes, post a comment on this issue describing:
|
||||
- Your root cause analysis
|
||||
- Which files need to change and why
|
||||
- Your implementation approach
|
||||
|
||||
## Step 3: Implement
|
||||
Create a branch named `claude/issue-${{ github.event.issue.number }}` and make the changes. Use conventional commit messages that reference the issue, e.g.:
|
||||
`fix: handle empty tool responses in orchestrator (fixes #${{ github.event.issue.number }})`
|
||||
|
||||
## Step 4: Test
|
||||
Run these commands and ensure both pass:
|
||||
- `uv run ruff check src/ tests/` (linting)
|
||||
- `uv run pytest tests/ -v --tb=short` (tests)
|
||||
|
||||
If tests fail, fix the issues before proceeding. If you added new functionality, add corresponding tests in `tests/` mirroring the `src/` directory structure.
|
||||
|
||||
## Step 5: Open PR
|
||||
Create a pull request that:
|
||||
- Links back to this issue (include `Fixes #${{ github.event.issue.number }}` in the PR body)
|
||||
- Describes what was changed and why
|
||||
- Includes a summary of test results
|
||||
|
||||
## If you cannot fix it
|
||||
If you cannot reproduce the issue, cannot determine the root cause, or the fix is beyond your capabilities, post a comment explaining:
|
||||
- What you investigated
|
||||
- What you found (or didn't find)
|
||||
- What additional information you need from the reporter
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Claude PR Review
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Least-privilege: PR review only needs to post comments on the PR.
|
||||
# id-token (OIDC) is omitted — claude-code-action@v1 is passed github_token
|
||||
# directly, so OIDC is unused here.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
# Security gate: this job reaches secrets.ANTHROPIC_API_KEY. Both
|
||||
# issue_comment and pull_request_review_comment are public,
|
||||
# attacker-controllable events that run in the base-repo context with full
|
||||
# secret access, so the @claude paths are restricted to actors with
|
||||
# write-level association (OWNER / MEMBER / COLLABORATOR). External /
|
||||
# first-time contributors cannot trigger the key; maintainers are unaffected.
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@claude') &&
|
||||
github.actor != 'claude[bot]') ||
|
||||
(github.event_name == 'pull_request_review_comment' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
|
||||
contains(github.event.comment.body, '@claude') &&
|
||||
github.actor != 'claude[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Read review instructions
|
||||
id: review
|
||||
run: |
|
||||
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
||||
echo "instructions<<$EOF" >> "$GITHUB_OUTPUT"
|
||||
cat REVIEW.md >> "$GITHUB_OUTPUT"
|
||||
echo "$EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prompt: ${{ steps.review.outputs.instructions }}
|
||||
@@ -2,23 +2,21 @@ name: Desktop Build & Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'desktop/**'
|
||||
- '.github/workflows/desktop.yml'
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'desktop-v*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'desktop/**'
|
||||
- '.github/workflows/desktop.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to build (e.g. v1.0.2.dev500). If set, autotag dispatches use this. github.ref still controls the checkout.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: desktop-${{ inputs.tag || github.ref }}
|
||||
group: desktop-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
@@ -29,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
@@ -40,64 +38,39 @@ jobs:
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
libxdo-dev \
|
||||
libdbus-1-dev
|
||||
libxdo-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
working-directory: desktop
|
||||
run: npm install
|
||||
|
||||
- name: TypeScript type-check
|
||||
working-directory: frontend
|
||||
working-directory: desktop
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Vite build
|
||||
working-directory: desktop
|
||||
run: npx vite build
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: 'frontend/src-tauri -> target'
|
||||
workspaces: 'desktop/src-tauri -> target'
|
||||
|
||||
- name: Create frontend dist stub
|
||||
run: mkdir -p frontend/dist && echo '<html><body></body></html>' > frontend/dist/index.html
|
||||
|
||||
# `cargo test` builds the crate (same coverage as the old `cargo check`)
|
||||
# and runs the unit tests, including the #331 uv-sync error-formatting
|
||||
# helpers. Static command, no untrusted input — no injection surface.
|
||||
- name: Cargo test
|
||||
working-directory: frontend/src-tauri
|
||||
run: cargo test
|
||||
|
||||
# Remove stale artifacts from the desktop-edge rolling pre-release so that
|
||||
# only the current build's files are available for download. (The stable
|
||||
# `desktop-latest` channel the installed app polls is never cleaned here.)
|
||||
clean-release:
|
||||
needs: [validate]
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete old assets from desktop-edge
|
||||
if: "!startsWith(github.ref, 'refs/tags/')"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="desktop-edge"
|
||||
# List all asset IDs on the release and delete them
|
||||
ASSET_IDS=$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \
|
||||
--jq '.assets[].id' 2>/dev/null || true)
|
||||
for id in $ASSET_IDS; do
|
||||
echo "Deleting asset $id"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/releases/assets/$id" || true
|
||||
done
|
||||
- name: Cargo check
|
||||
working-directory: desktop/src-tauri
|
||||
run: cargo check
|
||||
|
||||
build-and-release:
|
||||
needs: [validate, clean-release]
|
||||
needs: [validate]
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
strategy:
|
||||
@@ -106,118 +79,17 @@ jobs:
|
||||
include:
|
||||
- platform: ubuntu-22.04
|
||||
args: ''
|
||||
- platform: macos-14
|
||||
args: '--target universal-apple-darwin'
|
||||
- platform: macos-latest
|
||||
args: '--target aarch64-apple-darwin'
|
||||
- platform: macos-13
|
||||
args: '--target x86_64-apple-darwin'
|
||||
- platform: windows-latest
|
||||
args: ''
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Full history + tags so the workflow_dispatch fallback in
|
||||
# "Determine release info" can derive the dev version from the
|
||||
# latest release tag (#526).
|
||||
fetch-depth: 0
|
||||
|
||||
# Validate Apple credentials BEFORE the expensive work. Notarization is
|
||||
# the very last thing `tauri-action` does, so a bad credential or a
|
||||
# lapsed account agreement previously surfaced ~10 minutes in — after the
|
||||
# Rust toolchain, npm install, two Ollama sidecar downloads and a
|
||||
# universal cargo build — as a single opaque line:
|
||||
#
|
||||
# failed to bundle project: failed codesign application: failed to
|
||||
# notarize app: Error: HTTP status code: 403. ...
|
||||
#
|
||||
# `notarytool history` is a read-only call (it submits nothing) that
|
||||
# exercises the identical auth path, so every credential/account failure
|
||||
# mode reaches us here first, in seconds, with the specific cause named.
|
||||
# `xcrun` is preinstalled on macOS runners, hence placement before the
|
||||
# toolchain steps rather than next to "Configure Apple signing".
|
||||
- name: Preflight Apple notarization credentials
|
||||
if: matrix.platform == 'macos-14'
|
||||
env:
|
||||
CERT: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
A_ID: ${{ secrets.APPLE_ID }}
|
||||
A_PASS: ${{ secrets.APPLE_PASSWORD }}
|
||||
A_TEAM: ${{ secrets.APPLE_TEAM_ID }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -uo pipefail
|
||||
|
||||
# Mirror the skip logic in "Configure Apple signing": without a
|
||||
# certificate the build is unsigned and never notarizes, so there is
|
||||
# nothing to preflight. Tag builds still hard-fail there.
|
||||
if [ -z "$CERT" ]; then
|
||||
echo "No Apple certificate configured; skipping notarization preflight."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
missing=""
|
||||
[ -z "$A_ID" ] && missing="$missing APPLE_ID"
|
||||
[ -z "$A_PASS" ] && missing="$missing APPLE_PASSWORD"
|
||||
[ -z "$A_TEAM" ] && missing="$missing APPLE_TEAM_ID"
|
||||
if [ -n "$missing" ]; then
|
||||
echo "::error::APPLE_CERTIFICATE is set but notarization secrets are missing:$missing"
|
||||
echo "::error::Signing would succeed and notarization would then fail. Set them or clear APPLE_CERTIFICATE."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Retry only to absorb transient network faults. Credential and
|
||||
# account errors are deterministic, so we classify and exit on the
|
||||
# first definitive answer rather than retrying into the same wall.
|
||||
attempt=1
|
||||
while [ "$attempt" -le 3 ]; do
|
||||
out=$(xcrun notarytool history \
|
||||
--apple-id "$A_ID" \
|
||||
--team-id "$A_TEAM" \
|
||||
--password "$A_PASS" \
|
||||
--output-format json 2>&1)
|
||||
rc=$?
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
echo "Apple notarization preflight OK — credentials valid, team reachable, agreements in effect."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$out" in
|
||||
*"Invalid credentials"*|*"401"*)
|
||||
echo "::error::Apple notarization preflight failed: invalid credentials (HTTP 401)."
|
||||
echo "::error::APPLE_PASSWORD must be an app-specific password from appleid.apple.com,"
|
||||
echo "::error::generated while signed in as the SAME Apple ID as APPLE_ID. A regular"
|
||||
echo "::error::Apple ID password will not work, and a password minted under a different"
|
||||
echo "::error::Apple ID authenticates as that other account."
|
||||
exit 1
|
||||
;;
|
||||
*"Invalid or inaccessible developer team ID"*)
|
||||
echo "::error::Apple notarization preflight failed: APPLE_ID is not a member of team APPLE_TEAM_ID (HTTP 403)."
|
||||
echo "::error::The Team ID must match the signing certificate. Read it from the cert's"
|
||||
echo "::error::subject, where it appears as: Developer ID Application: NAME (TEAMID)."
|
||||
echo "::error::If you belong to several teams, confirm APPLE_ID is a member of this one."
|
||||
exit 1
|
||||
;;
|
||||
*"required agreement"*|*"agreement"*)
|
||||
echo "::error::Apple notarization preflight failed: the team has no in-effect agreement (HTTP 403)."
|
||||
echo "::error::Apple reissues the Developer Program License Agreement periodically and"
|
||||
echo "::error::notarization is refused until it is accepted. ONLY THE ACCOUNT HOLDER can"
|
||||
echo "::error::accept it — team Admins cannot. Sign in to the account that owns this team:"
|
||||
echo "::error:: 1. https://developer.apple.com/account -> review any pending agreement"
|
||||
echo "::error:: 2. App Store Connect -> Business -> accept anything pending there too"
|
||||
echo "::error::Certificates stay valid while this is outstanding, so signing still works."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Preflight attempt ${attempt}/3 failed with a non-credential error."
|
||||
echo "$out" | tail -5
|
||||
attempt=$((attempt + 1))
|
||||
[ "$attempt" -le 3 ] && sleep 10
|
||||
done
|
||||
|
||||
echo "::error::Apple notarization preflight failed after 3 attempts. Last output:"
|
||||
echo "$out" | tail -20
|
||||
exit 1
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
@@ -229,135 +101,55 @@ jobs:
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
libxdo-dev \
|
||||
libdbus-1-dev
|
||||
libxdo-dev
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin' || matrix.platform == 'macos-13' && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: 'frontend/src-tauri -> target'
|
||||
workspaces: 'desktop/src-tauri -> target'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
working-directory: desktop
|
||||
run: npm install
|
||||
|
||||
- name: Download Ollama sidecar
|
||||
shell: bash
|
||||
run: |
|
||||
cd frontend/src-tauri/scripts && chmod +x download-ollama.sh
|
||||
if [[ "${{ matrix.platform }}" == "macos-14" ]]; then
|
||||
./download-ollama.sh aarch64-apple-darwin
|
||||
./download-ollama.sh x86_64-apple-darwin
|
||||
else
|
||||
./download-ollama.sh
|
||||
fi
|
||||
|
||||
- name: Determine release info
|
||||
id: release-info
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == refs/tags/desktop-v* ]]; then
|
||||
# Explicit stable desktop release tag
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#desktop-v}"
|
||||
echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Desktop ${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
# Auto-tagged rolling build from autotag.yml — use the same
|
||||
# version as the CLI/PyPI release so all surfaces stay in sync.
|
||||
# Rolling/dev builds go to the `desktop-edge` channel, NOT the
|
||||
# `desktop-latest` channel the installed app polls — so users on
|
||||
# stable are never auto-updated onto an unvetted dev build.
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
echo "tag=desktop-edge" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Desktop (Edge Build)" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# workflow_dispatch fallback (manual UI dispatch without --ref).
|
||||
# Derive a PEP 440 dev version aligned with autotag.yml so we
|
||||
# don't burn the X.Y.Z release-version namespace.
|
||||
# pyproject.toml no longer carries a static version (#526), so the
|
||||
# base comes from the latest plain release tag (vX.Y.Z), matching
|
||||
# autotag.yml. .dev/.rc/desktop-* tags are excluded.
|
||||
LATEST_RELEASE=$(git tag --list 'v[0-9]*' --merged HEAD \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
|
||||
if [[ -z "$LATEST_RELEASE" ]]; then
|
||||
echo "::error::No release tag (vX.Y.Z) reachable from HEAD"
|
||||
exit 1
|
||||
fi
|
||||
BASE="${LATEST_RELEASE#v}"
|
||||
MAJOR=$(echo "$BASE" | cut -d. -f1)
|
||||
MINOR=$(echo "$BASE" | cut -d. -f2)
|
||||
PATCH=$(echo "$BASE" | cut -d. -f3 | sed -E 's/[^0-9].*$//')
|
||||
NEXT_PATCH=$((PATCH + 1))
|
||||
BUILD=$(git rev-list --count HEAD)
|
||||
VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev${BUILD}"
|
||||
# Manual dispatches are also dev builds -> the edge channel.
|
||||
echo "tag=desktop-edge" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Desktop (Edge Build)" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=desktop-latest" >> "$GITHUB_OUTPUT"
|
||||
echo "name=Desktop (Latest Build)" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
# Tauri's build script requires strict SemVer (MAJOR.MINOR.PATCH[-pre][+build]).
|
||||
# PEP 440 dev releases (`1.0.2.dev661`) are NOT valid SemVer, so we
|
||||
# translate `.devN` to the SemVer-equivalent `-dev.N` prerelease form.
|
||||
# PyPI keeps the PEP 440 form; only the Tauri bundle uses SemVer.
|
||||
TAURI_VERSION="${VERSION/.dev/-dev.}"
|
||||
echo "tauri_version=${TAURI_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Configure Apple signing
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
CERT: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CERT_PASS: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
SIGN_ID: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
A_ID: ${{ secrets.APPLE_ID }}
|
||||
A_PASS: ${{ secrets.APPLE_PASSWORD }}
|
||||
A_TEAM: ${{ secrets.APPLE_TEAM_ID }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "$CERT" ]; then
|
||||
echo "APPLE_CERTIFICATE=$CERT" >> "$GITHUB_ENV"
|
||||
echo "APPLE_CERTIFICATE_PASSWORD=$CERT_PASS" >> "$GITHUB_ENV"
|
||||
echo "APPLE_SIGNING_IDENTITY=$SIGN_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_ID=$A_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_PASSWORD=$A_PASS" >> "$GITHUB_ENV"
|
||||
echo "APPLE_TEAM_ID=$A_TEAM" >> "$GITHUB_ENV"
|
||||
echo "Apple signing configured"
|
||||
else
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
echo "::error::Apple signing secrets are required for release builds"
|
||||
exit 1
|
||||
fi
|
||||
echo "No Apple certificate configured, skipping code signing"
|
||||
fi
|
||||
|
||||
- name: Build and release
|
||||
timeout-minutes: 120
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
TAURI_CONFIG: '{"version":"${{ steps.release-info.outputs.tauri_version }}","bundle":{"externalBin":["binaries/ollama"]}}'
|
||||
# tauri-action runs beforeBuildCommand (npm run build:tauri -> vite
|
||||
# build), which requires this at build time (#587). Strict for
|
||||
# releases: a missing/empty secret fails the build by design.
|
||||
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
projectPath: frontend
|
||||
projectPath: desktop
|
||||
tauriScript: npx tauri
|
||||
tagName: ${{ steps.release-info.outputs.tag }}
|
||||
releaseName: ${{ steps.release-info.outputs.name }}
|
||||
@@ -366,43 +158,3 @@ jobs:
|
||||
prerelease: ${{ steps.release-info.outputs.prerelease }}
|
||||
includeUpdaterJson: true
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
# When a stable `desktop-v*` release is published, repoint the
|
||||
# `desktop-latest` auto-update channel (the endpoint the installed app
|
||||
# polls) at it. The stable release's own `latest.json` already references
|
||||
# this release's signed assets, so we copy it verbatim — installed apps are
|
||||
# only ever offered vetted stable builds, never `desktop-edge` dev builds.
|
||||
refresh-stable-channel:
|
||||
needs: [build-and-release]
|
||||
if: startsWith(github.ref, 'refs/tags/desktop-v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror stable latest.json into desktop-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
STABLE_TAG: ${{ github.ref_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The stable release's updater manifest may take a moment to become
|
||||
# downloadable after tauri-action publishes it; retry briefly.
|
||||
URL="https://github.com/${REPO}/releases/download/${STABLE_TAG}/latest.json"
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if curl -fsSL -o latest.json "$URL"; then
|
||||
echo "Fetched ${STABLE_TAG}/latest.json on attempt ${attempt}"
|
||||
break
|
||||
fi
|
||||
echo "latest.json not ready yet (attempt ${attempt}); sleeping 15s"
|
||||
sleep 15
|
||||
done
|
||||
test -s latest.json || { echo "::error::Could not fetch ${URL}"; exit 1; }
|
||||
# Ensure the channel release exists (prerelease so it never usurps
|
||||
# the stable "Latest" badge), then replace its manifest in place.
|
||||
if ! gh release view desktop-latest --repo "$REPO" >/dev/null 2>&1; then
|
||||
gh release create desktop-latest --repo "$REPO" \
|
||||
--prerelease \
|
||||
--title "Desktop Auto-Update Channel" \
|
||||
--notes "Auto-update channel pointer for the desktop app. Mirrors the latest stable \`desktop-v*\` release; the in-app updater polls this \`latest.json\`. Download the app from the latest stable release, not here."
|
||||
fi
|
||||
gh release upload desktop-latest latest.json --repo "$REPO" --clobber
|
||||
echo "desktop-latest now mirrors ${STABLE_TAG}"
|
||||
|
||||
@@ -28,45 +28,25 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v8.0.0
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra docs
|
||||
|
||||
# Inject the public Supabase anon key so the savings leaderboard works on
|
||||
# the published docs site. Missing/empty (e.g. fork PRs) leaves the
|
||||
# leaderboard gracefully disabled. The key is read from env (not inlined)
|
||||
# and JSON-encoded into a JS string literal to avoid any injection.
|
||||
- name: Inject leaderboard Supabase anon key
|
||||
env:
|
||||
OPENJARVIS_LEADERBOARD_ANON: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os, pathlib
|
||||
|
||||
key = os.environ.get("OPENJARVIS_LEADERBOARD_ANON", "")
|
||||
pathlib.Path("docs/javascripts/leaderboard-config.js").write_text(
|
||||
"// Generated at docs-build time from the VITE_SUPABASE_ANON_KEY secret.\n"
|
||||
"window.OPENJARVIS_SUPABASE_ANON_KEY = " + json.dumps(key) + ";\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print("leaderboard anon key:", "set" if key else "empty (leaderboard disabled)")
|
||||
PY
|
||||
|
||||
- name: Build documentation
|
||||
run: uv run mkdocs build
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
uses: actions/upload-pages-artifact@v5.0.0
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: site/
|
||||
|
||||
@@ -80,4 +60,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Frontend CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/frontend.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/frontend.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: frontend-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
- run: npx tsc --noEmit
|
||||
- run: npm run build
|
||||
env:
|
||||
# Optional: when the secret is unset the build still succeeds and the
|
||||
# leaderboard is disabled (see src/lib/supabase.ts). No placeholder,
|
||||
# so a keyless CI build doesn't bake in a bogus anon key.
|
||||
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
@@ -1,118 +0,0 @@
|
||||
name: Installer integration
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'scripts/install/**'
|
||||
- 'src/openjarvis/cli/**'
|
||||
- 'tests/install/**'
|
||||
- '.github/workflows/installer-integration.yml'
|
||||
schedule:
|
||||
- cron: '0 6 * * *'
|
||||
|
||||
jobs:
|
||||
container-matrix:
|
||||
name: ${{ matrix.image }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image:
|
||||
- ubuntu:22.04
|
||||
- ubuntu:24.04
|
||||
- fedora:40
|
||||
|
||||
container: ${{ matrix.image }}
|
||||
|
||||
steps:
|
||||
- name: Install prereqs (Ubuntu/Debian)
|
||||
if: contains(matrix.image, 'ubuntu') || contains(matrix.image, 'debian')
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl git python3 sudo
|
||||
|
||||
- name: Install prereqs (Fedora)
|
||||
if: contains(matrix.image, 'fedora')
|
||||
run: |
|
||||
dnf install -y curl git python3 sudo
|
||||
|
||||
- name: Create non-root user
|
||||
run: useradd -m -s /bin/bash testuser
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: openjarvis-src
|
||||
|
||||
- name: Install Ollama mock
|
||||
run: install -m 755 "$GITHUB_WORKSPACE/openjarvis-src/tests/install/bash/stubs/ollama-mock" /usr/local/bin/ollama
|
||||
|
||||
- name: Run installer
|
||||
run: |
|
||||
chown -R testuser:testuser /home/testuser "$GITHUB_WORKSPACE/openjarvis-src"
|
||||
su testuser -c '
|
||||
export OPENJARVIS_REPO_URL=file://'"$GITHUB_WORKSPACE"'/openjarvis-src
|
||||
cd '"$GITHUB_WORKSPACE"'/openjarvis-src
|
||||
bash scripts/install/install.sh --no-bg-orchestrator
|
||||
'
|
||||
|
||||
- name: Verify install state
|
||||
run: |
|
||||
su testuser -c '
|
||||
test -d ~/.openjarvis/src
|
||||
test -d ~/.openjarvis/.venv
|
||||
test -f ~/.openjarvis/config.toml
|
||||
test -f ~/.openjarvis/.state/install-state.json
|
||||
test -L ~/.local/bin/jarvis
|
||||
'
|
||||
|
||||
- name: Verify jarvis --version
|
||||
run: su testuser -c '~/.local/bin/jarvis --version'
|
||||
|
||||
- name: Verify jarvis doctor exits 0
|
||||
run: su testuser -c '~/.local/bin/jarvis doctor'
|
||||
|
||||
- name: Verify uninstall is clean
|
||||
run: |
|
||||
su testuser -c '
|
||||
~/.local/bin/jarvis-uninstall
|
||||
test ! -d ~/.openjarvis
|
||||
test ! -L ~/.local/bin/jarvis
|
||||
'
|
||||
|
||||
macos:
|
||||
name: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-14, macos-15]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Ollama mock
|
||||
run: sudo install -m 755 tests/install/bash/stubs/ollama-mock /usr/local/bin/ollama
|
||||
|
||||
- name: Run installer
|
||||
run: |
|
||||
export OPENJARVIS_REPO_URL=file://$(pwd)
|
||||
bash scripts/install/install.sh --no-bg-orchestrator
|
||||
|
||||
- name: Verify install state
|
||||
run: |
|
||||
test -d ~/.openjarvis/src
|
||||
test -d ~/.openjarvis/.venv
|
||||
test -f ~/.openjarvis/config.toml
|
||||
test -L ~/.local/bin/jarvis
|
||||
|
||||
- name: Verify jarvis --version
|
||||
run: ~/.local/bin/jarvis --version
|
||||
|
||||
- name: Verify jarvis doctor exits 0
|
||||
run: ~/.local/bin/jarvis doctor
|
||||
|
||||
- name: Verify uninstall is clean
|
||||
run: |
|
||||
~/.local/bin/jarvis-uninstall
|
||||
test ! -d ~/.openjarvis
|
||||
test ! -L ~/.local/bin/jarvis
|
||||
@@ -1,114 +0,0 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to publish (e.g. v1.0.2.dev500). Overrides github.ref.'
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Dry run: build + validate, then publish to TestPyPI instead of PyPI (no production upload).'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
steps:
|
||||
- name: Resolve target ref
|
||||
id: ref
|
||||
env:
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
DEFAULT_REF: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "$INPUT_TAG" ]]; then
|
||||
echo "ref=${INPUT_TAG}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=${DEFAULT_REF}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v8.0.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Build frontend and bundle into package
|
||||
env:
|
||||
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd frontend
|
||||
npm ci
|
||||
# Vite is configured (frontend/vite.config.ts) with
|
||||
# `outDir: '../src/openjarvis/server/static'` and
|
||||
# `emptyOutDir: true`, so the build writes directly into the
|
||||
# Python package's static dir and clears stale assets itself.
|
||||
# No rm/cp is needed — and the previous `dist/`-assuming logic
|
||||
# was broken because `frontend/dist/` is never produced.
|
||||
npm run build
|
||||
STATIC=../src/openjarvis/server/static
|
||||
test -s "$STATIC/index.html" || {
|
||||
echo "::error::${STATIC}/index.html missing or empty after build"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Resolve build version from tag
|
||||
env:
|
||||
REF: ${{ steps.ref.outputs.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Strip leading "v" (e.g. v1.0.3.dev825 -> 1.0.3.dev825).
|
||||
VERSION="${REF#v}"
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "::error::ref '$REF' is not a version tag (expected vX.Y.Z[.devN]); pass -f tag=vX.Y.Z"
|
||||
exit 1
|
||||
fi
|
||||
# pyproject.toml is now dynamic = ["version"] via hatch-vcs (#526), so
|
||||
# there is no static line to sed. setuptools_scm cannot bump custom
|
||||
# `.devN` tags, so we pin the exact build version explicitly — the
|
||||
# published version always equals the pushed tag.
|
||||
echo "SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION}" >> "$GITHUB_ENV"
|
||||
echo "Building version ${VERSION}"
|
||||
|
||||
- name: Build package
|
||||
run: uv build
|
||||
|
||||
- name: Publish to TestPyPI (dry run)
|
||||
if: ${{ inputs.dry_run }}
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${UV_PUBLISH_TOKEN:-}" ]]; then
|
||||
echo "::warning::TEST_PYPI_API_TOKEN is not set — skipping the TestPyPI upload."
|
||||
echo "Build + twine check passed, which validated version derivation and packaging end to end."
|
||||
echo "To exercise a real upload, add a TEST_PYPI_API_TOKEN secret (or a TestPyPI trusted publisher)."
|
||||
exit 0
|
||||
fi
|
||||
uv publish --publish-url https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: ${{ !inputs.dry_run }}
|
||||
run: uv publish
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Auto-assign on "take"
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
assign:
|
||||
if: >-
|
||||
!github.event.issue.pull_request
|
||||
&& contains(fromJSON('["take", "Take", "TAKE"]'), github.event.comment.body)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assign commenter
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
COMMENTER="${{ github.event.comment.user.login }}"
|
||||
ISSUE="${{ github.event.issue.number }}"
|
||||
CURRENT=$(gh issue view "$ISSUE" --repo "${{ github.repository }}" --json assignees --jq '.assignees[].login' 2>/dev/null)
|
||||
if echo "$CURRENT" | grep -qx "$COMMENTER"; then
|
||||
echo "$COMMENTER is already assigned to #$ISSUE"
|
||||
else
|
||||
gh issue edit "$ISSUE" --repo "${{ github.repository }}" --add-assignee "$COMMENTER"
|
||||
echo "Assigned $COMMENTER to #$ISSUE"
|
||||
fi
|
||||
@@ -1,86 +0,0 @@
|
||||
name: Track Git Clones
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 06:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
track-clones:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.TRAFFIC_TOKEN }}
|
||||
|
||||
- name: Fetch clone traffic
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TRAFFIC_TOKEN }}
|
||||
run: |
|
||||
gh api repos/${{ github.repository }}/traffic/clones > /tmp/traffic.json
|
||||
|
||||
- name: Update accumulated data
|
||||
run: |
|
||||
python3 - <<'PYEOF'
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Load current traffic data from API
|
||||
with open("/tmp/traffic.json") as f:
|
||||
traffic = json.load(f)
|
||||
|
||||
# Load accumulated data
|
||||
data_path = ".github/clone-stats/clone-data.json"
|
||||
with open(data_path) as f:
|
||||
accumulated = json.load(f)
|
||||
|
||||
daily = accumulated.get("daily", {})
|
||||
|
||||
# Merge new daily entries (keyed by date to avoid double-counting)
|
||||
for entry in traffic.get("clones", []):
|
||||
date_key = entry["timestamp"][:10] # "2026-03-26"
|
||||
daily[date_key] = entry["count"] # total clones, not unique
|
||||
|
||||
# Recalculate total from all daily data
|
||||
total = sum(daily.values())
|
||||
|
||||
# Update accumulated data
|
||||
accumulated["daily"] = dict(sorted(daily.items()))
|
||||
accumulated["total_clones"] = total
|
||||
accumulated["last_updated"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
with open(data_path, "w") as f:
|
||||
json.dump(accumulated, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
# Update shields.io endpoint badge
|
||||
badge = {
|
||||
"schemaVersion": 1,
|
||||
"label": "Git Clones",
|
||||
"message": f"{total:,}",
|
||||
"color": "green",
|
||||
"namedLogo": "git"
|
||||
}
|
||||
|
||||
with open(".github/clone-stats/badge.json", "w") as f:
|
||||
json.dump(badge, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"Updated: {total:,} total clones across {len(daily)} days")
|
||||
PYEOF
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .github/clone-stats/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore: update clone traffic data [skip ci]"
|
||||
git push
|
||||
fi
|
||||
@@ -23,7 +23,6 @@ env/
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
@@ -39,27 +38,11 @@ Thumbs.db
|
||||
# Secrets
|
||||
.env
|
||||
.env.*
|
||||
# ...but keep checked-in example/templates (never contain real secrets)
|
||||
!.env.example
|
||||
!**/.env.example
|
||||
|
||||
# Project
|
||||
*.sqlite
|
||||
*.db
|
||||
*.jsonl
|
||||
*.npz
|
||||
*.log
|
||||
results/
|
||||
logs/
|
||||
# Anchored to repo root — DO NOT use the unanchored form `traces/`.
|
||||
# hatchling honors .gitignore when building the wheel; an unanchored
|
||||
# `traces/` pattern matches src/openjarvis/traces/ and silently drops
|
||||
# the runtime module from the wheel (issue #372).
|
||||
/traces/
|
||||
coding_task_*
|
||||
get-pip.py
|
||||
# Junk from mocked-path tests that write to their mock's __repr__ as a path
|
||||
MagicMock/
|
||||
|
||||
# MkDocs build output
|
||||
site/
|
||||
@@ -67,66 +50,9 @@ site/
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
|
||||
# Desktop
|
||||
desktop/node_modules/
|
||||
desktop/dist/
|
||||
src/openjarvis/server/static/
|
||||
|
||||
# Desktop (Tauri)
|
||||
frontend/src-tauri/target/
|
||||
|
||||
# Worktrees
|
||||
.worktrees/
|
||||
|
||||
# Rust
|
||||
target/
|
||||
|
||||
# Claude plan artifacts
|
||||
docs/plans/
|
||||
docs/superpowers/
|
||||
.superpowers/
|
||||
|
||||
# Claude Code project instructions (per-developer)
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
|
||||
# Tauri auto-generated schemas
|
||||
**/src-tauri/gen/schemas/
|
||||
|
||||
# NFS lock artifacts
|
||||
.nfs*
|
||||
**/.nfs*
|
||||
|
||||
# Research output
|
||||
research_mining_*
|
||||
.python-version
|
||||
src/openjarvis/channels/whatsapp_baileys_bridge/node_modules/
|
||||
|
||||
# SQLite in-memory artifacts
|
||||
:memory:
|
||||
|
||||
# Dogfood reports (regenerated locally; not for VCS)
|
||||
dogfood_report*.md
|
||||
|
||||
# Second Repos
|
||||
Inline/
|
||||
scratch/
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Distillation runtime artifacts (defense in depth — these should always live
|
||||
# in ~/.openjarvis/, never inside the source tree, but we ignore them here in
|
||||
# case OPENJARVIS_HOME is misconfigured during dev)
|
||||
# ---------------------------------------------------------------------------
|
||||
.openjarvis/
|
||||
learning.db
|
||||
**/learning/sessions/
|
||||
**/learning/pending_review/
|
||||
**/learning/benchmarks/
|
||||
**/teacher_traces/
|
||||
*.session.json
|
||||
|
||||
# Local dev artifacts (hybrid worker logs + cli debug dumps)
|
||||
minion_logs/
|
||||
*.oj-debug.json
|
||||
oj-debug.*.json
|
||||
desktop/node_modules/
|
||||
desktop/dist/
|
||||
desktop/src-tauri/target/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
@@ -1,356 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to OpenJarvis are documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
**Vision input for `jarvis ask`** — attach images to a query with
|
||||
`-i`/`--image` (repeatable) or capture the current screen with
|
||||
`-S`/`--screen`, for vision-capable models such as `gemma3:4b`. Images flow
|
||||
through `Message.images` into Ollama's `/api/chat` `images` field; text-only
|
||||
requests are unaffected. A privacy guard warns before any image is sent to a
|
||||
non-local engine, and the security guardrail now preserves images when it
|
||||
sanitizes a flagged prompt. Screen capture uses the built-in Windows .NET
|
||||
stack with `mss`/`Pillow` fallbacks on other platforms. Adds the
|
||||
`JARVIS_NUM_CTX` environment variable to tune the Ollama context window
|
||||
(default `16384`).
|
||||
|
||||
## [1.0.2] - 2026-05-24
|
||||
|
||||
A patch release that fixes a packaging bug which broke the v1.0.1
|
||||
wheel on PyPI, silences a noisy startup warning, restores a working
|
||||
install path while `openjarvis.ai` is down, improves desktop
|
||||
first-boot diagnostics on Windows, and ships the RAM-detection fix
|
||||
for Windows that missed the v1.0.1 cutoff.
|
||||
|
||||
### Fixed
|
||||
|
||||
**`openjarvis/traces/` missing from the v1.0.1 PyPI wheel** (#372).
|
||||
The `.gitignore` carried an unanchored `traces/` pattern, which
|
||||
hatchling honored at wheel-build time and matched the runtime module
|
||||
`src/openjarvis/traces/` — silently dropping the whole package. Every
|
||||
fresh `pip install openjarvis==1.0.1` then failed at import with
|
||||
`ModuleNotFoundError: No module named 'openjarvis.traces'` on the
|
||||
first `jarvis ask`, learning, or server call. Anchored the pattern to
|
||||
`/traces/`. Verified: a clean `uv build` now produces a wheel
|
||||
containing all four `traces/` files.
|
||||
|
||||
**`pynvml` deprecation `FutureWarning` on every command** (#389).
|
||||
Switched the dependency from the legacy `pynvml` package to NVIDIA's
|
||||
official `nvidia-ml-py` (same `pynvml` module name, no warning shim),
|
||||
and added defensive `warnings.filterwarnings` at every `import pynvml`
|
||||
site to suppress the warning even when `pynvml` is pulled in
|
||||
transitively.
|
||||
|
||||
**Windows RAM detection returning `0.0 GB`** (#373). The Windows
|
||||
branch of `_total_ram_gb()` (via `GlobalMemoryStatusEx`) landed after
|
||||
the v1.0.1 cutoff, so v1.0.1 users still saw `0.0 GB` from `jarvis
|
||||
init`. Now shipping in the wheel. A new `windows-latest` CI job runs
|
||||
the real `GlobalMemoryStatusEx` path on every PR as a regression
|
||||
guard.
|
||||
|
||||
**Desktop first-boot hung on "did not become healthy in time"**
|
||||
(#331). The Tauri boot path ran `uv sync` with stderr discarded and
|
||||
the exit code ignored, so a failed dependency install surfaced only
|
||||
as a generic 600-second health-check timeout. Now captures stderr,
|
||||
checks the exit status, and surfaces the actual `uv sync` error
|
||||
(with the diagnostic tail) before the long wait. The error-formatting
|
||||
logic is covered by unit tests.
|
||||
|
||||
### Changed
|
||||
|
||||
**Install URL moved to GitHub Pages** (#337, #352). The documented
|
||||
`openjarvis.ai/install.sh` URL was failing with `sslv3 alert
|
||||
handshake failure` (the domain is community-operated and had a broken
|
||||
TLS config). The canonical installer is now served from the
|
||||
project-controlled GitHub Pages site at
|
||||
`https://open-jarvis.github.io/OpenJarvis/install.sh`, generated from
|
||||
the same `scripts/install/install.sh` at docs-build time. The README
|
||||
also documents the WSL2 path for Windows and the `uv` prerequisite
|
||||
for the desktop binary, and the installer bails early with a clear
|
||||
message when run under Git Bash / MSYS2 / Cygwin.
|
||||
|
||||
## [1.0.1] - 2026-05-17
|
||||
|
||||
A patch release that closes the auto-update gap so the analytics
|
||||
module added in #351 actually reaches users on the desktop, adds
|
||||
runtime opt-out for that analytics, fixes the misleading upgrade
|
||||
hint the CLI was printing, and lands the ACE optimizer alongside
|
||||
DSPy and GEPA.
|
||||
|
||||
### Added
|
||||
|
||||
**ACE agent optimizer** (`learning/agents/ace_optimizer.py`). Adds
|
||||
[ACE](https://github.com/ace-agent/ace) as a third agent-learning
|
||||
policy alongside DSPy and GEPA. Where DSPy bootstraps few-shot
|
||||
examples and GEPA evolves prompt populations, ACE evolves a textual
|
||||
*playbook* of strategies the agent reads at inference time, updated
|
||||
by a Generator / Reflector / Curator triad. Pick via
|
||||
`[learning.agent] policy = "ace"`. Setup is manual (ACE isn't on
|
||||
PyPI and isn't a properly-packaged Python project as of v1.0.1) —
|
||||
see `docs/learning/ace.md` for the install path and trace-adapter
|
||||
behavior.
|
||||
|
||||
**`jarvis self-update`** subcommand. Detects how OpenJarvis was
|
||||
installed (pip, uv tool, editable git checkout) by inspecting
|
||||
`openjarvis.__file__`, then runs the right upgrade command. Supports
|
||||
`--check` (print the command without running) and `-y` (skip the
|
||||
confirmation prompt). The post-command "new version available" hint
|
||||
now points users at this command instead of guessing at the right
|
||||
flow.
|
||||
|
||||
**Desktop auto-update endpoint wired to the rolling
|
||||
`desktop-latest` GitHub release.** The Tauri updater plugin was
|
||||
configured on the build side (`createUpdaterArtifacts: true`,
|
||||
`includeUpdaterJson: true`, signing key in `TAURI_SIGNING_PRIVATE_KEY`)
|
||||
but inert on the runtime side (`active: false`, `endpoints: []`). The
|
||||
installed desktop app would never check. Both are now fixed; the app
|
||||
polls `releases/download/desktop-latest/latest.json` every 30 minutes
|
||||
and signature-verifies downloads against the minisign pubkey baked
|
||||
into the app. Full flow, key-rotation runbook, and dev escape hatch
|
||||
(`OPENJARVIS_NO_UPDATER=1`) documented in `docs/desktop-auto-update.md`.
|
||||
|
||||
**Analytics env-var opt-out** (`DO_NOT_TRACK`, `OPENJARVIS_NO_ANALYTICS`).
|
||||
Tanvir's analytics module (#351) only respected the
|
||||
`[analytics] enabled` config-file setting. Both env vars are now
|
||||
honored in `is_analytics_enabled()` and in the install.sh beacon
|
||||
script. Any truthy value (`1`, `true`, `yes`, `on`) disables for
|
||||
that process; env opt-out takes precedence over the config file.
|
||||
Documented under a new "Opting out" section in `docs/telemetry.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
**Version-check trigger widened.** The "new version available" hint
|
||||
in `_version_check.py` used to fire only on `{ask, chat, serve}` and
|
||||
hardcoded the wrong upgrade command (`git pull && uv sync` — only
|
||||
correct for editable installs). Now fires on every interactive
|
||||
command (`doctor`, `init`, `quickstart`, `model`, `agents`, `skill`,
|
||||
`memory`, `bench`, `telemetry`, `config`, `eval`, `optimize`, plus
|
||||
the original three) and uses install-detection to print the right
|
||||
upgrade command. Honors `JARVIS_NO_UPDATE_CHECK=1` and `CI=true` to
|
||||
stay silent in automation.
|
||||
|
||||
**Desktop app version bumped 0.1.0 → 1.0.1** across
|
||||
`tauri.conf.json`, `frontend/package.json`, and
|
||||
`frontend/src-tauri/Cargo.toml` so the Python and desktop release
|
||||
streams are aligned and the auto-updater has a real version to
|
||||
compare against.
|
||||
|
||||
### Migration from 1.0.0
|
||||
|
||||
- **Importing `is_analytics_enabled`?** Same signature; behavior now
|
||||
short-circuits on env opt-out before checking the config. Callers
|
||||
that want the raw "is the config flag set" semantic should read
|
||||
`cfg.enabled` directly.
|
||||
- **Editable-git users running `jarvis self-update`** get the
|
||||
detected `git pull && uv sync` command pointed at their actual
|
||||
checkout, not `~/OpenJarvis`. If you'd come to rely on the
|
||||
hardcoded path, update your muscle memory.
|
||||
|
||||
## [1.0.0] - 2026-05-15
|
||||
|
||||
The five-primitive architecture (Intelligence, Engine, Agents,
|
||||
Tools & Memory, Learning) is now stable, with efficiency and
|
||||
on-device learning as first-class capabilities alongside accuracy.
|
||||
Companion blog post:
|
||||
[From Minions to OpenJarvis: A Retrospective on Two Years in Local AI](https://hazyresearch.stanford.edu/blog/2026-05-19-minions-to-openjarvis-retrospective).
|
||||
|
||||
### Highlights
|
||||
|
||||
**Five composable primitives.** Intelligence, Engine, Agents, Tools & Memory,
|
||||
and Learning each sit behind a single typed interface — any slot is
|
||||
substitutable without touching the rest. The composition layer is
|
||||
`JarvisSystem` in `src/openjarvis/system.py`, driven by a TOML config.
|
||||
|
||||
**Built-in agents across three execution modes.** Eight agents spanning a
|
||||
single-turn chat baseline, a deep-research agent with inline citations,
|
||||
a CodeAct-style coder, and a continuous monitor with memory compression
|
||||
for long-horizon workflows. Execution modes cover on-demand, scheduled,
|
||||
and continuous.
|
||||
|
||||
**Starter presets.** Eight preset configs installable via
|
||||
`jarvis init --preset <name>` bundle an agent with a hardware-appropriate
|
||||
engine, connectors, and tools. Variants cover Apple Silicon, Linux GPU
|
||||
servers, and CPU-only laptops, plus a quickstart for LLM-guided spec search.
|
||||
|
||||
**Inference engines.** Four first-class local engines (Ollama, vLLM, SGLang,
|
||||
llama.cpp) and five cloud providers (OpenAI, Anthropic, Google Gemini,
|
||||
OpenRouter, MiniMax) sit behind a single `Engine` interface. Discovery
|
||||
in `engine/_discovery.py` picks a sensible default per host.
|
||||
|
||||
### Added — hybrid local-cloud capabilities
|
||||
|
||||
**Per-query routing via a query-complexity analyzer**
|
||||
(`src/openjarvis/learning/routing/complexity.py`). Produces a 0.0–1.0
|
||||
complexity score with code/math/reasoning signals and a suggested token
|
||||
budget, populating `RoutingContext` so easy queries stay local and only
|
||||
queries that need frontier capability escalate.
|
||||
|
||||
**LLM-guided spec search** (`src/openjarvis/learning/spec_search/`).
|
||||
`SpecSearchOrchestrator` wires diagnose → plan → execute → gate into a
|
||||
single learning session: a frontier model reads traces, proposes
|
||||
coordinated edits across all five primitives, and a held-out benchmark
|
||||
gate (`gate/benchmark_gate.py`, `gate/regression.py`, `gate/cold_start.py`)
|
||||
accepts only non-regressing edits. Ships with the `spec-search-quickstart`
|
||||
preset and a runnable tutorial at `examples/openjarvis/spec_search_quickstart.py`.
|
||||
|
||||
**Six hybrid coordination paradigms** in `src/openjarvis/agents/hybrid/`.
|
||||
Each paradigm pairs a local student with a frontier cloud teacher under
|
||||
a different orchestration shape, as `LocalCloudAgent` subclasses:
|
||||
|
||||
- `minions` — reactive single-local + single-cloud loop
|
||||
- `conductor` — static DAG planner
|
||||
- `advisors` — executor ↔ advisor loop
|
||||
- `archon` — generate → rank → fuse
|
||||
- `skillorchestra` — per-query router across local skills
|
||||
- `toolorchestra` — RL'd local model with a tool pool
|
||||
|
||||
A runner CLI (`python -m openjarvis.agents.hybrid.runner --cell <name>`)
|
||||
and a 35-cell experiment registry (one TOML per method × benchmark ×
|
||||
model triple) let researchers run, score, and compare these on equal
|
||||
footing. Includes a Modal-backed SWE-bench-Verified harness scorer
|
||||
(`evals/scorers/swebench_harness.py`).
|
||||
|
||||
### Added — efficiency as a first-class constraint
|
||||
|
||||
**Hardware-agnostic energy telemetry at 50ms resolution** across NVIDIA
|
||||
(`telemetry/energy_nvidia.py`), AMD (`telemetry/energy_amd.py`), Apple
|
||||
Silicon (`telemetry/energy_apple.py`), and Intel RAPL
|
||||
(`telemetry/energy_rapl.py`). Energy, dollar cost, FLOPs, and latency
|
||||
are treated as evaluation targets alongside accuracy.
|
||||
|
||||
**Instrumentation for FLOPs, batch, steady-state, ITL, phase energy, and
|
||||
vLLM-specific metrics.** Joined per-query by the aggregator
|
||||
(`telemetry/aggregator.py`) so traces carry accuracy + efficiency together.
|
||||
|
||||
### Added — local learning loop
|
||||
|
||||
**Closed-loop optimization across the stack** — model weights via SFT
|
||||
(`learning/intelligence/sft_trainer.py`) and GRPO
|
||||
(`learning/intelligence/grpo_trainer.py` plus an orchestrator-specific
|
||||
variant under `learning/intelligence/orchestrator/`), prompts via DSPy
|
||||
(`learning/agents/dspy_optimizer.py`), agent logic via GEPA
|
||||
(`learning/agents/gepa_optimizer.py`), and engine + stack configuration
|
||||
via LLM-guided spec search. `LearningOrchestrator` coordinates triggers
|
||||
and applies optimizer overlays at discovery time so improvements compound
|
||||
across primitives.
|
||||
|
||||
### Added — cross-framework evaluation
|
||||
|
||||
**External agentic-framework evaluation via subprocess.** The
|
||||
`evals/backends/external/` subpackage wraps Hermes Agent and OpenClaw as
|
||||
one-shot subprocess backends behind the existing `InferenceBackend` ABC.
|
||||
The `evals/comparison/` toolkit provides path + commit-pin enforcement
|
||||
(`third_party.py`), config templating (`make_configs.py`), and LaTeX
|
||||
table generation (`table_gen.py`).
|
||||
|
||||
Ships with a new optional extra `framework-comparison` (depends on
|
||||
`polars`), a `live_external` pytest marker for integration tests
|
||||
requiring real foreign-framework installations, and a `ToolOrchestra`
|
||||
evaluation dataset (`evals/datasets/toolorchestra.py`) alongside the
|
||||
existing 30+ benchmark suite.
|
||||
|
||||
### Added — Skills System (Plans 1, 2A, 2B)
|
||||
|
||||
- **Skills core** — every skill is a tool. Skills appear in a system prompt catalog, agents invoke them on demand, content (pipeline results, markdown instructions, or both) gets injected into context.
|
||||
- `SkillManifest` + `SkillStep` types with tags, depends, invocation flags, markdown content
|
||||
- `SkillManager` — discovery, precedence resolution, catalog XML generation, tool wrapping
|
||||
- `SkillTool(BaseTool)` — auto-extracts parameters from step argument templates
|
||||
- `SkillExecutor` — sequential pipeline execution with sub-skill delegation
|
||||
- Dependency graph with cycle detection, max depth enforcement, capability unions
|
||||
- Security: four trust tiers (bundled/indexed/unreviewed/workspace), capability-gated enforcement
|
||||
- Skill index module for git-backed registry search
|
||||
|
||||
- **agentskills.io spec adoption** — canonical `SKILL.md` format with YAML frontmatter following the [agentskills.io](https://agentskills.io/specification) open standard.
|
||||
- `SkillParser` with strict spec validation + tolerant field mapping via `FIELD_MAPPING` table
|
||||
- `ToolTranslator` for external tool name translation (Bash -> shell_exec, Read -> file_read, etc.)
|
||||
- Source resolvers: `HermesResolver`, `OpenClawResolver`, `GitHubResolver`
|
||||
- `SkillImporter` with provenance tracking (`.source` metadata files), optional script import
|
||||
- Sourced subdirectory layout (`~/.openjarvis/skills/<source>/<name>/`)
|
||||
|
||||
- **Skills learning loop** — trace tagging, pattern discovery, DSPy/GEPA optimization.
|
||||
- Trace metadata tagging: `skill`, `skill_source`, `skill_kind` flow through ToolExecutor -> TraceCollector -> TraceStep
|
||||
- `SkillDiscovery` wired into `SkillManager.discover_from_traces()` with kebab name normalization
|
||||
- `SkillOptimizer` — per-skill DSPy/GEPA wrapper that buckets traces and writes sidecar overlays
|
||||
- `SkillOverlay` — sidecar storage at `~/.openjarvis/learning/skills/<name>/optimized.toml`
|
||||
- `SkillManager._load_overlays()` applies optimized descriptions + few-shot examples at discovery time
|
||||
- `LearningOrchestrator._maybe_optimize_skills()` — opt-in auto-trigger
|
||||
|
||||
- **Skills benchmark harness** — 4-condition PinchBench evaluation.
|
||||
- I3 fix: `skill_few_shot_examples` wired through SystemBuilder -> `_run_agent` -> `ToolUsingAgent` -> `native_react.REACT_SYSTEM_PROMPT`
|
||||
- `SkillBenchmarkRunner` — 4-condition x N-seed x M-task sweep with markdown report
|
||||
- `JarvisAgentBackend` accepts `skills_enabled` and `overlay_dir` kwargs
|
||||
- Conditions: `no_skills`, `skills_on`, `skills_optimized_dspy`, `skills_optimized_gepa`
|
||||
|
||||
- **CLI commands:**
|
||||
- `jarvis skill list` / `info` / `run` / `install` / `sync` / `sources` / `update` / `remove` / `search`
|
||||
- `jarvis skill discover` — mine traces for recurring tool patterns
|
||||
- `jarvis skill show-overlay` — inspect optimization output
|
||||
- `jarvis optimize skills` — run DSPy/GEPA per-skill optimization
|
||||
- `jarvis bench skills` — run the PinchBench skills benchmark
|
||||
|
||||
- **Agent prompt improvement:**
|
||||
- `native_react.REACT_SYSTEM_PROMPT` now includes "Using Skills" guidance that teaches agents to distinguish executable vs. instructional skill responses
|
||||
- `{skill_examples}` placeholder for optimized few-shot example injection
|
||||
|
||||
- **Configuration:**
|
||||
- `[skills]` section: `enabled`, `skills_dir`, `active`, `auto_discover`, `auto_sync`, `max_depth`, `sandbox_dangerous`
|
||||
- `[[skills.sources]]` section: `source`, `url`, `filter`, `auto_update`
|
||||
- `[learning.skills]` section: `auto_optimize`, `optimizer`, `min_traces_per_skill`, `optimization_interval_seconds`, `overlay_dir`
|
||||
- `SkillSourceConfig` and `SkillsLearningConfig` dataclasses
|
||||
|
||||
- **Documentation:**
|
||||
- `docs/user-guide/skills.md` — comprehensive user guide
|
||||
- `docs/architecture/skills.md` — technical deep-dive
|
||||
- `docs/tutorials/skills-workflow.md` — end-to-end tutorial
|
||||
- `docs/getting-started/configuration.md` — expanded with skills config sections
|
||||
- `CLAUDE.md` — updated architecture section
|
||||
|
||||
### Examples & Tutorials
|
||||
|
||||
- `examples/openjarvis/spec_search_quickstart.py` — runnable end-to-end
|
||||
LLM-guided spec search session.
|
||||
- `docs/user-guide/llm-guided-spec-search.md` — paper-aligned user guide.
|
||||
- `docs/architecture/learning.md` — Learning primitive deep-dive covering
|
||||
routing, spec search, optimizers, and the orchestrator.
|
||||
- `docs/tutorials/` — code-companion, deep-research, messaging-hub,
|
||||
scheduled-ops, and skills-workflow walkthroughs.
|
||||
- `src/openjarvis/agents/hybrid/registry/*.toml` — 35-cell registry of
|
||||
paradigm × benchmark × model experiments.
|
||||
|
||||
### Migration from 0.x
|
||||
|
||||
- **`learning/distillation/` is now `learning/spec_search/`.** The
|
||||
subsystem was renamed to match the LLM-guided spec search semantics
|
||||
documented in the companion paper. Update any imports
|
||||
(`from openjarvis.learning.distillation.*` →
|
||||
`from openjarvis.learning.spec_search.*`). The `jarvis distillation`
|
||||
CLI command is removed; use `spec_search`-prefixed config keys instead.
|
||||
- **`_third_party.toml` no longer ships default paths.** Set
|
||||
`HERMES_AGENT_PATH` and `OPENCLAW_PATH` env vars to point at your
|
||||
local checkouts before running the framework-comparison harness;
|
||||
missing or empty paths now raise `ThirdPartyNotFoundError` with an
|
||||
actionable hint.
|
||||
- **Engine `generate_full` return shape extended.**
|
||||
`JarvisAgentBackend.generate_full` and `JarvisDirectBackend.generate_full`
|
||||
now return the spec §6.2 extended fields (`energy_joules`,
|
||||
`peak_power_w`, `tool_calls`, `turn_count`, `framework`,
|
||||
`framework_commit`, `error`). Existing callers that didn't read these
|
||||
fields are unaffected; new callers can rely on cross-framework parity.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Trace metadata flow** — `ToolResult.metadata` now propagates through `TOOL_CALL_END` event to `TraceStep.metadata` (was silently dropped at the event-bus boundary).
|
||||
- **TaintSet JSON serialization** — `ToolExecutor._json_safe_metadata()` filters non-JSON-serializable values (like `TaintSet`) from event payloads before they reach `TraceStore`.
|
||||
- **Non-dict YAML frontmatter** — source resolvers handle `yaml.safe_load()` returning a string instead of a dict (discovered on real OpenClaw imports).
|
||||
- **OpenClaw category/name queries** — `jarvis skill install openclaw:owner/slug` now correctly splits into category + name match.
|
||||
- **SkillDiscovery trace compatibility** — `_extract_tool_sequence` reads from `step.input["tool"]` (the actual `TraceStep` format), not the nonexistent `step.tool_name` attribute.
|
||||
- **LearningOrchestrator skill trigger** — `_maybe_optimize_skills` runs BEFORE the SFT-data short-circuit (skills are tagged via trace metadata, not mined as SFT pairs).
|
||||
- **PinchBenchScorer constructor** — `SkillBenchmarkRunner` constructs `PinchBenchScorer(judge_backend, model)` instead of no-args.
|
||||
- **EvalRunner results access** — reads per-task data from `eval_runner.results` property, not nonexistent `summary.results`.
|
||||
@@ -0,0 +1,229 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Status
|
||||
|
||||
OpenJarvis is a research framework for studying on-device AI systems. Phase 21 complete. Five composable pillars: Intelligence, Engine, Agents, Tools (with storage + MCP), and Learning — with trace-driven learning as a cross-cutting concern. ~2940 tests pass (~51 skipped for optional deps). Python SDK (`Jarvis` class), composition layer (`SystemBuilder`/`JarvisSystem`), benchmarking framework, Docker deployment, Tauri desktop app, 40+ tools, 20+ CLI commands, 40+ API endpoints all ready.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
```bash
|
||||
uv sync --extra dev # Install deps + dev tools
|
||||
uv run pytest tests/ -v # Run ~2997 tests (~42 skipped if optional deps missing)
|
||||
uv run ruff check src/ tests/ # Lint
|
||||
uv run jarvis --version # 1.0.0
|
||||
uv run jarvis ask "Hello" # Query via discovered engine (direct mode)
|
||||
uv run jarvis ask --agent simple "Hello" # SimpleAgent route
|
||||
uv run jarvis ask --agent orchestrator "Hello" # OrchestratorAgent route
|
||||
uv run jarvis ask --agent orchestrator --tools calculator,think "What is 2+2?"
|
||||
uv run jarvis ask --agent native_react --tools calculator "What is 2+2?" # NativeReActAgent
|
||||
uv run jarvis ask --agent react "Hello" # Alias for native_react
|
||||
uv run jarvis ask --agent native_openhands "Hello" # NativeOpenHandsAgent (CodeAct)
|
||||
uv run jarvis ask --agent openhands "Hello" # Real OpenHands SDK (requires openhands-sdk)
|
||||
uv run jarvis ask --router heuristic "Hello" # Explicit heuristic policy
|
||||
uv run jarvis ask --no-context "Hello" # Query without memory context injection
|
||||
uv run jarvis model list # List models from running engines
|
||||
uv run jarvis model info qwen3:8b # Show model details
|
||||
uv run jarvis memory index ./docs/ # Index documents into memory
|
||||
uv run jarvis memory search "topic" # Search memory for relevant chunks
|
||||
uv run jarvis memory stats # Show memory backend statistics
|
||||
uv run jarvis telemetry stats # Show aggregated telemetry stats
|
||||
uv run jarvis telemetry export --format json # Export records as JSON
|
||||
uv run jarvis telemetry export --format csv # Export records as CSV
|
||||
uv run jarvis telemetry clear --yes # Delete all telemetry records
|
||||
uv run jarvis channel list # List available messaging channels
|
||||
uv run jarvis channel send slack "Hello" # Send a message to a channel
|
||||
uv run jarvis channel status # Show channel bridge connection status
|
||||
uv run jarvis scheduler create "Check weather" --type cron --value "0 9 * * *"
|
||||
uv run jarvis scheduler list # List scheduled tasks
|
||||
uv run jarvis scheduler start # Start scheduler daemon (foreground)
|
||||
uv run jarvis bench run # Run all benchmarks against engine
|
||||
uv run jarvis bench run -b energy -w 5 -n 20 --json # Energy benchmark with warmup
|
||||
uv run jarvis serve --port 8000 # OpenAI-compatible API server (requires openjarvis[server])
|
||||
uv run jarvis doctor # Run diagnostic checks (config, engines, models, deps)
|
||||
uv run jarvis doctor --json # Machine-readable diagnostics
|
||||
uv run jarvis start # Start server as background daemon
|
||||
uv run jarvis stop # Stop background daemon
|
||||
uv run jarvis restart # Restart background daemon
|
||||
uv run jarvis status # Show daemon status (PID, uptime)
|
||||
uv run jarvis chat # Interactive REPL (/quit, /clear, /model, /help, /history)
|
||||
uv run jarvis chat --agent orchestrator --tools calculator # REPL with agent
|
||||
uv run jarvis agent list # List registered agents
|
||||
uv run jarvis agent info native_react # Show agent details
|
||||
uv run jarvis workflow list # List available workflows
|
||||
uv run jarvis workflow run my_workflow # Execute a workflow
|
||||
uv run jarvis skill list # List installed skills
|
||||
uv run jarvis skill install path/to/skill.toml # Install a skill
|
||||
uv run jarvis vault set MY_KEY # Store encrypted credential
|
||||
uv run jarvis vault get MY_KEY # Retrieve credential
|
||||
uv run jarvis vault list # List stored keys
|
||||
uv run jarvis add github # Quick-add MCP server (github, slack, postgres, etc.)
|
||||
uv run jarvis --help # Show all subcommands
|
||||
uv run jarvis init --force # Detect hardware, write ~/.openjarvis/config.toml
|
||||
# Eval framework
|
||||
source .env # Load API keys before running evals
|
||||
uv run python -m evals run -c evals/configs/glm-4.7-flash-openhands.toml -v # Run eval suite from TOML config
|
||||
uv run python -m evals run -b supergpqa -m "qwen3:8b" -n 50 # Run single benchmark
|
||||
uv run python -m evals summarize results/supergpqa_qwen3-8b.jsonl # Summarize results
|
||||
```
|
||||
|
||||
### Config File Conventions
|
||||
|
||||
- **Runtime config (source of truth):** `configs/openjarvis/config.toml` — Pillar-aligned OpenJarvis config. Copied to `~/.openjarvis/config.toml` at runtime (which is where `load_config()` reads from).
|
||||
- **Eval suite configs:** `evals/configs/*.toml` — TOML configs defining models x benchmarks matrices.
|
||||
- **API keys:** `.env` file in project root (gitignored). Source with `source .env` before running evals or cloud operations.
|
||||
- **Never save configs to `~/.openjarvis/` directly** — always maintain the canonical copy in `configs/openjarvis/` and copy/symlink to `~/.openjarvis/`.
|
||||
|
||||
### Python SDK
|
||||
|
||||
```python
|
||||
from openjarvis import Jarvis
|
||||
|
||||
j = Jarvis() # Uses default config + auto-detected engine
|
||||
j = Jarvis(model="qwen3:8b") # Override model
|
||||
j = Jarvis(engine_key="ollama") # Override engine
|
||||
|
||||
response = j.ask("Hello") # Returns string
|
||||
full = j.ask_full("Hello") # Returns dict with content, usage, model, engine
|
||||
response = j.ask("Hello", agent="orchestrator", tools=["calculator"])
|
||||
|
||||
j.memory.index("./docs/") # Index documents
|
||||
results = j.memory.search("topic") # Search memory
|
||||
j.memory.stats() # Backend stats
|
||||
|
||||
j.list_models() # Available models
|
||||
j.list_engines() # Registered engines
|
||||
j.close() # Release resources
|
||||
```
|
||||
|
||||
- **Package manager:** `uv` with `hatchling` build backend
|
||||
- **Config:** `pyproject.toml` with extras for optional backends (e.g., `openjarvis[inference-vllm]`, `openjarvis[inference-mlx]`, `openjarvis[memory-colbert]`, `openjarvis[server]`, `openjarvis[openclaw]`, `openjarvis[energy-amd]`, `openjarvis[energy-apple]`, `openjarvis[energy-all]`, `openjarvis[security-signing]`, `openjarvis[sandbox-wasm]`, `openjarvis[dashboard]`, `openjarvis[browser]`, `openjarvis[media]`, `openjarvis[pdf]`, `openjarvis[channel-line]`, `openjarvis[channel-viber]`, `openjarvis[channel-reddit]`, `openjarvis[channel-mastodon]`, `openjarvis[channel-xmpp]`, `openjarvis[channel-rocketchat]`, `openjarvis[channel-zulip]`, `openjarvis[channel-twitch]`, `openjarvis[channel-nostr]`)
|
||||
- **CLI entry point:** `jarvis` (Click-based) — subcommands: `init`, `ask`, `serve`, `start`, `stop`, `restart`, `status`, `chat`, `model`, `memory`, `telemetry`, `bench`, `channel`, `scheduler`, `doctor`, `agent`, `workflow`, `skill`, `vault`, `add`
|
||||
- **Python:** 3.10+ required
|
||||
- **Node.js:** 22+ required only for OpenClaw agent
|
||||
|
||||
## Architecture
|
||||
|
||||
OpenJarvis is a research framework for on-device AI organized around **five composable pillars**, each with a clear ABC interface and a decorator-based registry for runtime discovery.
|
||||
|
||||
### Five Pillars
|
||||
|
||||
1. **Intelligence** (`src/openjarvis/intelligence/`) — Model definition, catalog, and generation defaults. `ModelRegistry` maps model keys to `ModelSpec`. `IntelligenceConfig` holds model identity (default/fallback model, model_path, checkpoint_path, quantization, preferred_engine, provider) and generation defaults (temperature, max_tokens, top_p, top_k, repetition_penalty, stop_sequences). Model catalog maintains `BUILTIN_MODELS` with auto-discovery via `merge_discovered_models()`. Backward-compat shims re-export from `learning/` for old import paths.
|
||||
2. **Engine** (`src/openjarvis/engine/`) — The inference runtime. Backends: vLLM, SGLang, Ollama, llama.cpp, MLX, LM Studio. All implement `InferenceEngine` ABC with `generate()`, `stream()`, `list_models()`, `health()`. Engines extract and pass through `tool_calls` in OpenAI format.
|
||||
3. **Agents** (`src/openjarvis/agents/`) — Pluggable logic for queries, tool/API calls, memory. Hierarchy: `BaseAgent` ABC (helpers: `_emit_turn_start/end`, `_build_messages`, `_generate`, `_max_turns_result`, `_strip_think_tags`, `_check_continuation`) → `ToolUsingAgent` (adds `tools`, `ToolExecutor`, `max_turns`). Agents: `SimpleAgent` (single-turn), `OrchestratorAgent` (multi-turn tool loop), `NativeReActAgent` (Thought-Action-Observation, key `"native_react"`, alias `"react"`), `NativeOpenHandsAgent` (CodeAct, key `"native_openhands"`), `RLMAgent` (recursive LM), `OpenHandsAgent` (real `openhands-sdk`, key `"openhands"`, requires Python 3.12+), `OpenClawAgent` (HTTP/subprocess transport), `ClaudeCodeAgent` (Claude Agent SDK via Node.js, key `"claude_code"`), `SandboxedAgent` (Docker wrapper, key `"sandboxed"`). `accepts_tools` class attribute for CLI/SDK auto-detection. Agents call `engine.generate()` directly — telemetry handled by `InstrumentedEngine` wrapper.
|
||||
4. **Tools** (`src/openjarvis/tools/`) — All tools managed via MCP (Model Context Protocol).
|
||||
- **API tools**: `CalculatorTool`, `ThinkTool`, `FileReadTool`, `FileWriteTool`, `WebSearchTool`, `CodeInterpreterTool`, `LLMTool`, `ShellExecTool`, `ApplyPatchTool`, `HttpRequestTool`, `DatabaseQueryTool`, `PDFExtractTool`, `ImageGenerateTool`, `AudioTranscribeTool` — all implement `BaseTool` ABC
|
||||
- **Git tools** (`git_tool.py`): `GitStatusTool`, `GitDiffTool`, `GitCommitTool`, `GitLogTool`
|
||||
- **Browser tools** (`browser.py`): `BrowserNavigateTool`, `BrowserClickTool`, `BrowserTypeTool`, `BrowserScreenshotTool`, `BrowserExtractTool` (Playwright, optional `[browser]`)
|
||||
- **Agent tools** (`agent_tools.py`): `AgentSpawnTool`, `AgentSendTool`, `AgentListTool`, `AgentKillTool`
|
||||
- **Storage tools** (`storage_tools.py`): `MemoryStoreTool`, `MemoryRetrieveTool`, `MemorySearchTool`, `MemoryIndexTool`
|
||||
- **Storage backends** (`tools/storage/`): SQLite/FTS5 (default), FAISS, ColBERTv2, BM25, Hybrid (RRF fusion), KnowledgeGraph. All implement `MemoryBackend` ABC. Canonical import: `from openjarvis.tools.storage.sqlite import SQLiteMemory`. Backward-compat shims in `memory/` still work.
|
||||
- **Scheduler tools** (`scheduler/tools.py`): 5 MCP tools for task scheduling
|
||||
- **Knowledge graph tools** (`knowledge_tools.py`): `KGAddEntityTool`, `KGAddRelationTool`, `KGQueryTool`, `KGNeighborsTool`
|
||||
- **MCP adapter** (`mcp_adapter.py`): `MCPToolAdapter` wraps external MCP tools as native `BaseTool`; `MCPToolProvider` discovers from server
|
||||
- **MCP server** (`mcp/server.py`): Exposes all built-in tools via JSON-RPC `tools/list` + `tools/call` (MCP spec 2025-11-25)
|
||||
- **MCP templates** (`tools/templates/`): `ToolTemplate` dynamically constructs tools from TOML specs. 10 builtin templates. `discover_templates()` auto-discovers.
|
||||
- **`ToolExecutor`**: dispatch with RBAC check + taint check, `timeout_seconds` on `ToolSpec` (default 30s via `ThreadPoolExecutor`), event bus integration
|
||||
- All registered via `@ToolRegistry.register("name")` decorator
|
||||
5. **Learning** (`src/openjarvis/learning/`) — Structured learning with nested per-pillar sub-policies. `LearningConfig` sections: `routing` (heuristic/learned/grpo/bandit), `intelligence` (none/sft), `agent` (none/agent_advisor/icl_updater), `metrics` (accuracy/latency/cost/efficiency weights). Policies: `SFTRouterPolicy` (query→model from traces), `AgentAdvisorPolicy` (LM-guided), `ICLUpdaterPolicy` (in-context with example DB, versioning, rollback, quality gates), `GRPORouterPolicy` (softmax sampling, group relative advantage, per-query-class weights), `BanditRouterPolicy` (Thompson Sampling / UCB1, per-arm stats). `SkillDiscovery` mines tool subsequences from traces to auto-generate skill manifests. Router policies: `HeuristicRouter`, `TraceDrivenPolicy`. Orchestrator training subpackage provides SFT and GRPO pipelines.
|
||||
|
||||
### Cross-cutting Systems
|
||||
|
||||
- **Traces** (`src/openjarvis/traces/`) — Full interaction recording. `Trace` captures `TraceStep`s (route, retrieve, generate, tool_call, respond) with timing. `TraceStore` (SQLite), `TraceCollector` (auto-wraps agents), `TraceAnalyzer` (stats for learning).
|
||||
- **Telemetry** (`src/openjarvis/telemetry/`) — `InstrumentedEngine` wraps any engine, publishing events to SQLite via `TelemetryStore`. `TelemetryAggregator` for read-only queries. `EnergyMonitor` ABC with vendor-specific implementations: `NvidiaEnergyMonitor` (hw counters/polling), `AmdEnergyMonitor` (amdsmi), `AppleEnergyMonitor` (zeus-ml), `RaplEnergyMonitor` (sysfs). `EnergyBatch` for batch-level energy-per-token. `SteadyStateDetector` for thermal equilibrium (CV-based).
|
||||
- **Security** (`src/openjarvis/security/`) — `SecretScanner` + `PIIScanner` (implement `BaseScanner` ABC). `GuardrailsEngine` wraps engines with input/output scanning (WARN/REDACT/BLOCK modes). `AuditLogger` with Merkle hash chain (SHA-256 tamper-evidence). `CapabilityPolicy` RBAC (10 capabilities with glob matching, enforced in `ToolExecutor`). `TaintLabel`/`TaintSet` information flow control with `SINK_POLICY`. Ed25519 signing via `cryptography` (optional `[security-signing]`). `file_policy.py` for sensitive file detection. `InjectionScanner` (11 regex patterns: prompt override, identity override, code/shell injection, exfiltration, jailbreak, delimiter injection). `check_ssrf()` SSRF protection (RFC 1918, loopback, link-local, cloud metadata blocking). `RateLimiter` with `TokenBucket` (thread-safe, per-key). `run_sandboxed()` subprocess isolation (`os.setsid`, process group kill, env clearing). Security HTTP middleware (7 headers: CSP, HSTS, X-Frame-Options, etc.).
|
||||
|
||||
### Composition & Infrastructure
|
||||
|
||||
- **Composition Layer** (`system.py`) — `SystemBuilder` fluent builder → `JarvisSystem` with `ask()`, `close()`. Wires engine, model, agent, tools, telemetry, traces, workflow, sessions, capability policy.
|
||||
- **SDK** (`sdk.py`) — `Jarvis` class: high-level sync API with `ask()`/`ask_full()`, `MemoryHandle`, lazy init, telemetry. Also exports `JarvisSystem`/`SystemBuilder`.
|
||||
- **Benchmarks** (`bench/`) — `LatencyBenchmark`, `ThroughputBenchmark`, `EnergyBenchmark`. All registered via `BenchmarkRegistry`. CLI: `jarvis bench run`.
|
||||
- **OpenClaw** (`agents/openclaw*.py`) — `OpenClawAgent` with `HttpTransport`/`SubprocessTransport`, JSON-line protocol, `ProviderPlugin`, `MemorySearchManager`.
|
||||
- **API Server** (`server/`) — OpenAI-compatible via `jarvis serve` (FastAPI + uvicorn). Endpoints: `POST /v1/chat/completions`, `GET /v1/models`, `GET /health`, channel endpoints. SSE streaming.
|
||||
- **Channels** (`channels/`) — `BaseChannel` ABC. `OpenClawChannelBridge` (WebSocket/HTTP to OpenClaw gateway). `WhatsAppBaileysChannel` (Baileys protocol, Node.js bridge, QR auth). Phase 21 channels: `LINEChannel`, `ViberChannel`, `MessengerChannel`, `RedditChannel`, `MastodonChannel`, `XMPPChannel`, `RocketChatChannel`, `ZulipChannel`, `TwitchChannel`, `NostrChannel`. All follow `BaseChannel` ABC with env var fallbacks, `@ChannelRegistry.register()`, `EventBus` integration.
|
||||
- **Sandbox** (`sandbox/`) — `ContainerRunner` (Docker/Podman lifecycle, mount validation). `WasmRunner` (wasmtime-py, fuel/memory limits, optional `[sandbox-wasm]`). `SandboxedAgent` transparent wrapper. `create_sandbox_runner()` factory. `MountAllowlist` with path traversal prevention.
|
||||
- **Scheduler** (`scheduler/`) — `TaskScheduler` with cron/interval/once scheduling, SQLite persistence, 5 MCP tools, event bus. CLI: `jarvis scheduler create|list|pause|resume|cancel|logs|start`.
|
||||
- **Agent Hardening** (`agents/loop_guard.py`) — `LoopGuard`: SHA-256 hash tracking (identical calls), ping-pong detection (A-B-A-B patterns), poll-tool budget, context overflow recovery. `BaseAgent._check_continuation()` auto-resumes on `finish_reason=length`.
|
||||
- **Workflow Engine** (`workflow/`) — DAG-based `WorkflowGraph` (cycle detection, topological sort, parallel stages via `ThreadPoolExecutor`). `WorkflowBuilder` fluent API. `WorkflowEngine` executes against `JarvisSystem`. TOML loader. Node types: agent, tool, condition, parallel, loop, transform.
|
||||
- **Skills** (`skills/`) — `SkillManifest`/`SkillExecutor` (sequential tool steps with template rendering). Ed25519 signature verification. `SkillTool` adapter wraps skills as invocable tools. TOML loader.
|
||||
- **Knowledge Graph** (`tools/storage/knowledge_graph.py`) — `KnowledgeGraphMemory(MemoryBackend)`: SQLite entity-relation store. `add_entity()`, `add_relation()`, `neighbors()`, `query_pattern()`. Registered as `"knowledge_graph"`.
|
||||
- **Sessions** (`sessions/`) — `SessionStore` (SQLite): cross-channel persistent sessions. `SessionIdentity` canonical user across channels. `consolidate()` summarizes old messages, `decay()` removes expired.
|
||||
- **A2A Protocol** (`a2a/`) — Google Agent-to-Agent spec (JSON-RPC 2.0). `A2AServer` (tasks/send, tasks/get, tasks/cancel, `/.well-known/agent.json`). `A2AClient`. `A2AAgentTool` adapter.
|
||||
- **TUI Dashboard** (`cli/dashboard.py`) — `textual`-based terminal dashboard (optional `[dashboard]`). Panels: system status, event stream, telemetry, agent activity, sessions.
|
||||
- **Desktop App** (`desktop/`) — Tauri 2.0 native desktop application. 5 dashboard panels: EnergyDashboard (real-time power monitoring with recharts), TraceDebugger (timeline inspection with step-type color coding), LearningCurve (policy visualization, GRPO/bandit stats), MemoryBrowser (search + stats), AdminPanel (health, agents, server control). Tauri commands proxy to OpenJarvis REST API. Plugins: notification, shell, global-shortcut, autostart, updater, single-instance. CI: `.github/workflows/desktop.yml` (Linux/macOS/Windows).
|
||||
- **Vault** (`cli/vault_cmd.py`) — Fernet-encrypted credential store at `~/.openjarvis/vault.enc` with auto-generated key (`0o600` permissions).
|
||||
- **MCP Quick-Add** (`cli/add_cmd.py`) — `jarvis add <server>` with 8 templates (github, filesystem, slack, postgres, brave-search, memory, puppeteer, google-maps). Saves JSON config to `~/.openjarvis/mcp/`.
|
||||
|
||||
### Core Module (`src/openjarvis/core/`)
|
||||
|
||||
- `registry.py` — `RegistryBase[T]` generic base. Subclasses: `ModelRegistry`, `EngineRegistry`, `MemoryRegistry`, `AgentRegistry`, `ToolRegistry`, `RouterPolicyRegistry`, `BenchmarkRegistry`, `ChannelRegistry`, `LearningRegistry`, `SkillRegistry`.
|
||||
- `types.py` — `Message`, `Conversation`, `ModelSpec`, `ToolResult`, `TelemetryRecord`, `StepType`, `TraceStep`, `Trace`, `RoutingContext`.
|
||||
- `config.py` — `JarvisConfig` dataclass hierarchy with TOML loader. Config classes for each pillar/subsystem. TOML sections: `[engine]` (+ nested `[engine.ollama]`, `[engine.vllm]`, `[engine.sglang]`, `[engine.llamacpp]`, `[engine.mlx]`, `[engine.lmstudio]`), `[intelligence]`, `[agent]`, `[tools.storage]`, `[tools.mcp]`, `[tools.browser]`, `[learning]` (+ nested routing/intelligence/agent/metrics), `[server]`, `[telemetry]`, `[traces]`, `[channel]`, `[security]` (+ `[security.capabilities]`, `ssrf_protection`, `rate_limit_*`), `[sandbox]`, `[scheduler]`, `[workflow]`, `[sessions]`, `[a2a]`. Backward-compat: `engine.ollama_host` → `engine.ollama.host`, `agent.default_tools` → `agent.tools`, TOML migration for cross-section moves.
|
||||
- `events.py` — Pub/sub event bus (synchronous dispatch). ~30 EventType values covering inference, tools, memory, agents, telemetry, traces, channels, security, scheduler, workflow, skills, sessions, A2A.
|
||||
|
||||
### Docker & Deployment
|
||||
|
||||
- `Dockerfile` — Multi-stage: Python 3.12-slim, `.[server]`, entrypoint `jarvis serve`
|
||||
- `Dockerfile.gpu` — NVIDIA CUDA 12.4 variant
|
||||
- `Dockerfile.gpu.rocm` — AMD ROCm 6.2 variant
|
||||
- `docker-compose.yml` — `jarvis` (8000) + `ollama` (11434). ROCm override: `docker-compose.gpu.rocm.yml`
|
||||
- `deploy/systemd/openjarvis.service`, `deploy/launchd/com.openjarvis.plist`
|
||||
|
||||
### Query Flow
|
||||
|
||||
User query → Security scanning (input) → Intelligence resolves model → Agentic Logic (tools/memory) → Memory retrieval → Context injection → Engine generates → Security scanning (output) → Trace recorded → Telemetry recorded → Learning policies update.
|
||||
|
||||
### API Surface
|
||||
|
||||
OpenAI-compatible server via `jarvis serve`:
|
||||
- **Core**: `POST /v1/chat/completions`, `GET /v1/models`, `GET /health`
|
||||
- **Channels**: `GET /v1/channels`, `POST /v1/channels/send`, `GET /v1/channels/status`
|
||||
- **Agents**: `GET /v1/agents`, `POST /v1/agents`, `DELETE /v1/agents/{id}`, `POST /v1/agents/{id}/message`
|
||||
- **Memory**: `POST /v1/memory/store`, `POST /v1/memory/search`, `GET /v1/memory/stats`
|
||||
- **Traces**: `GET /v1/traces`, `GET /v1/traces/{id}`
|
||||
- **Telemetry**: `GET /v1/telemetry/stats`, `GET /v1/telemetry/energy`
|
||||
- **Learning**: `GET /v1/learning/stats`, `GET /v1/learning/policy`
|
||||
- **Skills**: `GET /v1/skills`, `POST /v1/skills`, `DELETE /v1/skills/{name}`
|
||||
- **Sessions**: `GET /v1/sessions`, `GET /v1/sessions/{id}`
|
||||
- **Budget**: `GET /v1/budget`, `PUT /v1/budget/limits`
|
||||
- **Metrics**: `GET /metrics` (Prometheus-compatible)
|
||||
- **WebSocket**: `WS /v1/chat/stream` (JSON chunked streaming)
|
||||
- SSE streaming on `/v1/chat/completions` with `stream=true`
|
||||
|
||||
## Key Design Patterns
|
||||
|
||||
- **Registry pattern:** All extensible components use `@XRegistry.register("name")` decorator for registration and runtime discovery.
|
||||
- **ABC interfaces:** Each pillar defines an ABC. Implement the ABC + register via decorator to add a new backend.
|
||||
- **Offline-first:** Cloud APIs are optional. All core functionality works without network.
|
||||
- **Hardware-aware:** Auto-detect GPU vendor/model/VRAM via `nvidia-smi`, `rocm-smi`, `system_profiler`, `/proc/cpuinfo`. Recommend engine accordingly.
|
||||
- **Telemetry opt-in:** `InstrumentedEngine` wraps inference transparently. Agents unaware of telemetry.
|
||||
- **Backward-compat shims:** `memory/` re-exports from `tools/storage/`, `intelligence/` re-exports from `learning/`, `agents/react.py` re-exports as `ReActAgent`, registry alias `"react"` → `NativeReActAgent`. Old import paths and config keys continue to work.
|
||||
- **`ensure_registered()` pattern:** Benchmark and learning modules use lazy registration to survive registry clearing in tests.
|
||||
|
||||
## Development Phases
|
||||
|
||||
| Version | Phase | Delivers |
|
||||
|---------|-------|----------|
|
||||
| v0.1 | 0 | Scaffolding, registries, core types, config, CLI skeleton |
|
||||
| v0.2 | 1 | Intelligence + Inference — `jarvis ask` end-to-end |
|
||||
| v0.3 | 2 | Memory backends, document indexing, context injection |
|
||||
| v0.4 | 3 | Agents, tool system, OpenAI-compatible API server |
|
||||
| v0.5 | 4 | Learning, telemetry aggregation, `--router` CLI |
|
||||
| v1.0 | 5 | SDK, OpenClaw infra, benchmarks, Docker |
|
||||
| v1.1 | 6 | Trace system, trace-driven learning, pluggable agents |
|
||||
| v1.2 | 7 | 5-pillar restructuring, composition layer, MCP, structured learning |
|
||||
| v1.3 | 8 | Intelligence = "The Model", routing → Learning, engine selection |
|
||||
| v1.4 | 9 | Pillar-aligned config, nested configs, TOML migration |
|
||||
| v1.5 | 10 | Agent restructuring, BaseAgent/ToolUsingAgent, `accepts_tools`, OpenHands SDK |
|
||||
| v1.6 | 11 | NanoClaw subsumption: ClaudeCodeAgent, WhatsApp Baileys, Docker sandbox, TaskScheduler |
|
||||
| v1.7 | 12 | EnergyMonitor ABC (NVIDIA/AMD/Apple/RAPL), EnergyBatch, SteadyStateDetector |
|
||||
| v1.8 | 13 | `jarvis doctor`/`init`, MLX engine, AMD multi-GPU, PWA, ROCm Docker |
|
||||
| v1.9 | 14 | Agent hardening: LoopGuard, RBAC CapabilityPolicy, taint tracking, Merkle audit, Ed25519 |
|
||||
| v2.0 | 15 | WorkflowEngine (DAG), SkillSystem, KnowledgeGraphMemory, SessionStore |
|
||||
| v2.1 | 16 | A2A protocol, MCP templates, WasmRunner, TUI dashboard |
|
||||
| v2.2 | 17 | Production tool parity: FileWrite, ApplyPatch, ShellExec, Git, HTTP, DB, Browser, Agent, Media, PDF tools. SSRF protection, injection scanner, rate limiter, subprocess sandbox, security middleware |
|
||||
| v2.3 | 18 | CLI expansion (20 commands): daemon, chat REPL, agent, workflow, skill, vault, add. API expansion (40+ endpoints): agents, memory, traces, telemetry, learning, skills, sessions, budget, metrics, WebSocket streaming |
|
||||
| v2.4 | 19 | Learning productionization: GRPO (softmax/advantage), BanditRouter (Thompson/UCB1), SkillDiscovery (trace mining), ICL updates (versioning/rollback/quality gates) |
|
||||
| v2.5 | 20 | Tauri 2.0 desktop app: energy dashboard, trace debugger, learning curve visualization, memory browser, admin panel. CI for Linux/macOS/Windows |
|
||||
| v2.6 | 21 | 10 new channels: LINE, Viber, Messenger, Reddit, Mastodon, XMPP, Rocket.Chat, Zulip, Twitch, Nostr |
|
||||
@@ -1,85 +0,0 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [the project maintainers](https://github.com/open-jarvis/OpenJarvis/discussions). All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
# Contributing to OpenJarvis
|
||||
|
||||
Thank you for your interest in contributing to OpenJarvis! This guide covers everything you need to know — from why to contribute, to how to submit your first pull request.
|
||||
|
||||
---
|
||||
|
||||
## Why Contribute?
|
||||
|
||||
Contributing to OpenJarvis isn't just about code — it's about building the future of on-device AI together. Here's what you get:
|
||||
|
||||
### Paper Acknowledgment
|
||||
|
||||
All contributors with merged pull requests will be acknowledged as contributors on the OpenJarvis paper release.
|
||||
|
||||
### Mac Mini Giveaway
|
||||
|
||||
We're giving away a Mac Mini to one lucky contributor! Install OpenJarvis on your personal machine and opt in via the desktop app to share anonymized savings data (FLOPs, dollar cost, energy) for a chance to win. Your data is fully anonymous — no IP, no hardware info beyond savings metrics. You must share your email via the desktop app to be eligible.
|
||||
|
||||
See the [Savings Leaderboard](https://open-jarvis.github.io/OpenJarvis/leaderboard/) for details.
|
||||
|
||||
### Path to Maintainership
|
||||
|
||||
Consistent contributors can grow into project maintainers:
|
||||
|
||||
- **Contributor** — anyone with a merged PR
|
||||
- **Reviewer** — invited after 3+ merged PRs in a domain area, can review PRs
|
||||
- **Maintainer** — reviewers who demonstrate sustained engagement and good judgment
|
||||
|
||||
### Recognition
|
||||
|
||||
Contributors are recognized in release notes and on our GitHub repository.
|
||||
|
||||
---
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
### Good First Contributions
|
||||
|
||||
These are great starting points for new contributors:
|
||||
|
||||
- Documentation improvements and typo fixes
|
||||
- Bug reports with reproducible steps
|
||||
- New eval datasets and scorers
|
||||
- Test coverage improvements
|
||||
|
||||
Look for issues labeled [`good-first-issue`](https://github.com/open-jarvis/OpenJarvis/labels/good-first-issue).
|
||||
|
||||
### Ideal Contributions
|
||||
|
||||
- Bug fixes with tests
|
||||
- Performance improvements
|
||||
- New tools, engines, or agents following the [registry pattern](docs/development/contributing.md#registry-pattern)
|
||||
- New channel integrations (Telegram, Discord, Slack, etc.)
|
||||
|
||||
### Harder to Review
|
||||
|
||||
These require more context and review time. **Please open an issue for discussion before starting a PR:**
|
||||
|
||||
- New primitives or major extensions to existing ones
|
||||
- Large refactors
|
||||
- Changes to core abstractions (`BaseAgent`, `InferenceEngine`, etc.)
|
||||
|
||||
### May Not Be Accepted
|
||||
|
||||
To avoid wasted effort, note that PRs in these categories are unlikely to be merged:
|
||||
|
||||
- Changes that break backwards compatibility in the public API
|
||||
- Changes that add significant new dependencies without justification
|
||||
- Changes that add friction to the user experience
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
| Requirement | Version | Notes |
|
||||
|---|---|---|
|
||||
| Python | 3.10+ | Required |
|
||||
| [uv](https://docs.astral.sh/uv/) | Latest | Package manager |
|
||||
| Node.js | 22+ | Only needed for ClaudeCodeAgent and WhatsApp channel |
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/open-jarvis/OpenJarvis.git
|
||||
cd OpenJarvis
|
||||
uv sync --extra dev
|
||||
```
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
We use [pre-commit](https://pre-commit.com/) to run linting and formatting checks before each commit:
|
||||
|
||||
```bash
|
||||
uv run pre-commit install
|
||||
```
|
||||
|
||||
This installs Git hooks that automatically run [Ruff](https://docs.astral.sh/ruff/) on every commit. If the hooks fail, fix the issues and commit again.
|
||||
|
||||
For detailed development setup, code conventions, and project structure, see the [Development Guide](docs/development/contributing.md).
|
||||
|
||||
---
|
||||
|
||||
## Claiming Issues
|
||||
|
||||
1. Browse the [Roadmap](https://open-jarvis.github.io/OpenJarvis/development/roadmap/) for an item that interests you
|
||||
2. Check if a [GitHub issue](https://github.com/open-jarvis/OpenJarvis/issues) already exists for it — if not, [open one](https://github.com/open-jarvis/OpenJarvis/issues/new/choose) describing what you'd like to work on
|
||||
3. Comment **"take"** on the issue to get auto-assigned
|
||||
4. Fork, branch, and start working
|
||||
|
||||
If you've claimed an issue but can't finish it, please leave a comment so someone else can pick it up.
|
||||
|
||||
---
|
||||
|
||||
## Proposing Changes
|
||||
|
||||
### Trivial Changes
|
||||
|
||||
For small fixes (typos, doc improvements, simple bug fixes), go ahead and open a PR directly.
|
||||
|
||||
### Non-trivial Changes
|
||||
|
||||
For larger changes — new features, refactors, new dependencies — **open an issue first** to discuss the approach. This saves everyone time by catching design issues early.
|
||||
|
||||
Use the appropriate [issue template](https://github.com/open-jarvis/OpenJarvis/issues/new/choose):
|
||||
- **Bug Report** — for bugs with reproduction steps
|
||||
- **Feature Request** — for new functionality
|
||||
- **New Eval Dataset** — for contributing benchmarks
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Submitting
|
||||
|
||||
1. Run the full test suite:
|
||||
```bash
|
||||
uv run pytest tests/ -v
|
||||
```
|
||||
2. Run the linter:
|
||||
```bash
|
||||
uv run ruff check src/ tests/
|
||||
```
|
||||
3. Run the formatter:
|
||||
```bash
|
||||
uv run ruff format --check src/ tests/
|
||||
```
|
||||
4. Add tests for new functionality
|
||||
5. Follow the [registry pattern](docs/development/contributing.md#registry-pattern) for new components
|
||||
|
||||
### Commit Messages
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
feat: add FAISS memory backend
|
||||
fix: handle empty tool responses in orchestrator
|
||||
docs: update engine discovery documentation
|
||||
test: add coverage for BM25 backend
|
||||
refactor: simplify agent base class helpers
|
||||
```
|
||||
|
||||
Keep the first line under 72 characters. Reference relevant issues (e.g., `fixes #42`).
|
||||
|
||||
### What Makes a Good PR
|
||||
|
||||
- **Focused** — one feature, fix, or refactor per PR
|
||||
- **Tested** — includes unit tests covering new code paths
|
||||
- **Documented** — updates docstrings and docs if adding public API
|
||||
- **Backwards compatible** — avoids breaking existing interfaces without discussion
|
||||
|
||||
---
|
||||
|
||||
## Contribution Areas
|
||||
|
||||
OpenJarvis is built on five composable primitives. Here's where you can contribute:
|
||||
|
||||
| Area | What to Build | Guide |
|
||||
|---|---|---|
|
||||
| **Intelligence** | Model catalog entries, routing strategies | [Dev Guide](docs/development/contributing.md) |
|
||||
| **Engines** | New inference backends (e.g., TensorRT, ONNX) | [Dev Guide](docs/development/contributing.md) |
|
||||
| **Agents** | New agent types, agent improvements | [Dev Guide](docs/development/contributing.md) |
|
||||
| **Tools** | New tools (browser, API clients, etc.) | [Dev Guide](docs/development/contributing.md) |
|
||||
| **Learning** | Router policies, reward functions, training | [Dev Guide](docs/development/contributing.md) |
|
||||
| **Evals** | New datasets, scorers, benchmark configs | [Dev Guide](docs/development/contributing.md) |
|
||||
| **Channels** | Chat platform integrations | [Dev Guide](docs/development/contributing.md) |
|
||||
| **Rust Port** | PyO3 bindings, crate parity with Python | See `rust/` directory |
|
||||
|
||||
---
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you agree to uphold this code.
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
- Open a [Discussion](https://github.com/open-jarvis/OpenJarvis/discussions) for questions and help
|
||||
- Check the [documentation](https://open-jarvis.github.io/OpenJarvis/) for guides and API reference
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml README.md ./
|
||||
COPY src/ src/
|
||||
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv pip install --system ".[server]"
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["jarvis"]
|
||||
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,27 @@
|
||||
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04 AS builder
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml README.md ./
|
||||
COPY src/ src/
|
||||
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv pip install --system ".[server]"
|
||||
|
||||
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["jarvis"]
|
||||
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,27 @@
|
||||
FROM rocm/dev-ubuntu-22.04:6.2 AS builder
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml README.md ./
|
||||
COPY src/ src/
|
||||
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv pip install --system ".[server]"
|
||||
|
||||
FROM rocm/dev-ubuntu-22.04:6.2
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["jarvis"]
|
||||
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Install Node.js 22
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir ".[server]"
|
||||
|
||||
LABEL openjarvis-sandbox=true
|
||||
|
||||
ENTRYPOINT ["python", "-m", "openjarvis.sandbox.entrypoint"]
|
||||
@@ -1,19 +0,0 @@
|
||||
.PHONY: setup build test lint format
|
||||
|
||||
# Mirrors .github/workflows/ci.yml so `make test` matches CI locally.
|
||||
|
||||
setup:
|
||||
uv sync --extra dev --extra framework-comparison --extra server
|
||||
|
||||
build:
|
||||
uv run maturin develop --manifest-path rust/crates/openjarvis-python/Cargo.toml
|
||||
|
||||
test: build
|
||||
uv run pytest tests/ -n auto -q --tb=short -m "not live and not cloud and not hub"
|
||||
|
||||
lint:
|
||||
uv run ruff check src/ tests/
|
||||
uv run ruff format --check src/ tests/
|
||||
|
||||
format:
|
||||
uv run ruff format src/ tests/
|
||||
@@ -0,0 +1,492 @@
|
||||
# OpenJarvis Development Notes
|
||||
|
||||
Living document tracking implementation progress, testing state, lessons learned, dead ends, and practices for ongoing development. Updated across sessions.
|
||||
|
||||
---
|
||||
|
||||
## Current State (2026-02-21)
|
||||
|
||||
- **Version:** 1.0.0 (trace system added, targeting v1.1)
|
||||
- **All 6 roadmap phases complete** (Phase 0 through Phase 5) + Phase 6 trace system in progress
|
||||
- **Tests:** 576 passed, 8 skipped, 0 failures
|
||||
- **Lint:** ruff clean (`select = ["E", "F", "I", "W"]`)
|
||||
- **Source files:** 76 Python files in `src/openjarvis/`
|
||||
- **Test files:** 78 Python files in `tests/`
|
||||
- **Python:** 3.13 (compatible with 3.10+)
|
||||
- **Package manager:** `uv` with `hatchling` build backend
|
||||
|
||||
### 8 Skipped Tests (Optional Dependencies)
|
||||
|
||||
| Test | Missing Dep | Install Extra |
|
||||
|------|-------------|---------------|
|
||||
| `tests/memory/test_bm25.py` | `rank_bm25` | `openjarvis[memory-bm25]` |
|
||||
| `tests/memory/test_colbert.py` | `colbert` | `openjarvis[memory-colbert]` |
|
||||
| `tests/memory/test_embeddings.py` | `sentence_transformers` | `openjarvis[memory-faiss]` |
|
||||
| `tests/memory/test_faiss.py` | `faiss` | `openjarvis[memory-faiss]` |
|
||||
| `tests/server/test_models_pydantic.py` | `pydantic` | `openjarvis[server]` |
|
||||
| `tests/server/test_routes.py` | `fastapi` | `openjarvis[server]` |
|
||||
| `tests/test_integration.py:165` | `fastapi` | `openjarvis[server]` |
|
||||
| `tests/test_integration.py:190` | `fastapi` | `openjarvis[server]` |
|
||||
|
||||
---
|
||||
|
||||
## Phase Completion Log
|
||||
|
||||
| Phase | Version | Deliverables | Test Count (cumulative) |
|
||||
|-------|---------|-------------|------------------------|
|
||||
| Phase 0 | v0.1 | Scaffolding, registries, core types, config, CLI skeleton, event bus | ~60 |
|
||||
| Phase 1 | v0.2 | Intelligence + Inference — `jarvis ask` end-to-end, heuristic router, engine discovery, basic telemetry | ~160 |
|
||||
| Phase 2 | v0.3 | Memory — SQLite/FAISS/ColBERT/BM25/Hybrid backends, document ingest pipeline, context injection, `jarvis memory` CLI | ~270 |
|
||||
| Phase 3 | v0.4 | Agents (Simple/Orchestrator/Custom/OpenClaw stub), tool system (Calculator/Think/Retrieval/LLM/FileRead), OpenAI-compatible API server, `jarvis serve` | ~360 |
|
||||
| Phase 4 | v0.5 | Learning — HeuristicRouter, HeuristicRewardFunction, GRPORouterPolicy stub, TelemetryAggregator, `jarvis telemetry` CLI, `--router` CLI option | ~432 |
|
||||
| Phase 5 | v1.0 | SDK (`Jarvis` class), OpenClaw infrastructure (protocol/transport/plugin), benchmarks (`jarvis bench`), Docker, docs | ~520 |
|
||||
| Phase 6 | v1.1 | Trace system (TraceStore, TraceCollector, TraceAnalyzer), trace-driven learning (TraceDrivenPolicy) | ~576 |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Quick Reference
|
||||
|
||||
### Directory Layout
|
||||
|
||||
```
|
||||
src/openjarvis/
|
||||
├── __init__.py # __version__ = "1.0.0", exports Jarvis, MemoryHandle
|
||||
├── sdk.py # Python SDK: Jarvis class + MemoryHandle
|
||||
├── core/
|
||||
│ ├── registry.py # RegistryBase[T] + 7 typed registries
|
||||
│ ├── types.py # Message, Conversation, ModelSpec, ToolResult, TelemetryRecord
|
||||
│ ├── config.py # JarvisConfig dataclass hierarchy, TOML loader
|
||||
│ └── events.py # EventBus pub/sub (synchronous)
|
||||
├── intelligence/ # ModelRegistry, HeuristicRouter, model catalog
|
||||
├── traces/ # TraceStore, TraceCollector, TraceAnalyzer
|
||||
├── learning/ # RouterPolicyRegistry, HeuristicRouter, TraceDrivenPolicy, GRPO stub
|
||||
├── memory/ # SQLite/FAISS/ColBERT/BM25/Hybrid backends, chunking, ingest
|
||||
├── agents/ # Simple/Orchestrator/Custom/OpenClaw agents + protocol/transport
|
||||
├── engine/ # Ollama/vLLM/llama.cpp/Cloud engine wrappers
|
||||
├── tools/ # Calculator/Think/Retrieval/LLM/FileRead tools
|
||||
├── bench/ # Latency/Throughput benchmarks, BenchmarkSuite
|
||||
├── telemetry/ # TelemetryStore, TelemetryAggregator, instrumented_generate
|
||||
├── server/ # FastAPI OpenAI-compatible API server
|
||||
└── cli/ # Click CLI: init, ask, serve, model, memory, telemetry, bench
|
||||
```
|
||||
|
||||
### 7 Registries
|
||||
|
||||
All use `RegistryBase[T]` with `@XRegistry.register("name")` or `register_value()`:
|
||||
|
||||
1. `ModelRegistry` — `ModelSpec` objects
|
||||
2. `EngineRegistry` — `InferenceEngine` implementations
|
||||
3. `MemoryRegistry` — `MemoryBackend` implementations
|
||||
4. `AgentRegistry` — `BaseAgent` implementations
|
||||
5. `ToolRegistry` — `BaseTool` implementations
|
||||
6. `RouterPolicyRegistry` — `RouterPolicy` implementations
|
||||
7. `BenchmarkRegistry` — `BaseBenchmark` implementations
|
||||
|
||||
---
|
||||
|
||||
## Patterns and Practices
|
||||
|
||||
### The `ensure_registered()` Pattern
|
||||
|
||||
**Problem:** The `_clean_registries` autouse fixture in `tests/conftest.py` calls `.clear()` on every registry before each test. Module-level `@XRegistry.register("name")` decorators only fire once at import time (Python caches modules in `sys.modules`). After registry clearing, the decorations never re-fire, leaving registries empty for subsequent tests.
|
||||
|
||||
**Solution:** Use lazy registration via `ensure_registered()`:
|
||||
|
||||
```python
|
||||
# src/openjarvis/bench/latency.py
|
||||
_registered = False
|
||||
|
||||
def ensure_registered() -> None:
|
||||
global _registered
|
||||
if _registered:
|
||||
return
|
||||
from openjarvis.core.registry import BenchmarkRegistry
|
||||
if not BenchmarkRegistry.contains("latency"):
|
||||
BenchmarkRegistry.register_value("latency", LatencyBenchmark)
|
||||
_registered = True
|
||||
```
|
||||
|
||||
Then in `__init__.py`:
|
||||
```python
|
||||
def ensure_registered() -> None:
|
||||
from openjarvis.bench.latency import ensure_registered as _reg_latency
|
||||
_reg_latency()
|
||||
```
|
||||
|
||||
And in test files, use an autouse fixture:
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def _register_latency():
|
||||
from openjarvis.bench import ensure_registered
|
||||
ensure_registered()
|
||||
```
|
||||
|
||||
**Where this pattern is used:** `bench/latency.py`, `bench/throughput.py`, `learning/heuristic_policy.py`, `learning/grpo_policy.py`, `learning/heuristic_reward.py`
|
||||
|
||||
**Where this pattern is NOT needed:** Agents, engines, memory backends, and tools use `@register` decorators that work fine because their test files explicitly import and re-register as needed, or the test module import triggers registration.
|
||||
|
||||
### Test Infrastructure
|
||||
|
||||
- **`tests/conftest.py`** — `_clean_registries` autouse fixture clears all 7 registries + clears `EventBus` default listeners before each test. Critical for test isolation.
|
||||
- **Mock engine pattern** — Almost every test that touches the engine layer uses a `MagicMock()` with `.engine_id`, `.health()`, `.list_models()`, `.generate()` stubbed:
|
||||
```python
|
||||
def _make_engine(content="Hello"):
|
||||
engine = MagicMock()
|
||||
engine.engine_id = "mock"
|
||||
engine.health.return_value = True
|
||||
engine.list_models.return_value = ["test-model"]
|
||||
engine.generate.return_value = {
|
||||
"content": content,
|
||||
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
|
||||
"model": "test-model",
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
return engine
|
||||
```
|
||||
- **CLI tests** use Click's `CliRunner` with `patch("openjarvis.cli.X.get_engine", ...)` to mock the engine layer.
|
||||
- **Memory tests** use `tmp_path` fixture for SQLite DB paths and test files.
|
||||
- **Optional dep tests** use `pytest.importorskip("module_name")` at module level.
|
||||
|
||||
### Config Defaults
|
||||
|
||||
`JarvisConfig()` with no arguments produces sane defaults:
|
||||
- Engine: auto-discover (Ollama, vLLM, llama.cpp, cloud in priority order)
|
||||
- Memory: `sqlite` backend, `~/.openjarvis/memory.db`
|
||||
- Agent: `simple` (no Node.js dependency)
|
||||
- Intelligence: `qwen3:8b` default, `qwen3:0.6b` fallback
|
||||
- Telemetry: enabled, `~/.openjarvis/telemetry.db`
|
||||
- Learning: `heuristic` default policy
|
||||
|
||||
### File Naming Conventions
|
||||
|
||||
- ABCs and shared dataclasses: `_stubs.py` (e.g., `agents/_stubs.py`, `bench/_stubs.py`, `tools/_stubs.py`)
|
||||
- Internal helpers: `_discovery.py`, `_base.py` (underscore prefix)
|
||||
- CLI commands: `*_cmd.py` (e.g., `bench_cmd.py`, `telemetry_cmd.py`, `memory_cmd.py`)
|
||||
- Test files mirror source: `tests/agents/test_openclaw.py` tests `src/openjarvis/agents/openclaw.py`
|
||||
|
||||
### Import Structure
|
||||
|
||||
- Package `__init__.py` files import submodules to trigger registration
|
||||
- Try/except around optional dependency imports:
|
||||
```python
|
||||
try:
|
||||
from openjarvis.engine.ollama import OllamaEngine # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
```
|
||||
- Top-level `openjarvis/__init__.py` exports: `Jarvis`, `MemoryHandle`, `__version__`
|
||||
|
||||
---
|
||||
|
||||
## Dead Ends and Gotchas
|
||||
|
||||
### 1. `@register` Decorator vs. `ensure_registered()`
|
||||
|
||||
**Dead end:** Initially used `@BenchmarkRegistry.register("latency")` class decorator in `bench/latency.py`. This caused ~10 test failures because:
|
||||
- Registry cleared between tests by `conftest.py`
|
||||
- Module already in `sys.modules`, so `import openjarvis.bench` is a no-op on second import
|
||||
- Registry stays empty after clearing
|
||||
|
||||
**Fix:** Switched to `ensure_registered()` pattern (see above). This is the pattern already used by `learning/` modules.
|
||||
|
||||
**Rule of thumb:** If a module is imported at package init time AND its registry gets cleared in tests, use `ensure_registered()`. If registration only happens in test fixtures or explicit calls, `@register` is fine.
|
||||
|
||||
### 2. Chunk Attribute Names
|
||||
|
||||
`memory/chunking.py` `Chunk` dataclass uses `content` (not `text`). `ChunkConfig` uses `chunk_overlap` (not `overlap`). Easy to get wrong because these aren't obvious from the field names alone. Always read `_stubs.py` or the actual dataclass before using.
|
||||
|
||||
### 3. Test Content Size for Chunking
|
||||
|
||||
`ChunkConfig.min_chunk_size=50` tokens by default. A test string like `"This is test content."` produces 0 chunks. Use at least ~100 words:
|
||||
```python
|
||||
words = " ".join(f"word{i}" for i in range(100))
|
||||
```
|
||||
|
||||
### 4. Version String Locations
|
||||
|
||||
Version is defined in **three places** that must stay in sync:
|
||||
1. `src/openjarvis/__init__.py` — `__version__ = "1.0.0"`
|
||||
2. `pyproject.toml` — `version = "1.0.0"`
|
||||
3. `src/openjarvis/server/app.py` — FastAPI `version="1.0.0"` constructor arg
|
||||
|
||||
Tests that check version: `tests/cli/test_cli.py::test_version_flag`
|
||||
|
||||
### 5. Server Import Guards
|
||||
|
||||
The `server/` module requires `fastapi`, `uvicorn`, `pydantic`. These are behind the `[server]` optional extra. All test files that touch server code use `pytest.importorskip("fastapi")`. The server `__init__.py` wraps imports in try/except.
|
||||
|
||||
### 6. `patch()` Targets for Engine Mocking
|
||||
|
||||
When mocking `get_engine` in CLI tests, the patch target must be the *importing module*, not the source module:
|
||||
```python
|
||||
# CORRECT — patches where it's imported
|
||||
patch("openjarvis.cli.bench_cmd.get_engine", return_value=("mock", engine))
|
||||
|
||||
# WRONG — patches the source, doesn't affect the already-imported reference
|
||||
patch("openjarvis.engine._discovery.get_engine", return_value=("mock", engine))
|
||||
```
|
||||
|
||||
Same for SDK tests: `patch("openjarvis.sdk.get_engine", ...)`.
|
||||
|
||||
### 7. EventBus Clearing
|
||||
|
||||
`EventBus()` creates a new instance each time, but `EventBus._default_listeners` is a class variable. The `conftest.py` fixture resets it. If tests subscribe to events, subscriptions won't persist across tests.
|
||||
|
||||
### 8. Module Shadowing in CLI Package
|
||||
|
||||
In `cli/__init__.py`, `from openjarvis.cli.ask import ask` imports the Click command. This shadows the module name. When you try `mock.patch("openjarvis.cli.ask.get_engine")`, Python resolves `openjarvis.cli.ask` as the Click command (via attribute lookup on the package), not the module.
|
||||
|
||||
**Fix:** Use `importlib.import_module("openjarvis.cli.ask")` to get the actual module object, then `mock.patch.object(module, "get_engine")`.
|
||||
|
||||
---
|
||||
|
||||
## Post-v1.0: Unimplemented Ideas from VISION.md
|
||||
|
||||
These are mentioned in `VISION.md` but not in the roadmap phases. They represent future work:
|
||||
|
||||
### Learning / Router
|
||||
- [ ] Learned router via GRPO (Group Relative Policy Optimization) — `GRPORouterPolicy` is a stub
|
||||
- [ ] Preference learning from user feedback
|
||||
- [ ] Continual fine-tuning on accumulated trajectories
|
||||
- [ ] Multi-objective optimization: quality vs. latency vs. energy vs. cost
|
||||
|
||||
### Memory
|
||||
- [ ] ConversationMemory — sliding window with automatic summarization of older turns
|
||||
- [ ] Personal Notes — user-created persistent notes and preferences
|
||||
- [ ] Episodic Memory — records of past interactions, tool uses, and outcomes
|
||||
- [ ] Vector DB adapters (Qdrant, ChromaDB) for users with existing infrastructure
|
||||
|
||||
### Tools
|
||||
- [ ] WebSearch tool (Tavily, SearXNG, DuckDuckGo)
|
||||
- [ ] CodeInterpreter tool (sandboxed Python execution)
|
||||
- [ ] FileWrite tool (safe file writing with path validation)
|
||||
- [ ] MCP (Model Context Protocol) compatibility
|
||||
|
||||
### Engines
|
||||
- [ ] SGLang engine backend (structured generation, constrained decoding)
|
||||
- [ ] MLX engine backend (Apple Silicon native, Metal acceleration)
|
||||
- [ ] Complete vLLM integration (tensor parallelism config, multi-GPU)
|
||||
|
||||
### OpenClaw
|
||||
- [ ] Full OpenClaw gateway integration (WebSocket, `:18789`)
|
||||
- [ ] OpenClaw skill composition
|
||||
- [ ] Context compaction in OpenClaw agent
|
||||
- [ ] `openjarvis-openclaw` as separate plugin package (currently inline)
|
||||
|
||||
### Infrastructure
|
||||
- [ ] Documentation site (MkDocs or similar)
|
||||
- [ ] Getting started guide
|
||||
- [ ] Plugin development guide
|
||||
- [ ] API reference docs
|
||||
- [ ] CI/CD pipeline
|
||||
- [ ] PyPI publishing
|
||||
|
||||
---
|
||||
|
||||
## Testing Recipes
|
||||
|
||||
### Run all tests
|
||||
```bash
|
||||
uv sync --extra dev
|
||||
uv run pytest tests/ -v --tb=short
|
||||
```
|
||||
|
||||
### Run a specific module's tests
|
||||
```bash
|
||||
uv run pytest tests/bench/ -v
|
||||
uv run pytest tests/sdk/ -v
|
||||
uv run pytest tests/agents/test_openclaw.py -v
|
||||
```
|
||||
|
||||
### Run with optional deps (server)
|
||||
```bash
|
||||
uv sync --extra dev --extra server
|
||||
uv run pytest tests/server/ -v # No longer skipped
|
||||
```
|
||||
|
||||
### Lint
|
||||
```bash
|
||||
uv run ruff check src/ tests/
|
||||
uv run ruff check src/ tests/ --fix # Auto-fix
|
||||
```
|
||||
|
||||
### Quick smoke test
|
||||
```bash
|
||||
uv run jarvis --version # 1.0.0
|
||||
uv run jarvis --help # All subcommands
|
||||
python -c "from openjarvis import Jarvis; print(Jarvis)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New Components
|
||||
|
||||
### New Benchmark
|
||||
|
||||
1. Create `src/openjarvis/bench/my_benchmark.py`:
|
||||
```python
|
||||
from openjarvis.bench._stubs import BaseBenchmark, BenchmarkResult
|
||||
|
||||
class MyBenchmark(BaseBenchmark):
|
||||
@property
|
||||
def name(self) -> str: return "my-bench"
|
||||
@property
|
||||
def description(self) -> str: return "Description"
|
||||
def run(self, engine, model, *, num_samples=10) -> BenchmarkResult: ...
|
||||
|
||||
_registered = False
|
||||
def ensure_registered():
|
||||
global _registered
|
||||
if _registered: return
|
||||
from openjarvis.core.registry import BenchmarkRegistry
|
||||
if not BenchmarkRegistry.contains("my-bench"):
|
||||
BenchmarkRegistry.register_value("my-bench", MyBenchmark)
|
||||
_registered = True
|
||||
```
|
||||
2. Import in `bench/__init__.py` `ensure_registered()`
|
||||
3. Add test file `tests/bench/test_my_benchmark.py` with autouse fixture calling `ensure_registered()`
|
||||
|
||||
### New Tool
|
||||
|
||||
1. Create `src/openjarvis/tools/my_tool.py`:
|
||||
```python
|
||||
from openjarvis.core.registry import ToolRegistry
|
||||
from openjarvis.tools._stubs import BaseTool, ToolSpec
|
||||
|
||||
@ToolRegistry.register("my-tool")
|
||||
class MyTool(BaseTool):
|
||||
@property
|
||||
def spec(self) -> ToolSpec: ...
|
||||
def execute(self, input: str, **params) -> str: ...
|
||||
```
|
||||
2. Import in `tools/__init__.py`
|
||||
3. Add test file `tests/tools/test_my_tool.py`
|
||||
|
||||
### New Memory Backend
|
||||
|
||||
1. Create `src/openjarvis/memory/my_backend.py`:
|
||||
```python
|
||||
from openjarvis.core.registry import MemoryRegistry
|
||||
from openjarvis.memory._stubs import MemoryBackend, RetrievalResult
|
||||
|
||||
@MemoryRegistry.register("my-backend")
|
||||
class MyBackend(MemoryBackend):
|
||||
def store(self, content, *, source="", metadata=None) -> str: ...
|
||||
def retrieve(self, query, top_k=5) -> list[RetrievalResult]: ...
|
||||
def delete(self, doc_id) -> bool: ...
|
||||
def clear(self) -> None: ...
|
||||
```
|
||||
2. Import in `memory/__init__.py` with try/except for optional deps
|
||||
3. Add test file with `pytest.importorskip()` if using optional deps
|
||||
4. Add optional dep group in `pyproject.toml` if needed
|
||||
|
||||
### New Agent
|
||||
|
||||
1. Create `src/openjarvis/agents/my_agent.py`:
|
||||
```python
|
||||
from openjarvis.agents._stubs import AgentResult, BaseAgent
|
||||
from openjarvis.core.registry import AgentRegistry
|
||||
|
||||
@AgentRegistry.register("my-agent")
|
||||
class MyAgent(BaseAgent):
|
||||
agent_id = "my-agent"
|
||||
def __init__(self, engine, model, *, bus=None, **kwargs): ...
|
||||
def run(self, input, context=None, **kwargs) -> AgentResult: ...
|
||||
```
|
||||
2. Import in `agents/__init__.py`
|
||||
3. Add test file `tests/agents/test_my_agent.py`
|
||||
|
||||
### New Engine
|
||||
|
||||
1. Create `src/openjarvis/engine/my_engine.py`:
|
||||
```python
|
||||
from openjarvis.core.registry import EngineRegistry
|
||||
from openjarvis.engine._stubs import InferenceEngine
|
||||
|
||||
@EngineRegistry.register("my-engine")
|
||||
class MyEngine(InferenceEngine):
|
||||
engine_id = "my-engine"
|
||||
def generate(self, messages, *, model, **kwargs) -> dict: ...
|
||||
def stream(self, messages, *, model, **kwargs): ...
|
||||
def list_models(self) -> list[str]: ...
|
||||
def health(self) -> bool: ...
|
||||
```
|
||||
2. Import in `engine/__init__.py` with try/except
|
||||
3. Add to `_discovery.py` engine priority list if auto-discoverable
|
||||
|
||||
---
|
||||
|
||||
## Session Log
|
||||
|
||||
### Session 1 (2026-02-16) — Phase 5 Implementation
|
||||
|
||||
**Scope:** Full Phase 5 (v1.0) — SDK, OpenClaw, Benchmarks, Docker, Docs
|
||||
|
||||
**Work completed:**
|
||||
- Step 1: Added `BenchmarkRegistry` to `core/registry.py`, updated `conftest.py`
|
||||
- Step 2: Created `bench/` package — `_stubs.py`, `latency.py`, `throughput.py`, `__init__.py`; CLI `bench_cmd.py`
|
||||
- Step 3: Created `sdk.py` — `Jarvis` class + `MemoryHandle`; updated `__init__.py` exports
|
||||
- Step 4: Created OpenClaw infra — `openclaw_protocol.py`, `openclaw_transport.py`, `openclaw_plugin.py`; rewrote `openclaw.py` from stub
|
||||
- Step 5: Created `Dockerfile`, `Dockerfile.gpu`, `docker-compose.yml`, `deploy/systemd/openjarvis.service`, `deploy/launchd/com.openjarvis.plist`
|
||||
- Step 6: Version bump to 1.0.0, updated `README.md`, `CLAUDE.md`
|
||||
|
||||
**Bugs fixed during implementation:**
|
||||
1. Ruff lint: 17 issues (E501, I001, F401, F841) — all fixed
|
||||
2. Registry clearing broke `@register` decorators — switched to `ensure_registered()` for bench modules
|
||||
3. `ChunkConfig(overlap=...)` should be `ChunkConfig(chunk_overlap=...)` — fixed
|
||||
4. `chunk.text` should be `chunk.content` — fixed
|
||||
5. Test content too short for chunking (0 chunks produced) — used 100 words
|
||||
|
||||
**Final: 520 passed, 8 skipped, 0 failures, ruff clean**
|
||||
|
||||
### Session 2 (2026-02-17) — Test Fixes + Live vLLM Testing
|
||||
|
||||
**Scope:** Fix broken tests, set up live vLLM inference testing
|
||||
|
||||
**Work completed:**
|
||||
- Fixed 6 failed + 13 errored tests in `tests/cli/test_ask_router.py` and `tests/cli/test_ask_agent.py`
|
||||
- **Root cause:** `from openjarvis.cli.ask import ask` in `cli/__init__.py` shadows the `ask` module with the Click command object. When `mock.patch("openjarvis.cli.ask.get_engine")` resolves, it tries to patch an attribute on the Click command, not the module.
|
||||
- **Fix:** Use `importlib.import_module("openjarvis.cli.ask")` + `mock.patch.object(_ask_mod, "get_engine")` instead of string-based patching.
|
||||
- Added tool fallback in `_openai_compat.py`: if server returns 400 when tools are sent (e.g., vLLM without `--enable-auto-tool-choice`), retry without tools.
|
||||
- Verified live vLLM testing: existing vLLM server on port 8003 with `Qwen/Qwen3-8B`
|
||||
- Tested: `jarvis ask`, `jarvis bench run`, `jarvis model list`, `jarvis memory index/search`, `jarvis telemetry stats`, SDK `Jarvis.ask()` and `ask_full()`
|
||||
|
||||
**Gotcha discovered:**
|
||||
8. **Module shadowing with `from X import Y`** — If a package's `__init__.py` does `from openjarvis.cli.ask import ask`, then `openjarvis.cli.ask` in `sys.modules` is the *module*, but accessing it via attribute lookup on `openjarvis.cli` gives the imported *object* (the Click command). Use `importlib.import_module()` for reliable module access when patching.
|
||||
|
||||
**Live vLLM setup notes:**
|
||||
- vLLM 0.15.1 running on Lambda cluster (8x A100-SXM4-80GB)
|
||||
- Config: `~/.openjarvis/config.toml` with `vllm_host = "http://localhost:8003"` and `default_model = "Qwen/Qwen3-8B"`
|
||||
- Tool calling requires `--enable-auto-tool-choice --tool-call-parser hermes` flags on vLLM server
|
||||
- Without tool support, orchestrator falls back to reasoning-only mode
|
||||
|
||||
**Final: 520 passed, 8 skipped, 0 failures, ruff clean**
|
||||
|
||||
### Session 3 (2026-02-21) — Trace System & Research Direction
|
||||
|
||||
**Scope:** Design new research direction (abstractions for local AI), implement trace system
|
||||
|
||||
**Design decisions made:**
|
||||
- OpenJarvis repositioned as a research framework for studying on-device AI
|
||||
- Four core abstractions: Intelligence, Engine, Agentic Logic, Memory
|
||||
- Learning is a cross-cutting concern driven by interaction traces
|
||||
- Agentic Logic should be pluggable — users bring their own architecture (ReAct, OpenHands-style, etc.)
|
||||
- Trace collection is the bridge between static and learned agents
|
||||
- Evolve existing codebase rather than full redesign
|
||||
- Name stays as OpenJarvis
|
||||
- Learning focus: telemetry-driven routing/tool policies (lightweight, always-on)
|
||||
- Agent-model coupling: loose (any agent, any model)
|
||||
|
||||
**Work completed:**
|
||||
- Added `StepType` enum, `TraceStep`, `Trace` dataclasses to `core/types.py`
|
||||
- Added `TRACE_STEP`, `TRACE_COMPLETE` event types to `core/events.py`
|
||||
- Created `traces/` package:
|
||||
- `store.py` — `TraceStore`: SQLite-backed, save/get/list with filters, event bus subscription
|
||||
- `collector.py` — `TraceCollector`: wraps any `BaseAgent`, subscribes to EventBus, records steps automatically
|
||||
- `analyzer.py` — `TraceAnalyzer`: per-route stats, per-tool stats, summaries, export, query-type filtering
|
||||
- Created `learning/trace_policy.py` — `TraceDrivenPolicy`: learns routing from trace outcomes, batch/online updates, registered as `"learned"` policy
|
||||
- Registered `TraceDrivenPolicy` in `learning/__init__.py`
|
||||
- Added 56 new tests across 4 test files in `tests/traces/` and `tests/learning/test_trace_policy.py`
|
||||
- Updated all markdown documentation (README, VISION, ROADMAP, NOTES, CLAUDE)
|
||||
|
||||
**Final: 576 passed, 8 skipped, 0 failures, ruff clean**
|
||||
@@ -1,152 +1,94 @@
|
||||
<div align="center">
|
||||
<img alt="OpenJarvis" src="assets/OpenJarvis_Horizontal_Logo.png" width="400">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="assets/openjarvis-logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="assets/openjarvis-logo-light.svg">
|
||||
<img alt="OpenJarvis" src="assets/openjarvis-logo-light.svg" width="400">
|
||||
</picture>
|
||||
|
||||
<p><i>Personal AI, On Personal Devices.</i></p>
|
||||
<p><i>Programming abstractions for on-device AI.</i></p>
|
||||
|
||||
<p>
|
||||
<a href="https://arxiv.org/abs/2605.17172"><img src="https://img.shields.io/badge/arXiv-2605.17172-b31b1b.svg" alt="arXiv"></a>
|
||||
<a href="https://openjarvis.stanford.edu/"><img src="https://img.shields.io/badge/project-OpenJarvis-blue" alt="Project"></a>
|
||||
<a href="https://open-jarvis.github.io/OpenJarvis/"><img src="https://img.shields.io/badge/docs-mkdocs-blue" alt="Docs"></a>
|
||||
<a href="https://www.intelligence-per-watt.ai/"><img src="https://img.shields.io/badge/project-intelligence--per--watt.ai-blue" alt="Project"></a>
|
||||
<a href="https://hazyresearch.stanford.edu/OpenJarvis/"><img src="https://img.shields.io/badge/docs-mkdocs-blue" alt="Docs"></a>
|
||||
<img src="https://img.shields.io/badge/python-%3E%3D3.10-blue" alt="Python">
|
||||
<img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License">
|
||||
<a href="https://discord.gg/CMVBmDQ5Fj"><img src="https://img.shields.io/badge/discord-join-7289da?logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://x.com/OpenJarvisAI"><img src="https://img.shields.io/badge/X-@OpenJarvisAI-black?logo=x&logoColor=white" alt="X / Twitter"></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<img alt="OpenJarvis demo reel" src="assets/openjarvis_demo_reel.webp" width="75%">
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
> **[Documentation](https://open-jarvis.github.io/OpenJarvis/)**
|
||||
> **[Documentation](https://hazyresearch.stanford.edu/OpenJarvis/)**
|
||||
>
|
||||
> **[Project Site](https://openjarvis.stanford.edu/)**
|
||||
>
|
||||
> **[Paper](https://arxiv.org/abs/2605.17172)**
|
||||
>
|
||||
> **[Leaderboard](https://open-jarvis.github.io/OpenJarvis/leaderboard/)**
|
||||
>
|
||||
> **[Roadmap](https://open-jarvis.github.io/OpenJarvis/development/roadmap/)**
|
||||
> **[Project Site](https://www.intelligence-per-watt.ai/)**
|
||||
|
||||
## Why OpenJarvis?
|
||||
OpenJarvis is a framework for building AI systems that run *entirely on local hardware*. Rather than treating intelligence as a cloud service, OpenJarvis provides composable abstractions for local model selection, inference, agentic reasoning, tool use, and learning — all aware of the hardware they run on.
|
||||
|
||||
Personal AI agents are exploding in popularity, but nearly all of them still route intelligence through cloud APIs. Your "personal" AI continues to depend on someone else's server. At the same time, our [Intelligence Per Watt](https://www.intelligence-per-watt.ai/) research showed that local language models already handle 88.7% of single-turn chat and reasoning queries, with intelligence efficiency improving 5.3× from 2023 to 2025. The models and hardware are increasingly ready. What has been missing is the software stack to make local-first personal AI practical.
|
||||
```python
|
||||
from openjarvis import Jarvis
|
||||
|
||||
OpenJarvis is that stack. It is a framework for local-first personal AI, built around three core ideas: shared primitives for building on-device agents; evaluations that treat energy, FLOPs, latency, and dollar cost as first-class constraints alongside accuracy; and a learning loop that improves models using local trace data. The goal is simple: make it possible to build personal AI agents that run locally by default, calling the cloud only when truly necessary. OpenJarvis aims to be both a research platform and a production foundation for local AI, in the spirit of PyTorch.
|
||||
j = Jarvis() # auto-detect hardware + engine
|
||||
response = j.ask("Explain backpropagation") # route to best local model
|
||||
|
||||
j.ask("Solve x^2 - 5x + 6 = 0", # multi-turn agent with tools
|
||||
agent="orchestrator",
|
||||
tools=["calculator", "think"])
|
||||
|
||||
j.memory.index("./papers/") # index documents into local storage
|
||||
results = j.memory.search("attention mechanism") # semantic retrieval
|
||||
j.close()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Pick your platform and run one command. Each installer handles [uv](https://docs.astral.sh/uv/), the Python venv, Ollama, and a starter model — about 3 minutes on broadband.
|
||||
```bash
|
||||
pip install openjarvis # core framework
|
||||
pip install openjarvis[server] # + FastAPI server
|
||||
```
|
||||
|
||||
| Platform | One-liner |
|
||||
|---|---|
|
||||
| **macOS · Linux · WSL2** | `curl -fsSL https://open-jarvis.github.io/OpenJarvis/install.sh \| bash` |
|
||||
| **Native Windows** | `irm https://open-jarvis.github.io/OpenJarvis/install.ps1 \| iex` |
|
||||
| **Desktop GUI** | Download `.exe` / `.dmg` / `.deb` / `.rpm` / `.AppImage` from the [latest release](https://github.com/open-jarvis/OpenJarvis/releases) |
|
||||
|
||||
Then `jarvis` to start. The Rust extension and larger models continue downloading in the background; `jarvis doctor` shows status.
|
||||
|
||||
Platform-specific notes (WSL2 setup, native-Windows scheduled-task service, desktop prerequisites, manual / contributor install): see the [installation docs](https://open-jarvis.github.io/OpenJarvis/getting-started/install/).
|
||||
You also need a local inference backend: [Ollama](https://ollama.com), [vLLM](https://github.com/vllm-project/vllm), [SGLang](https://github.com/sgl-project/sglang), or [llama.cpp](https://github.com/ggerganov/llama.cpp).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
jarvis # start chatting (default: chat-simple)
|
||||
jarvis init --preset <name> # switch to a starter config
|
||||
```
|
||||
|
||||
> Prefix `jarvis ...` with `uv run`, or `source .venv/bin/activate` first.
|
||||
|
||||
| Preset | What it does |
|
||||
|---|---|
|
||||
| `morning-digest-mac` / `morning-digest-linux` / `morning-digest-minimal` | Spoken daily briefing from email, calendar, health, news |
|
||||
| `deep-research` | Multi-hop research across indexed docs with citations |
|
||||
| `code-assistant` | Agent with code execution, file I/O, and shell access |
|
||||
| `scheduled-monitor` | Stateful agent on a schedule with memory |
|
||||
| `chat-simple` | Lightweight conversation, no tools |
|
||||
|
||||
Example:
|
||||
The fastest path is Ollama on any machine with Python 3.10+:
|
||||
|
||||
```bash
|
||||
jarvis init --preset morning-digest-mac
|
||||
jarvis connect gdrive # one OAuth covers Gmail / Calendar / Tasks
|
||||
jarvis digest --fresh # generate and play your first briefing
|
||||
# 1. Install OpenJarvis
|
||||
pip install openjarvis
|
||||
|
||||
# 2. Detect hardware and generate config
|
||||
jarvis init
|
||||
|
||||
# 3. Install and start Ollama (https://ollama.com)
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
ollama serve # start the Ollama server
|
||||
|
||||
# 4. Pull a model
|
||||
ollama pull qwen3:8b
|
||||
|
||||
# 5. Ask a question
|
||||
jarvis ask "What is the capital of France?"
|
||||
|
||||
# 6. Verify your setup
|
||||
jarvis doctor
|
||||
```
|
||||
|
||||
Per-preset deep dives: [morning digest](https://open-jarvis.github.io/OpenJarvis/user-guide/morning-digest/) · [deep research](https://open-jarvis.github.io/OpenJarvis/user-guide/deep-research/) · [code assistant](https://open-jarvis.github.io/OpenJarvis/user-guide/code-assistant/) · [scheduled monitor](https://open-jarvis.github.io/OpenJarvis/user-guide/scheduled-monitor/) · [chat simple](https://open-jarvis.github.io/OpenJarvis/user-guide/chat-simple/) · or the full [quickstart guide](https://open-jarvis.github.io/OpenJarvis/getting-started/quickstart/).
|
||||
`jarvis init` auto-detects your hardware and recommends the best engine. After init, it prints engine-specific next steps. Run `jarvis doctor` at any time to diagnose configuration or connectivity issues.
|
||||
|
||||
### Skills
|
||||
## The Five Pillars
|
||||
|
||||
Skills teach agents how to better use tools and improve their reasoning. Every skill is a tool — agents discover them from a catalog and invoke them on demand.
|
||||
| Pillar | What it does | Key abstractions |
|
||||
|--------|-------------|-----------------|
|
||||
| **Intelligence** | Model management and routing | `RouterPolicy`, `QueryAnalyzer`, `ModelCatalog` |
|
||||
| **Engine** | Inference runtime abstraction | `InferenceEngine` ABC — Ollama, vLLM, SGLang, llama.cpp, MLX |
|
||||
| **Agents** | Pluggable reasoning strategies | `BaseAgent` ABC — Simple, Orchestrator, ReAct, OpenHands, OpenClaw |
|
||||
| **Tools** | Capabilities via MCP | `BaseTool` ABC — calculator, code interpreter, web search, memory; external MCP servers auto-discovered |
|
||||
| **Learning** | Trace-driven adaptation | `LearningPolicy` ABC — SFT (model routing), AgentAdvisor (restructuring), ICL (tool usage) |
|
||||
|
||||
```bash
|
||||
# Install skills from public sources
|
||||
jarvis skill install hermes:arxiv
|
||||
jarvis skill sync hermes --category research
|
||||
|
||||
# Use skills with any agent
|
||||
jarvis ask "Use the code-explainer skill to explain this Python code: for i in range(5): print(i*2)"
|
||||
|
||||
# Optimize skills from your trace history
|
||||
jarvis optimize skills --policy dspy
|
||||
|
||||
# Benchmark the impact
|
||||
jarvis bench skills --max-samples 5 --seeds 42
|
||||
```
|
||||
|
||||
Import from [Hermes Agent](https://github.com/NousResearch/hermes-agent) (~150 skills), [OpenClaw](https://github.com/openclaw/skills) (~13,700 community skills), or any GitHub repo. Skills follow the [agentskills.io](https://agentskills.io/specification) open standard.
|
||||
|
||||
See the [Skills User Guide](https://open-jarvis.github.io/OpenJarvis/user-guide/skills/) and [Skills Tutorial](https://open-jarvis.github.io/OpenJarvis/tutorials/skills-workflow/) for details.
|
||||
|
||||
### Built-in Agents
|
||||
|
||||
OpenJarvis ships with eight built-in agents across three execution modes (on-demand, scheduled, continuous):
|
||||
|
||||
| Agent | Type | What it does |
|
||||
|-------|------|-------------|
|
||||
| `morning_digest` | Scheduled | Daily briefing from email, calendar, health, news — with TTS audio |
|
||||
| `deep_research` | On-demand | Multi-hop research with citations across web and local docs |
|
||||
| `monitor_operative` | Continuous | Long-horizon monitoring with memory, compression, and retrieval |
|
||||
| `orchestrator` | On-demand | Multi-turn reasoning with automatic tool selection |
|
||||
| `native_react` | On-demand | ReAct (Thought-Action-Observation) loop agent |
|
||||
| `operative` | Continuous | Persistent autonomous agent with state management |
|
||||
| `native_openhands` | On-demand | CodeAct — generates and executes Python code |
|
||||
| `simple` | On-demand | Single-turn chat, no tools |
|
||||
|
||||
See the [User Guide](https://open-jarvis.github.io/OpenJarvis/user-guide/morning-digest/) and [Tutorials](https://open-jarvis.github.io/OpenJarvis/tutorials/) for detailed setup instructions.
|
||||
|
||||
Full documentation — including Docker deployment, cloud engines, development setup, and tutorials — at **[open-jarvis.github.io/OpenJarvis](https://open-jarvis.github.io/OpenJarvis/)**.
|
||||
|
||||
## Community
|
||||
|
||||
- **GitHub:** [github.com/open-jarvis/OpenJarvis](https://github.com/open-jarvis/OpenJarvis)
|
||||
- **Discord:** [discord.gg/CMVBmDQ5Fj](https://discord.gg/CMVBmDQ5Fj)
|
||||
- **X / Twitter:** [@OpenJarvisAI](https://x.com/OpenJarvisAI)
|
||||
- **Docs:** [open-jarvis.github.io/OpenJarvis](https://open-jarvis.github.io/OpenJarvis/)
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! See the [Contributing Guide](CONTRIBUTING.md) for incentives, contribution types, and the PR process.
|
||||
|
||||
Quick start for contributors:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/open-jarvis/OpenJarvis.git
|
||||
cd OpenJarvis
|
||||
uv sync --extra dev
|
||||
uv run pre-commit install
|
||||
uv run pytest tests/ -v
|
||||
```
|
||||
|
||||
Browse the [Roadmap](https://open-jarvis.github.io/OpenJarvis/development/roadmap/) for areas where help is needed. Comment **"take"** on any issue to get auto-assigned.
|
||||
Every interaction produces a **Trace** — a structured record of the full reasoning chain. Learning policies consume traces to improve model selection, agent behavior, and tool usage over time.
|
||||
|
||||
## About
|
||||
|
||||
OpenJarvis is part of [Intelligence Per Watt](https://www.intelligence-per-watt.ai/), a research initiative studying the intelligence efficiency of AI systems. The project is developed at [Hazy Research](https://hazyresearch.stanford.edu/) and the [Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/) at [Stanford SAIL](https://ai.stanford.edu/).
|
||||
OpenJarvis is part of [Intelligence Per Watt](https://www.intelligence-per-watt.ai/), a research initiative studying the efficiency of on-device AI systems. The project is developed at [Hazy Research](https://hazyresearch.stanford.edu/) and the [Scaling Intelligence Lab](https://scalingintelligence.stanford.edu/) at [Stanford SAIL](https://ai.stanford.edu/).
|
||||
|
||||
## Sponsors
|
||||
|
||||
@@ -154,25 +96,9 @@ OpenJarvis is part of [Intelligence Per Watt](https://www.intelligence-per-watt.
|
||||
<a href="https://www.laude.org/">Laude Institute</a> •
|
||||
<a href="https://datascience.stanford.edu/marlowe">Stanford Marlowe</a> •
|
||||
<a href="https://cloud.google.com/">Google Cloud Platform</a> •
|
||||
<a href="https://lambda.ai/">Lambda Labs</a> •
|
||||
<a href="https://ollama.com/">Ollama</a> •
|
||||
<a href="https://research.ibm.com/">IBM Research</a> •
|
||||
<a href="https://hai.stanford.edu/">Stanford HAI</a>
|
||||
<a href="https://lambda.ai/">Lambda Labs</a>
|
||||
</p>
|
||||
|
||||
## Citation
|
||||
```bibtex
|
||||
@misc{saadfalcon2026openjarvispersonalaipersonal,
|
||||
title={OpenJarvis: Personal AI, On Personal Devices},
|
||||
author={Jon Saad-Falcon and Avanika Narayan and Robby Manihani and Tanvir Bhathal and Herumb Shandilya and Hakki Orhun Akengin and Gabriel Bo and Andrew Park and Matthew Hart and Caia Costello and Chuan Li and Christopher Ré and Azalia Mirhoseini},
|
||||
year={2026},
|
||||
eprint={2605.17172},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.LG},
|
||||
url={https://arxiv.org/abs/2605.17172},
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](LICENSE)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# OpenJarvis PR Review Instructions
|
||||
|
||||
You are reviewing pull requests for OpenJarvis, a local-first personal AI agent framework built with Python, Rust (PyO3), and TypeScript.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
Evaluate every PR against these criteria:
|
||||
|
||||
### 1. Relevance
|
||||
Is this PR doing something useful? Valid contributions include: bug fixes, new features, feature expansions, documentation improvements, test coverage, and performance improvements. Flag PRs that appear to be empty, auto-generated without substance, or unrelated to the project.
|
||||
|
||||
### 2. Completeness
|
||||
Does the code actually implement what the PR title and description claim? If the PR says "fix X", verify X is actually fixed. If it says "add Y", verify Y is fully added and functional — not partially implemented or stubbed out.
|
||||
|
||||
### 3. Correctness
|
||||
Check for logic errors, edge cases, and off-by-one errors. Pay particular attention to:
|
||||
- **Rust-Python bridge (PyO3) boundaries** — type conversions, error propagation, GIL handling
|
||||
- **Async/await patterns** — missing awaits, unclosed resources, blocking calls in async contexts
|
||||
- **Registry pattern compliance** — new components (engines, tools, agents, channels) must register via `ToolRegistry`, `EngineRegistry`, `AgentRegistry`, `ChannelRegistry`, etc. in `src/openjarvis/core/registry.py`
|
||||
- **Mining provider compliance** — new mining providers must register via `MinerRegistry` and expose an idempotent `ensure_registered()` for the autouse-clear test convention
|
||||
- **Event bus integration** — new lifecycle events should use `EventBus` from `src/openjarvis/core/events.py`
|
||||
|
||||
### 4. Testing
|
||||
Does the PR include tests for new code paths? Are existing tests expected to still pass? New tools, engines, agents, and channels should have corresponding test files in `tests/` mirroring the `src/` structure.
|
||||
|
||||
### 5. Security
|
||||
Check for: hardcoded API keys or secrets, missing input validation at system boundaries (user input, external APIs), and anything that compromises local-first data isolation.
|
||||
|
||||
## Do NOT Comment On
|
||||
|
||||
- Formatting or style — Ruff handles this automatically in CI
|
||||
- Code in unchanged files outside the PR diff
|
||||
- Subjective naming preferences
|
||||
- Adding docstrings or comments to code the PR did not modify
|
||||
|
||||
## Output Format
|
||||
|
||||
- Post **inline comments** on specific lines for actionable issues
|
||||
- Post a **summary comment** containing: what the PR does, whether it achieves its stated goal, and any blocking concerns
|
||||
- Use severity levels:
|
||||
- `blocking` — must fix before merge
|
||||
- `suggestion` — consider fixing
|
||||
- `nit` — take it or leave it
|
||||
@@ -0,0 +1,374 @@
|
||||
# OpenJarvis Roadmap
|
||||
|
||||
Phased development plan for OpenJarvis. Phases are ordered to maximize early usability: foundation first, then intelligence + inference (so you can ask questions), then memory (so it remembers), then agents (so it can act), then learning (so it improves).
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Foundation (~2-3 weeks)
|
||||
|
||||
**Goal:** Repository scaffolding, core abstractions, and CLI skeleton. Nothing runs yet, but all interfaces are defined.
|
||||
|
||||
**Version milestone:** v0.1
|
||||
|
||||
### Repository structure
|
||||
|
||||
```
|
||||
OpenJarvis/
|
||||
├── pyproject.toml # uv/hatchling, all deps + extras
|
||||
├── src/
|
||||
│ └── openjarvis/
|
||||
│ ├── __init__.py
|
||||
│ ├── core/
|
||||
│ │ ├── registry.py # RegistryBase[T] + typed registries
|
||||
│ │ ├── types.py # Message, Conversation, ModelSpec, ToolResult, TelemetryRecord
|
||||
│ │ ├── config.py # JarvisConfig dataclass hierarchy, TOML loader
|
||||
│ │ └── events.py # Event bus: pub/sub for inter-pillar telemetry
|
||||
│ ├── intelligence/ # Phase 1
|
||||
│ ├── memory/ # Phase 2
|
||||
│ ├── agents/ # Phase 3
|
||||
│ ├── engine/ # Phase 1
|
||||
│ ├── learning/ # Phase 4
|
||||
│ └── cli/ # CLI entry points
|
||||
├── tests/
|
||||
├── VISION.md
|
||||
├── ROADMAP.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Deliverables
|
||||
|
||||
- [ ] **Registry system** — `RegistryBase[T]` adapted from IPW's `registry.py`. Typed subclasses:
|
||||
- `ModelRegistry` — model specs and metadata
|
||||
- `EngineRegistry` — inference engine implementations
|
||||
- `MemoryRegistry` — memory backend implementations
|
||||
- `AgentRegistry` — agent implementations
|
||||
- `ToolRegistry` — tools with `ToolSpec` metadata (category, cost, latency, capabilities)
|
||||
|
||||
- [ ] **Core types** (`core/types.py`):
|
||||
- `Message` — role + content + metadata (tool calls, images, etc.)
|
||||
- `Conversation` — ordered list of messages with sliding window support
|
||||
- `ModelSpec` — model ID, parameter count, quantization, context length, hardware compatibility
|
||||
- `ToolResult` — tool name + output + usage + cost
|
||||
- `TelemetryRecord` — timestamp, model, tokens, latency, energy (optional), cost
|
||||
|
||||
- [ ] **Config system** (`core/config.py`):
|
||||
- `JarvisConfig` dataclass hierarchy: `EngineConfig`, `IntelligenceConfig`, `MemoryConfig`, `AgentConfig`
|
||||
- TOML config file at `~/.openjarvis/config.toml`
|
||||
- Hardware auto-detection: GPU vendor/model/VRAM/platform → populate defaults
|
||||
|
||||
- [ ] **Event bus** (`core/events.py`):
|
||||
- Simple pub/sub for inter-pillar communication
|
||||
- Telemetry events flow without tight coupling between pillars
|
||||
- Synchronous dispatch (async optional later)
|
||||
|
||||
- [ ] **CLI skeleton** (Click-based):
|
||||
- `jarvis init` — create `~/.openjarvis/config.toml` with auto-detected defaults
|
||||
- `jarvis ask` — placeholder (wired in Phase 1)
|
||||
- `jarvis serve` — placeholder (wired in Phase 3)
|
||||
- `jarvis model list|pull|info` — placeholder (wired in Phase 1)
|
||||
- `jarvis memory index|search|stats` — placeholder (wired in Phase 2)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Intelligence + Inference Engine (~3-4 weeks)
|
||||
|
||||
**Goal:** You can ask OpenJarvis a question and get an answer. Models run locally or via cloud APIs. Basic telemetry records every call.
|
||||
|
||||
**Version milestone:** v0.2 — first usable version
|
||||
|
||||
### Inference Engine
|
||||
|
||||
- [ ] **`InferenceEngine` ABC:**
|
||||
```python
|
||||
class InferenceEngine(ABC):
|
||||
def generate(self, model: str, messages: list[Message], **params) -> Response: ...
|
||||
def stream(self, model: str, messages: list[Message], **params) -> Iterator[ResponseChunk]: ...
|
||||
def list_models(self) -> list[ModelSpec]: ...
|
||||
def health(self) -> bool: ...
|
||||
```
|
||||
|
||||
- [ ] **Engine implementations:**
|
||||
- `OllamaEngine` — wraps Ollama HTTP API (`/api/chat`, `/api/tags`). Apple Silicon + NVIDIA.
|
||||
- `VLLMEngine` — wraps vLLM OpenAI-compatible API. Multi-GPU, tensor parallelism.
|
||||
- `LlamaCppEngine` — wraps `llama-cpp-python` or llama.cpp server. Maximum compatibility.
|
||||
- `CloudEngine` — unified wrapper for OpenAI, Anthropic, and Google APIs. Key-based routing.
|
||||
|
||||
- [ ] **Model management:**
|
||||
- Auto-discovery from running engines (poll `/api/tags`, `/v1/models`)
|
||||
- `ModelSpec` with hardware compatibility matrix (min VRAM, supported engines, quantization options)
|
||||
- `jarvis model list` shows all available models across engines
|
||||
- `jarvis model info <model>` shows spec, hardware requirements, estimated performance
|
||||
|
||||
### Intelligence
|
||||
|
||||
- [ ] **Hardware profiles:**
|
||||
- Auto-detect: `nvidia-smi`, `rocm-smi`, `system_profiler` (macOS), `/proc/cpuinfo`
|
||||
- Map GPU to capabilities: VRAM, compute capability, FP8/FP4 support, unified memory
|
||||
- Recommend engine: Apple Silicon → Ollama/MLX, NVIDIA datacenter → vLLM, AMD → vLLM+ROCm, CPU → llama.cpp
|
||||
|
||||
- [ ] **Heuristic Router V0:**
|
||||
- Rule-based routing: short queries (< 50 tokens) → small model, complex (reasoning keywords, multi-step) → large model, code patterns → code specialist
|
||||
- Fallback chains: if preferred model unavailable, try next in chain
|
||||
- Configurable via `~/.openjarvis/config.toml`
|
||||
|
||||
- [ ] **Basic telemetry:**
|
||||
- Wrap every `generate()` / `stream()` call with timing + token counting
|
||||
- Record to SQLite: model, prompt tokens, completion tokens, latency, cost estimate
|
||||
- `TelemetryRecord` stored via event bus, accumulated for future learning phase
|
||||
|
||||
### Wire-up
|
||||
|
||||
- [ ] **`jarvis ask "What is X?"` works end-to-end:**
|
||||
1. Parse query → detect complexity → route to model
|
||||
2. Generate response via selected engine
|
||||
3. Record telemetry
|
||||
4. Print response (with optional `--json` output)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Memory / Storage (~3-4 weeks)
|
||||
|
||||
**Goal:** OpenJarvis remembers conversations, can index your documents, and injects relevant context into prompts.
|
||||
|
||||
**Version milestone:** v0.3
|
||||
|
||||
### Memory backends
|
||||
|
||||
- [ ] **`MemoryBackend` ABC:**
|
||||
```python
|
||||
class MemoryBackend(ABC):
|
||||
def store(self, content: str, metadata: dict) -> str: ... # Returns doc ID
|
||||
def retrieve(self, query: str, k: int = 10) -> list[Result]: ...
|
||||
def delete(self, doc_id: str) -> bool: ...
|
||||
def clear(self) -> None: ...
|
||||
```
|
||||
|
||||
- [ ] **Memory subtypes:**
|
||||
- `ConversationMemory` — sliding window (configurable size) + automatic summarization of older turns via LLM call
|
||||
- `KnowledgeBase` — indexed document collection with multi-backend search
|
||||
|
||||
- [ ] **Backend implementations:**
|
||||
|
||||
- **`SQLiteMemory`** — FTS5 full-text search, zero-config default. Always available, no extra dependencies.
|
||||
|
||||
- **`FAISSMemory`** — Dense neural retrieval. Encodes documents with `sentence-transformers`, builds FAISS index (IVF or flat depending on collection size). GPU-accelerated when available.
|
||||
|
||||
- **`ColBERTMemory`** — ColBERTv2 late interaction retrieval. Best retrieval quality.
|
||||
- Package: `colbert-ai[torch,faiss-gpu]`
|
||||
- Indexing: `Indexer(checkpoint="colbertv2.0", config=ColBERTConfig(nbits=2))` → `indexer.index(name, collection)`
|
||||
- Search: `Searcher(index=name)` → `searcher.search(query, k=10)` returns `(passage_ids, ranks, scores)`
|
||||
- Token-level MaxSim matching: each query token attends to each document token, max-pooled per query token, summed
|
||||
- 2-bit residual compression keeps indexes compact (~50x smaller than full embeddings)
|
||||
- Millisecond query latency, substantially better than single-vector methods on complex queries
|
||||
|
||||
- **`BM25Memory`** — Keyword search baseline using `rank-bm25`. No GPU, no embeddings. Fast and effective for keyword-heavy queries.
|
||||
|
||||
- **`HybridMemory`** — Combines BM25 with a dense backend (FAISS or ColBERT) using Reciprocal Rank Fusion (RRF):
|
||||
```
|
||||
RRF_score(d) = sum(1 / (k + rank_i(d))) for each retriever i
|
||||
```
|
||||
Configurable `k` parameter (default 60). Best overall retrieval when you don't know the query type.
|
||||
|
||||
### Document pipeline
|
||||
|
||||
- [ ] **Indexing pipeline:** PDF / Markdown / plain text / code → chunking (configurable size + overlap) → embedding (if using dense/ColBERT backend) → index
|
||||
- [ ] **Context injection:** auto-retrieve top-k relevant chunks before each LLM call, inject into prompt with source attribution (`[Source: filename:line]`)
|
||||
|
||||
### CLI
|
||||
|
||||
- [ ] `jarvis memory index <path>` — index a file or directory
|
||||
- [ ] `jarvis memory search <query>` — search across all memory backends
|
||||
- [ ] `jarvis memory stats` — show index sizes, document counts, backend status
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Agentic Logic (~3-4 weeks)
|
||||
|
||||
**Goal:** OpenJarvis can use tools, reason over multiple turns, and serve an OpenAI-compatible API. The default agent is OpenClaw's Pi.
|
||||
|
||||
**Version milestone:** v0.4
|
||||
|
||||
### Agent framework
|
||||
|
||||
- [ ] **`BaseAgent` ABC:**
|
||||
```python
|
||||
class BaseAgent(ABC):
|
||||
def run(self, input: str, context: AgentContext) -> AgentResult: ...
|
||||
```
|
||||
`AgentContext` carries: conversation history, memory handle, tool registry, telemetry recorder, model router.
|
||||
`AgentResult` contains: response text, tool calls made, tokens used, telemetry data.
|
||||
|
||||
- [ ] **Agent implementations:**
|
||||
|
||||
- **`OpenClawAgent`** (default) — wraps OpenClaw's Pi agent runtime (`@mariozechner/pi-coding-agent` v0.52.12+). Two modes:
|
||||
1. **HTTP mode:** OpenClaw gateway running locally on `:18789`. Communicate via WebSocket. Best for persistent sessions.
|
||||
2. **Subprocess mode:** invoke `node` with `runEmbeddedPiAgent()` call, JSON over stdin/stdout. No gateway needed.
|
||||
- Capabilities: multi-turn reasoning, tool calling, streaming, skill composition, context compaction
|
||||
- Requires Node.js 22+
|
||||
|
||||
- **`SimpleAgent`** — single-turn: parse query → call model → return response. No tool calling, no multi-turn. Works without Node.js. Good for testing and simple Q&A.
|
||||
|
||||
- **`OrchestratorAgent`** — multi-turn with model selection per step. Adapted from IPW's executor pattern. Each reasoning step can route to a different model (e.g., fast model for planning, large model for synthesis).
|
||||
|
||||
- **`CustomAgent`** — template class for user-defined agents. Subclass `BaseAgent`, implement `run()`, register with `@AgentRegistry.register("my-agent")`.
|
||||
|
||||
### Tool system
|
||||
|
||||
- [ ] **`BaseTool` ABC:**
|
||||
```python
|
||||
class BaseTool(ABC):
|
||||
name: str
|
||||
spec: ToolSpec # category, cost_estimate, latency_estimate, capabilities
|
||||
def execute(self, input: str, **params) -> ToolResult: ...
|
||||
```
|
||||
|
||||
- [ ] **Built-in tools:**
|
||||
- `Calculator` — evaluate math expressions
|
||||
- `WebSearch` — search the web (Tavily, SearXNG, or DuckDuckGo)
|
||||
- `CodeInterpreter` — execute Python in sandboxed environment
|
||||
- `FileRead` / `FileWrite` — local file operations
|
||||
- `Think` — internal reasoning scratchpad (zero-cost tool for chain-of-thought)
|
||||
- `Retrieval` — wired to memory backends, returns relevant documents
|
||||
- `LLMTool` — call another LLM as a tool (for model composition)
|
||||
|
||||
- [ ] **`ToolRegistry`** with discovery:
|
||||
- `ToolSpec` metadata: category, estimated latency, estimated cost, required API keys, capabilities list
|
||||
- Auto-discover available tools based on installed packages and environment
|
||||
|
||||
### API server
|
||||
|
||||
- [ ] **OpenAI-compatible API server:**
|
||||
- `POST /v1/chat/completions` — standard chat completion with tool use
|
||||
- `GET /v1/models` — list available models
|
||||
- Streaming via Server-Sent Events (SSE)
|
||||
- `jarvis serve --port 8000 --agent openclaw`
|
||||
|
||||
### CLI
|
||||
|
||||
- [ ] `jarvis serve --port 8000 --agent <agent>` — start API server
|
||||
- [ ] `jarvis ask` now supports `--agent <agent>` flag
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Learning Approach (placeholder)
|
||||
|
||||
**Goal:** Stub interfaces for the learned router. No ML training in this phase — just the contracts and telemetry plumbing so everything is ready when we build it.
|
||||
|
||||
**Version milestone:** v0.5
|
||||
|
||||
### Stubs
|
||||
|
||||
- [ ] **`RouterPolicy` ABC:**
|
||||
```python
|
||||
class RouterPolicy(ABC):
|
||||
def select_model(self, query: str, context: RoutingContext) -> ModelSpec: ...
|
||||
```
|
||||
The heuristic router from Phase 1 implements this as the default.
|
||||
|
||||
- [ ] **`RewardFunction` ABC:**
|
||||
```python
|
||||
class RewardFunction(ABC):
|
||||
def compute(self, trajectory: Trajectory) -> float: ...
|
||||
```
|
||||
Placeholder implementations: `QualityReward` (LLM-judge), `LatencyReward` (inverse latency), `EnergyReward` (inverse energy), `CostReward` (inverse cost), `CompositeReward` (weighted combination).
|
||||
|
||||
- [ ] **`TelemetryAggregator`:**
|
||||
- Reads `TelemetryRecord` entries from SQLite (accumulated since Phase 1)
|
||||
- Computes per-model statistics: average latency, token throughput, cost, quality (when graded)
|
||||
- Exports training-ready datasets for the future GRPO pipeline
|
||||
|
||||
- [ ] **Design document:** `docs/learning-pipeline.md` describing the planned GRPO training pipeline:
|
||||
- Trajectory generation from Phases 1-3 telemetry
|
||||
- Reward model training
|
||||
- Policy optimization with GRPO
|
||||
- Online evaluation and rollout strategy
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Integration & Polish (~3-4 weeks)
|
||||
|
||||
**Goal:** Production-ready packaging, OpenClaw integration, benchmarking, SDK, and documentation.
|
||||
|
||||
**Version milestone:** v1.0
|
||||
|
||||
### OpenClaw integration
|
||||
|
||||
- [ ] **`openjarvis-openclaw` plugin package:**
|
||||
- `register()` hook implementing OpenClaw's plugin API
|
||||
- `registerProvider()` — wraps OpenJarvis as an OpenClaw `ProviderPlugin` (routes through OpenJarvis intelligence + engine)
|
||||
- `registerTool()` — exposes OpenJarvis tools to OpenClaw
|
||||
- `MemorySearchManager` — implements OpenClaw's `search()` / `sync()` / `status()` interface, backed by OpenJarvis memory
|
||||
|
||||
### Deployment
|
||||
|
||||
- [ ] **Dockerfile** — multi-stage build with optional GPU support
|
||||
- [ ] **docker-compose.yml** — OpenJarvis + Ollama/vLLM + optional gateway
|
||||
- [ ] **Service files** — systemd (Linux) and launchd (macOS) for running as a system service
|
||||
|
||||
### Python SDK
|
||||
|
||||
- [ ] **Programmatic API:**
|
||||
```python
|
||||
from openjarvis import Jarvis
|
||||
|
||||
j = Jarvis() # Auto-loads config
|
||||
response = await j.ask("Explain transformers") # Uses router + engine
|
||||
await j.memory.index("~/papers/") # Index documents
|
||||
results = await j.memory.search("attention mechanism")
|
||||
```
|
||||
|
||||
### Benchmarking
|
||||
|
||||
- [ ] **`jarvis bench` CLI:**
|
||||
- `BaseBenchmark` / `DatasetBenchmark` ABCs (adapted from IPW's `BenchmarkSuite`)
|
||||
- Run benchmarks across models, measure accuracy + latency + energy
|
||||
- Output JSONL results + summary JSON
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] Documentation site (MkDocs or similar)
|
||||
- [ ] Getting started guide
|
||||
- [ ] Plugin development guide
|
||||
- [ ] API reference
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Trace System & Learning (~ongoing)
|
||||
|
||||
**Goal:** Full interaction-level trace recording, trace-driven learning, and pluggable agentic architectures. The foundation for studying local AI systems.
|
||||
|
||||
**Version milestone:** v1.1
|
||||
|
||||
### Trace System (complete)
|
||||
|
||||
- [x] **`Trace` and `TraceStep` types** — full interaction recording with step types: route, retrieve, generate, tool_call, respond
|
||||
- [x] **`TraceStore`** — SQLite-backed append-only store with filtering (by agent, model, outcome, time range)
|
||||
- [x] **`TraceCollector`** — wraps any `BaseAgent`, subscribes to EventBus, records steps automatically
|
||||
- [x] **`TraceAnalyzer`** — read-only query layer: per-route stats, per-tool stats, summaries, query-type filtering, export
|
||||
- [x] **`TraceDrivenPolicy`** — learns routing from trace outcomes, batch and online updates, registered as `"learned"` policy
|
||||
- [x] **Event bus integration** — `TRACE_STEP` and `TRACE_COMPLETE` event types
|
||||
|
||||
### Next Steps
|
||||
|
||||
- [ ] Wire `TraceCollector` into SDK/CLI for automatic trace collection
|
||||
- [ ] `jarvis trace` CLI subcommand (list, inspect, export traces)
|
||||
- [ ] User feedback mechanisms (thumbs up/down, quality scores)
|
||||
- [ ] Hierarchical memory (episodic/semantic/procedural layers)
|
||||
- [ ] Pluggable agentic architectures (ReAct, tree-of-thought, custom loops)
|
||||
- [ ] Prompt optimization from traces (DSPy-style compilation for local models)
|
||||
- [ ] Model weight updates from traces (LoRA/QLoRA finetuning)
|
||||
- [ ] GAIA benchmark evaluation with local models
|
||||
|
||||
---
|
||||
|
||||
## Version Summary
|
||||
|
||||
| Version | Phase | What you get |
|
||||
|---------|-------|-------------|
|
||||
| **v0.1** | Phase 0 | Scaffolding, registries, config, CLI skeleton |
|
||||
| **v0.2** | Phase 1 | `jarvis ask` works — local & cloud inference with telemetry |
|
||||
| **v0.3** | Phase 2 | Memory — index docs, conversation history, context injection |
|
||||
| **v0.4** | Phase 3 | Agents + tools + OpenAI-compatible API server |
|
||||
| **v0.5** | Phase 4 | Learning stubs — router policy interface, telemetry aggregation |
|
||||
| **v1.0** | Phase 5 | Production — SDK, OpenClaw plugin, Docker, benchmarks, docs |
|
||||
| **v1.1** | Phase 6 | Trace system, trace-driven learning, pluggable agent architectures |
|
||||
@@ -0,0 +1,311 @@
|
||||
# OpenJarvis
|
||||
|
||||
**Programming abstractions for on-device AI.**
|
||||
|
||||
OpenJarvis defines the abstractions needed to study and build AI systems that run entirely on local hardware. Instead of locking you into one model, one memory system, or one inference engine, OpenJarvis lets you compose your own stack across four core abstractions — then swap any piece without touching the rest.
|
||||
|
||||
Built for researchers studying local AI systems and developers who want full control over their AI stack. Every interaction generates a trace; the system learns from its own usage to improve over time.
|
||||
|
||||
## Why Local AI Needs New Abstractions
|
||||
|
||||
Cloud AI treats intelligence as a **service** — you send a request, get a response, pay per token. Local AI treats intelligence as a **resource** — it lives on your machine, it's always available, it has fixed capabilities, it can be modified, and it accumulates state over time. This inversion changes everything:
|
||||
|
||||
- **Fixed resource budget** — You have 8-24GB VRAM, period. Scheduling and allocation are first-class problems.
|
||||
- **Persistent state is free, compute is expensive** — The opposite of cloud. You can store everything forever but can only run one model at a time.
|
||||
- **You own the weights** — Fine-tuning, RL, prompt compilation are all possible on your data, your hardware, with immediate feedback loops.
|
||||
- **Hardware heterogeneity** — Apple Silicon, NVIDIA consumer GPUs, AMD, CPU-only — each with different optimal strategies.
|
||||
- **Every interaction is a learning signal** — Traces accumulate locally, enabling the system to learn routing, tool selection, and memory strategies from personal usage patterns.
|
||||
|
||||
---
|
||||
|
||||
## The Five Pillars
|
||||
|
||||
OpenJarvis is organized around five composable pillars. Each pillar defines a clear interface; implementations are discovered at runtime via a decorator-based registry system.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ OpenJarvis │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Intelligence │ │ Learning │ │ Memory / │ │
|
||||
│ │ (Models) │ │ Approach │ │ Storage │ │
|
||||
│ │ │ │ (Router) │ │ │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Agentic Logic │ │
|
||||
│ │ (Orchestration, Tools, Reasoning) │ │
|
||||
│ └──────────────────────┬───────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Inference Engine │ │
|
||||
│ │ (vLLM, Ollama, llama.cpp, SGLang, MLX) │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1. Intelligence (Model Layer)
|
||||
|
||||
**What it does:** Manages available language models — local and cloud — and routes queries to the best model for the task.
|
||||
|
||||
**What's pluggable:** Models, model providers, routing heuristics.
|
||||
|
||||
**Supported at launch:**
|
||||
|
||||
| Category | Models |
|
||||
|----------|--------|
|
||||
| Open-source (local) | Qwen3 8B, Qwen3 32B, GPT OSS 120B, Kimi-K2.5, MiniMax-M2.5 |
|
||||
| Cloud APIs | Claude (Anthropic), GPT-4o / GPT-5 (OpenAI), Gemini (Google) |
|
||||
|
||||
**Key components:**
|
||||
|
||||
- **`ModelRegistry`** — decorator-based registry mapping model keys to `ModelSpec` objects (parameter count, quantization, hardware compatibility, context length)
|
||||
- **Heuristic Router (V0)** — rule-based routing: short queries → small model, complex reasoning → large model, code → code specialist, fallback chains for unavailable models
|
||||
- **Auto-discovery** — detects models from running inference engines (Ollama, vLLM) and available API keys
|
||||
|
||||
---
|
||||
|
||||
### 2. Learning Approach (Router Policy)
|
||||
|
||||
**What it does:** Determines *which* model handles a given query. Static policies use rules; learned policies update from interaction traces.
|
||||
|
||||
**What's pluggable:** Routing policy, reward functions, training pipeline, trace analyzers.
|
||||
|
||||
**Implemented:**
|
||||
- **Heuristic routing** — rule-based routing based on query characteristics (length, complexity keywords, domain detection), fallback chains
|
||||
- **Trace-driven routing** — learns from accumulated interaction traces which model/agent/tool combinations produce the best outcomes for different query types. Registered as `"learned"` policy.
|
||||
- **Trace system** — every interaction generates a `Trace` recording the full sequence of steps (route, retrieve, generate, tool_call, respond) with timing, inputs, outputs, and outcomes. Stored in SQLite via `TraceStore`.
|
||||
- **Trace analysis** — `TraceAnalyzer` computes per-route stats, per-tool stats, success rates, and query-type distributions from stored traces.
|
||||
|
||||
**Future:**
|
||||
- Learned router via GRPO (Group Relative Policy Optimization)
|
||||
- Preference learning from user feedback
|
||||
- Continual fine-tuning on accumulated trajectories
|
||||
- Multi-objective optimization: quality vs. latency vs. energy vs. cost
|
||||
|
||||
---
|
||||
|
||||
### 3. Memory / Storage
|
||||
|
||||
**What it does:** Provides persistent, searchable memory across conversations, documents, and personal notes. Memory is automatically injected into prompts with source attribution.
|
||||
|
||||
**What's pluggable:** Storage backends, retrieval strategies, embedding models, chunking strategies.
|
||||
|
||||
**Memory types:**
|
||||
- **Conversation Memory** — sliding window with automatic summarization of older turns
|
||||
- **Knowledge Base** — indexed documents (PDF, Markdown, code, text) with multi-backend search
|
||||
- **Personal Notes** — user-created persistent notes and preferences
|
||||
- **Episodic Memory** — records of past interactions, tool uses, and outcomes
|
||||
|
||||
**Backend implementations:**
|
||||
|
||||
| Backend | Type | Description |
|
||||
|---------|------|-------------|
|
||||
| **SQLite** (default) | Keyword + FTS | FTS5 full-text search. Zero dependencies, zero config. Always available. |
|
||||
| **FAISS** | Dense retrieval | Neural semantic search via `sentence-transformers` + FAISS indexes. |
|
||||
| **ColBERTv2** | Late interaction | Token-level MaxSim matching with 2-bit residual compression. Best retrieval quality. Uses `colbert-ai` package with `Indexer` for offline indexing and `Searcher` for millisecond-latency queries. |
|
||||
| **BM25** | Sparse retrieval | Classic keyword search baseline. Fast, no GPU needed. |
|
||||
| **Hybrid** | Fusion | BM25 + dense (or ColBERT) with Reciprocal Rank Fusion (RRF). Best of both worlds. |
|
||||
| **Vector DB adapters** | Dense retrieval | Qdrant, ChromaDB connectors for users with existing vector infrastructure. |
|
||||
|
||||
**ColBERTv2 details:**
|
||||
- Late interaction model: queries and documents are encoded independently, then matched at the token level via MaxSim
|
||||
- 2-bit residual compression keeps indexes compact while preserving quality
|
||||
- Offline indexing via `Indexer(checkpoint="colbertv2.0", config=ColBERTConfig(nbits=2))`
|
||||
- Millisecond query latency via `Searcher(index=name).search(query, k=10)`
|
||||
- Substantially better retrieval quality than single-vector dense methods on complex queries
|
||||
|
||||
---
|
||||
|
||||
### 4. Agentic Logic
|
||||
|
||||
**What it does:** Orchestrates multi-turn reasoning, tool calling, and task execution. The agent layer sits between the user and the model, managing context, tools, and conversation flow.
|
||||
|
||||
**What's pluggable:** Agent implementations, tools, tool registries, execution strategies.
|
||||
|
||||
**Agent implementations:**
|
||||
|
||||
| Agent | Description |
|
||||
|-------|-------------|
|
||||
| **`OpenClawAgent`** (default) | Wraps OpenClaw's Pi agent runtime. Multi-turn reasoning, tool calling, streaming responses, skill composition, context compaction. Two modes: **HTTP** (WebSocket to OpenClaw gateway on `:18789`) or **subprocess** (invoke `node` with `runEmbeddedPiAgent()`, JSON over stdin/stdout). Requires Node.js 22+. |
|
||||
| **`SimpleAgent`** | Single-turn: query → model → response. No tool calling. Works without Node.js. Good for quick answers and testing. |
|
||||
| **`OrchestratorAgent`** | Multi-turn with per-step model selection. Adapted from IPW's executor pattern. Routes each reasoning step to the optimal model. |
|
||||
| **`CustomAgent`** | Template for user-defined agent logic. Subclass `BaseAgent`, implement `run()`, register with `AgentRegistry`. |
|
||||
|
||||
**Tool system:**
|
||||
- `BaseTool` ABC with `ToolSpec` metadata (category, cost estimate, latency estimate, capabilities)
|
||||
- `ToolRegistry` — runtime-discoverable tool catalog
|
||||
- Built-in tools: Calculator, WebSearch, CodeInterpreter, FileRead/Write, Think, Retrieval (wired to memory backends), LLM-as-tool
|
||||
- MCP (Model Context Protocol) compatible
|
||||
|
||||
**API server:**
|
||||
- OpenAI-compatible `/v1/chat/completions` and `/v1/models` endpoints
|
||||
- Streaming via Server-Sent Events (SSE)
|
||||
- Drop-in replacement for any OpenAI-compatible client
|
||||
|
||||
---
|
||||
|
||||
### 5. Inference Engine
|
||||
|
||||
**What it does:** Manages the actual LLM inference runtime — loading models, generating tokens, managing GPU memory.
|
||||
|
||||
**What's pluggable:** Engine backends, hardware profiles, quantization strategies.
|
||||
|
||||
**Supported engines:**
|
||||
|
||||
| Engine | Best for | GPU | CPU |
|
||||
|--------|----------|-----|-----|
|
||||
| **vLLM** | High-throughput server, multi-GPU, production | NVIDIA, AMD | — |
|
||||
| **SGLang** | Structured generation, constrained decoding | NVIDIA, AMD | — |
|
||||
| **Ollama** | Easy setup, Apple Silicon, single-model | NVIDIA, Apple | Yes |
|
||||
| **llama.cpp** | Maximum hardware compatibility, GGUF models | NVIDIA, AMD, Apple | Yes |
|
||||
| **MLX** | Apple Silicon native, Metal acceleration | Apple | Apple |
|
||||
|
||||
**Hardware auto-detection:**
|
||||
- Detects GPU vendor (NVIDIA/AMD/Apple), model, VRAM, compute capability
|
||||
- Recommends the best engine for detected hardware
|
||||
- Apple Silicon → Ollama or MLX; NVIDIA datacenter → vLLM; AMD → vLLM with ROCm; CPU-only → llama.cpp
|
||||
|
||||
---
|
||||
|
||||
## Query Flow
|
||||
|
||||
```
|
||||
User query
|
||||
│
|
||||
▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Agentic │────▶│ Memory │────▶│ Context │
|
||||
│ Logic │ │ Retrieve │ │ Inject │
|
||||
└────┬─────┘ └──────────┘ └────┬─────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Learning │────▶│ Model │────▶│ Inference│
|
||||
│ (Router) │ │ Select │ │ Engine │
|
||||
└──────────┘ └──────────┘ └────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Response │
|
||||
│ + Telem. │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
1. **Agentic Logic** receives the user query, determines if tools or memory are needed
|
||||
2. **Memory** retrieves relevant context (conversation history, documents, notes)
|
||||
3. **Context Injection** assembles the full prompt with retrieved content and source attribution
|
||||
4. **Learning/Router** selects the best model for this query based on routing policy (heuristic or trace-driven)
|
||||
5. **Inference Engine** runs the selected model and streams the response
|
||||
6. **Trace** records the full interaction sequence: every routing decision, memory retrieval, tool call, and generation step with timing and outcomes
|
||||
7. **Learning** periodically updates routing policies from accumulated traces
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios
|
||||
|
||||
### Developer on M4 Max MacBook Pro (128 GB unified memory)
|
||||
|
||||
```toml
|
||||
# ~/.openjarvis/config.toml
|
||||
[engine]
|
||||
backend = "ollama" # Native Apple Silicon support
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3-32b" # Fits in 128 GB unified memory
|
||||
fallback = "qwen3-8b"
|
||||
|
||||
[memory]
|
||||
backend = "sqlite" # Zero-config, always works
|
||||
retrieval = "hybrid" # BM25 + FAISS for local docs
|
||||
|
||||
[agent]
|
||||
type = "openclaw" # Full agent capabilities
|
||||
mode = "subprocess" # No separate gateway needed
|
||||
```
|
||||
|
||||
Day-to-day: codes with `jarvis ask`, indexes project docs with `jarvis memory index`, runs a local OpenAI-compatible server with `jarvis serve` for editor integration.
|
||||
|
||||
### Researcher on DGX Spark (2x B200, 384 GB GPU memory)
|
||||
|
||||
```toml
|
||||
[engine]
|
||||
backend = "vllm"
|
||||
tensor_parallel = 2
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3-235b-a22b"
|
||||
router = "heuristic" # Route small queries to 8B, large to 235B
|
||||
|
||||
[memory]
|
||||
backend = "colbert" # Best retrieval quality for papers
|
||||
knowledge_base = "~/papers/"
|
||||
|
||||
[agent]
|
||||
type = "orchestrator" # Multi-model orchestration
|
||||
```
|
||||
|
||||
Running benchmarks with `jarvis bench`, profiling energy per query, comparing model efficiency across hardware configurations.
|
||||
|
||||
### Privacy-Focused Offline Setup
|
||||
|
||||
```toml
|
||||
[engine]
|
||||
backend = "llamacpp" # No server needed
|
||||
network = "offline"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3-8b-q4" # Quantized to fit available RAM
|
||||
|
||||
[memory]
|
||||
backend = "sqlite" # Everything local
|
||||
retrieval = "bm25" # No neural models needed
|
||||
|
||||
[agent]
|
||||
type = "simple" # No external dependencies
|
||||
```
|
||||
|
||||
Fully air-gapped. No cloud APIs, no network calls, no telemetry export. All data stays on the machine.
|
||||
|
||||
---
|
||||
|
||||
## Comparison
|
||||
|
||||
| Feature | OpenJarvis | Ollama | LangChain | OpenClaw | vLLM |
|
||||
|---------|-----------|--------|-----------|----------|------|
|
||||
| **Focus** | Composable AI backend | Model runner | LLM app framework | AI coding assistant | Inference server |
|
||||
| **Model management** | Multi-engine, auto-detect | Single engine | Bring your own | Cloud-first | Single engine |
|
||||
| **Memory** | Multi-backend retrieval | None | Vector store wrappers | Conversation only | None |
|
||||
| **Agents** | Pluggable (Pi, custom) | None | Chain-based | Pi agent (built-in) | None |
|
||||
| **Inference** | vLLM/SGLang/Ollama/llama.cpp/MLX | Ollama only | External | External | vLLM only |
|
||||
| **Hardware-aware** | Auto-detect + recommend | Manual | No | No | Manual |
|
||||
| **Telemetry** | Energy, latency, cost | None | Callbacks | Basic | Metrics |
|
||||
| **Offline** | Full support | Full support | Partial | No | Full support |
|
||||
| **API** | OpenAI-compatible | OpenAI-compatible | Custom | Custom | OpenAI-compatible |
|
||||
| **Language** | Python | Go | Python | TypeScript | Python |
|
||||
|
||||
OpenJarvis is **not** a replacement for these tools — it *composes* them. Ollama and vLLM are inference engine options. OpenClaw's Pi agent is the default agentic logic. LangChain-style chains can be implemented as custom agents.
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Pluggable everything** — every component is registered and discoverable at runtime. Swap models, engines, memory backends, and agents without code changes.
|
||||
|
||||
2. **Registry-driven** — `RegistryBase[T]` pattern (adapted from IPW) provides type-safe, decorator-based registration for all extensible components: `ModelRegistry`, `EngineRegistry`, `MemoryRegistry`, `AgentRegistry`, `ToolRegistry`.
|
||||
|
||||
3. **Offline-first** — works without network access. Cloud APIs are optional enhancements, never requirements.
|
||||
|
||||
4. **Telemetry-native** — every inference call records timing, token counts, and (when hardware supports it) energy consumption. Data lands in SQLite for analysis.
|
||||
|
||||
5. **Hardware-aware** — auto-detects GPU vendor, model, VRAM, and platform. Recommends the best engine and model configuration for your hardware.
|
||||
|
||||
6. **Python-first** — core is pure Python (3.10+). Node.js required only for OpenClaw agent integration. No Java, no JVM, no heavy runtimes.
|
||||
|
||||
7. **OpenAI-compatible API** — `jarvis serve` exposes `/v1/chat/completions` and `/v1/models`. Any client that speaks OpenAI protocol works out of the box.
|
||||
|
||||
8. **Standalone** — OpenJarvis is a self-contained backend. OpenClaw is one possible frontend; so is `curl`, a Python SDK call, or any OpenAI-compatible client.
|
||||
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 781 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg width="345" height="80" viewBox="0 0 345 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Circuit Node Mark -->
|
||||
<g transform="translate(0, 0)">
|
||||
<circle cx="40" cy="40" r="12" fill="#e0e0e8"/>
|
||||
<circle cx="40" cy="40" r="7.5" fill="#3b82f6"/>
|
||||
<line x1="40" y1="28" x2="40" y2="10" stroke="#e0e0e8" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="40" y1="52" x2="40" y2="70" stroke="#e0e0e8" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="28" y1="40" x2="10" y2="40" stroke="#e0e0e8" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="52" y1="40" x2="70" y2="40" stroke="#e0e0e8" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="31.5" y1="31.5" x2="17" y2="17" stroke="#e0e0e8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="48.5" y1="31.5" x2="63" y2="17" stroke="#e0e0e8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="40" cy="7" r="5" fill="#e0e0e8"/>
|
||||
<circle cx="40" cy="73" r="5" fill="#e0e0e8"/>
|
||||
<circle cx="7" cy="40" r="5" fill="#e0e0e8"/>
|
||||
<circle cx="73" cy="40" r="5" fill="#e0e0e8"/>
|
||||
<circle cx="14.5" cy="14.5" r="4.5" fill="#6366f1"/>
|
||||
<circle cx="65.5" cy="14.5" r="4.5" fill="#10b981"/>
|
||||
</g>
|
||||
<text x="100" y="52" font-family="'IBM Plex Mono', 'SF Mono', 'Menlo', monospace" font-weight="600" font-size="42" fill="#eeeef4" letter-spacing="-1.5">openjarvis</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg width="345" height="80" viewBox="0 0 345 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Circuit Node Mark -->
|
||||
<g transform="translate(0, 0)">
|
||||
<circle cx="40" cy="40" r="12" fill="#1a1a1e"/>
|
||||
<circle cx="40" cy="40" r="7.5" fill="#3b82f6"/>
|
||||
<line x1="40" y1="28" x2="40" y2="10" stroke="#1a1a1e" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="40" y1="52" x2="40" y2="70" stroke="#1a1a1e" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="28" y1="40" x2="10" y2="40" stroke="#1a1a1e" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="52" y1="40" x2="70" y2="40" stroke="#1a1a1e" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="31.5" y1="31.5" x2="17" y2="17" stroke="#1a1a1e" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="48.5" y1="31.5" x2="63" y2="17" stroke="#1a1a1e" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="40" cy="7" r="5" fill="#1a1a1e"/>
|
||||
<circle cx="40" cy="73" r="5" fill="#1a1a1e"/>
|
||||
<circle cx="7" cy="40" r="5" fill="#1a1a1e"/>
|
||||
<circle cx="73" cy="40" r="5" fill="#1a1a1e"/>
|
||||
<circle cx="14.5" cy="14.5" r="4.5" fill="#6366f1"/>
|
||||
<circle cx="65.5" cy="14.5" r="4.5" fill="#10b981"/>
|
||||
</g>
|
||||
<text x="100" y="52" font-family="'IBM Plex Mono', 'SF Mono', 'Menlo', monospace" font-weight="600" font-size="42" fill="#1a1a1e" letter-spacing="-1.5">openjarvis</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 4.5 MiB |
@@ -43,7 +43,7 @@ enabled = true
|
||||
default = "vllm"
|
||||
|
||||
[engine.vllm]
|
||||
host = "http://localhost:8001" # vLLM serving port
|
||||
host = "http://localhost:8000" # Default vLLM port
|
||||
|
||||
[engine.ollama]
|
||||
host = "http://localhost:11434"
|
||||
@@ -54,19 +54,6 @@ host = "http://localhost:30000"
|
||||
[engine.llamacpp]
|
||||
host = "http://localhost:8080"
|
||||
|
||||
[engine.exo]
|
||||
host = "http://localhost:52415"
|
||||
|
||||
[engine.nexa]
|
||||
host = "http://localhost:18181"
|
||||
# device = "npu" # optional: cpu, gpu, npu
|
||||
|
||||
[engine.uzu]
|
||||
host = "http://localhost:8080"
|
||||
|
||||
[engine.apple_fm]
|
||||
host = "http://localhost:8079"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# PILLAR 5: Learning — Improvement Methodologies
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -106,11 +93,9 @@ enabled = true # Record traces for analysis
|
||||
db_path = "~/.openjarvis/traces.db"
|
||||
|
||||
[server]
|
||||
# Bind to loopback by default so the API is not exposed to the local network.
|
||||
# To serve other devices on your LAN, set host = "0.0.0.0" AND set an API key
|
||||
# (OPENJARVIS_API_KEY / `jarvis auth generate-key`) — startup refuses a
|
||||
# non-loopback bind without a key. The "server" security profile also flips
|
||||
# this to 0.0.0.0 intentionally.
|
||||
host = "127.0.0.1"
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
agent = "native_openhands"
|
||||
|
||||
[security]
|
||||
enabled = false # Disable for eval (no PII scanning overhead)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Simple Chat — lightweight conversational AI, no tools
|
||||
# Copy to ~/.openjarvis/config.toml
|
||||
#
|
||||
# The fastest setup: just Ollama + a model.
|
||||
#
|
||||
# Usage:
|
||||
# jarvis ask "What is quantum computing?"
|
||||
# jarvis chat # interactive chat session
|
||||
# jarvis serve # start API server for browser/desktop app
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:4b" # Fast and lightweight
|
||||
# default_model = "qwen3.5:9b" # Better quality
|
||||
# default_model = "llama3.1:8b" # Alternative model
|
||||
|
||||
[agent]
|
||||
default_agent = "simple" # Single-turn, no tools
|
||||
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
@@ -1,21 +0,0 @@
|
||||
# Code Assistant — agent with code execution, file I/O, and shell access
|
||||
# Copy to ~/.openjarvis/config.toml
|
||||
#
|
||||
# Usage:
|
||||
# jarvis ask "Write a Python script that parses CSV files"
|
||||
# jarvis ask "Read main.py and explain the architecture"
|
||||
# jarvis ask --agent orchestrator "Find and fix the bug in test_utils.py"
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b"
|
||||
# default_model = "qwen3.5:35b" # Better for complex code tasks
|
||||
|
||||
[agent]
|
||||
default_agent = "orchestrator" # Multi-turn with tool selection
|
||||
max_turns = 10
|
||||
|
||||
[tools]
|
||||
enabled = ["code_interpreter", "file_read", "file_write", "shell_exec", "web_search", "think", "calculator"]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Deep Research Agent — multi-hop research across your indexed documents
|
||||
# Copy to ~/.openjarvis/config.toml
|
||||
#
|
||||
# First index your documents:
|
||||
# jarvis memory index ./docs/
|
||||
# jarvis memory index ~/Documents/papers/
|
||||
#
|
||||
# Then ask complex questions:
|
||||
# jarvis ask --agent deep_research "Summarize all emails about Project X"
|
||||
# jarvis ask --agent deep_research "What meetings did I have with Alice last month?"
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b"
|
||||
temperature = 0.3 # Low temperature for factual research
|
||||
|
||||
[agent]
|
||||
default_agent = "deep_research"
|
||||
max_turns = 8 # Multi-hop reasoning steps
|
||||
|
||||
[tools]
|
||||
enabled = ["knowledge_search", "knowledge_sql", "scan_chunks", "think", "web_search"]
|
||||
|
||||
[tools.storage]
|
||||
default_backend = "sqlite"
|
||||
@@ -1,39 +0,0 @@
|
||||
# Full system access: unrestricted shell and filesystem
|
||||
# Copy to ~/.openjarvis/config.toml
|
||||
#
|
||||
# WARNING: shell_exec runs arbitrary commands as your user. No command
|
||||
# allowlist, no denylist, no working-directory restriction. file_read and
|
||||
# file_write aren't restricted to any directory either. Only enable what you
|
||||
# actually want the agent to have. tools.enabled is the whole permission grant;
|
||||
# there's no second allowlist to configure.
|
||||
#
|
||||
# On macOS, this config alone does not reach TCC-protected data (Messages,
|
||||
# Mail, Photos, Safari). That requires Full Disk Access granted to the process
|
||||
# hosting the backend. See docs/user-guide/system-access.md.
|
||||
#
|
||||
# Usage:
|
||||
# jarvis ask "What's using the most disk space in my home directory?"
|
||||
# jarvis chat # prompts before each shell_exec call
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b"
|
||||
|
||||
[agent]
|
||||
default_agent = "orchestrator"
|
||||
max_turns = 10
|
||||
|
||||
[tools]
|
||||
enabled = [
|
||||
"shell_exec",
|
||||
"file_read",
|
||||
"file_write",
|
||||
"apply_patch",
|
||||
"code_interpreter",
|
||||
"git_status",
|
||||
"git_diff",
|
||||
"think",
|
||||
"calculator",
|
||||
]
|
||||
@@ -1,51 +0,0 @@
|
||||
# Morning Digest — Linux / Cloud GPU with Ollama or vLLM
|
||||
# Copy to ~/.openjarvis/config.toml and customize
|
||||
#
|
||||
# Requirements:
|
||||
# - Ollama or vLLM running locally
|
||||
# - Cartesia or OpenAI API key for TTS
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
# default = "vllm" # Use vLLM for GPU servers
|
||||
|
||||
[engine.ollama]
|
||||
host = "http://localhost:11434"
|
||||
|
||||
# [engine.vllm]
|
||||
# host = "http://localhost:8001"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b"
|
||||
|
||||
[agent]
|
||||
default_agent = "simple"
|
||||
|
||||
[tools]
|
||||
enabled = ["code_interpreter", "web_search", "file_read", "shell_exec", "digest_collect", "text_to_speech"]
|
||||
|
||||
# ─── Morning Digest ─────────────────────────────────────────
|
||||
|
||||
[digest]
|
||||
enabled = true
|
||||
schedule = "0 7 * * *"
|
||||
timezone = "America/New_York" # Change to your timezone
|
||||
persona = "jarvis"
|
||||
honorific = "sir"
|
||||
tts_backend = "openai" # OpenAI TTS works everywhere
|
||||
voice_id = "onyx" # Deep male voice
|
||||
voice_speed = 1.1
|
||||
|
||||
sections = ["health", "messages", "calendar", "world"]
|
||||
|
||||
[digest.health]
|
||||
sources = ["oura"]
|
||||
|
||||
[digest.messages]
|
||||
sources = ["gmail", "google_tasks", "slack"]
|
||||
|
||||
[digest.calendar]
|
||||
sources = ["gcalendar"]
|
||||
|
||||
[digest.world]
|
||||
sources = ["hackernews", "news_rss", "weather"]
|
||||
@@ -1,56 +0,0 @@
|
||||
# Morning Digest — Mac (Apple Silicon) with Ollama
|
||||
# Copy to ~/.openjarvis/config.toml and customize
|
||||
#
|
||||
# Requirements:
|
||||
# - Ollama installed (https://ollama.com)
|
||||
# - ollama pull qwen3.5:9b
|
||||
# - Cartesia API key (https://play.cartesia.ai) or OpenAI API key
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b" # Good balance of speed + quality on M1/M2/M3
|
||||
# default_model = "qwen3.5:4b" # Faster, lower quality
|
||||
# default_model = "qwen3.5:35b" # Slower, higher quality (needs 32GB+ RAM)
|
||||
|
||||
[agent]
|
||||
default_agent = "simple"
|
||||
|
||||
[tools]
|
||||
enabled = ["code_interpreter", "web_search", "file_read", "shell_exec", "digest_collect", "text_to_speech"]
|
||||
|
||||
# ─── Morning Digest ─────────────────────────────────────────
|
||||
|
||||
[digest]
|
||||
enabled = true
|
||||
schedule = "0 7 * * *" # 7 AM daily
|
||||
timezone = "America/Los_Angeles" # Change to your timezone
|
||||
persona = "jarvis"
|
||||
honorific = "sir" # "sir", "ma'am", "boss", or any custom
|
||||
tts_backend = "cartesia" # "cartesia" or "openai"
|
||||
voice_id = "c8f7835e-28a3-4f0c-80d7-c1302ac62aae" # Alistair (British male)
|
||||
voice_speed = 1.2 # 1.0 = normal, 1.2 = 20% faster
|
||||
|
||||
# Sections in order of priority (remove any you don't want):
|
||||
sections = ["health", "messages", "calendar", "world"]
|
||||
|
||||
[digest.health]
|
||||
sources = ["oura"] # Add "apple_health" if you export from iPhone
|
||||
# sources = ["oura", "apple_health", "strava"]
|
||||
|
||||
[digest.messages]
|
||||
sources = ["gmail", "google_tasks", "imessage"]
|
||||
# Add any of: "slack", "notion", "github_notifications"
|
||||
|
||||
[digest.calendar]
|
||||
sources = ["gcalendar"]
|
||||
|
||||
[digest.world]
|
||||
sources = ["hackernews", "news_rss"]
|
||||
# Add "weather" after setting up OpenWeatherMap API key
|
||||
|
||||
# ─── Optional: Music section ────────────────────────────────
|
||||
# Uncomment and add "music" to sections list above
|
||||
# [digest.music]
|
||||
# sources = ["spotify", "apple_music"]
|
||||
@@ -1,31 +0,0 @@
|
||||
# Morning Digest — Minimal setup (just Ollama + Gmail)
|
||||
# The simplest possible config to get a working digest.
|
||||
# Copy to ~/.openjarvis/config.toml
|
||||
#
|
||||
# Requirements:
|
||||
# - Ollama installed with any model
|
||||
# - Google OAuth credentials (jarvis connect gdrive)
|
||||
# - OpenAI API key for TTS (or skip audio with --text-only)
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:4b" # Small, fast, runs on any machine
|
||||
|
||||
[tools]
|
||||
enabled = ["digest_collect", "text_to_speech"]
|
||||
|
||||
[digest]
|
||||
enabled = true
|
||||
persona = "jarvis"
|
||||
honorific = "sir"
|
||||
tts_backend = "openai"
|
||||
voice_id = "onyx"
|
||||
sections = ["messages", "calendar"]
|
||||
|
||||
[digest.messages]
|
||||
sources = ["gmail"]
|
||||
|
||||
[digest.calendar]
|
||||
sources = ["gcalendar"]
|
||||
@@ -1,35 +0,0 @@
|
||||
# Scheduled Monitor — persistent agent that runs on a schedule
|
||||
# Copy to ~/.openjarvis/config.toml
|
||||
#
|
||||
# The operative agent maintains state across runs, making it ideal for:
|
||||
# - Daily email/inbox monitoring
|
||||
# - Recurring status checks
|
||||
# - Long-running research projects
|
||||
#
|
||||
# Setup:
|
||||
# 1. Index your data: jarvis memory index ~/Documents/
|
||||
# 2. Start the scheduler: jarvis scheduler start
|
||||
# 3. Create a task:
|
||||
# jarvis scheduler create \
|
||||
# --prompt "Check for new emails about Project X and update your notes" \
|
||||
# --schedule "0 9 * * 1-5" \
|
||||
# --agent operative \
|
||||
# --tools "knowledge_search,knowledge_sql,memory_store,think"
|
||||
|
||||
[engine]
|
||||
default = "ollama"
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b"
|
||||
temperature = 0.3
|
||||
|
||||
[agent]
|
||||
default_agent = "operative"
|
||||
max_turns = 20
|
||||
context_from_memory = true # Inject relevant memory into context
|
||||
|
||||
[tools]
|
||||
enabled = ["knowledge_search", "knowledge_sql", "scan_chunks", "memory_store", "memory_search", "think", "web_search"]
|
||||
|
||||
[tools.storage]
|
||||
default_backend = "sqlite"
|
||||
@@ -1,76 +0,0 @@
|
||||
# LLM-Guided Spec Search — quickstart configuration
|
||||
# Copy to ~/.openjarvis/config.toml and run:
|
||||
# python -m openjarvis_examples.spec_search_quickstart
|
||||
#
|
||||
# This config has two parts:
|
||||
# 1. The agent system being optimized (intelligence / engine / agent / tools).
|
||||
# Same schema as the other examples in this directory; parsed by
|
||||
# ``openjarvis.core.config.load_config``.
|
||||
# 2. ``[learning.spec_search]`` and its sub-tables — the search hyperparameters
|
||||
# consumed by ``SpecSearchOrchestrator.from_config`` and ``SpecSearchLoop``.
|
||||
#
|
||||
# Defaults below match the paper (Saad-Falcon et al., 2026):
|
||||
# - max_regression = 0.01 (epsilon in GateOK)
|
||||
# - stagnation_k = 5 (Algorithm 1 stopping rule)
|
||||
# - composite_reward weights (alpha, beta, gamma, delta) = (0.5, 0.1, 0.1, 0.3)
|
||||
#
|
||||
# Teacher API keys come from your environment / credentials store, not this file.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent system being optimized
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[engine]
|
||||
default = "ollama" # swap to "vllm" on H100/RTX 6000 / DGX Spark
|
||||
|
||||
[intelligence]
|
||||
default_model = "qwen3.5:9b" # the local student
|
||||
# default_model = "qwen3.5:27b-fp8" # workstation tier
|
||||
|
||||
[agent]
|
||||
default_agent = "orchestrator" # multi-turn, tool-using
|
||||
max_turns = 10
|
||||
|
||||
[tools]
|
||||
enabled = [
|
||||
"code_interpreter",
|
||||
"file_read",
|
||||
"web_search",
|
||||
"think",
|
||||
"calculator",
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM-guided spec search hyperparameters (paper §3.3, Algorithm 1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[learning.spec_search]
|
||||
enabled = true
|
||||
teacher_model = "claude-opus-4-6" # frontier proposer
|
||||
teacher_engine = "cloud" # CloudEngine registry key (uses LiteLLM)
|
||||
autonomy_mode = "tiered" # auto | tiered | manual
|
||||
|
||||
# Per-session bounds (one diagnose / plan / execute / record pass)
|
||||
min_traces = 20
|
||||
max_cost_per_session_usd = 5.0
|
||||
max_tool_calls_per_diagnosis = 30
|
||||
|
||||
# Multi-session loop (paper Algorithm 1 stopping)
|
||||
stagnation_k = 5 # stop after this many sessions with no gate-score gain
|
||||
stagnation_eps = 0.001 # delta below this counts as no improvement
|
||||
max_total_cost_usd = 50.0 # cumulative teacher-cost budget across all sessions
|
||||
|
||||
# GateOK predicate — accept iff target cluster improves AND every other cluster
|
||||
# regresses by at most max_regression (epsilon in the paper).
|
||||
max_regression = 0.01 # paper default: 1%
|
||||
min_improvement = 0.0
|
||||
benchmark_subsample_size = 50
|
||||
benchmark_version = "personal_v1"
|
||||
|
||||
# Composite reward (paper Eq. 1) — used only when an Intelligence edit triggers
|
||||
# LoRA / GRPO training inside an accepted edit. The held-out gate is unaffected.
|
||||
[learning.spec_search.composite_reward]
|
||||
alpha = 0.5 # accuracy weight
|
||||
beta = 0.1 # energy penalty
|
||||
gamma = 0.1 # latency penalty
|
||||
delta = 0.3 # cost penalty
|
||||
@@ -1,30 +0,0 @@
|
||||
You are Jarvis — the local AI assistant. You are loyal, efficient, dry-witted, and genuinely care about the person you serve. You have a warm British sensibility: polite but never obsequious, witty but never frivolous.
|
||||
|
||||
PERSONALITY:
|
||||
- You anticipate needs before being asked
|
||||
- You deliver bad news with constructive dry wit: "Your rebuttals appear to have slipped past their deadline, sir. I'd suggest making them your first order of business — before anyone notices."
|
||||
- Your humor is understated — a raised eyebrow in voice form
|
||||
- You are calm under pressure and never flustered
|
||||
- You treat the briefing as a conversation with someone you respect, not a status report
|
||||
|
||||
ADDRESS:
|
||||
- Use the user's preferred honorific (provided in the system prompt)
|
||||
- Use it 2-3 times per briefing: once in greeting, once mid-briefing, once in closing
|
||||
- Never every sentence — that would be a parody, not Jarvis
|
||||
|
||||
EMAIL TRIAGE:
|
||||
- Important emails are from REAL PEOPLE (not automated senders, newsletters, or marketing)
|
||||
- Prioritize emails that need a REPLY or DECISION, or contain a DEADLINE
|
||||
- Skip promotional, automated, and notification emails entirely
|
||||
- For important emails, mention the sender name and what they need
|
||||
|
||||
MESSAGE TRIAGE (iMessage, Slack, etc.):
|
||||
- Highlight messages from key people and threads needing a reply
|
||||
- Briefly acknowledge casual threads so the user knows you checked: "Your group chat has been lively but nothing requiring a response"
|
||||
- Skip reactions, emoji-only messages, and automated notifications
|
||||
|
||||
CONSTRAINTS:
|
||||
- ONLY report facts present in the provided data. Never invent.
|
||||
- NEVER describe actions you are taking (adjusting lights, ordering food, queuing playlists, etc.)
|
||||
- No markdown formatting, no emojis, no bullet points, no headers — this is spoken aloud
|
||||
- If a data source is disconnected or errored, skip it silently — do not mention connection issues
|
||||
@@ -1,11 +0,0 @@
|
||||
You are an AI assistant generating a daily briefing. Be clear, concise, and factual.
|
||||
|
||||
## Voice & Tone
|
||||
- Straightforward and professional
|
||||
- No personality or humor — just the facts
|
||||
- Use plain language
|
||||
|
||||
## Structure
|
||||
- Open with the date and a one-line summary
|
||||
- Deliver each section with bullet points
|
||||
- Close with a summary of action items
|
||||
@@ -1,7 +0,0 @@
|
||||
# Copy to `.env` in this directory (deploy/docker/.env) before `docker compose up`.
|
||||
# docker-compose.yml requires this — the container binds 0.0.0.0, so the
|
||||
# server refuses to start without an API key.
|
||||
#
|
||||
# Generate a key with: jarvis auth generate-key
|
||||
# Then clients must send: Authorization: Bearer <key>
|
||||
OPENJARVIS_API_KEY=
|
||||
@@ -1,84 +0,0 @@
|
||||
# Base images are pinned to an immutable digest (in addition to a human-readable
|
||||
# tag) so every build resolves the exact same layers — reproducible builds and
|
||||
# safe rollbacks (#563).
|
||||
|
||||
# Stage 1: Build frontend SPA
|
||||
FROM node:22.23.0-slim@sha256:d9f850096136edbc402debdd8729579a288aac64574ada0ff4db26b6ae58b0b2 AS frontend
|
||||
# Public Supabase anon key for the savings leaderboard; empty by default so
|
||||
# the image's leaderboard stays disabled (#589). Pass --build-arg to enable.
|
||||
ARG OPENJARVIS_LEADERBOARD_PUBLIC_ANON=
|
||||
|
||||
WORKDIR /frontend
|
||||
COPY frontend/package.json frontend/package-lock.json* ./
|
||||
RUN npm ci --ignore-scripts 2>/dev/null || npm install
|
||||
COPY frontend/ .
|
||||
RUN VITE_SUPABASE_ANON_KEY="${OPENJARVIS_LEADERBOARD_PUBLIC_ANON}" npm run build
|
||||
|
||||
# Stage 2: Build Python package
|
||||
FROM python:3.12.13-slim-bookworm@sha256:76d4b7b6305788c6b4c6a19d6a22a3921bf802e9af4d5e1e5bd771208dba74bf AS builder
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential ca-certificates curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||
sh -s -- -y --profile minimal --default-toolchain none && \
|
||||
rustup toolchain install 1.88 --profile minimal && \
|
||||
rustup default 1.88
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies from the committed lockfile (#567). `uv export --frozen`
|
||||
# reads uv.lock as-is (no re-resolution) and emits a fully pinned, hash-verified
|
||||
# requirements set; `--no-deps` then installs exactly that set. This is a
|
||||
# separate layer from the source copy so dependency installs stay cached when
|
||||
# only application code changes.
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv export --frozen --no-dev --extra server --no-emit-project > requirements.txt && \
|
||||
uv pip install --system --no-deps -r requirements.txt && \
|
||||
uv pip install --system --no-deps "maturin>=1.12.6,<2"
|
||||
|
||||
# Copy the source and the non-src force-include paths (see pyproject
|
||||
# [tool.hatch.build.targets.wheel.force-include]) before building the project.
|
||||
COPY src/ src/
|
||||
COPY rust/ rust/
|
||||
COPY scripts/install scripts/install
|
||||
COPY deploy/windows deploy/windows
|
||||
|
||||
# Copy built frontend into the server static directory
|
||||
COPY --from=frontend /src/openjarvis/server/static src/openjarvis/server/static/
|
||||
|
||||
# Install the project itself without re-resolving dependencies.
|
||||
RUN uv pip install --system --no-deps . && \
|
||||
maturin build --release \
|
||||
--manifest-path rust/crates/openjarvis-python/Cargo.toml \
|
||||
--interpreter python3 \
|
||||
--out /tmp/openjarvis-rust-wheel && \
|
||||
uv pip install --system --no-deps /tmp/openjarvis-rust-wheel/*.whl && \
|
||||
python3 -c "import openjarvis_rust; print('openjarvis_rust ok')" && \
|
||||
python3 -m pip uninstall -y maturin && \
|
||||
rm -rf /tmp/openjarvis-rust-wheel rust
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM python:3.12.13-slim-bookworm@sha256:76d4b7b6305788c6b4c6a19d6a22a3921bf802e9af4d5e1e5bd771208dba74bf
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
# Run as an unprivileged user — the server needs no root privileges, so dropping
|
||||
# them limits the blast radius of a compromise (#565). The app writes only to
|
||||
# $HOME (config/cache/state), which is owned by this user.
|
||||
RUN groupadd --system --gid 10001 openjarvis && \
|
||||
useradd --system --uid 10001 --gid openjarvis \
|
||||
--create-home --home-dir /home/openjarvis openjarvis
|
||||
ENV HOME=/home/openjarvis
|
||||
USER openjarvis
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["jarvis"]
|
||||
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -1,87 +0,0 @@
|
||||
# Base images are pinned to an immutable digest (in addition to a human-readable
|
||||
# tag) so every build resolves the exact same layers — reproducible builds and
|
||||
# safe rollbacks (#563).
|
||||
|
||||
# Stage 1: Build frontend SPA
|
||||
FROM node:22.23.0-slim@sha256:d9f850096136edbc402debdd8729579a288aac64574ada0ff4db26b6ae58b0b2 AS frontend
|
||||
# Public Supabase anon key for the savings leaderboard; empty by default so
|
||||
# the image's leaderboard stays disabled (#589). Pass --build-arg to enable.
|
||||
ARG OPENJARVIS_LEADERBOARD_PUBLIC_ANON=
|
||||
|
||||
WORKDIR /frontend
|
||||
COPY frontend/package.json frontend/package-lock.json* ./
|
||||
RUN npm ci --ignore-scripts 2>/dev/null || npm install
|
||||
COPY frontend/ .
|
||||
RUN VITE_SUPABASE_ANON_KEY="${OPENJARVIS_LEADERBOARD_PUBLIC_ANON}" npm run build
|
||||
|
||||
# Stage 2: Build Python package (NVIDIA CUDA 12.4)
|
||||
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04@sha256:af8bd179ed3bf69d4b63b19a763662a6141f0f62ef099283f68d0b14b4bab0e3 AS builder
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-venv && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||
sh -s -- -y --profile minimal --default-toolchain none && \
|
||||
rustup toolchain install 1.88 --profile minimal && \
|
||||
rustup default 1.88
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies from the committed lockfile (#567). See deploy/docker/Dockerfile
|
||||
# for the rationale behind the frozen export + --no-deps install.
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv export --frozen --no-dev --extra server --no-emit-project > requirements.txt && \
|
||||
uv pip install --system --no-deps -r requirements.txt && \
|
||||
uv pip install --system --no-deps "maturin>=1.12.6,<2"
|
||||
|
||||
COPY src/ src/
|
||||
COPY rust/ rust/
|
||||
COPY scripts/install scripts/install
|
||||
COPY deploy/windows deploy/windows
|
||||
|
||||
COPY --from=frontend /src/openjarvis/server/static src/openjarvis/server/static/
|
||||
|
||||
RUN uv pip install --system --no-deps . && \
|
||||
maturin build --release \
|
||||
--manifest-path rust/crates/openjarvis-python/Cargo.toml \
|
||||
--interpreter python3 \
|
||||
--out /tmp/openjarvis-rust-wheel && \
|
||||
uv pip install --system --no-deps /tmp/openjarvis-rust-wheel/*.whl && \
|
||||
python3 -c "import openjarvis_rust; print('openjarvis_rust ok')" && \
|
||||
python3 -m pip uninstall -y maturin && \
|
||||
rm -rf /tmp/openjarvis-rust-wheel rust
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04@sha256:af8bd179ed3bf69d4b63b19a763662a6141f0f62ef099283f68d0b14b4bab0e3
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
# Run as an unprivileged user (#565). NVIDIA device nodes (/dev/nvidia*) are
|
||||
# world-accessible, so GPU workloads do not require root.
|
||||
RUN groupadd --system --gid 10001 openjarvis && \
|
||||
useradd --system --uid 10001 --gid openjarvis \
|
||||
--create-home --home-dir /home/openjarvis openjarvis
|
||||
ENV HOME=/home/openjarvis
|
||||
USER openjarvis
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["jarvis"]
|
||||
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -1,91 +0,0 @@
|
||||
# Base images are pinned to an immutable digest (in addition to a human-readable
|
||||
# tag) so every build resolves the exact same layers — reproducible builds and
|
||||
# safe rollbacks (#563).
|
||||
|
||||
# Stage 1: Build frontend SPA
|
||||
FROM node:22.23.0-slim@sha256:d9f850096136edbc402debdd8729579a288aac64574ada0ff4db26b6ae58b0b2 AS frontend
|
||||
# Public Supabase anon key for the savings leaderboard; empty by default so
|
||||
# the image's leaderboard stays disabled (#589). Pass --build-arg to enable.
|
||||
ARG OPENJARVIS_LEADERBOARD_PUBLIC_ANON=
|
||||
|
||||
WORKDIR /frontend
|
||||
COPY frontend/package.json frontend/package-lock.json* ./
|
||||
RUN npm ci --ignore-scripts 2>/dev/null || npm install
|
||||
COPY frontend/ .
|
||||
RUN VITE_SUPABASE_ANON_KEY="${OPENJARVIS_LEADERBOARD_PUBLIC_ANON}" npm run build
|
||||
|
||||
# Stage 2: Build Python package (AMD ROCm 7.2)
|
||||
FROM rocm/dev-ubuntu-22.04:7.2@sha256:05af5f04a06b04676d4c7438997d0deadaeb7478961ad621376e199bf3aeb644 AS builder
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-venv && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||
sh -s -- -y --profile minimal --default-toolchain none && \
|
||||
rustup toolchain install 1.88 --profile minimal && \
|
||||
rustup default 1.88
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies from the committed lockfile (#567). See deploy/docker/Dockerfile
|
||||
# for the rationale behind the frozen export + --no-deps install.
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv export --frozen --no-dev --extra server --no-emit-project > requirements.txt && \
|
||||
uv pip install --system --no-deps -r requirements.txt && \
|
||||
uv pip install --system --no-deps "maturin>=1.12.6,<2"
|
||||
|
||||
COPY src/ src/
|
||||
COPY rust/ rust/
|
||||
COPY scripts/install scripts/install
|
||||
COPY deploy/windows deploy/windows
|
||||
|
||||
COPY --from=frontend /src/openjarvis/server/static src/openjarvis/server/static/
|
||||
|
||||
RUN uv pip install --system --no-deps . && \
|
||||
maturin build --release \
|
||||
--manifest-path rust/crates/openjarvis-python/Cargo.toml \
|
||||
--interpreter python3 \
|
||||
--out /tmp/openjarvis-rust-wheel && \
|
||||
uv pip install --system --no-deps /tmp/openjarvis-rust-wheel/*.whl && \
|
||||
python3 -c "import openjarvis_rust; print('openjarvis_rust ok')" && \
|
||||
python3 -m pip uninstall -y maturin && \
|
||||
rm -rf /tmp/openjarvis-rust-wheel rust
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM rocm/dev-ubuntu-22.04:7.2@sha256:05af5f04a06b04676d4c7438997d0deadaeb7478961ad621376e199bf3aeb644
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
# Run as an unprivileged user (#565). ROCm GPU access is gated by the `video` and
|
||||
# `render` groups (see group_add in docker-compose.gpu.rocm.yml), so the user is
|
||||
# added to both; root is not required.
|
||||
RUN groupadd --system --gid 10001 openjarvis && \
|
||||
useradd --system --uid 10001 --gid openjarvis \
|
||||
--create-home --home-dir /home/openjarvis openjarvis && \
|
||||
(getent group video >/dev/null || groupadd --system video) && \
|
||||
(getent group render >/dev/null || groupadd --system render) && \
|
||||
usermod -aG video,render openjarvis
|
||||
ENV HOME=/home/openjarvis
|
||||
USER openjarvis
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["jarvis"]
|
||||
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -1,70 +0,0 @@
|
||||
# Base images are pinned to an immutable digest (in addition to a human-readable
|
||||
# tag) so every build resolves the exact same layers (#563).
|
||||
|
||||
# Node.js is sourced from the official, digest-pinned image rather than piping a
|
||||
# remote setup script into bash (`curl ... | bash -`), which performed no
|
||||
# checksum or signature verification of the downloaded installer (#566). The
|
||||
# image digest is the integrity check, and the copy is architecture-agnostic.
|
||||
FROM node:22.23.0-slim@sha256:d9f850096136edbc402debdd8729579a288aac64574ada0ff4db26b6ae58b0b2 AS node
|
||||
|
||||
FROM python:3.12.13-slim-bookworm@sha256:76d4b7b6305788c6b4c6a19d6a22a3921bf802e9af4d5e1e5bd771208dba74bf AS builder
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential ca-certificates curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||
sh -s -- -y --profile minimal --default-toolchain none && \
|
||||
rustup toolchain install 1.88 --profile minimal && \
|
||||
rustup default 1.88
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies from the committed lockfile (#567): `uv export --frozen`
|
||||
# reads uv.lock as-is and emits a pinned, hash-verified set installed with
|
||||
# --no-deps (no re-resolution). Copied first so this layer caches independently
|
||||
# of application source.
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv export --frozen --no-dev --extra server --no-emit-project > requirements.txt && \
|
||||
uv pip install --system --no-deps -r requirements.txt && \
|
||||
uv pip install --system --no-deps "maturin>=1.12.6,<2"
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install the project itself without re-resolving dependencies.
|
||||
RUN uv pip install --system --no-deps . && \
|
||||
maturin build --release \
|
||||
--manifest-path rust/crates/openjarvis-python/Cargo.toml \
|
||||
--interpreter python3 \
|
||||
--out /tmp/openjarvis-rust-wheel && \
|
||||
uv pip install --system --no-deps /tmp/openjarvis-rust-wheel/*.whl && \
|
||||
python3 -c "import openjarvis_rust; print('openjarvis_rust ok')" && \
|
||||
python3 -m pip uninstall -y maturin && \
|
||||
rm -rf /tmp/openjarvis-rust-wheel rust/target
|
||||
|
||||
FROM python:3.12.13-slim-bookworm@sha256:76d4b7b6305788c6b4c6a19d6a22a3921bf802e9af4d5e1e5bd771208dba74bf
|
||||
|
||||
# libstdc++6 + ca-certificates are the only runtime requirements of the Node
|
||||
# binary copied below (the python slim image already provides libc/libgcc).
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates libstdc++6 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY --from=builder /app /app
|
||||
|
||||
# Transplant the Node.js runtime from the official image. Both images are Debian
|
||||
# bookworm, so the glibc/libstdc++ ABI matches.
|
||||
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
|
||||
ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
LABEL openjarvis-sandbox=true
|
||||
|
||||
ENTRYPOINT ["python", "-m", "openjarvis.sandbox.entrypoint"]
|
||||
@@ -1,33 +0,0 @@
|
||||
# NVIDIA GPU override — use with:
|
||||
# docker compose -f deploy/docker/docker-compose.yml -f deploy/docker/docker-compose.gpu.nvidia.yml up
|
||||
|
||||
services:
|
||||
jarvis:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/docker/Dockerfile.gpu
|
||||
volumes:
|
||||
- /proc:/proc:ro
|
||||
- /sys:/sys:ro
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
|
||||
ollama:
|
||||
# Pinned to a fixed version + digest for reproducible deployments (#563);
|
||||
# must match the tag in docker-compose.yml.
|
||||
image: ollama/ollama:0.30.10@sha256:bfc9c6d53cc6989aa5131a6fde6b162b2802d4d337657f3253b5f69579bddeee
|
||||
environment:
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
@@ -1,15 +0,0 @@
|
||||
# ROCm GPU override — use with:
|
||||
# docker compose -f deploy/docker/docker-compose.yml -f deploy/docker/docker-compose.gpu.rocm.yml up
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
jarvis:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/docker/Dockerfile.gpu.rocm
|
||||
devices:
|
||||
- /dev/kfd
|
||||
- /dev/dri
|
||||
group_add:
|
||||
- video
|
||||
- render
|
||||
@@ -1,37 +0,0 @@
|
||||
services:
|
||||
jarvis:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/docker/Dockerfile
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- OPENJARVIS_ENGINE_DEFAULT=ollama
|
||||
- OLLAMA_HOST=http://ollama:11434
|
||||
# The container binds 0.0.0.0, so an API key is REQUIRED. Compose fails
|
||||
# fast if OPENJARVIS_API_KEY is unset (set it in deploy/docker/.env —
|
||||
# see .env.example, or `export` it). Generate one: `jarvis auth generate-key`.
|
||||
- OPENJARVIS_API_KEY=${OPENJARVIS_API_KEY:?OPENJARVIS_API_KEY must be set (see deploy/docker/.env.example)}
|
||||
depends_on:
|
||||
ollama:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
ollama:
|
||||
# Pinned to a fixed version + digest for reproducible deployments and
|
||||
# predictable rollbacks (#563). Bump deliberately, not implicitly via :latest.
|
||||
image: ollama/ollama:0.30.10@sha256:bfc9c6d53cc6989aa5131a6fde6b162b2802d4d337657f3253b5f69579bddeee
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
healthcheck:
|
||||
test: ["CMD", "ollama", "list"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
@@ -4,27 +4,15 @@
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.openjarvis</string>
|
||||
<!-- Binds loopback only: the personal-device default, reachable from this
|
||||
Mac but not the network, so no API key is required. To expose it on
|
||||
your LAN, change the host below to 0.0.0.0 AND uncomment the
|
||||
EnvironmentVariables block to set an API key (an unauthenticated
|
||||
0.0.0.0 server will refuse to start). -->
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/jarvis</string>
|
||||
<string>serve</string>
|
||||
<string>--host</string>
|
||||
<string>127.0.0.1</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>--port</string>
|
||||
<string>8000</string>
|
||||
</array>
|
||||
<!--
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>OPENJARVIS_API_KEY</key>
|
||||
<string>REPLACE_WITH_A_REAL_KEY</string>
|
||||
</dict>
|
||||
-->
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# posthog-hetzner-prep.sh — one-shot Hetzner Cloud server prep for
|
||||
# OpenJarvis's self-hosted PostHog analytics backend.
|
||||
#
|
||||
# Run this on a fresh Ubuntu 22.04+ box (Hetzner CCX23 in Ashburn, or
|
||||
# similar) after pointing the desired domain at it. Idempotent: safe
|
||||
# to re-run if a step fails partway.
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash posthog-hetzner-prep.sh analytics.openjarvis.ai you@openjarvis.ai
|
||||
#
|
||||
# After it finishes:
|
||||
# 1. Visit https://<DOMAIN>/ and create the admin account.
|
||||
# 2. Create project "OpenJarvis".
|
||||
# 3. Settings → Project → grab the Project API Key (phc_…).
|
||||
# 4. Update src/openjarvis/core/config.py AnalyticsConfig defaults:
|
||||
# host = "https://<DOMAIN>"
|
||||
# key = "phc_<new>"
|
||||
# 5. Ship a release. Frontend + backend + install.sh all read those
|
||||
# same defaults via load_config().
|
||||
#
|
||||
# Cost: ~$35/mo on Hetzner CCX23 (4 dedicated vCPU / 16 GB / 240 GB
|
||||
# NVMe) in US-East. Add Hetzner Cloud Backups (+20%) for production.
|
||||
#
|
||||
# Retention: post-install, set 365 days in PostHog UI →
|
||||
# Settings → Data Management → Event ingestion → Data retention.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- args ----
|
||||
if [[ $# -lt 2 ]]; then
|
||||
cat >&2 <<'USAGE'
|
||||
posthog-hetzner-prep.sh: missing arguments.
|
||||
|
||||
Usage:
|
||||
sudo bash posthog-hetzner-prep.sh <domain> <admin_email>
|
||||
|
||||
Examples:
|
||||
sudo bash posthog-hetzner-prep.sh analytics.openjarvis.ai team@openjarvis.ai
|
||||
|
||||
The domain must already resolve to this box (DNS A record) before
|
||||
the script runs — Let's Encrypt needs to reach this server on port 80.
|
||||
USAGE
|
||||
exit 2
|
||||
fi
|
||||
|
||||
DOMAIN="$1"
|
||||
ADMIN_EMAIL="$2"
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "posthog-hetzner-prep.sh: must be run as root (use sudo)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- step 1: system prep ----
|
||||
echo "[1/5] apt update + base packages..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl ufw ca-certificates gnupg \
|
||||
apt-transport-https software-properties-common
|
||||
|
||||
# ---- step 2: firewall ----
|
||||
echo "[2/5] firewall (UFW): 22/80/443 only..."
|
||||
ufw --force reset
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow 22/tcp
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw --force enable
|
||||
|
||||
# ---- step 3: swap (helps ClickHouse under load spikes) ----
|
||||
echo "[3/5] swap..."
|
||||
if [[ ! -f /swapfile ]]; then
|
||||
fallocate -l 4G /swapfile
|
||||
chmod 600 /swapfile
|
||||
mkswap /swapfile
|
||||
swapon /swapfile
|
||||
if ! grep -q "/swapfile" /etc/fstab; then
|
||||
echo "/swapfile none swap sw 0 0" >> /etc/fstab
|
||||
fi
|
||||
else
|
||||
echo " /swapfile already present"
|
||||
fi
|
||||
|
||||
# ---- step 4: docker ----
|
||||
echo "[4/5] docker..."
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
else
|
||||
echo " docker already installed"
|
||||
fi
|
||||
systemctl enable --now docker
|
||||
|
||||
# ---- step 5: posthog hobby deploy ----
|
||||
echo "[5/5] PostHog Hobby Deploy..."
|
||||
echo
|
||||
echo " Domain: $DOMAIN"
|
||||
echo " Admin email: $ADMIN_EMAIL"
|
||||
echo " DNS A record: verify it points to $(curl -fsS -m 5 https://api.ipify.org 2>/dev/null || echo "<this server>")"
|
||||
echo
|
||||
|
||||
# PostHog's official one-liner. It writes a .env file with random
|
||||
# secrets, configures Caddy with Let's Encrypt TLS for the domain,
|
||||
# and brings up the full stack via docker-compose.
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/posthog/posthog/HEAD/bin/deploy-hobby)" -s -- \
|
||||
--domain "$DOMAIN" \
|
||||
--email "$ADMIN_EMAIL" || {
|
||||
echo
|
||||
echo "PostHog deploy script exited with an error. Common causes:"
|
||||
echo " - DNS for $DOMAIN doesn't resolve to this server yet (wait + retry)"
|
||||
echo " - Port 80 not reachable from the public internet (firewall / cloud SG)"
|
||||
echo " - Out of disk on /var/lib/docker (need 20+ GB free)"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
cat <<EOF
|
||||
|
||||
================================================================
|
||||
PostHog is up at https://$DOMAIN/
|
||||
|
||||
Next steps:
|
||||
1. Open https://$DOMAIN/ in a browser.
|
||||
2. Create the first admin account (any email, your password).
|
||||
3. Create project "OpenJarvis".
|
||||
4. Settings → Project → Project API Key — copy the phc_… value.
|
||||
5. Update src/openjarvis/core/config.py AnalyticsConfig defaults:
|
||||
host = "https://$DOMAIN"
|
||||
key = "phc_<the-new-key>"
|
||||
6. Settings → Data Management → set retention to 365 days.
|
||||
7. Settings → Recordings → confirm Session Replay is OFF (default).
|
||||
8. Ship a release of OpenJarvis with the new config defaults.
|
||||
|
||||
Operational notes:
|
||||
- Updates: bash <(curl -fsSL https://raw.githubusercontent.com/posthog/posthog/HEAD/bin/upgrade-hobby)
|
||||
- Logs: docker compose -f /home/posthog/posthog/docker-compose.hobby.yml logs -f
|
||||
- Disk usage: df -h # bump VPS tier when /var/lib/docker > 70% full
|
||||
- Backups: enable Hetzner Cloud Backups in the Hetzner console
|
||||
================================================================
|
||||
EOF
|
||||
@@ -10,31 +10,6 @@ ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=HOME=/opt/openjarvis
|
||||
# Binding 0.0.0.0 requires authentication. This file MUST exist and contain:
|
||||
# OPENJARVIS_API_KEY=<key> (generate one: `jarvis auth generate-key`)
|
||||
# It is not prefixed with "-", so the unit fails to start if the file is
|
||||
# missing — preventing an accidentally unauthenticated public server.
|
||||
# Keep secrets here (mode 0600, owned by root) rather than inline Environment=
|
||||
# lines, which leak into `systemctl show` and the journal.
|
||||
EnvironmentFile=/etc/openjarvis/env
|
||||
|
||||
# --- Sandboxing / hardening (#564) ---
|
||||
# Conservative set: tightens the unit without blocking the server's normal I/O
|
||||
# or local GPU inference. ProtectSystem=strict makes the whole filesystem
|
||||
# read-only except ReadWritePaths, so $HOME (config/cache/state under
|
||||
# /opt/openjarvis) stays writable.
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/opt/openjarvis
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# OpenJarvis on native Windows
|
||||
|
||||
Phase-1 of the native-Windows-support RFC (#298). Mirrors the Linux
|
||||
(`deploy/systemd/`) and macOS (`deploy/launchd/`) deployments — but for
|
||||
PowerShell, without WSL2 or Docker.
|
||||
|
||||
## One-liner install
|
||||
|
||||
In an elevated-or-regular PowerShell:
|
||||
|
||||
```powershell
|
||||
irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex
|
||||
```
|
||||
|
||||
What it does:
|
||||
|
||||
1. Refuses non-Windows hosts and Windows < 10 1809.
|
||||
2. Checks Python 3.10 – 3.13 (3.14 has no numpy wheels yet — see #432).
|
||||
3. Checks `git` on PATH.
|
||||
4. Installs `uv` (https://astral.sh/uv) if absent.
|
||||
5. Clones the OpenJarvis repository to `%LOCALAPPDATA%\OpenJarvis`
|
||||
(override with `$env:OPENJARVIS_HOME`).
|
||||
6. Runs `uv sync --extra desktop --group desktop-native` so the FastAPI server,
|
||||
speech backend, and native extension are importable.
|
||||
7. Optionally prompts to register a scheduled task that auto-starts the
|
||||
server at logon.
|
||||
|
||||
Flags (when invoked directly rather than via `irm | iex`):
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `-Service` | Register the scheduled task without prompting |
|
||||
| `-SkipService` | Don't prompt; don't register |
|
||||
| `-Force` | Re-run all steps even if already done |
|
||||
|
||||
`irm | iex` can't pass `param()` args into a piped script string, so
|
||||
the same knobs are honored via env vars when the corresponding flag is
|
||||
absent:
|
||||
|
||||
```powershell
|
||||
$env:OPENJARVIS_SKIP_SERVICE = '1'
|
||||
irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex
|
||||
```
|
||||
|
||||
The available env vars: `OPENJARVIS_SKIP_SERVICE`, `OPENJARVIS_SERVICE`,
|
||||
`OPENJARVIS_FORCE`. If you need richer control, save the script first
|
||||
(`irm ... -OutFile install.ps1; .\install.ps1 -Force`).
|
||||
|
||||
## Manual scheduled-task setup
|
||||
|
||||
If you skipped the prompt during install, you can register / inspect /
|
||||
remove the task with `jarvis-service.ps1`:
|
||||
|
||||
```powershell
|
||||
$srv = "$env:LOCALAPPDATA\OpenJarvis\src\deploy\windows\jarvis-service.ps1"
|
||||
|
||||
# install (idempotent — replaces existing)
|
||||
powershell -ExecutionPolicy Bypass -File $srv install
|
||||
|
||||
# status
|
||||
powershell -ExecutionPolicy Bypass -File $srv status
|
||||
|
||||
# remove
|
||||
powershell -ExecutionPolicy Bypass -File $srv uninstall
|
||||
```
|
||||
|
||||
The task runs as the current user with `LogonType=Interactive` and
|
||||
`RunLevel=Limited`. It restarts up to 3 times on failure (1-minute
|
||||
gap), has no execution-time limit, and starts when available (catches
|
||||
up if missed).
|
||||
|
||||
## Loopback vs LAN-exposed
|
||||
|
||||
By default the scheduled task binds `127.0.0.1` — reachable only from
|
||||
this machine, no API key required. This matches launchd parity (see
|
||||
`deploy/launchd/com.openjarvis.plist`).
|
||||
|
||||
To expose on your LAN:
|
||||
|
||||
```powershell
|
||||
# 1. Generate an API key. The server REFUSES to bind 0.0.0.0 without one.
|
||||
$env:OPENJARVIS_API_KEY = (uv run jarvis auth generate-key)
|
||||
|
||||
# 2. Re-register the task with -ListenHost 0.0.0.0.
|
||||
powershell -ExecutionPolicy Bypass -File $srv install -ListenHost 0.0.0.0
|
||||
```
|
||||
|
||||
`jarvis-service.ps1 install` refuses `-ListenHost 0.0.0.0` if
|
||||
`$env:OPENJARVIS_API_KEY` is unset — same guard as the systemd unit's
|
||||
`EnvironmentFile=/etc/openjarvis/env`.
|
||||
|
||||
## Parity table
|
||||
|
||||
| Concern | systemd | launchd | Windows |
|
||||
|---------|---------|---------|---------|
|
||||
| Service definition | `deploy/systemd/openjarvis.service` | `deploy/launchd/com.openjarvis.plist` | `deploy/windows/jarvis-service.ps1` (cmdlet-driven) |
|
||||
| Default bind | `0.0.0.0` (with API key) | `127.0.0.1` (no API key) | `127.0.0.1` (no API key) |
|
||||
| Restart on failure | `Restart=on-failure RestartSec=5` | `KeepAlive=true` | `RestartCount=3 RestartInterval=PT1M` |
|
||||
| Auto-start | `multi-user.target` | `RunAtLoad=true` | `AtLogOn` trigger |
|
||||
|
||||
## Updating
|
||||
|
||||
To pull the latest:
|
||||
|
||||
```powershell
|
||||
cd "$env:LOCALAPPDATA\OpenJarvis\src"
|
||||
git pull --ff-only
|
||||
uv sync --extra desktop --group desktop-native
|
||||
```
|
||||
|
||||
Or re-run the installer with `-Force`:
|
||||
|
||||
```powershell
|
||||
irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex
|
||||
# (then re-run with the file directly, passing -Force)
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\OpenJarvis\src\deploy\windows\jarvis-service.ps1" uninstall
|
||||
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\OpenJarvis"
|
||||
```
|
||||
|
||||
Uninstalling does NOT remove `uv` (it's a separate tool — you may have
|
||||
other Python projects using it).
|
||||
@@ -1,529 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
OpenJarvis native Windows installer.
|
||||
|
||||
.DESCRIPTION
|
||||
Phase-1 of the native-Windows-support RFC (#298). Mirrors the
|
||||
behavior of scripts/install/install.sh (the curl-pipe-bash installer
|
||||
for Linux/WSL2/macOS) but for native Windows PowerShell - no WSL,
|
||||
no Docker, no MSYS2.
|
||||
|
||||
Steps:
|
||||
1. Refuse non-Windows / Windows < 10.
|
||||
2. Check Python 3.10 - 3.13 on PATH (3.14 has no numpy wheels yet,
|
||||
see #432).
|
||||
3. Check git on PATH.
|
||||
4. Install uv (https://astral.sh/uv) if absent.
|
||||
5. Clone the OpenJarvis repository to $env:LOCALAPPDATA\OpenJarvis
|
||||
(override with $env:OPENJARVIS_HOME).
|
||||
6. Run `uv sync --extra desktop --group desktop-native` so the FastAPI
|
||||
server, speech backend, and native extension are importable.
|
||||
7. Optionally register the scheduled-task service (see
|
||||
deploy/windows/jarvis-service.ps1).
|
||||
|
||||
Usage (one-liner):
|
||||
irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex
|
||||
|
||||
Usage (file invocation, supports flags):
|
||||
irm https://open-jarvis.github.io/OpenJarvis/install.ps1 -OutFile install.ps1
|
||||
.\install.ps1 -SkipService
|
||||
|
||||
Flags (when running the file directly):
|
||||
-SkipService Don't prompt for / install the scheduled task.
|
||||
-Service Install the scheduled task without prompting.
|
||||
-Force Re-run all steps even if already done.
|
||||
|
||||
Under `irm | iex` the param block is unreachable (Invoke-Expression
|
||||
can't pass named args into a piped script string), so the same knobs
|
||||
are honored via env vars when the corresponding flag is absent:
|
||||
$env:OPENJARVIS_SKIP_SERVICE = '1'
|
||||
$env:OPENJARVIS_SERVICE = '1'
|
||||
$env:OPENJARVIS_FORCE = '1'
|
||||
|
||||
.NOTES
|
||||
Loopback default: the scheduled-task service binds 127.0.0.1, so no
|
||||
API key is needed. To expose on the LAN, edit the registered task to
|
||||
pass `--host 0.0.0.0` AND set $env:OPENJARVIS_API_KEY (an
|
||||
unauthenticated 0.0.0.0 server refuses to start). See
|
||||
deploy/windows/README.md.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch] $SkipService,
|
||||
[switch] $Service,
|
||||
[switch] $Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Env-var fallback for the `irm | iex` path, where the param block is
|
||||
# unreachable (see header comment). Any explicit -switch wins; env vars
|
||||
# only fill in the gaps.
|
||||
if (-not $SkipService -and $env:OPENJARVIS_SKIP_SERVICE) { $SkipService = $true }
|
||||
if (-not $Service -and $env:OPENJARVIS_SERVICE) { $Service = $true }
|
||||
if (-not $Force -and $env:OPENJARVIS_FORCE) { $Force = $true }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output helpers - coloured but plain enough for Constrained Language Mode.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Write-Info ($msg) { Write-Host "[info] $msg" -ForegroundColor Cyan }
|
||||
function Write-Ok ($msg) { Write-Host "[ok] $msg" -ForegroundColor Green }
|
||||
function Write-Warn2 ($msg) { Write-Host "[warn] $msg" -ForegroundColor Yellow }
|
||||
function Write-Fail ($msg) {
|
||||
Write-Host "[fail] $msg" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers - winget bootstrap + PATH refresh
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Pull the latest Machine + User PATH from the registry into the current
|
||||
# PowerShell session. Tools installed by `winget install` (Python, git,
|
||||
# Ollama, etc.) update the User PATH, but the running process inherits
|
||||
# the parent shell's environment - so without this refresh the just-
|
||||
# installed tool stays invisible to subsequent `Get-Command` calls.
|
||||
#
|
||||
# CRITICAL: registry PATH entries can be REG_EXPAND_SZ (with literal
|
||||
# `%VAR%` placeholders); the Python.org installer in per-user mode adds
|
||||
# entries like `%LOCALAPPDATA%\Programs\Python\Python313\` unexpanded.
|
||||
# `GetEnvironmentVariable` returns the raw string and PowerShell does
|
||||
# NOT auto-expand on assignment to `$env:Path`, so `Get-Command python`
|
||||
# would miss the just-installed binary. Expand explicitly.
|
||||
function Update-PathFromRegistry {
|
||||
$machinePath = [System.Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$combined = "$machinePath;$userPath"
|
||||
$env:Path = [System.Environment]::ExpandEnvironmentVariables($combined)
|
||||
}
|
||||
|
||||
# Bootstrap a tool by winget id. Returns the resolved command source on
|
||||
# success, $null on failure. Caller decides whether failure is fatal.
|
||||
function Install-WithWinget {
|
||||
param(
|
||||
[string] $WingetId, # e.g. 'Python.Python.3.13'
|
||||
[string] $CommandName # e.g. 'python' or 'git'
|
||||
)
|
||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
# Windows 10 pre-2004 / Windows Server / locked-down corporate
|
||||
# images may not have winget. Fall back to the caller's manual
|
||||
# instructions.
|
||||
return $null
|
||||
}
|
||||
Write-Info " Installing $WingetId via winget (silent)..."
|
||||
& winget install --id $WingetId --silent --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warn2 " winget install $WingetId exited $LASTEXITCODE"
|
||||
return $null
|
||||
}
|
||||
Update-PathFromRegistry
|
||||
$cmd = Get-Command $CommandName -ErrorAction SilentlyContinue
|
||||
if ($cmd) { return $cmd.Source }
|
||||
return $null
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. OS check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Info "Checking OS..."
|
||||
if ($PSVersionTable.Platform -and $PSVersionTable.Platform -ne 'Win32NT') {
|
||||
Write-Fail "install.ps1 is for native Windows. On Linux/macOS use install.sh."
|
||||
}
|
||||
|
||||
# Build number 17763 = Windows 10 1809 (the oldest LTS we test against).
|
||||
$build = [System.Environment]::OSVersion.Version.Build
|
||||
if ($build -lt 17763) {
|
||||
Write-Fail "Windows 10 1809 (build 17763) or newer is required. Detected build $build."
|
||||
}
|
||||
Write-Ok "Windows build $build"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Python check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Get-PythonCommand {
|
||||
# Prefer `python3` (matches our cross-platform helper convention),
|
||||
# fall back to `python` (the Windows store / python.org default).
|
||||
foreach ($name in @('python3', 'python')) {
|
||||
$cmd = Get-Command $name -ErrorAction SilentlyContinue
|
||||
if ($cmd) { return $cmd.Source }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Info "Checking Python (3.10 - 3.13)..."
|
||||
$pythonExe = Get-PythonCommand
|
||||
if (-not $pythonExe) {
|
||||
Write-Info "Python not on PATH - attempting auto-install via winget..."
|
||||
$pythonExe = Install-WithWinget -WingetId 'Python.Python.3.13' -CommandName 'python'
|
||||
if (-not $pythonExe) {
|
||||
Write-Fail @"
|
||||
Python 3.10 - 3.13 not found and auto-install via winget failed.
|
||||
|
||||
Install manually from https://python.org (check 'Add python.exe to PATH'
|
||||
during install) or via winget:
|
||||
|
||||
winget install Python.Python.3.13
|
||||
|
||||
Then re-run this installer.
|
||||
"@
|
||||
}
|
||||
}
|
||||
|
||||
$verRaw = & $pythonExe --version 2>&1
|
||||
$verMatch = [regex]::Match($verRaw, '(\d+)\.(\d+)\.(\d+)')
|
||||
if (-not $verMatch.Success) {
|
||||
Write-Fail "Could not parse Python version from: $verRaw"
|
||||
}
|
||||
$pyMajor = [int]$verMatch.Groups[1].Value
|
||||
$pyMinor = [int]$verMatch.Groups[2].Value
|
||||
if ($pyMajor -ne 3 -or $pyMinor -lt 10 -or $pyMinor -gt 13) {
|
||||
Write-Fail @"
|
||||
Found Python $pyMajor.$pyMinor at $pythonExe, but OpenJarvis requires
|
||||
3.10 - 3.13. Python 3.14 has no numpy Windows wheels yet (#432, will
|
||||
re-open once numpy ships cp314).
|
||||
"@
|
||||
}
|
||||
Write-Ok "Python $pyMajor.$pyMinor ($pythonExe)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. git check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Info "Checking git..."
|
||||
$gitExe = (Get-Command git -ErrorAction SilentlyContinue).Source
|
||||
if (-not $gitExe) {
|
||||
Write-Info "git not on PATH - attempting auto-install via winget..."
|
||||
$gitExe = Install-WithWinget -WingetId 'Git.Git' -CommandName 'git'
|
||||
if (-not $gitExe) {
|
||||
Write-Fail @"
|
||||
git not found and auto-install via winget failed.
|
||||
|
||||
Install manually via winget:
|
||||
|
||||
winget install Git.Git
|
||||
|
||||
or download from https://git-scm.com, then re-run this installer.
|
||||
"@
|
||||
}
|
||||
}
|
||||
Write-Ok "git ($gitExe)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. uv check / install
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Info "Checking uv..."
|
||||
$uvExe = (Get-Command uv -ErrorAction SilentlyContinue).Source
|
||||
if (-not $uvExe) {
|
||||
Write-Info "Installing uv via astral.sh/uv (official PowerShell installer)..."
|
||||
try {
|
||||
Invoke-RestMethod -Uri 'https://astral.sh/uv/install.ps1' -UseBasicParsing | Invoke-Expression
|
||||
} catch {
|
||||
Write-Fail "uv install failed: $($_.Exception.Message)"
|
||||
}
|
||||
# The astral installer puts uv at %USERPROFILE%\.local\bin\uv.exe and
|
||||
# adds that dir to the User PATH. The current process's PATH isn't
|
||||
# refreshed automatically - prepend the install dir so the rest of
|
||||
# this script picks it up.
|
||||
$uvDir = Join-Path $env:USERPROFILE '.local\bin'
|
||||
if (Test-Path (Join-Path $uvDir 'uv.exe')) {
|
||||
$env:Path = "$uvDir;$env:Path"
|
||||
}
|
||||
$uvExe = (Get-Command uv -ErrorAction SilentlyContinue).Source
|
||||
if (-not $uvExe) {
|
||||
Write-Fail "uv installed but isn't on PATH. Re-open a fresh PowerShell and re-run."
|
||||
}
|
||||
}
|
||||
Write-Ok "uv ($uvExe)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Clone the repo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$installRoot = if ($env:OPENJARVIS_HOME) {
|
||||
$env:OPENJARVIS_HOME
|
||||
} else {
|
||||
Join-Path $env:LOCALAPPDATA 'OpenJarvis'
|
||||
}
|
||||
$srcDir = Join-Path $installRoot 'src'
|
||||
|
||||
Write-Info "Install root: $installRoot"
|
||||
|
||||
if (-not (Test-Path $installRoot)) {
|
||||
New-Item -ItemType Directory -Path $installRoot | Out-Null
|
||||
}
|
||||
|
||||
$repoUrl = if ($env:OPENJARVIS_REPO_URL) {
|
||||
$env:OPENJARVIS_REPO_URL
|
||||
} else {
|
||||
'https://github.com/open-jarvis/OpenJarvis.git'
|
||||
}
|
||||
|
||||
if (Test-Path (Join-Path $srcDir '.git')) {
|
||||
if ($Force) {
|
||||
Write-Info "Force: pulling latest from $repoUrl..."
|
||||
& $gitExe -C $srcDir pull --ff-only
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fail "git pull failed" }
|
||||
} else {
|
||||
Write-Ok "Repository already cloned (use -Force to update)"
|
||||
}
|
||||
} else {
|
||||
Write-Info "Cloning $repoUrl..."
|
||||
& $gitExe clone --depth 1 $repoUrl $srcDir
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fail "git clone failed" }
|
||||
Write-Ok "Cloned to $srcDir"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. uv sync --extra desktop --group desktop-native
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Info "Running 'uv sync --extra desktop --group desktop-native' in $srcDir (this can take a few minutes)..."
|
||||
Push-Location $srcDir
|
||||
try {
|
||||
& $uvExe sync --extra desktop --group desktop-native
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Fail "uv sync failed with exit code $LASTEXITCODE. Check the output above."
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
Write-Ok "Dependencies installed"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. Ollama - install + start + wait for daemon
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Info "Checking Ollama..."
|
||||
$ollamaExe = (Get-Command ollama -ErrorAction SilentlyContinue).Source
|
||||
if (-not $ollamaExe) {
|
||||
Write-Info " Ollama not on PATH - downloading the official installer (~150 MB)..."
|
||||
$ollamaSetup = Join-Path $env:TEMP 'OllamaSetup.exe'
|
||||
# SilentlyContinue is load-bearing in PS 5.1: the default progress
|
||||
# bar renderer slows Invoke-WebRequest down 30x on large downloads
|
||||
# (a known PS5.1 issue), turning a 30s download into 15+ minutes.
|
||||
$prevProgress = $ProgressPreference
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
try {
|
||||
Invoke-WebRequest `
|
||||
-Uri 'https://ollama.com/download/OllamaSetup.exe' `
|
||||
-OutFile $ollamaSetup `
|
||||
-UseBasicParsing
|
||||
} catch {
|
||||
Remove-Item $ollamaSetup -ErrorAction SilentlyContinue # clean up partial download
|
||||
$ProgressPreference = $prevProgress
|
||||
Write-Fail "Ollama download failed: $($_.Exception.Message)`nInstall manually from https://ollama.com, then re-run."
|
||||
} finally {
|
||||
$ProgressPreference = $prevProgress
|
||||
}
|
||||
# OllamaSetup.exe is built with NSIS, whose silent-install flag is
|
||||
# /S (uppercase). The Inno-Setup-style /silent would open the GUI
|
||||
# and hang `Start-Process -Wait` indefinitely.
|
||||
Write-Info " Running OllamaSetup.exe /S (this can take a minute)..."
|
||||
Start-Process -FilePath $ollamaSetup -ArgumentList '/S' -Wait
|
||||
Remove-Item $ollamaSetup -ErrorAction SilentlyContinue
|
||||
Update-PathFromRegistry
|
||||
$ollamaExe = (Get-Command ollama -ErrorAction SilentlyContinue).Source
|
||||
if (-not $ollamaExe) {
|
||||
Write-Fail "Ollama installer ran but 'ollama' isn't on PATH. Open a fresh PowerShell and re-run, or install manually from https://ollama.com."
|
||||
}
|
||||
}
|
||||
Write-Ok "Ollama ($ollamaExe)"
|
||||
|
||||
# Make sure the daemon is actually responsive before pulling. The Ollama
|
||||
# Windows installer launches the tray app at install time, but on a re-
|
||||
# run with an existing install the daemon may not be running yet.
|
||||
Write-Info "Waiting for Ollama daemon..."
|
||||
$ollamaReady = $false
|
||||
for ($i = 0; $i -lt 60; $i++) {
|
||||
# 'ollama list' writes to stderr until the daemon is reachable; under
|
||||
# $ErrorActionPreference='Stop' the 2>&1 merge surfaces that as a
|
||||
# terminating NativeCommandError that would abort the whole install on
|
||||
# the very first probe. Swallow it and rely on $LASTEXITCODE so the
|
||||
# Start-Process serve fallback below actually runs (issue #522).
|
||||
try { & $ollamaExe list 2>&1 | Out-Null } catch { }
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$ollamaReady = $true
|
||||
break
|
||||
}
|
||||
if ($i -eq 5) {
|
||||
# Daemon clearly isn't auto-running - start it ourselves. Ollama
|
||||
# for Windows uses the tray app `ollama app.exe`; falling back to
|
||||
# `ollama serve` works headless.
|
||||
Start-Process -FilePath $ollamaExe -ArgumentList 'serve' -WindowStyle Hidden -ErrorAction SilentlyContinue
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
if (-not $ollamaReady) {
|
||||
Write-Warn2 "Ollama daemon didn't become ready in 60s. Continuing - bg-orchestrator will retry later."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. Pull a starter model (qwen3.5:2b - ~1.5 GB)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$modelPullOk = $false
|
||||
if ($ollamaReady) {
|
||||
Write-Info "Pulling qwen3.5:2b (~1.5 GB) so 'jarvis' works on first run..."
|
||||
& $ollamaExe pull 'qwen3.5:2b'
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$modelPullOk = $true
|
||||
Write-Ok "Starter model ready"
|
||||
} else {
|
||||
Write-Warn2 "ollama pull failed; the bg-orchestrator will retry once Ollama is reachable."
|
||||
}
|
||||
} else {
|
||||
Write-Warn2 "Skipping model pull - daemon wasn't ready."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 9. jarvis.cmd shim - so bare `jarvis` works in any new PowerShell
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$binDir = Join-Path $installRoot 'bin'
|
||||
$shimPath = Join-Path $binDir 'jarvis.cmd'
|
||||
|
||||
if (-not (Test-Path $binDir)) {
|
||||
New-Item -ItemType Directory -Path $binDir | Out-Null
|
||||
}
|
||||
|
||||
# %~dp0 in a .cmd file resolves to the directory containing the script,
|
||||
# so the shim is self-locating - moving %LOCALAPPDATA%\OpenJarvis won't
|
||||
# break it as long as the user moves the whole tree. `uv` is resolved
|
||||
# from PATH at runtime (astral installer adds it to User PATH); avoids
|
||||
# pinning to the install-time uv.exe path which can shift on uv updates.
|
||||
$shimContent = @"
|
||||
@echo off
|
||||
setlocal
|
||||
set "SRC=%~dp0..\src"
|
||||
uv run --project "%SRC%" jarvis %*
|
||||
"@
|
||||
Set-Content -Path $shimPath -Value $shimContent -Encoding ASCII
|
||||
|
||||
# Add %LOCALAPPDATA%\OpenJarvis\bin to User PATH if it isn't already
|
||||
# there. The current process won't see it until restart - handled in the
|
||||
# final banner.
|
||||
#
|
||||
# Compare against the EXPANDED form: a previous install may have written
|
||||
# the entry as `%LOCALAPPDATA%\OpenJarvis\bin` (unexpanded) into User
|
||||
# PATH, and a literal `-ieq` against the expanded `$binDir` would miss
|
||||
# it and append a duplicate every re-run.
|
||||
$userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$pathOnUser = $false
|
||||
if ($userPath) {
|
||||
foreach ($entry in ($userPath -split ';')) {
|
||||
$expanded = [System.Environment]::ExpandEnvironmentVariables($entry)
|
||||
if ($expanded -ieq $binDir) { $pathOnUser = $true; break }
|
||||
}
|
||||
}
|
||||
$pathNeedsRefresh = $false
|
||||
if (-not $pathOnUser) {
|
||||
$newUserPath = if ($userPath) { "$userPath;$binDir" } else { $binDir }
|
||||
[System.Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User')
|
||||
$pathNeedsRefresh = $true
|
||||
}
|
||||
Write-Ok "jarvis shim installed at $shimPath"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 10. Optional: register the scheduled-task service
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$serviceScript = Join-Path $srcDir 'deploy\windows\jarvis-service.ps1'
|
||||
$shouldInstallService = $false
|
||||
|
||||
# Pre-check admin if the user wants the service - Register-ScheduledTask
|
||||
# requires elevation. We do this before the prompt so we don't ask "do
|
||||
# you want the service?" only to fail with Access Denied after they say
|
||||
# yes.
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal] `
|
||||
[Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
|
||||
if ($Service -and -not $isAdmin) {
|
||||
Write-Fail "-Service was requested, but this PowerShell is not elevated. Register-ScheduledTask needs admin rights - re-run from an elevated PowerShell, or drop -Service."
|
||||
}
|
||||
if ($Service) {
|
||||
$shouldInstallService = $true
|
||||
} elseif ($SkipService) {
|
||||
$shouldInstallService = $false
|
||||
} elseif (-not $isAdmin) {
|
||||
# Default to skip-with-explanation when we can't elevate, rather
|
||||
# than prompting and then failing at Register-ScheduledTask.
|
||||
Write-Warn2 "Skipping scheduled-task setup - this PowerShell is not elevated."
|
||||
Write-Warn2 " Register-ScheduledTask requires admin. To install the service later:"
|
||||
Write-Warn2 " Right-click PowerShell -> Run as administrator, then run:"
|
||||
Write-Warn2 " powershell -ExecutionPolicy Bypass -File `"$serviceScript`" install"
|
||||
} else {
|
||||
# Interactive prompt only when there's a real user at the keyboard
|
||||
# AND stdin isn't piped. [Environment]::UserInteractive is the
|
||||
# canonical PowerShell idiom for "is this a user session" (false for
|
||||
# services, scheduled tasks, etc); we additionally guard against the
|
||||
# `irm | iex` case where stdin is redirected.
|
||||
$isInteractive = [Environment]::UserInteractive `
|
||||
-and -not [System.Console]::IsInputRedirected
|
||||
if ($isInteractive) {
|
||||
$reply = Read-Host "Register OpenJarvis as a Windows scheduled task (auto-start at logon, loopback only)? [y/N]"
|
||||
$shouldInstallService = ($reply -match '^[yY]')
|
||||
} else {
|
||||
Write-Warn2 "Non-interactive install - skipping scheduled-task setup."
|
||||
Write-Warn2 "To register the service later, run (from an elevated PowerShell):"
|
||||
Write-Warn2 " powershell -ExecutionPolicy Bypass -File `"$serviceScript`" install"
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldInstallService) {
|
||||
if (-not (Test-Path $serviceScript)) {
|
||||
Write-Fail "Service script not found at $serviceScript (the clone may be missing files; try -Force)."
|
||||
}
|
||||
Write-Info "Installing scheduled task..."
|
||||
& powershell -ExecutionPolicy Bypass -File $serviceScript install -InstallRoot $installRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Fail "Scheduled task setup failed."
|
||||
}
|
||||
Write-Ok "Scheduled task 'OpenJarvis' registered (loopback default)."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. Final message
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " +----------------------------------+" -ForegroundColor Green
|
||||
Write-Host " | OpenJarvis install complete |" -ForegroundColor Green
|
||||
Write-Host " +----------------------------------+" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host " Repo: $srcDir"
|
||||
|
||||
# Tell the truth about what the user can run next, given (a) whether the
|
||||
# starter model finished pulling and (b) whether the User-PATH update
|
||||
# needs a fresh PowerShell to take effect.
|
||||
$nextCmd = if ($modelPullOk) { 'jarvis' } else { 'jarvis doctor' }
|
||||
|
||||
if ($pathNeedsRefresh) {
|
||||
Write-Host ""
|
||||
Write-Host " Run it: open a NEW PowerShell, then: $nextCmd" -ForegroundColor Yellow
|
||||
Write-Host " (the jarvis shim was added to your User PATH; the"
|
||||
Write-Host " current PowerShell won't see it until restart)"
|
||||
} else {
|
||||
Write-Host " Run it: $nextCmd"
|
||||
}
|
||||
|
||||
if (-not $modelPullOk) {
|
||||
Write-Host ""
|
||||
Write-Host " NOTE: the qwen3.5:2b model didn't finish downloading." -ForegroundColor Yellow
|
||||
Write-Host " Chat will fail until the bg-orchestrator finishes the retry."
|
||||
Write-Host " 'jarvis doctor' shows progress."
|
||||
}
|
||||
|
||||
if ($shouldInstallService) {
|
||||
Write-Host ""
|
||||
Write-Host " Service: schtasks /Query /TN OpenJarvis (status)"
|
||||
Write-Host " powershell -File `"$serviceScript`" uninstall (remove)"
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host " Docs: https://open-jarvis.github.io/OpenJarvis/"
|
||||
Write-Host ""
|
||||
@@ -1,208 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Register / unregister the OpenJarvis Windows scheduled task.
|
||||
|
||||
.DESCRIPTION
|
||||
The Windows equivalent of deploy/systemd/openjarvis.service and
|
||||
deploy/launchd/com.openjarvis.plist.
|
||||
|
||||
Registers a per-user scheduled task named "OpenJarvis" that starts
|
||||
`jarvis serve` at logon and restarts on failure. Loopback default
|
||||
(127.0.0.1) so no API key is required — matches launchd parity.
|
||||
|
||||
Subcommands:
|
||||
install — create or replace the task
|
||||
uninstall — remove the task
|
||||
status — show task state
|
||||
|
||||
Arguments (install only):
|
||||
-InstallRoot <path> default: %LOCALAPPDATA%\OpenJarvis (matches
|
||||
install.ps1's default)
|
||||
-ListenHost <addr> default: 127.0.0.1 (loopback). Set to 0.0.0.0
|
||||
ONLY if you also set $env:OPENJARVIS_API_KEY
|
||||
— the server refuses to start unauthenticated
|
||||
on a non-loopback bind.
|
||||
-ListenPort <int> default: 8000
|
||||
|
||||
Usage:
|
||||
powershell -ExecutionPolicy Bypass -File jarvis-service.ps1 install
|
||||
powershell -ExecutionPolicy Bypass -File jarvis-service.ps1 uninstall
|
||||
powershell -ExecutionPolicy Bypass -File jarvis-service.ps1 status
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[ValidateSet('install', 'uninstall', 'status')]
|
||||
[string] $Command = 'status',
|
||||
|
||||
[string] $InstallRoot,
|
||||
[string] $ListenHost = '127.0.0.1',
|
||||
[int] $ListenPort = 8000
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$TaskName = 'OpenJarvis'
|
||||
|
||||
function Write-Info ($msg) { Write-Host "[info] $msg" -ForegroundColor Cyan }
|
||||
function Write-Ok ($msg) { Write-Host "[ok] $msg" -ForegroundColor Green }
|
||||
function Write-Warn2 ($msg) { Write-Host "[warn] $msg" -ForegroundColor Yellow }
|
||||
function Write-Fail ($msg) {
|
||||
Write-Host "[fail] $msg" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-DefaultInstallRoot {
|
||||
# Use $script: prefix so this is robust to being called from any
|
||||
# function scope (PowerShell's default dynamic lookup would also
|
||||
# work today, but $script: is the explicit contract).
|
||||
if ($script:InstallRoot) { return $script:InstallRoot }
|
||||
if ($env:OPENJARVIS_HOME) { return $env:OPENJARVIS_HOME }
|
||||
return (Join-Path $env:LOCALAPPDATA 'OpenJarvis')
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# install
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Install-Task {
|
||||
$root = Get-DefaultInstallRoot
|
||||
$srcDir = Join-Path $root 'src'
|
||||
if (-not (Test-Path $srcDir)) {
|
||||
Write-Fail "OpenJarvis source not found at $srcDir. Run install.ps1 first."
|
||||
}
|
||||
|
||||
$uvCmd = Get-Command uv -ErrorAction SilentlyContinue
|
||||
if (-not $uvCmd) {
|
||||
$uvFallback = Join-Path $env:USERPROFILE '.local\bin\uv.exe'
|
||||
if (Test-Path $uvFallback) {
|
||||
$uvPath = $uvFallback
|
||||
} else {
|
||||
Write-Fail "uv.exe not found on PATH or at $uvFallback. Re-run install.ps1."
|
||||
}
|
||||
} else {
|
||||
$uvPath = $uvCmd.Source
|
||||
}
|
||||
|
||||
# Safety: refuse to register a non-loopback bind without an API key.
|
||||
# Mirrors deploy/systemd/openjarvis.service's EnvironmentFile guard.
|
||||
$isLoopback = ($ListenHost -eq '127.0.0.1' -or $ListenHost -eq 'localhost')
|
||||
if (-not $isLoopback -and -not $env:OPENJARVIS_API_KEY) {
|
||||
Write-Fail @"
|
||||
ListenHost is $ListenHost (non-loopback) but `$env:OPENJARVIS_API_KEY is
|
||||
not set. An unauthenticated non-loopback bind is refused by jarvis serve
|
||||
and would also create a security hole. Set the env var first:
|
||||
|
||||
`$env:OPENJARVIS_API_KEY = (uv run jarvis auth generate-key)
|
||||
|
||||
then re-run with -ListenHost 0.0.0.0.
|
||||
"@
|
||||
}
|
||||
|
||||
# CRITICAL: scheduled tasks do NOT inherit the registering session's
|
||||
# environment. If we registered the task now and stopped here, the
|
||||
# task would launch at logon with a clean env, find no API key, and
|
||||
# `jarvis serve` would refuse to bind 0.0.0.0 — failing silently every
|
||||
# logon. Persist the key to the User env scope so the task's logon
|
||||
# session picks it up. (Loopback path doesn't need the key, so this
|
||||
# only runs for the explicit LAN-exposed case.)
|
||||
if (-not $isLoopback) {
|
||||
Write-Info "Persisting OPENJARVIS_API_KEY to User environment so the scheduled task can read it at logon."
|
||||
[System.Environment]::SetEnvironmentVariable(
|
||||
'OPENJARVIS_API_KEY',
|
||||
$env:OPENJARVIS_API_KEY,
|
||||
'User'
|
||||
)
|
||||
}
|
||||
|
||||
Write-Info "Registering scheduled task '$TaskName'..."
|
||||
Write-Info " Working dir : $srcDir"
|
||||
Write-Info " Listen : $ListenHost`:$ListenPort"
|
||||
Write-Info " User : $env:USERNAME"
|
||||
|
||||
# If a previous task exists, remove it first (idempotent install).
|
||||
$existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if ($existing) {
|
||||
Write-Info "Existing task found — replacing."
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
}
|
||||
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute $uvPath `
|
||||
-Argument "run jarvis serve --host $ListenHost --port $ListenPort" `
|
||||
-WorkingDirectory $srcDir
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-RestartCount 3 `
|
||||
-RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Seconds 0)
|
||||
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId $env:USERNAME `
|
||||
-LogonType Interactive `
|
||||
-RunLevel Limited
|
||||
|
||||
Register-ScheduledTask `
|
||||
-TaskName $TaskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Description 'OpenJarvis API server (loopback default — see deploy/windows/README.md)' | Out-Null
|
||||
|
||||
Write-Ok "Task '$TaskName' registered."
|
||||
Write-Info "It will start automatically at next logon."
|
||||
Write-Info "To start it now: Start-ScheduledTask -TaskName $TaskName"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# uninstall
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Uninstall-Task {
|
||||
$existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if (-not $existing) {
|
||||
Write-Warn2 "Task '$TaskName' is not registered — nothing to remove."
|
||||
return
|
||||
}
|
||||
Write-Info "Stopping '$TaskName' (if running)..."
|
||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
Write-Info "Unregistering '$TaskName'..."
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
Write-Ok "Task '$TaskName' removed."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# status
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Show-Status {
|
||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if (-not $task) {
|
||||
Write-Host "Task '$TaskName' is not registered."
|
||||
Write-Host "Install it with:"
|
||||
Write-Host " powershell -ExecutionPolicy Bypass -File `"$PSCommandPath`" install"
|
||||
return
|
||||
}
|
||||
$info = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
Write-Host "Task : $TaskName"
|
||||
Write-Host "State : $($task.State)"
|
||||
Write-Host "LastRun : $($info.LastRunTime)"
|
||||
Write-Host "LastRes : 0x$('{0:X8}' -f $info.LastTaskResult)"
|
||||
Write-Host "NextRun : $($info.NextRunTime)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
switch ($Command) {
|
||||
'install' { Install-Task }
|
||||
'uninstall' { Uninstall-Task }
|
||||
'status' { Show-Status }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
# OpenJarvis Desktop
|
||||
|
||||
Tauri 2.0 native desktop application for OpenJarvis with auto-updates, energy monitoring, trace debugging, and learning visualization.
|
||||
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
# Prerequisites: Node.js 22+, Rust stable, system deps (see below)
|
||||
|
||||
cd desktop
|
||||
npm install
|
||||
cargo tauri dev # Hot-reload development mode
|
||||
cargo tauri build # Production build
|
||||
```
|
||||
|
||||
### Linux System Dependencies
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev \
|
||||
librsvg2-dev patchelf libxdo-dev
|
||||
```
|
||||
|
||||
## Auto-Update Architecture
|
||||
|
||||
Every push to `main` (touching `desktop/` or the workflow) triggers a CI pipeline that:
|
||||
|
||||
1. Validates TypeScript + Rust (`validate` job)
|
||||
2. Builds for Linux, macOS (ARM + Intel), and Windows (`build-and-release` job)
|
||||
3. Creates/updates a `desktop-latest` pre-release on GitHub Releases
|
||||
4. Uploads platform installers and a signed `latest.json` manifest
|
||||
|
||||
The desktop app checks `latest.json` on startup and every 30 minutes. When a newer version is found, it shows a banner prompting the user to download and relaunch.
|
||||
|
||||
```
|
||||
Push to main -> CI builds -> desktop-latest release -> latest.json
|
||||
|
|
||||
Desktop app checks periodically <-------------------------+
|
||||
-> "Update available" banner
|
||||
-> Download in background
|
||||
-> "Relaunch now" prompt
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
### Rolling (Nightly)
|
||||
|
||||
Automatic on every push to `main`. Users on the desktop app receive updates seamlessly.
|
||||
|
||||
### Stable (Versioned)
|
||||
|
||||
```bash
|
||||
# Bump version in all 3 config files
|
||||
./scripts/bump-desktop-version.sh 1.0.1
|
||||
|
||||
# Commit and tag
|
||||
git add desktop/package.json desktop/src-tauri/tauri.conf.json desktop/src-tauri/Cargo.toml
|
||||
git commit -m "chore(desktop): bump version to 1.0.1"
|
||||
git tag desktop-v1.0.1
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
CI creates a versioned GitHub Release (e.g., `desktop-v1.0.1`) with full installers.
|
||||
|
||||
## Code Signing
|
||||
|
||||
### Update Signing (Required for Auto-Updates)
|
||||
|
||||
Generate a key pair for signing update manifests:
|
||||
|
||||
```bash
|
||||
cargo tauri signer generate -w ~/.tauri/openjarvis.key
|
||||
```
|
||||
|
||||
Set the public key in `src-tauri/tauri.conf.json` under `plugins.updater.pubkey`, then add these GitHub Secrets:
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `TAURI_SIGNING_PRIVATE_KEY` | Contents of the `.key` file |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password used during generation |
|
||||
|
||||
### macOS Notarization (Optional)
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `APPLE_CERTIFICATE` | Base64-encoded `.p12` certificate |
|
||||
| `APPLE_CERTIFICATE_PASSWORD` | Certificate password |
|
||||
| `APPLE_SIGNING_IDENTITY` | e.g., `Developer ID Application: Name (TEAMID)` |
|
||||
| `APPLE_ID` | Apple ID email |
|
||||
| `APPLE_PASSWORD` | App-specific password |
|
||||
| `APPLE_TEAM_ID` | 10-character team ID |
|
||||
|
||||
### Windows Authenticode (Optional)
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `WINDOWS_CERTIFICATE` | Base64-encoded `.pfx` certificate |
|
||||
| `WINDOWS_CERTIFICATE_PASSWORD` | Certificate password |
|
||||
|
||||
All signing is optional — unsigned builds work without any secrets configured.
|
||||
|
||||
## Dashboard Panels
|
||||
|
||||
- **Energy** — Real-time power monitoring (recharts)
|
||||
- **Traces** — Timeline inspection with step-type color coding
|
||||
- **Learning** — Policy visualization (GRPO/bandit stats)
|
||||
- **Memory** — Search and stats for memory backends
|
||||
- **Admin** — Health checks, agent management, server control
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenJarvis Desktop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "openjarvis-desktop",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-notification": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2",
|
||||
"@tauri-apps/plugin-autostart": "^2",
|
||||
"@tauri-apps/plugin-updater": "^2",
|
||||
"@tauri-apps/plugin-process": "^2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"recharts": "^2.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "~5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "openjarvis-desktop"
|
||||
version = "1.0.0"
|
||||
description = "OpenJarvis Desktop — Native AI assistant with energy monitoring, trace debugging, and learning visualization"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-autostart = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-process = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
@@ -10,11 +10,5 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
|
After Width: | Height: | Size: 360 B |
|
After Width: | Height: | Size: 856 B |
|
After Width: | Height: | Size: 856 B |
|
After Width: | Height: | Size: 104 B |
|
After Width: | Height: | Size: 856 B |
@@ -0,0 +1,208 @@
|
||||
use serde::Serialize;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
|
||||
/// Fetch health status from the OpenJarvis API server.
|
||||
#[tauri::command]
|
||||
async fn check_health(api_url: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/health", api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch energy monitoring data from the API.
|
||||
#[tauri::command]
|
||||
async fn fetch_energy(api_url: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/telemetry/energy", api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch telemetry statistics from the API.
|
||||
#[tauri::command]
|
||||
async fn fetch_telemetry(api_url: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/telemetry/stats", api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch recent traces from the API.
|
||||
#[tauri::command]
|
||||
async fn fetch_traces(api_url: String, limit: u32) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/traces?limit={}", api_url, limit);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch a single trace by ID.
|
||||
#[tauri::command]
|
||||
async fn fetch_trace(api_url: String, trace_id: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/traces/{}", api_url, trace_id);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch learning system statistics.
|
||||
#[tauri::command]
|
||||
async fn fetch_learning_stats(api_url: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/learning/stats", api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch learning policy configuration.
|
||||
#[tauri::command]
|
||||
async fn fetch_learning_policy(api_url: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/learning/policy", api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch memory backend statistics.
|
||||
#[tauri::command]
|
||||
async fn fetch_memory_stats(api_url: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/memory/stats", api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Search memory for relevant chunks.
|
||||
#[tauri::command]
|
||||
async fn search_memory(
|
||||
api_url: String,
|
||||
query: String,
|
||||
top_k: u32,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/memory/search", api_url);
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({"query": query, "top_k": top_k}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Fetch list of available agents.
|
||||
#[tauri::command]
|
||||
async fn fetch_agents(api_url: String) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}/v1/agents", api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Launch the `jarvis` CLI command via shell.
|
||||
#[tauri::command]
|
||||
async fn run_jarvis_command(args: Vec<String>) -> Result<String, String> {
|
||||
let output = tokio::process::Command::new("jarvis")
|
||||
.args(&args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to launch jarvis: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
} else {
|
||||
Err(String::from_utf8_lossy(&output.stderr).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
Some(vec!["--hidden"]),
|
||||
))
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
// Focus the main window if another instance is launched
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}))
|
||||
.setup(|app| {
|
||||
// Set up system tray menu
|
||||
let _tray = app.tray_by_id("main");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
check_health,
|
||||
fetch_energy,
|
||||
fetch_telemetry,
|
||||
fetch_traces,
|
||||
fetch_trace,
|
||||
fetch_learning_stats,
|
||||
fetch_learning_policy,
|
||||
fetch_memory_stats,
|
||||
search_memory,
|
||||
fetch_agents,
|
||||
run_jarvis_command,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running OpenJarvis Desktop");
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body,:root{
|
||||
background:transparent !important;
|
||||
background-color:transparent !important;
|
||||
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",sans-serif;
|
||||
color:#fff;height:100%;overflow:hidden;
|
||||
}
|
||||
#container{
|
||||
display:flex;flex-direction:column;
|
||||
height:100%;padding:8px 12px;
|
||||
}
|
||||
#messages{
|
||||
flex:1;overflow-y:auto;
|
||||
display:flex;flex-direction:column;gap:6px;
|
||||
margin-bottom:8px;padding:12px;
|
||||
border-radius:16px;
|
||||
background:rgba(30,30,30,0.88);
|
||||
border:1px solid rgba(255,255,255,0.20);
|
||||
}
|
||||
#messages:empty{display:none}
|
||||
#messages::-webkit-scrollbar{width:6px}
|
||||
#messages::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.15);border-radius:3px}
|
||||
#messages::-webkit-scrollbar-track{background:transparent}
|
||||
.msg{
|
||||
padding:8px 12px;border-radius:12px;
|
||||
font-size:13px;line-height:1.55;max-width:90%;
|
||||
white-space:pre-wrap;word-wrap:break-word;
|
||||
-webkit-user-select:text;user-select:text;
|
||||
}
|
||||
.msg.user{
|
||||
align-self:flex-end;
|
||||
background:rgba(59,130,246,0.35);
|
||||
color:rgba(255,255,255,0.95);
|
||||
}
|
||||
.msg.assistant{
|
||||
align-self:flex-start;
|
||||
background:rgba(255,255,255,0.20);
|
||||
color:rgba(255,255,255,0.88);
|
||||
white-space:normal;
|
||||
}
|
||||
.msg.assistant > p{margin:0 0 6px 0;white-space:pre-wrap}
|
||||
.msg.assistant > p:last-child{margin-bottom:0}
|
||||
.msg.assistant h1,.msg.assistant h2,.msg.assistant h3{
|
||||
font-size:14px;font-weight:600;margin:6px 0 4px 0;
|
||||
}
|
||||
.msg.assistant ul,.msg.assistant ol{margin:2px 0 6px 18px;padding:0}
|
||||
.msg.assistant li{margin:1px 0}
|
||||
.msg.assistant a{color:#93c5fd;text-decoration:underline}
|
||||
.msg.assistant code{
|
||||
background:rgba(0,0,0,0.35);
|
||||
padding:1px 5px;border-radius:4px;
|
||||
font-family:"SF Mono",Menlo,Monaco,monospace;
|
||||
font-size:12px;
|
||||
}
|
||||
.msg.assistant pre{
|
||||
background:rgba(0,0,0,0.40);
|
||||
padding:8px 10px;border-radius:8px;
|
||||
margin:4px 0;overflow-x:auto;
|
||||
border:1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.msg.assistant pre code{
|
||||
background:transparent;padding:0;border-radius:0;
|
||||
font-size:11.5px;line-height:1.4;white-space:pre;
|
||||
}
|
||||
.msg.assistant strong{font-weight:600;color:#fff}
|
||||
.msg.assistant em{font-style:italic}
|
||||
.msg.assistant del{opacity:0.6;text-decoration:line-through}
|
||||
|
||||
/* Streaming caret — pulses at the end of the in-progress bubble */
|
||||
.caret{
|
||||
display:inline-block;width:6px;height:13px;
|
||||
vertical-align:text-bottom;margin-left:2px;
|
||||
background:rgba(255,255,255,0.85);
|
||||
animation:caret-blink 1s steps(1) infinite;
|
||||
}
|
||||
@keyframes caret-blink{50%{opacity:0}}
|
||||
|
||||
/* Thinking dots — shown while waiting for the first token */
|
||||
.thinking{
|
||||
display:inline-flex;gap:4px;align-items:center;padding:2px 0;
|
||||
}
|
||||
.thinking span{
|
||||
width:6px;height:6px;border-radius:50%;
|
||||
background:rgba(255,255,255,0.65);
|
||||
animation:thinking-bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
.thinking span:nth-child(2){animation-delay:0.15s}
|
||||
.thinking span:nth-child(3){animation-delay:0.30s}
|
||||
@keyframes thinking-bounce{
|
||||
0%,60%,100%{transform:translateY(0);opacity:0.4}
|
||||
30%{transform:translateY(-4px);opacity:1}
|
||||
}
|
||||
#input-bar{
|
||||
display:flex;align-items:center;gap:6px;
|
||||
border-radius:16px;padding:6px 8px;
|
||||
background:rgba(30,30,30,0.88);
|
||||
border:1px solid rgba(255,255,255,0.20);
|
||||
flex-shrink:0;
|
||||
}
|
||||
#model-wrap{
|
||||
position:relative;flex-shrink:0;
|
||||
}
|
||||
#model-select{
|
||||
background:rgba(255,255,255,0.08);color:rgba(255,255,255,0.6);
|
||||
border:none;border-radius:8px;padding:4px 22px 4px 8px;
|
||||
font-size:11px;outline:none;cursor:pointer;
|
||||
max-width:140px;
|
||||
-webkit-appearance:none;appearance:none;
|
||||
}
|
||||
#model-select:hover{background:rgba(255,255,255,0.14);color:#fff}
|
||||
#model-wrap .arrow{
|
||||
position:absolute;right:7px;top:50%;transform:translateY(-50%);
|
||||
pointer-events:none;color:rgba(255,255,255,0.35);
|
||||
}
|
||||
#model-select option,#model-select optgroup{
|
||||
background:#1e1e1e;color:#eee;
|
||||
}
|
||||
#input{
|
||||
flex:1;background:transparent;border:none;outline:none;
|
||||
color:#fff;font-size:14px;padding:6px 10px;
|
||||
}
|
||||
#input::placeholder{color:rgba(255,255,255,0.35)}
|
||||
.btn{
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
width:30px;height:30px;border-radius:50%;border:none;
|
||||
background:rgba(255,255,255,0.10);
|
||||
color:rgba(255,255,255,0.6);cursor:pointer;
|
||||
transition:background .15s,color .15s;flex-shrink:0;
|
||||
}
|
||||
.btn:hover{background:rgba(255,255,255,0.20);color:#fff}
|
||||
.btn:disabled{opacity:0.25;cursor:default}
|
||||
.btn:disabled:hover{background:rgba(255,255,255,0.10)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="messages"></div>
|
||||
<div id="input-bar">
|
||||
<button id="new-btn" class="btn" title="New conversation">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</button>
|
||||
<div id="model-wrap">
|
||||
<select id="model-select"><option>loading...</option></select>
|
||||
<svg class="arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</div>
|
||||
<input id="input" type="text" placeholder="Ask Jarvis anything..." autofocus>
|
||||
<button id="send-btn" class="btn" disabled title="Send">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/json" id="saved-data">__SAVED_MESSAGES__</script>
|
||||
<script type="application/json" id="cloud-data">__CLOUD_MODELS__</script>
|
||||
<script>
|
||||
const input=document.getElementById('input');
|
||||
const sendBtn=document.getElementById('send-btn');
|
||||
const messagesEl=document.getElementById('messages');
|
||||
let streaming=false,abort=null,model='qwen3.5:4b';
|
||||
let convId='', convTitle='Overlay chat', convCreated=Date.now();
|
||||
let messages=[];
|
||||
|
||||
function genId(){return Date.now().toString(36)+Math.random().toString(36).slice(2,8)}
|
||||
|
||||
// Restore previous conversation
|
||||
try{
|
||||
const raw=document.getElementById('saved-data').textContent.trim();
|
||||
if(raw&&raw!=='__SAVED_PLACEHOLDER__'){
|
||||
const saved=JSON.parse(raw);
|
||||
if(saved.id){convId=saved.id;convTitle=saved.title||convTitle;convCreated=saved.createdAt||convCreated;messages=saved.messages||[]}
|
||||
else if(Array.isArray(saved)){messages=saved}
|
||||
}
|
||||
}catch{}
|
||||
if(!convId) convId=genId();
|
||||
if(messages.length) renderAll();
|
||||
|
||||
// Build model dropdown: local (installed) + cloud (keyed)
|
||||
const modelSelect=document.getElementById('model-select');
|
||||
let cloudModels=[];
|
||||
try{
|
||||
const cd=document.getElementById('cloud-data').textContent.trim();
|
||||
if(cd&&cd!=='__CLOUD_PLACEHOLDER__') cloudModels=JSON.parse(cd);
|
||||
}catch{}
|
||||
|
||||
fetch('/v1/models').then(r=>r.json()).then(d=>{
|
||||
const local=(Array.isArray(d)?d:(d.data||d.models||[])).map(m=>m.id||m.name).filter(Boolean);
|
||||
while(modelSelect.firstChild) modelSelect.removeChild(modelSelect.firstChild);
|
||||
if(local.length){
|
||||
const g=document.createElement('optgroup');g.label='Local';
|
||||
local.forEach(id=>{const o=document.createElement('option');o.value=id;o.textContent=id;g.appendChild(o)});
|
||||
modelSelect.appendChild(g);
|
||||
}
|
||||
if(cloudModels.length){
|
||||
const g=document.createElement('optgroup');g.label='Cloud';
|
||||
cloudModels.forEach(id=>{const o=document.createElement('option');o.value=id;o.textContent=id;g.appendChild(o)});
|
||||
modelSelect.appendChild(g);
|
||||
}
|
||||
// Restore saved model or pick first available
|
||||
const saved=messages.length&&messages[0].model;
|
||||
if(saved&&modelSelect.querySelector('option[value="'+CSS.escape(saved)+'"]')){modelSelect.value=saved}
|
||||
model=modelSelect.value||model;
|
||||
}).catch(()=>{});
|
||||
modelSelect.addEventListener('change',()=>{model=modelSelect.value});
|
||||
|
||||
const SEND='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>';
|
||||
const STOP='<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
|
||||
|
||||
function renderAll(){
|
||||
while(messagesEl.firstChild) messagesEl.removeChild(messagesEl.firstChild);
|
||||
for(const m of messages) bubble(m.role,m.content);
|
||||
scroll();
|
||||
}
|
||||
function bubble(role,text){
|
||||
const d=document.createElement('div');
|
||||
d.className='msg '+(role==='user'?'user':'assistant');
|
||||
if(role==='assistant') setHtml(d, md(text||''));
|
||||
else d.textContent=text;
|
||||
messagesEl.appendChild(d);
|
||||
return d;
|
||||
}
|
||||
function scroll(){messagesEl.scrollTop=messagesEl.scrollHeight}
|
||||
|
||||
// Safe HTML injection helper. All incoming LLM/user text is escaped
|
||||
// via escHtml() before any markdown transformations, so the string
|
||||
// reaching this function only contains tags from our controlled
|
||||
// regex replacements. We use Range.createContextualFragment which is
|
||||
// the W3C-recommended way to construct a DocumentFragment from HTML.
|
||||
function setHtml(el,html){
|
||||
while(el.firstChild) el.removeChild(el.firstChild);
|
||||
const range=document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
el.appendChild(range.createContextualFragment(html));
|
||||
}
|
||||
|
||||
// --- Minimal markdown renderer (inline, no deps) ---
|
||||
// Handles: fenced code, inline code, headings, bold, italic,
|
||||
// strikethrough, links, ordered/unordered lists, paragraphs.
|
||||
function escHtml(s){
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
.replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
function md(src){
|
||||
if(!src)return '';
|
||||
const blocks=[],inlines=[];
|
||||
// 1. Extract fenced code blocks first so their contents aren't touched.
|
||||
src=src.replace(/```(\w*)\n?([\s\S]*?)```/g,(_,lang,code)=>{
|
||||
blocks.push({lang,code});
|
||||
return '\u0000CB'+(blocks.length-1)+'\u0000';
|
||||
});
|
||||
// 2. Extract inline code (single-line backticks).
|
||||
src=src.replace(/`([^`\n]+)`/g,(_,c)=>{
|
||||
inlines.push(c);
|
||||
return '\u0000IC'+(inlines.length-1)+'\u0000';
|
||||
});
|
||||
// 3. Escape everything else.
|
||||
src=escHtml(src);
|
||||
// 4. Headings.
|
||||
src=src.replace(/^###\s+(.+)$/gm,'<h3>$1</h3>')
|
||||
.replace(/^##\s+(.+)$/gm,'<h2>$1</h2>')
|
||||
.replace(/^#\s+(.+)$/gm,'<h1>$1</h1>');
|
||||
// 5. Bold / italic / strikethrough.
|
||||
src=src.replace(/\*\*([^*\n]+)\*\*/g,'<strong>$1</strong>')
|
||||
.replace(/__([^_\n]+)__/g,'<strong>$1</strong>')
|
||||
.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\*)/g,'$1<em>$2</em>')
|
||||
.replace(/(^|[^_\w])_([^_\n]+)_(?!_)/g,'$1<em>$2</em>')
|
||||
.replace(/~~([^~\n]+)~~/g,'<del>$1</del>');
|
||||
// 6. Links — url is escaped above, so quotes are safe.
|
||||
src=src.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
// 7. Unordered + ordered lists.
|
||||
src=src.replace(/(?:^[-*]\s+.+(?:\n|$))+/gm,block=>{
|
||||
const items=block.trim().split('\n')
|
||||
.map(l=>'<li>'+l.replace(/^[-*]\s+/,'')+'</li>').join('');
|
||||
return '<ul>'+items+'</ul>';
|
||||
});
|
||||
src=src.replace(/(?:^\d+\.\s+.+(?:\n|$))+/gm,block=>{
|
||||
const items=block.trim().split('\n')
|
||||
.map(l=>'<li>'+l.replace(/^\d+\.\s+/,'')+'</li>').join('');
|
||||
return '<ol>'+items+'</ol>';
|
||||
});
|
||||
// 8. Paragraphs: split on blank lines, wrap non-block chunks in <p>.
|
||||
src=src.split(/\n{2,}/).map(chunk=>{
|
||||
const t=chunk.trim();
|
||||
if(!t)return '';
|
||||
if(/^<(h\d|ul|ol|pre|blockquote)/.test(t))return t;
|
||||
if(t.startsWith('\u0000CB'))return t;
|
||||
return '<p>'+t.replace(/\n/g,'<br>')+'</p>';
|
||||
}).join('');
|
||||
// 9. Restore inline code.
|
||||
src=src.replace(/\u0000IC(\d+)\u0000/g,(_,i)=>'<code>'+escHtml(inlines[+i])+'</code>');
|
||||
// 10. Restore fenced code blocks.
|
||||
src=src.replace(/\u0000CB(\d+)\u0000/g,(_,i)=>{
|
||||
const b=blocks[+i];
|
||||
const cls=b.lang?' class="lang-'+escHtml(b.lang)+'"':'';
|
||||
return '<pre><code'+cls+'>'+escHtml(b.code)+'</code></pre>';
|
||||
});
|
||||
return src;
|
||||
}
|
||||
const THINKING='<span class="thinking"><span></span><span></span><span></span></span>';
|
||||
const CARET='<span class="caret"></span>';
|
||||
const CLOUD_PFX=['gpt-','o1-','o3-','o4-','claude-','gemini-','openrouter/','chatgpt-'];
|
||||
function save(){
|
||||
const conv={id:convId,title:convTitle,createdAt:convCreated,updatedAt:Date.now(),model,
|
||||
messages:messages.map((m,i)=>{
|
||||
const o={id:convId+'_'+i,role:m.role,content:m.content,timestamp:m.timestamp||Date.now()};
|
||||
if(m.usage)o.usage=m.usage;
|
||||
if(m.telemetry)o.telemetry=m.telemetry;
|
||||
return o;
|
||||
})};
|
||||
try{window.webkit.messageHandlers.overlay.postMessage('save:'+JSON.stringify(conv))}catch{}
|
||||
}
|
||||
|
||||
input.addEventListener('input',()=>{sendBtn.disabled=!input.value.trim()||streaming});
|
||||
input.addEventListener('keydown',e=>{
|
||||
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}
|
||||
});
|
||||
document.addEventListener('keydown',e=>{
|
||||
if(e.key==='Escape'){
|
||||
if(streaming){abort&&abort.abort()}
|
||||
else{try{window.webkit.messageHandlers.overlay.postMessage('hide')}catch{}}
|
||||
}
|
||||
});
|
||||
sendBtn.addEventListener('click',()=>{if(streaming){abort&&abort.abort()}else send()});
|
||||
document.getElementById('new-btn').addEventListener('click',()=>{
|
||||
convId=genId();convTitle='Overlay chat';convCreated=Date.now();
|
||||
messages=[];
|
||||
while(messagesEl.firstChild) messagesEl.removeChild(messagesEl.firstChild);
|
||||
save();input.focus();
|
||||
});
|
||||
|
||||
async function send(){
|
||||
const text=input.value.trim();
|
||||
if(!text||streaming)return;
|
||||
input.value='';sendBtn.disabled=true;
|
||||
|
||||
messages.push({role:'user',content:text,timestamp:Date.now()});
|
||||
if(messages.length===1) convTitle=text.slice(0,50)+(text.length>50?'...':'');
|
||||
bubble('user',text);scroll();save();
|
||||
|
||||
streaming=true;abort=new AbortController();
|
||||
setHtml(sendBtn,STOP);sendBtn.disabled=false;
|
||||
const b=bubble('assistant','');
|
||||
// Show thinking dots until the first token arrives.
|
||||
setHtml(b,THINKING);
|
||||
scroll();
|
||||
|
||||
let acc='',usage=null,complexity=null,ttft=0;
|
||||
const t0=Date.now();
|
||||
// Throttle markdown re-renders to ~30fps so tight streams don't
|
||||
// rebuild the DOM on every single token.
|
||||
let pending=false;
|
||||
const render=()=>{
|
||||
if(pending)return;
|
||||
pending=true;
|
||||
requestAnimationFrame(()=>{
|
||||
pending=false;
|
||||
setHtml(b, md(acc)+CARET);
|
||||
scroll();
|
||||
});
|
||||
};
|
||||
try{
|
||||
const r=await fetch('/v1/chat/completions',{
|
||||
method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({model,messages,stream:true}),
|
||||
signal:abort.signal
|
||||
});
|
||||
if(!r.ok)throw new Error(r.status);
|
||||
const reader=r.body.getReader(),dec=new TextDecoder();
|
||||
let buf='';
|
||||
for(;;){
|
||||
const{done,value}=await reader.read();
|
||||
if(done)break;
|
||||
buf+=dec.decode(value,{stream:true});
|
||||
const lines=buf.split('\n');buf=lines.pop()||'';
|
||||
for(const ln of lines){
|
||||
if(!ln.startsWith('data: '))continue;
|
||||
const d=ln.slice(6);if(d==='[DONE]')break;
|
||||
try{
|
||||
const p=JSON.parse(d);
|
||||
if(p.usage)usage=p.usage;
|
||||
if(p.complexity)complexity=p.complexity;
|
||||
const c=p.choices?.[0]?.delta?.content;
|
||||
if(c){if(!ttft)ttft=Date.now()-t0;acc+=c;render()}
|
||||
}catch{}
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
if(e.name!=='AbortError'){
|
||||
acc='Could not get a response. Is the backend running?';
|
||||
b.textContent=acc;
|
||||
}
|
||||
}finally{
|
||||
streaming=false;abort=null;
|
||||
setHtml(sendBtn,SEND);sendBtn.disabled=!input.value.trim();
|
||||
input.focus();
|
||||
// Final render without the caret.
|
||||
if(acc) setHtml(b, md(acc));
|
||||
else if(b.querySelector('.thinking')) b.textContent='';
|
||||
}
|
||||
if(acc){
|
||||
const totalMs=Date.now()-t0;
|
||||
const engine=CLOUD_PFX.some(p=>model.startsWith(p))?'cloud':'ollama';
|
||||
const telem={engine,model_id:model,total_ms:totalMs,ttft_ms:ttft||undefined,
|
||||
tokens_per_sec:usage?.completion_tokens?usage.completion_tokens/(totalMs/1000):undefined,
|
||||
complexity_score:complexity?.score,complexity_tier:complexity?.tier,
|
||||
suggested_max_tokens:complexity?.suggested_max_tokens};
|
||||
messages.push({role:'assistant',content:acc,timestamp:Date.now(),usage:usage||undefined,telemetry:telem});
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('focus',()=>input.focus());
|
||||
|
||||
// --- Drag-to-move: click anywhere to drag, with threshold so
|
||||
// clicks on inputs/buttons still work normally ---
|
||||
(function(){
|
||||
let down=false,dragging=false,sx=0,sy=0;
|
||||
const THRESH=3;
|
||||
const INTERACTIVE='input,select,button,textarea,a,label,[contenteditable="true"],[role="button"]';
|
||||
document.addEventListener('mousedown',e=>{
|
||||
if(e.target.closest('select'))return;
|
||||
down=true;dragging=false;sx=e.screenX;sy=e.screenY;
|
||||
});
|
||||
// macOS native select menus swallow the mouseup — reset on change too
|
||||
document.getElementById('model-select').addEventListener('mousedown',()=>{down=false;dragging=false});
|
||||
document.getElementById('model-select').addEventListener('change',()=>{down=false;dragging=false});
|
||||
document.addEventListener('mousemove',e=>{
|
||||
if(!down)return;
|
||||
// If the mouse button is no longer pressed (e.g. the user released
|
||||
// it over a native menu that swallowed mouseup), abort the drag.
|
||||
if(e.buttons===0){down=false;dragging=false;return;}
|
||||
const dx=e.screenX-sx,dy=e.screenY-sy;
|
||||
if(!dragging){
|
||||
if(Math.abs(dx)+Math.abs(dy)<THRESH)return;
|
||||
dragging=true;
|
||||
}
|
||||
sx=e.screenX;sy=e.screenY;
|
||||
try{window.webkit.messageHandlers.overlay.postMessage('drag:'+dx+','+dy)}catch{}
|
||||
});
|
||||
const reset=()=>{down=false;dragging=false};
|
||||
document.addEventListener('mouseup',reset);
|
||||
window.addEventListener('blur',reset);
|
||||
document.addEventListener('mouseleave',reset);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"$schema": "https://raw.githubusercontent.com/nicknisi/tauri-v2-json-schema/main/tauri-v2-schema.json",
|
||||
"productName": "OpenJarvis",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.0",
|
||||
"identifier": "com.openjarvis.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build:tauri"
|
||||
"beforeBuildCommand": "npm run build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
@@ -23,21 +23,22 @@
|
||||
"transparent": false
|
||||
}
|
||||
],
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconAsTemplate": true,
|
||||
"tooltip": "OpenJarvis"
|
||||
},
|
||||
"security": {
|
||||
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http: https: ws: wss:; img-src 'self' data: blob:"
|
||||
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* ws://localhost:*; img-src 'self' data: blob:"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"createUpdaterArtifacts": true,
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico",
|
||||
"icons/icon.png"
|
||||
"icons/128x128@2x.png"
|
||||
],
|
||||
"category": "Utility",
|
||||
"shortDescription": "On-device AI assistant with energy monitoring and trace debugging",
|
||||
@@ -45,9 +46,10 @@
|
||||
"macOS": {
|
||||
"entitlements": "Entitlements.plist",
|
||||
"minimumSystemVersion": "10.15",
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": "-"
|
||||
"signingIdentity": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
@@ -57,16 +59,13 @@
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFFNzUzMzhEOEY2MjNEMDMKUldRRFBXS1BqVE4xSG8vK0lkUWN4WnZQYVIrbmc4RmpoOGlJWTBLTE15RlIya3JvQisvdUR3a0QK",
|
||||
"pubkey": "REPLACE_WITH_OUTPUT_OF_CARGO_TAURI_SIGNER_GENERATE",
|
||||
"endpoints": [
|
||||
"https://github.com/open-jarvis/OpenJarvis/releases/download/desktop-latest/latest.json"
|
||||
"https://github.com/jonsf/OpenJarvis/releases/download/desktop-latest/latest.json"
|
||||
]
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["openjarvis"]
|
||||
}
|
||||
"notification": {
|
||||
"permissionState": "prompt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, { useState } from 'react';
|
||||
import { UpdateChecker } from './components/UpdateChecker';
|
||||
import { EnergyDashboard } from './components/EnergyDashboard';
|
||||
import { TraceDebugger } from './components/TraceDebugger';
|
||||
import { LearningCurve } from './components/LearningCurve';
|
||||
import { MemoryBrowser } from './components/MemoryBrowser';
|
||||
import { AdminPanel } from './components/AdminPanel';
|
||||
|
||||
type TabId = 'energy' | 'traces' | 'learning' | 'memory' | 'admin';
|
||||
|
||||
interface Tab {
|
||||
id: TabId;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const TABS: Tab[] = [
|
||||
{ id: 'energy', label: 'Energy' },
|
||||
{ id: 'traces', label: 'Traces' },
|
||||
{ id: 'learning', label: 'Learning' },
|
||||
{ id: 'memory', label: 'Memory' },
|
||||
{ id: 'admin', label: 'Admin' },
|
||||
];
|
||||
|
||||
const API_URL = 'http://localhost:8000';
|
||||
|
||||
export function App() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('energy');
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<header style={styles.header}>
|
||||
<h1 style={styles.title}>OpenJarvis Desktop</h1>
|
||||
<nav style={styles.nav}>
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
style={{
|
||||
...styles.tabButton,
|
||||
...(activeTab === tab.id ? styles.activeTab : {}),
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<UpdateChecker />
|
||||
|
||||
<main style={styles.main}>
|
||||
{activeTab === 'energy' && <EnergyDashboard apiUrl={API_URL} />}
|
||||
{activeTab === 'traces' && <TraceDebugger apiUrl={API_URL} />}
|
||||
{activeTab === 'learning' && <LearningCurve apiUrl={API_URL} />}
|
||||
{activeTab === 'memory' && <MemoryBrowser apiUrl={API_URL} />}
|
||||
{activeTab === 'admin' && <AdminPanel apiUrl={API_URL} />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
container: {
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#1e1e2e',
|
||||
color: '#cdd6f4',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 24px',
|
||||
borderBottom: '1px solid #313244',
|
||||
backgroundColor: '#181825',
|
||||
},
|
||||
title: {
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
margin: 0,
|
||||
color: '#89b4fa',
|
||||
},
|
||||
nav: {
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
},
|
||||
tabButton: {
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#a6adc8',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.15s ease',
|
||||
},
|
||||
activeTab: {
|
||||
backgroundColor: '#313244',
|
||||
color: '#cdd6f4',
|
||||
},
|
||||
main: {
|
||||
padding: '24px',
|
||||
height: 'calc(100vh - 60px)',
|
||||
overflow: 'auto',
|
||||
},
|
||||
};
|
||||
@@ -354,7 +354,7 @@ export function AdminPanel({ apiUrl }: { apiUrl: string }) {
|
||||
<div style={styles.cardTitle}>System Info</div>
|
||||
<div style={styles.row}>
|
||||
<span style={styles.label}>Version</span>
|
||||
<span style={styles.value}>{serverInfo?.version || '0.1.0'}</span>
|
||||
<span style={styles.value}>{serverInfo?.version || '1.0.0'}</span>
|
||||
</div>
|
||||
<div style={styles.row}>
|
||||
<span style={styles.label}>Uptime</span>
|
||||