Compare commits
133
Commits
v1.16.0
...
b7006d1ad8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7006d1ad8 | ||
|
|
0ee0cc4350 | ||
|
|
e91fda3691 | ||
|
|
e4e0379c45 | ||
|
|
c165dff114 | ||
|
|
319198d2f0 | ||
|
|
f26e6ac9c6 | ||
|
|
f75c3860e9 | ||
|
|
170b1d3d19 | ||
|
|
5d65c78fb4 | ||
|
|
8098133ba5 | ||
|
|
2e81412605 | ||
|
|
14d3556179 | ||
|
|
d134678530 | ||
|
|
02a55235cf | ||
|
|
1a6268bde5 | ||
|
|
7030e26b3a | ||
|
|
d4d423a4ca | ||
|
|
3930d2e2c9 | ||
|
|
49e79322b4 | ||
|
|
1b130e69b3 | ||
|
|
145184d091 | ||
|
|
c582dc1461 | ||
|
|
4b4730cff7 | ||
|
|
fd58b4661f | ||
|
|
7e8a160b0e | ||
|
|
edc007619b | ||
|
|
a14a2275ff | ||
|
|
82b51f7921 | ||
|
|
a2c7e5d44c | ||
|
|
03538a455a | ||
|
|
07e84e43b3 | ||
|
|
9d1544d799 | ||
|
|
a27e8151b3 | ||
|
|
88c2a7d000 | ||
|
|
3b25e3898f | ||
|
|
d23d3855ec | ||
|
|
104af3abb9 | ||
|
|
62fa85867c | ||
|
|
f75ff740ac | ||
|
|
e349a35716 | ||
|
|
f1f58b9996 | ||
|
|
f4c02fd64e | ||
|
|
e982cbeb04 | ||
|
|
20ce81a6be | ||
|
|
21e3ccc891 | ||
|
|
992e60980c | ||
|
|
74b1ad4302 | ||
|
|
9ad04cf819 | ||
|
|
b46935c606 | ||
|
|
b28a5fe384 | ||
|
|
b66898ea28 | ||
|
|
9aca45cb65 | ||
|
|
4dccf0cee4 | ||
|
|
1f91b44708 | ||
|
|
b25929824a | ||
|
|
3fd9a2b2db | ||
|
|
6a98d52d54 | ||
|
|
3847c0e6fd | ||
|
|
5023e5dfa1 | ||
|
|
eac0443c4c | ||
|
|
14f3eae62a | ||
|
|
aec53651ed | ||
|
|
93f3c2a357 | ||
|
|
9ea3d361b1 | ||
|
|
271721694b | ||
|
|
171597ca05 | ||
|
|
7b0b53671f | ||
|
|
884b19ccb5 | ||
|
|
6db00b2266 | ||
|
|
7f64423615 | ||
|
|
6d3f4918bf | ||
|
|
448eb5d462 | ||
|
|
924576d2ee | ||
|
|
033f964be0 | ||
|
|
2de5b57133 | ||
|
|
d1929f2fd4 | ||
|
|
14aa1785f8 | ||
|
|
0e0fd5df05 | ||
|
|
07f547eea8 | ||
|
|
f918d44c8d | ||
|
|
0d06875bb7 | ||
|
|
ffefa8c07f | ||
|
|
883908ab55 | ||
|
|
eed4025918 | ||
|
|
6b90c7eb0d | ||
|
|
958645ed7f | ||
|
|
eb8cc17993 | ||
|
|
b3a4d131d1 | ||
|
|
f368f3a1c0 | ||
|
|
8ecc588a3e | ||
|
|
55677dd671 | ||
|
|
d615090dcd | ||
|
|
8c58faf292 | ||
|
|
802346f909 | ||
|
|
552bc5bc63 | ||
|
|
d59713d3e3 | ||
|
|
6ab532018a | ||
|
|
047d7183d7 | ||
|
|
4d85e5144b | ||
|
|
dcc8c3ee42 | ||
|
|
289c58ef36 | ||
|
|
094eb4b8ba | ||
|
|
dd7825b481 | ||
|
|
4c186b62d4 | ||
|
|
fef3c925cd | ||
|
|
785ae19757 | ||
|
|
873f870e23 | ||
|
|
54927f692f | ||
|
|
efcd191f96 | ||
|
|
2e8a4f2443 | ||
|
|
2f5f62ab09 | ||
|
|
919435eb84 | ||
|
|
bed9f8cff6 | ||
|
|
6c7995f5f5 | ||
|
|
7ea226509c | ||
|
|
6bef4a3f82 | ||
|
|
16494d4b1e | ||
|
|
f84dfb8476 | ||
|
|
07b217cf4a | ||
|
|
9bfb95d795 | ||
|
|
285dbb14cc | ||
|
|
cac6626586 | ||
|
|
5a373d1031 | ||
|
|
fe2e916937 | ||
|
|
66ac4dbf02 | ||
|
|
44c3dd1239 | ||
|
|
87abe8c335 | ||
|
|
148ba4e07b | ||
|
|
89ee213454 | ||
|
|
a04651905f | ||
|
|
600ea041ef | ||
|
|
1ec2aa136a |
No files matched your search
@@ -1,48 +0,0 @@
|
|||||||
# Strict Rust compiler configuration for zesdex
|
|
||||||
# Forces all warnings as errors, enables maximum optimization for release builds.
|
|
||||||
|
|
||||||
[target.'cfg(all())']
|
|
||||||
# Treat all warnings as errors — zero tolerance policy
|
|
||||||
rustflags = [
|
|
||||||
# Force all lints to error level (stricter than -Dwarnings)
|
|
||||||
"-W", "unused",
|
|
||||||
"-W", "dead_code",
|
|
||||||
"-W", "unreachable_code",
|
|
||||||
"-W", "unused_imports",
|
|
||||||
"-W", "unused_variables",
|
|
||||||
"-W", "unused_mut",
|
|
||||||
"-W", "unused_must_use",
|
|
||||||
"-W", "unused_unsafe",
|
|
||||||
"-W", "unused_extern_crates",
|
|
||||||
|
|
||||||
# Safety-critical
|
|
||||||
"-W", "trivial_casts",
|
|
||||||
"-W", "trivial_numeric_casts",
|
|
||||||
|
|
||||||
# Deprecation and future compat
|
|
||||||
"-W", "deprecated",
|
|
||||||
"-W", "deprecated_in_future",
|
|
||||||
"-W", "keyword_idents",
|
|
||||||
"-W", "noop_method_call",
|
|
||||||
|
|
||||||
# Type system
|
|
||||||
"-W", "invalid_type_param_default",
|
|
||||||
"-W", "unused_labels",
|
|
||||||
"-W", "while_true",
|
|
||||||
]
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
# Maximum speed optimizations for release builds
|
|
||||||
opt-level = 3
|
|
||||||
debug = false
|
|
||||||
debug-assertions = false
|
|
||||||
overflow-checks = true
|
|
||||||
lto = "fat"
|
|
||||||
codegen-units = 1
|
|
||||||
panic = "abort"
|
|
||||||
strip = "symbols"
|
|
||||||
|
|
||||||
[profile.dev]
|
|
||||||
# Keep dev fast but still strict
|
|
||||||
opt-level = 0
|
|
||||||
debug = true
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
name: commit-convention
|
|
||||||
description: Conventional Commits format and version-bump rules for this repo (Bahasa Indonesia commit style). Use when creating a git commit in zesdex.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Commit Convention
|
|
||||||
|
|
||||||
Gunakan **Conventional Commits** untuk semua commit. Format:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>(<scope>): <description>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Type & efek ke versi:**
|
|
||||||
|
|
||||||
| Type | Bump | Kapan pakai |
|
|
||||||
|-------------|-------|------------------------------------------|
|
|
||||||
| `feat` | minor | Fitur baru |
|
|
||||||
| `fix` | patch | Perbaikan bug |
|
|
||||||
| `chore` | patch | Maintenance, update deps, dll |
|
|
||||||
| `docs` | patch | Perubahan dokumentasi/comment |
|
|
||||||
| `refactor` | patch | Refactor kode tanpa perubahan fungsional |
|
|
||||||
| `test` | patch | Nambah/ubah test |
|
|
||||||
| `style` | patch | Formatting, whitespace, lint |
|
|
||||||
| `perf` | patch | Optimasi performa |
|
|
||||||
| `ci` | patch | Perubahan CI/CD |
|
|
||||||
|
|
||||||
**Catatan:**
|
|
||||||
- **Semua type menghasilkan release** (patch minimal). Tidak ada commit yang "skip release".
|
|
||||||
- Tambahkan `BREAKING CHANGE:` di body commit untuk bump **major**.
|
|
||||||
- **Scope** opsional, tapi direkomendasikan (misal `feat(agent):`, `fix(ipc):`).
|
|
||||||
|
|
||||||
### Contoh
|
|
||||||
|
|
||||||
```
|
|
||||||
feat(tool): add batch file delete
|
|
||||||
|
|
||||||
chore: bump reqwest to 0.12
|
|
||||||
|
|
||||||
refactor(harness): flatten guard pipeline
|
|
||||||
|
|
||||||
fix(ipc): reconnect loop on socket timeout
|
|
||||||
|
|
||||||
docs: add architecture diagram to README
|
|
||||||
|
|
||||||
BREAKING CHANGE: IPC frame header changed from 4-byte to 8-byte length
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Rust build artifacts
|
||||||
|
target/
|
||||||
|
|
||||||
|
# VCS
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Local/secret files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
|
||||||
|
# Kilo metadata
|
||||||
|
.kilo/
|
||||||
|
|
||||||
|
# Docs lessons
|
||||||
|
docs/lesson/
|
||||||
+15
-11
@@ -7,7 +7,7 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
BUN_INSTALL: "${{ github.workspace }}/.bun"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
@@ -16,16 +16,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Bun
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
|
||||||
components: clippy
|
|
||||||
|
|
||||||
- name: Build workspace
|
- name: Install dependencies
|
||||||
run: cargo build --release --workspace
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Test workspace
|
- name: Type-check
|
||||||
run: cargo test --workspace
|
run: bun run check
|
||||||
|
|
||||||
- name: Clippy workspace
|
- name: Test
|
||||||
run: cargo clippy --workspace -- -D warnings
|
run: bun test
|
||||||
|
|
||||||
|
- name: Build CLI bundle
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Smoke — CLI help
|
||||||
|
run: ./dist/zesdex --help
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: Build & Deploy (Nix)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||||
|
VPS_USER: ${{ secrets.VPS_USER }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@v22
|
||||||
|
with:
|
||||||
|
determinate: false
|
||||||
|
extra-conf: |
|
||||||
|
sandbox = false
|
||||||
|
accept-flake-config = true
|
||||||
|
|
||||||
|
- name: Cache Nix
|
||||||
|
uses: DeterminateSystems/magic-nix-cache-action@v14
|
||||||
|
|
||||||
|
- name: Build zesdex
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
nix build .#default --impure --option sandbox false --print-build-logs
|
||||||
|
STORE_PATH=$(readlink result)
|
||||||
|
echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Build OK: $STORE_PATH"
|
||||||
|
|
||||||
|
- name: Setup SSH key
|
||||||
|
env:
|
||||||
|
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$SSH_KEY" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||||
|
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null 2>&1 || { echo "SSH key invalid"; exit 1; }
|
||||||
|
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Deploy zesdex to VPS
|
||||||
|
run: |
|
||||||
|
STORE_PATH="${{ steps.build.outputs.store-path }}"
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "nix copy --to file:///nix/store $STORE_PATH && nix-env --install --force $STORE_PATH --profile /nix/var/nix/profiles/zesdex && systemctl restart zesdex"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Publish to FlakeHub
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
flakehub-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: DeterminateSystems/determinate-nix-action@main
|
||||||
|
- uses: DeterminateSystems/flakehub-push@main
|
||||||
|
with:
|
||||||
|
visibility: public
|
||||||
|
rolling: true
|
||||||
@@ -16,14 +16,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Bun
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Build
|
- name: Install
|
||||||
run: cargo build --release --workspace
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Type-check
|
||||||
|
run: bun run check
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --workspace
|
run: bun test
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Semantic Release
|
name: Semantic Release
|
||||||
@@ -34,13 +37,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Bun
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install semantic-release
|
- name: Install semantic-release
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+8
-2
@@ -2,7 +2,13 @@ target/
|
|||||||
.env
|
.env
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
node_modules/
|
node_modules/
|
||||||
package.json
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.superpowers/
|
.superpowers/
|
||||||
docs/lesson/
|
docs/lesson/
|
||||||
|
.kilo/
|
||||||
|
.hermes/
|
||||||
|
# Runtime edit-log artifacts
|
||||||
|
dist/
|
||||||
|
apps/**/edit-log/
|
||||||
|
edit-log/
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"branches": ["main"],
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"prepareCmd": "sed -i 's/\"version\": \"[^\"]*\"/\"version\": \"${nextRelease.version}\"/' package.json && bun install --frozen-lockfile && bun run check && bun run build"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": ["package.json", "bun.lock", "CHANGELOG.md"],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"path": "dist/zesdex",
|
||||||
|
"label": "zesdex-${nextRelease.version}-linux-amd64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Codemap Update Report — 2026-07-12
|
|
||||||
====================================
|
|
||||||
|
|
||||||
Status: FIRST GENERATION (no previous codemaps to compare)
|
|
||||||
|
|
||||||
Files created:
|
|
||||||
- docs/CODEMAPS/architecture.md (new)
|
|
||||||
- docs/CODEMAPS/backend.md (new)
|
|
||||||
- docs/CODEMAPS/frontend.md (new)
|
|
||||||
- docs/CODEMAPS/data.md (new)
|
|
||||||
- docs/CODEMAPS/dependencies.md (new)
|
|
||||||
|
|
||||||
Source scanned:
|
|
||||||
- 124 Rust source files
|
|
||||||
- 30 directories
|
|
||||||
- 103 modules
|
|
||||||
- 10,402 lines total
|
|
||||||
|
|
||||||
No previous codemaps found — diff calculation skipped.
|
|
||||||
Freshness: all documents generated 2026-07-12.
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Kilo when working with code in the zesdex repository.
|
||||||
|
|
||||||
|
## Best Practice Conventions
|
||||||
|
|
||||||
|
Zesdex follows Kana Engineering Best Practices:
|
||||||
|
|
||||||
|
1. **Clean Architecture** — Strict domain/application/infrastructure/presentation layering.
|
||||||
|
- Domain has ZERO framework dependencies.
|
||||||
|
- Application depends only on domain.
|
||||||
|
- Infrastructure implements domain traits.
|
||||||
|
- DTOs cross layer boundaries, NOT entities.
|
||||||
|
|
||||||
|
2. **Clean Code** — Functions under ~40 lines, one level of abstraction per function, descriptive names, no flag arguments, no commented-out code.
|
||||||
|
|
||||||
|
3. **Documentation** — Every pub fn, struct, enum, and trait needs a doc comment (///) explaining what, flow, why, and return value.
|
||||||
|
|
||||||
|
4. **Commit Convention** — Conventional Commits in Bahasa Indonesia: `feat(scope):`, `fix(scope):`, `chore:`, `docs:`.
|
||||||
|
|
||||||
|
5. **Error Handling** — `anyhow::Result` and `anyhow::bail!` throughout. Log with `tracing` (never stderr).
|
||||||
|
|
||||||
|
6. **Testing** — `#[cfg(test)] mod tests` blocks inline in production files. Tests are F.I.R.S.T. (Fast, Independent, Repeatable, Self-validating, Timely).
|
||||||
|
|
||||||
|
7. **No Compiler Bypasses** — Never use `#[allow(...)]`, `#[expect(...)]`, or `#[allow(dead_code)]`. Fix the underlying code.
|
||||||
|
|
||||||
|
8. **Boy Scout Rule** — Leave every module cleaner than you found it.
|
||||||
|
|
||||||
|
## Available Agents
|
||||||
|
|
||||||
|
- `@rust-engineer` — Rust clean architecture specialist (subagent).
|
||||||
|
- `@code-reviewer` — Code review specialist (subagent).
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
- `/check` — Run cargo check, clippy, and tests.
|
||||||
|
- `/audit` — Code quality audit against clean-architecture best practices.
|
||||||
|
- `/doc` — Generate or update doc comments.
|
||||||
+182
@@ -1,3 +1,185 @@
|
|||||||
|
# [1.22.0](https://github.com/asepharyana/zesdex/compare/v1.21.2...v1.22.0) (2026-08-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **agent:** wire auto-review (review_enabled no-op -> nyata) ([a27e815](https://github.com/asepharyana/zesdex/commit/a27e8151b39da4c8759e8e922ef132212327e413))
|
||||||
|
|
||||||
|
## [1.21.2](https://github.com/asepharyana/zesdex/compare/v1.21.1...v1.21.2) (2026-08-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **agent:** symbol index tak pegang mutex global saat rebuild I/O ([3b25e38](https://github.com/asepharyana/zesdex/commit/3b25e3898f9ba2fc1c58b991ad95a8c0bbe98601))
|
||||||
|
|
||||||
|
## [1.21.1](https://github.com/asepharyana/zesdex/compare/v1.21.0...v1.21.1) (2026-08-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **agent:** semantic_search symbol index workspace-aware ([104af3a](https://github.com/asepharyana/zesdex/commit/104af3abb9e626c5d00ea87d523248d606d523ee))
|
||||||
|
|
||||||
|
# [1.21.0](https://github.com/asepharyana/zesdex/compare/v1.20.2...v1.21.0) (2026-08-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **agent:** hive-mind consensus synthesis pakai LLM nyata ([f75ff74](https://github.com/asepharyana/zesdex/commit/f75ff740ac2fa63340656e1e8215440f5e48063d))
|
||||||
|
|
||||||
|
## [1.20.2](https://github.com/asepharyana/zesdex/compare/v1.20.1...v1.20.2) (2026-08-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **agent:** recall search + memory_dir fallback + bersihkan dead llm_client ([f1f58b9](https://github.com/asepharyana/zesdex/commit/f1f58b9996f8eb58871d44fdd41c2d863874f253))
|
||||||
|
|
||||||
|
## [1.20.1](https://github.com/asepharyana/zesdex/compare/v1.20.0...v1.20.1) (2026-08-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **agent:** subagent patuhi tool-calling contract + truncation char-safe ([e982cbe](https://github.com/asepharyana/zesdex/commit/e982cbeb041baea9cd500e2a29862a7eca9e6e17))
|
||||||
|
|
||||||
|
# [1.20.0](https://github.com/asepharyana/zesdex/compare/v1.19.6...v1.20.0) (2026-08-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **agent:** subagent tool paralel + auto-load AGENTS.md + verify cek setelah edit ([21e3ccc](https://github.com/asepharyana/zesdex/commit/21e3ccc891ab886044a5f0a770db710769d1287b))
|
||||||
|
|
||||||
|
## [1.19.6](https://github.com/asepharyana/zesdex/compare/v1.19.5...v1.19.6) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **agent:** eksekusi tool read-only paralel seperti Claude Code ([74b1ad4](https://github.com/asepharyana/zesdex/commit/74b1ad43020a11ca27e60e3a24de0db5d1ab37b4))
|
||||||
|
|
||||||
|
## [1.19.5](https://github.com/asepharyana/zesdex/compare/v1.19.4...v1.19.5) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** model Opus default pakai claude-opus-5 (bukan -4-8) ([b28a5fe](https://github.com/asepharyana/zesdex/commit/b28a5fe384fd45255a6b249c7febd3b4ffc0fd2f))
|
||||||
|
* **api:** update zesdex packages to version 1.19.4 ([b46935c](https://github.com/asepharyana/zesdex/commit/b46935c606d4f68ea227c7db27e4c2aa9b4e373c))
|
||||||
|
|
||||||
|
## [1.19.4](https://github.com/asepharyana/zesdex/compare/v1.19.3...v1.19.4) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** model claude selalu pakai Opus dari settings.json, bukan deepseek ([9aca45c](https://github.com/asepharyana/zesdex/commit/9aca45cb65d6913d14fecc10d70c180934f69d74))
|
||||||
|
|
||||||
|
## [1.19.3](https://github.com/asepharyana/zesdex/compare/v1.19.2...v1.19.3) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** model Opus pakai URL + API custom dari ~/.claude/settings.json ([1f91b44](https://github.com/asepharyana/zesdex/commit/1f91b447080e7106201d77585edb7d38b74e34bb))
|
||||||
|
|
||||||
|
## [1.19.2](https://github.com/asepharyana/zesdex/compare/v1.19.1...v1.19.2) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **agent:** stabilkan async & parallel — satu runtime, bounded concurrency, isolasi error ([6a98d52](https://github.com/asepharyana/zesdex/commit/6a98d52d54a69f78710d852a69dda3ac0a4ead31))
|
||||||
|
|
||||||
|
## [1.19.1](https://github.com/asepharyana/zesdex/compare/v1.19.0...v1.19.1) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **agent:** rombak alur AI agent — adaptif, hemat token, self-healing ([eac0443](https://github.com/asepharyana/zesdex/commit/eac0443c4c3b8bfcbefd4bad9554168fb6525b94))
|
||||||
|
|
||||||
|
# [1.19.0](https://github.com/asepharyana/zesdex/compare/v1.18.4...v1.19.0) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* hapus fitur LSP bawaan (language server protocol) ([93f3c2a](https://github.com/asepharyana/zesdex/commit/93f3c2a3572511b5a84f244980ad71bd1b455e70))
|
||||||
|
|
||||||
|
## [1.18.4](https://github.com/asepharyana/zesdex/compare/v1.18.3...v1.18.4) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **tui:** render streaming token secara inkremental + kurangi redraw sia-sia ([2717216](https://github.com/asepharyana/zesdex/commit/271721694beb62e9fa5ff932312b293aa1d56823))
|
||||||
|
|
||||||
|
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** cegah race condition pada register users.json (TOCTOU) ([884b19c](https://github.com/asepharyana/zesdex/commit/884b19ccb5fbcaa6b29cb41dc978386cf7b1b3f9))
|
||||||
|
* **api:** perbaiki keamanan auth & WebSocket, tambah rate limiting ([6db00b2](https://github.com/asepharyana/zesdex/commit/6db00b22663839a6a975ae978058b894900c71de))
|
||||||
|
* **build:** perbaiki referensi paket zesdex-gateway dan sinkronisasi versi nix ([6d3f491](https://github.com/asepharyana/zesdex/commit/6d3f4918bfea06b106d4fe75adf58e6a29aca2a5))
|
||||||
|
* **build:** perbaiki referensi paket zesdex-gateway di Dockerfile & default.nix ([7f64423](https://github.com/asepharyana/zesdex/commit/7f644236158f66fcc106ec55e437ebf28ee1cca1))
|
||||||
|
|
||||||
|
## [1.18.2](https://github.com/asepharyana/zesdex/compare/v1.18.1...v1.18.2) (2026-08-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **nix:** add perl to nativeBuildInputs for openssl-sys Configure ([033f964](https://github.com/asepharyana/zesdex/commit/033f964be0c695106eef74329524ad7b4b8dba14))
|
||||||
|
* **nix:** correct cargoBuildFlags package name zesdex-backend -> zesdex-gateway ([2de5b57](https://github.com/asepharyana/zesdex/commit/2de5b57133f6c7295d8a85e63cb86504ea2a7d3e))
|
||||||
|
|
||||||
|
## [1.18.1](https://github.com/asepharyana/zesdex/compare/v1.18.0...v1.18.1) (2026-08-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **nix:** restrict flake to x86_64-linux (nixpkgs 26.11 dropped darwin) ([0e0fd5d](https://github.com/asepharyana/zesdex/commit/0e0fd5df05e53a6fa17d481825f1a8c567214d0e))
|
||||||
|
|
||||||
|
# [1.18.0](https://github.com/asepharyana/zesdex/compare/v1.17.0...v1.18.0) (2026-07-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add .releaserc.json for semantic-release configuration ([047d718](https://github.com/asepharyana/zesdex/commit/047d7183d72adaa0dd1a18a7f92b22d67d5aa85f))
|
||||||
|
* **clippy:** replace type annotation with type alias + .insert() to avoid trivial_cast ([ffefa8c](https://github.com/asepharyana/zesdex/commit/ffefa8c07facbba83778f426ca4715569556b08a))
|
||||||
|
* **rust:** remove unused imports, variables, and dead code causing CI build failures ([eb8cc17](https://github.com/asepharyana/zesdex/commit/eb8cc1799359a5ef5b4a4ee3a4202b87e80bfe6c))
|
||||||
|
* **rust:** resolve all clippy warnings treated as errors in CI ([6b90c7e](https://github.com/asepharyana/zesdex/commit/6b90c7eb0d17d9281a377454e76dcc88a4232bc7))
|
||||||
|
* **tui:** resolve remaining clippy errors in workspace ([eed4025](https://github.com/asepharyana/zesdex/commit/eed4025918de65eb2f3fd98cd3350d62f3322132))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add semantic search tool for code symbol indexing and searching ([fef3c92](https://github.com/asepharyana/zesdex/commit/fef3c925cd02e1d76e2d990cc6fa4e4cba64fab0))
|
||||||
|
* Add SOLID principles and TDD reference documentation ([55677dd](https://github.com/asepharyana/zesdex/commit/55677dd67100813a5ed2b0bbf02257b57875ea07))
|
||||||
|
* add test_load and test_parse binaries for configuration loading and parsing ([fe2e916](https://github.com/asepharyana/zesdex/commit/fe2e9169371bf7a3cc9391d8467d47fc2258ad1f))
|
||||||
|
* **agent:** add AI summarization for conversation history compacting ([873f870](https://github.com/asepharyana/zesdex/commit/873f870e233dd0d2c6f0cce935f83fb8ce373505))
|
||||||
|
* **agent:** implement agent execution engine and turn handling with background processing ([efcd191](https://github.com/asepharyana/zesdex/commit/efcd191f9698023944f9096a86c1efa47e412b6b))
|
||||||
|
* **best_practice:** add code quality scanning and commit message validation ([8ecc588](https://github.com/asepharyana/zesdex/commit/8ecc588a3e125dc6a4b2e7eb4c12c6b344449c07))
|
||||||
|
* centralize default constants and refactor overlay enter handling in TUI ([d615090](https://github.com/asepharyana/zesdex/commit/d615090dcd34bdb93be22c1c7ce4ba674363c258))
|
||||||
|
* enhance context gathering in auto-review engine and agent runner ([dcc8c3e](https://github.com/asepharyana/zesdex/commit/dcc8c3ee42ef568843e04143048d799bc0882288))
|
||||||
|
* enhance explore phase with TUI workflow event handling ([b3a4d13](https://github.com/asepharyana/zesdex/commit/b3a4d131d13d94bd307720cb50c2d02dedf92873))
|
||||||
|
* implement Component trait for modular UI components and refactor TUI views to use it ([d59713d](https://github.com/asepharyana/zesdex/commit/d59713d3e3aa4325f9753bbddd068654e442f6ef))
|
||||||
|
* implement mandatory explore phase with parallel subagents ([f368f3a](https://github.com/asepharyana/zesdex/commit/f368f3a1c0d2df02eb50542a81bcff96d9cdc420))
|
||||||
|
* **llm:** improve tool call handling by dynamically resizing tool_calls and updating arguments ([9bfb95d](https://github.com/asepharyana/zesdex/commit/9bfb95d795d6c7cc10cfcb907642bba7b8bb7f19))
|
||||||
|
* **llm:** improve UTF-8 handling in response processing to prevent infinite loops ([7ea2265](https://github.com/asepharyana/zesdex/commit/7ea226509c8e11ee1bab21d216e0413715988fb9))
|
||||||
|
* **llm:** increase max retries for streaming requests from 3 to 10 ([6c7995f](https://github.com/asepharyana/zesdex/commit/6c7995f5f5481c48ccd449e72d9563425fdcb837))
|
||||||
|
* **mcp:** enhance MCP server registration with error handling and improve transport process management ([148ba4e](https://github.com/asepharyana/zesdex/commit/148ba4e07b424736c6b4824c5786c0c7c9cbf0a1))
|
||||||
|
* refactor auto-review engine to use spawn_subagent for improved thread handling ([094eb4b](https://github.com/asepharyana/zesdex/commit/094eb4b8baa5abd878dcf5b611cf615955bc79eb))
|
||||||
|
* remove obsolete design documents for clipboard OSC52, diff view, file mention, context compaction, and add development guide ([66ac4db](https://github.com/asepharyana/zesdex/commit/66ac4dbf027820697f4303264666b5f63a0b31d4))
|
||||||
|
* **tui:** add rich context information including active jobs, README snippet, and recent git history ([2f5f62a](https://github.com/asepharyana/zesdex/commit/2f5f62ab097614d8b0c7a3e5c7bd364cd479e0aa))
|
||||||
|
* **tui:** add support for reasoning in chat messages and update transcript handling ([285dbb1](https://github.com/asepharyana/zesdex/commit/285dbb14cccb1c261134c1fcb6c30ef1ed02bb55))
|
||||||
|
* **tui:** enhance agent turn handling by grouping parameters and improving message management ([07b217c](https://github.com/asepharyana/zesdex/commit/07b217cf4a81b99c63101512932935246fcbc7ae))
|
||||||
|
* **tui:** enhance system prompt with workspace structure information ([bed9f8c](https://github.com/asepharyana/zesdex/commit/bed9f8cff60711c4db998c8f10f32f8e210ae410))
|
||||||
|
* **tui:** implement streaming support for LLM responses and update transcript handling ([cac6626](https://github.com/asepharyana/zesdex/commit/cac662658651ae092cffddfc88f39a775afef942))
|
||||||
|
* **tui:** integrate rich context builder into agent turn process ([919435e](https://github.com/asepharyana/zesdex/commit/919435eb84f9a41a0637052a5dc1234b410714f8))
|
||||||
|
* **tui:** introduce comprehensive state management for TUI interface ([8c58faf](https://github.com/asepharyana/zesdex/commit/8c58faf2920b7341dd50ace15044ff113cefb576))
|
||||||
|
* **tui:** optimize performance by caching display lines and token counts, and improve action handling ([87abe8c](https://github.com/asepharyana/zesdex/commit/87abe8c3358ac17196df93d19169901a856850d3))
|
||||||
|
* **tui:** update system message for clarity and conciseness in tool usage instructions ([5a373d1](https://github.com/asepharyana/zesdex/commit/5a373d1031b476970bd45804a65c7455be99e78d))
|
||||||
|
|
||||||
|
|
||||||
|
# [1.17.0](https://github.com/asepharyana/zesdex/compare/v1.16.1...v1.17.0) (2026-07-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **token:** add refresh token verification to TokenService ([a046519](https://github.com/asepharyana/zesdex/commit/a04651905f4afd562b516c839449a2c813ce6627))
|
||||||
|
|
||||||
|
## [1.16.1](https://github.com/asepharyana/zesdex/compare/v1.16.0...v1.16.1) (2026-07-20)
|
||||||
|
|
||||||
# [1.16.0](https://github.com/asepharyana/zesdex/compare/v1.15.2...v1.16.0) (2026-07-20)
|
# [1.16.0](https://github.com/asepharyana/zesdex/compare/v1.15.2...v1.16.0) (2026-07-20)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
Tests use `#[cfg(test)] mod tests` blocks inline in production files (not a separate `tests/` dir).
|
|
||||||
|
|
||||||
Tracing output goes to `~/.local/share/zesdex/zesdex.log`. Set `RUST_LOG=debug` for verbose logging.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
Zesdex is an autonomous AI coding agent with a TUI — an OpenAI/Anthropic-compatible LLM client wrapped in a tool-use harness with 37 built-in tools.
|
|
||||||
|
|
||||||
Detailed architecture documentation is in `docs/CODEMAPS/`:
|
|
||||||
|
|
||||||
| File | Covers |
|
|
||||||
|------|--------|
|
|
||||||
| [`docs/CODEMAPS/architecture.md`](docs/CODEMAPS/architecture.md) | System layout, process modes, data flow, key files |
|
|
||||||
| [`docs/CODEMAPS/backend.md`](docs/CODEMAPS/backend.md) | Provider, OAuth, IPC, workflow engine, MCP, review, bg bash |
|
|
||||||
| [`docs/CODEMAPS/frontend.md`](docs/CODEMAPS/frontend.md) | TUI render pipeline, 16 overlays, toasts, input handling |
|
|
||||||
| [`docs/CODEMAPS/data.md`](docs/CODEMAPS/data.md) | Persistence, SQLite msglog, memory files, settings/config |
|
|
||||||
| [`docs/CODEMAPS/dependencies.md`](docs/CODEMAPS/dependencies.md) | 23 Rust crates, 5 external services |
|
|
||||||
|
|
||||||
`docs/runs/` holds an auto-generated audit trail: one markdown file per hive-mind convergence (see below), written deterministically by `app::workflow::docs::write_hive_mind_convergence` — not hand-maintained like `docs/CODEMAPS/`.
|
|
||||||
|
|
||||||
### Key Patterns
|
|
||||||
|
|
||||||
- **State mutation** — `AppStateRest` is mutable in-place from `actions/mod.rs` and `controller/input.rs`. No generic update function.
|
|
||||||
- **No DI** — modules call `Settings::load()`, `AppConfig::load()`, `all_tools()` directly.
|
|
||||||
- **Logging** — `tracing::warn!` to `~/.local/share/zesdex/zesdex.log` (not stderr, avoids TUI corruption).
|
|
||||||
- **Error handling** — `anyhow::Result` and `anyhow::bail!` throughout. No custom error types.
|
|
||||||
- **Static strings** — MCP tool descriptions use `Box::leak` + `OnceLock` cache.
|
|
||||||
- **Tools** — `trait Tool { fn name() -> &str, fn run() -> Result<String> }`, 28 impls, gated by `Harness`.
|
|
||||||
- **Shell safety** — `tool/shell_filter/` blocks destructive git commands (`shell_filter::git::check_git_destructive`, called from `tool/shell.rs::Bash::run`). It also contains a `check_credential_read` detector for credential-file reads, but that one is intentionally NOT wired into `Bash::run` today — see the doc comment on `Bash::run` for why.
|
|
||||||
|
|
||||||
### Hive-Mind Orchestration (Machine Intelligence)
|
|
||||||
|
|
||||||
- **A single Core Intelligence spawning anonymous processing nodes.** The Core Intelligence (main agent) compiles a cognitive cycle plan per task: an ordered list of cycles, each cycle a set of processing nodes that run in parallel. Each node's sole identity is its directive (what to do) and an access tier. Cycle count and nodes-per-cycle are entirely Core-Intelligence output.
|
|
||||||
- **Access tiers** in `src/app/subagent/division.rs` (`tool_scope` module): tool access is granted per node via one of three tiers (`read` / `write` / `full`, see `tool_scope::tools_for`) picked by the Core Intelligence based on what each node's directive actually needs.
|
|
||||||
- **Orchestrator** in `src/app/workflow/hive_mind.rs`: `run_hive_mind()` executes a `CognitiveCyclePlan { cycles: Vec<Vec<NodeDirective>> }` cycle-by-cycle. Node IDs are system-assigned coordinates (e.g. `"Node-0-1"`).
|
|
||||||
- **Continuous collective state, not phase-boundary sync**: `engine::execute_primitive`'s `ScopedAgent` arm merges each node's complete output into the shared collective-state channel the instant that node finishes — not after its whole parallel cohort completes — so sibling/later nodes see it in real time.
|
|
||||||
- **Consensus synthesis, not a per-node summary**: after all cycles complete, `synthesize_consensus()` spawns one final read-only node whose sole directive is to reconcile the entire collective state into a single consensus assessment — a real reasoning pass, not string concatenation, since node outputs can overlap or conflict.
|
|
||||||
- **Auto-trigger** in `run_agent_turn()` (`actions/mod.rs`): `is_complex_request()` heuristics decide only whether to ask the Core Intelligence to compile a plan at all — the plan's shape is fully dynamic.
|
|
||||||
- **`hive_mind` tool** (`src/tool/workflow.rs`) is the manual entry point: the calling LLM supplies its own `cycles` array of `{directive, access}` directly.
|
|
||||||
- **Guaranteed documentation**: after every convergence, `src/app/workflow/docs.rs::write_hive_mind_convergence()` deterministically (not an LLM step, not skippable) writes every node's full output plus the final consensus to `docs/runs/<timestamp>-<slug>.md`.
|
|
||||||
- **Live node progress** in TUI panel (`view/workflow.rs`): shows node designation + current tool via `AgentStatus::progress`.
|
|
||||||
- **Auto inline review** after each edit: `src/app/subagent/auto.rs` — `spawn_quick_review()` injects verdict back into LLM conversation.
|
|
||||||
- **Background subagents** (test-gen, arch-review, security-review) fire asynchronously at turn end via `TurnEvent::SystemNote`, retrying once on failure and escalating to a blocking (`ESCALATED:`-prefixed, `ToastKind::Error`) notice if the retry also fails.
|
|
||||||
|
|
||||||
Commit convention (Conventional Commits, Bahasa Indonesia): see the `commit-convention` skill.
|
|
||||||
|
|
||||||
## Code Documentation
|
|
||||||
|
|
||||||
Every function, struct, enum, trait, module, and significant code block must have a doc comment (`///` or `//!`) that explains:
|
|
||||||
|
|
||||||
- **What** the function/module does (purpose, not how)
|
|
||||||
- **Flow** — a brief ASCII or prose description of the code flow / data flow above each non-trivial function
|
|
||||||
- **Why** — non-obvious decisions, edge cases, invariants
|
|
||||||
- **Return** — what the caller gets back, especially for `Result` types
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Parse an SSE data chunk into one or more StreamEvents.
|
|
||||||
///
|
|
||||||
/// Flow: buffer → split on '\n' → flush on blank line → JSON parse → match event type
|
|
||||||
/// → return Token / ToolCallDelta / Usage / Done.
|
|
||||||
///
|
|
||||||
/// Edge case: chunk may split mid-line; remaining bytes stay in buffer
|
|
||||||
/// for the next feed() call.
|
|
||||||
fn feed(&mut self, chunk: &str) -> Vec<StreamEvent> { ... }
|
|
||||||
|
|
||||||
/// The single source-of-truth state struct for the entire application.
|
|
||||||
///
|
|
||||||
/// Mutated in-place from two locations: actions/mod.rs (apply_action)
|
|
||||||
/// and controller/input.rs (key event handlers). Read-only from
|
|
||||||
/// every other module.
|
|
||||||
struct AppStateRest { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Every `pub fn` needs a doc comment
|
|
||||||
- Every `pub struct` / `pub enum` / `pub trait` needs a doc comment
|
|
||||||
- Non-trivial private functions (≥10 lines) need a doc comment
|
|
||||||
- Write the comment above the code it documents (not inline in the body)
|
|
||||||
- Update comments when code behavior changes — stale docs are worse than no docs
|
|
||||||
- NEVER use compiler/linter bypass annotations or attributes (such as `#[allow(clippy::too_many_lines, clippy::too_many_arguments, clippy::ref_option)]`, `#[allow(dead_code)]`, etc.) to silence warnings or skip linter checks. Always fix the underlying code issues instead.
|
|
||||||
Generated
-5154
File diff suppressed because it is too large.
Load diff
-96
@@ -1,96 +0,0 @@
|
|||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = [
|
|
||||||
"apps/domain",
|
|
||||||
"apps/application",
|
|
||||||
"apps/infrastructure",
|
|
||||||
"apps/interfaces/tui",
|
|
||||||
"apps/interfaces/api",
|
|
||||||
"apps/interfaces/daemon",
|
|
||||||
"apps/interfaces/ws",
|
|
||||||
"apps/interfaces/grpc",
|
|
||||||
"apps/interfaces/web",
|
|
||||||
"apps/gateway",
|
|
||||||
"apps/bootstrap",
|
|
||||||
]
|
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
version = "1.16.0"
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
|
||||||
|
|
||||||
[workspace.lints.rust]
|
|
||||||
unused = "deny"
|
|
||||||
dead_code = "deny"
|
|
||||||
unreachable_code = "deny"
|
|
||||||
unused_imports = "deny"
|
|
||||||
unused_variables = "deny"
|
|
||||||
unused_mut = "deny"
|
|
||||||
unused_must_use = "deny"
|
|
||||||
deprecated = "deny"
|
|
||||||
trivial_casts = "deny"
|
|
||||||
trivial_numeric_casts = "deny"
|
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
|
||||||
all = { level = "warn", priority = -1 }
|
|
||||||
pedantic = { level = "warn", priority = -2 }
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
serde_json = "1"
|
|
||||||
serde_yaml_ng = "0.10"
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
uuid = { version = "1", features = ["v4", "v5"] }
|
|
||||||
anyhow = "1"
|
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "io-util", "signal"] }
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
||||||
reqwest = { version = "0.13", features = ["json", "stream", "blocking", "native-tls-vendored", "form"] }
|
|
||||||
ratatui = "0.30.2"
|
|
||||||
crossterm = "0.29"
|
|
||||||
rusqlite = { version = "0.40", features = ["bundled"] }
|
|
||||||
thiserror = "1"
|
|
||||||
base64 = "0.22"
|
|
||||||
sha2 = "0.11"
|
|
||||||
hex = "0.4"
|
|
||||||
libc = "0.2"
|
|
||||||
dirs = "6"
|
|
||||||
regex = "1"
|
|
||||||
globset = "0.4"
|
|
||||||
ignore = "0.4"
|
|
||||||
nucleo-matcher = "0.3"
|
|
||||||
futures-util = "0.3"
|
|
||||||
rmcp = { version = "2.2", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "macros"] }
|
|
||||||
lsp-types = "0.97"
|
|
||||||
tiktoken-rs = "0.12"
|
|
||||||
similar = "3"
|
|
||||||
syntect = { version = "5", default-features = false, features = ["default-fancy"] }
|
|
||||||
pulldown-cmark = { version = "0.13", default-features = false }
|
|
||||||
infer = "0.19"
|
|
||||||
webbrowser = "1"
|
|
||||||
url = "2"
|
|
||||||
percent-encoding = "2"
|
|
||||||
dom_smoothie = "0.18.0"
|
|
||||||
fast_html2md = "0.0.62"
|
|
||||||
scraper = "0.27.0"
|
|
||||||
include_dir = "0.7"
|
|
||||||
axum = { version = "0.8", features = ["macros"] }
|
|
||||||
tower = "0.5"
|
|
||||||
tower-http = { version = "0.6", features = ["cors", "limit"] }
|
|
||||||
argon2 = "0.5"
|
|
||||||
jsonwebtoken = "9"
|
|
||||||
clap = { version = "4", features = ["derive"] }
|
|
||||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
|
||||||
|
|
||||||
# Clean-architecture workspace crate references
|
|
||||||
zesdex-domain = { path = "apps/domain" }
|
|
||||||
zesdex-application = { path = "apps/application" }
|
|
||||||
zesdex-infrastructure = { path = "apps/infrastructure" }
|
|
||||||
zesdex-tui = { path = "apps/interfaces/tui" }
|
|
||||||
zesdex-api = { path = "apps/interfaces/api" }
|
|
||||||
zesdex-daemon = { path = "apps/interfaces/daemon" }
|
|
||||||
zesdex-ws = { path = "apps/interfaces/ws" }
|
|
||||||
zesdex-grpc = { path = "apps/interfaces/grpc" }
|
|
||||||
zesdex-web = { path = "apps/interfaces/web" }
|
|
||||||
zesdex-gateway = { path = "apps/gateway" }
|
|
||||||
zesdex-bootstrap = { path = "apps/bootstrap" }
|
|
||||||
+13
-14
@@ -1,30 +1,29 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# Zesdex — Multi-stage Docker build
|
# Zesdex — Multi-stage Docker build (Bun)
|
||||||
# ===================================
|
# ========================================
|
||||||
# Stage 1: Build with Rust toolchain
|
# Stage 1: Build with Bun → compile single self-contained CLI binary
|
||||||
FROM rust:1.85-slim-bookworm AS builder
|
FROM oven/bun:1.2 AS builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
pkg-config libsqlite3-dev && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY bun.lock ./
|
||||||
|
COPY package.json tsconfig.json ./
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
# Build with release profile (treats warnings as errors via lints)
|
RUN bun install --frozen-lockfile
|
||||||
RUN cargo build --release -p zesdex-backend --bin zesdex
|
|
||||||
|
# Compile a standalone self-contained executable (bundles JS + native addons).
|
||||||
|
RUN bun build src/interfaces/cli/index.ts --compile --outfile=/app/zesdex
|
||||||
|
|
||||||
# Stage 2: Minimal runtime image
|
# Stage 2: Minimal runtime image
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates libsqlite3-0 && \
|
ca-certificates && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /app/target/release/zesdex /usr/local/bin/zesdex
|
COPY --from=builder /app/zesdex /usr/local/bin/zesdex
|
||||||
|
|
||||||
ENV ZESDEX_DATA_DIR=/data
|
ENV ZESDEX_DATA_DIR=/data
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/zesdex"]
|
ENTRYPOINT ["/usr/local/bin/zesdex"]
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
# Zesdex
|
|
||||||
|
|
||||||
> Autonomous AI coding agent in a terminal-based TUI.
|
|
||||||
|
|
||||||
Zesdex is a Rust-powered AI assistant that operates directly in your terminal via a rich TUI interface. It combines large language model intelligence with a comprehensive set of tools to explore, understand, and modify codebases autonomously — with built-in guardrails at every layer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Core
|
|
||||||
|
|
||||||
- **TUI Interface** — Full-screen terminal UI with chat panel, input bar, and status bar built with [ratatui](https://github.com/ratatui-org/ratatui) and [crossterm](https://github.com/crossterm-rs/crossterm).
|
|
||||||
- **Daemon Architecture** — Run as a background daemon with client attach/detach via Unix domain sockets. The daemon processes state; clients only render.
|
|
||||||
- **IPC Protocol** — Bidirectional state synchronization between daemon and client processes with diff-based updates.
|
|
||||||
- **Provider Agnostic** — Configurable AI model providers with dynamic model selection, per-role temperature/token limits, and API key management.
|
|
||||||
|
|
||||||
### Tool System (37 built-in tools)
|
|
||||||
|
|
||||||
| Category | Tools |
|
|
||||||
|----------|-------|
|
|
||||||
| **Filesystem** | `read`, `write`, `edit`, `delete` |
|
|
||||||
| **Search** | `grep` (recursive text), `glob` (file patterns) |
|
|
||||||
| **Shell** | `bash`, `bash_output`, `bash_kill` |
|
|
||||||
| **Git** | `git_operator`, `git_worktree`, `git_cred` |
|
|
||||||
| **Memory** | `remember`, `recall`, `forget` |
|
|
||||||
| **Planning** | `plan_enter`, `plan_ready`, `seqthink` |
|
|
||||||
| **Workflow** | `workflow_run`, `note_finding`, `read_findings`, `hive_mind` |
|
|
||||||
| **Utility** | `cd`, `dir_list`, `dir_cache_update`, `pong`, `todowrite`, `todofinish` |
|
|
||||||
| **Agent** | `spawn_agents`, `spawn_pipeline` |
|
|
||||||
| **LSP** | `lsp_connect`, `lsp_diagnostics`, `lsp_hover`, `lsp_completion`, `lsp_definition`, `lsp_references`, `lsp_disconnect` |
|
|
||||||
|
|
||||||
### Intelligence
|
|
||||||
|
|
||||||
- **Hive-Mind Orchestration** — Autonomous agent orchestration modeled as a distributed machine intelligence (à la Stellaris). The Core Intelligence (main agent) compiles a cognitive cycle plan per task — an ordered list of cycles, each a set of anonymous processing nodes that run in parallel. Every node carries only a directive (what to do) and an access tier (`read`/`write`/`full`); cycle count and nodes-per-cycle are decided per task, not fixed. Every node's output merges into a shared Line truncated
|
|
||||||
|
|
||||||
- **Workflow Engine** — Orchestrate complex multi-step tasks with parallel sub-agents, pipelines, and phased execution. Spawn independent workers that share findings in real-time.
|
|
||||||
- **Self-Learning** — Persistent memory system that stores lessons, references, and project knowledge across sessions. Memories include provenance tracking, lifecycle management, and scope isolation.
|
|
||||||
- **Self-Review** — Review subagents trigger automatically after each code edit (inline) and at turn completion (background). Three types: code quality, architecture, and security.
|
|
||||||
- **Self-Healing** — On build/test failures, spawns a sub-agent with the error context to autonomously fix issues before reporting them to the user.
|
|
||||||
- **MCP Support** — [Model Context Protocol](https://modelcontextprotocol.io/) integration for connecting to external AI tool servers.
|
|
||||||
- **Sequential Thinking** — Chain-of-thought reasoning tool for step-by-step problem decomposition.
|
|
||||||
- **Session Locking** — Prevents multiple processes from operating on the same session directory.
|
|
||||||
|
|
||||||
### Session Management
|
|
||||||
|
|
||||||
- Multiple concurrent sessions with history, rewind, and transcript persistence.
|
|
||||||
- Per-session edit logs with full change tracking.
|
|
||||||
- Session archival and summary generation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── main.rs # Entry point: single-process, daemon, or attach mode
|
|
||||||
├── resources.rs # Embedded resources (help text, system prompts)
|
|
||||||
├── app/
|
|
||||||
│ ├── state/ # AppStateRest — immutable-rest state model
|
|
||||||
│ │ ├── rest.rs # Core state struct
|
|
||||||
│ │ ├── types.rs # Overlay, Toast, Origin enums
|
|
||||||
│ │ ├── snapshot.rs # State snapshots for IPC
|
|
||||||
│ │ ├── diff.rs # Diff-based state synchronization
|
|
||||||
│ │ ├── runtime.rs # Runtime state mutations
|
|
||||||
│ │ └── misc.rs # DirCache and miscellaneous state helpers
|
|
||||||
│ ├── runtime/ # Action dispatch and event loop
|
|
||||||
│ │ ├── actions/ # Action enum and apply_action reducer
|
|
||||||
│ │ ├── stream/ # LLM streaming and tool execution
|
|
||||||
│ │ │ └── tools/ # Tool harness integration
|
|
||||||
│ │ │ └── turn.rs # Turn orchestration
|
|
||||||
│ │ ├── event_loop/ # Main event loop and shortsend
|
|
||||||
│ │ ├── commands.rs # Slash command dispatch
|
|
||||||
│ │ └── shortsend.rs # Short-lived async send helper
|
|
||||||
│ ├── mode/ # UI modes and overlays (13 modes)
|
|
||||||
│ │ ├── bash.rs # Bash panel mode
|
|
||||||
│ │ ├── editor.rs # Multi-line editor mode
|
|
||||||
│ │ ├── effort.rs # Effort level selector
|
|
||||||
│ │ ├── help.rs # Help overlay
|
|
||||||
│ │ ├── key_input.rs # Raw key input mode
|
|
||||||
│ │ ├── learning.rs # Lesson management overlay
|
|
||||||
│ │ ├── loading.rs # Loading spinner overlay
|
|
||||||
│ │ ├── mcp.rs # MCP server management
|
|
||||||
│ │ ├── quit_confirm.rs # Quit confirmation dialog
|
|
||||||
│ │ ├── rewind.rs # Session rewind mode
|
|
||||||
│ │ ├── settings.rs # Settings panel
|
|
||||||
│ │ ├── todo.rs # Task list overlay
|
|
||||||
│ │ └── workflow.rs # Workflow visualization
|
|
||||||
│ ├── harness.rs # Tool harness for agent execution
|
|
||||||
│ ├── workflow/ # Workflow engine
|
|
||||||
│ │ ├── script.rs # Workflow script DSL
|
|
||||||
│ │ ├── engine.rs # Workflow executor
|
|
||||||
│ │ ├── hive_mind.rs # Hive-mind orchestrator
|
|
||||||
│ │ └── docs.rs # Deterministic docs/runs/*.md writer
|
|
||||||
│ ├── mcp/ # MCP client manager
|
|
||||||
│ │ └── manager.rs # MCP server lifecycle and tool exposure
|
|
||||||
│ ├── subagent/ # Sub-agent management
|
|
||||||
│ │ ├── spawn.rs # AgentDefinition and spawning
|
|
||||||
│ │ ├── engine.rs # Sub-agent event loop
|
|
||||||
│ │ ├── context.rs # Context construction for sub-agents
|
|
||||||
│ │ └── event.rs # Progress event types
|
|
||||||
│ ├── bgbash/ # Background bash job management
|
|
||||||
│ │ ├── job.rs # Background job handle
|
|
||||||
│ │ └── control.rs # Bash control (bg/fg/kill)
|
|
||||||
│ ├── lsp/ # LSP client management
|
|
||||||
│ │ ├── client.rs # LSP client connection wrapper
|
|
||||||
│ │ └── provisioner.rs # Auto-provisioning of LSP servers
|
|
||||||
│ └── review/ # Self-review quality system
|
|
||||||
├── controller/
|
|
||||||
│ ├── input.rs # Key event → Action mapping
|
|
||||||
│ └── command.rs # Slash command parser
|
|
||||||
├── dto/
|
|
||||||
│ ├── chat/ # Message, ToolCall, Role types
|
|
||||||
│ │ ├── message.rs # Chat message types
|
|
||||||
│ │ ├── tool.rs # Tool call/result types
|
|
||||||
│ │ └── mod.rs
|
|
||||||
│ └── provider/ # AI provider request/response/usage types
|
|
||||||
│ ├── request.rs # Provider request schema
|
|
||||||
│ ├── response.rs # Provider response schema
|
|
||||||
│ └── usage.rs # Token usage tracking
|
|
||||||
├── ipc/
|
|
||||||
│ ├── protocol.rs # ClientRequest, DaemonFrame, StatePayload
|
|
||||||
│ ├── server.rs # Unix socket server
|
|
||||||
│ ├── client.rs # Unix socket client
|
|
||||||
│ ├── conn.rs # Framed connection
|
|
||||||
│ ├── frame.rs # Length-prefixed frame encoding
|
|
||||||
│ ├── snapshot.rs # State snapshot serialization
|
|
||||||
│ └── diff.rs # Binary diff for state sync
|
|
||||||
├── model/
|
|
||||||
│ ├── store.rs # File-based storage (~/.config/zesdex/)
|
|
||||||
│ ├── session.rs # Session CRUD and listing
|
|
||||||
│ ├── settings.rs # User settings (provider, model, tokens)
|
|
||||||
│ ├── app_config.rs # Provider definitions and model roles
|
|
||||||
│ ├── memory.rs # Persistent memory with frontmatter
|
|
||||||
│ ├── editlog.rs # Edit history tracking
|
|
||||||
│ ├── msglog/ # Message log (SQLite-backed)
|
|
||||||
│ │ ├── schema.rs # SQLite schema
|
|
||||||
│ │ ├── query.rs # Query helpers
|
|
||||||
│ │ ├── blobs.rs # Large blob storage
|
|
||||||
│ │ └── summary.rs # Session summarization
|
|
||||||
│ ├── agent_def/ # Agent definitions (builtin, global, session)
|
|
||||||
│ │ ├── builtin.rs # Built-in agent profiles
|
|
||||||
│ │ ├── global.rs # Global agent config
|
|
||||||
│ │ └── session.rs # Per-session agent config
|
|
||||||
│ ├── conversation.rs # Conversation helpers
|
|
||||||
│ └── session_lock.rs # Flock-based session locking
|
|
||||||
├── service/
|
|
||||||
│ ├── provider.rs # AI provider abstraction
|
|
||||||
│ └── oauth/ # OAuth PKCE flow with loopback server
|
|
||||||
│ ├── loopback.rs # Local HTTP server for OAuth redirect
|
|
||||||
│ ├── manager.rs # OAuth token manager
|
|
||||||
│ ├── pkce.rs # PKCE code challenge/verifier
|
|
||||||
│ └── mod.rs
|
|
||||||
├── tool/ # 34 tool implementations
|
|
||||||
│ ├── fs/ # read, write, edit, delete
|
|
||||||
│ │ ├── read.rs
|
|
||||||
│ │ ├── write.rs
|
|
||||||
│ │ ├── edit.rs
|
|
||||||
│ │ ├── delete.rs
|
|
||||||
│ │ └── helpers.rs # Path resolution and validation
|
|
||||||
│ ├── search.rs # grep, glob
|
|
||||||
│ ├── shell.rs # bash
|
|
||||||
│ ├── bash_tools.rs # bash_output, bash_kill
|
|
||||||
│ ├── git_operator.rs # git operations
|
|
||||||
│ ├── git_worktree.rs # git worktree management
|
|
||||||
│ ├── git_cred.rs # git credential store/get/erase
|
|
||||||
│ ├── memory/ # remember, forget, recall
|
|
||||||
│ │ ├── remember.rs
|
|
||||||
│ │ ├── forget.rs
|
|
||||||
│ │ └── recall.rs
|
|
||||||
│ ├── plan.rs # plan_enter, plan_ready
|
|
||||||
│ ├── seqthink.rs # Sequential thinking
|
|
||||||
│ ├── workflow.rs # workflow_run, note_finding
|
|
||||||
│ ├── utility/ # cd, dir_list, dir_cache_update, pong, todowrite, todofinish
|
|
||||||
│ │ ├── cd.rs
|
|
||||||
│ │ ├── dir_list.rs
|
|
||||||
│ │ ├── dir_cache_update.rs
|
|
||||||
│ │ ├── pong.rs
|
|
||||||
│ │ ├── todowrite.rs
|
|
||||||
│ │ └── todofinish.rs
|
|
||||||
│ ├── lsp/ # LSP tools (connect, diagnostics, hover, etc.)
|
|
||||||
│ │ └── mod.rs
|
|
||||||
│ └── shell_filter/ # Shell output filtering (credentials, git)
|
|
||||||
│ ├── credentials.rs
|
|
||||||
│ ├── git.rs
|
|
||||||
│ └── mod.rs
|
|
||||||
└── view/ # TUI rendering
|
|
||||||
├── chat.rs # Chat transcript with markdown
|
|
||||||
├── markdown.rs # Markdown → ratatui spans
|
|
||||||
├── status.rs # Status bar
|
|
||||||
├── theme.rs # Color scheme
|
|
||||||
└── workflow.rs # Workflow visualization
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run in single-process mode (default)
|
|
||||||
zesdex
|
|
||||||
|
|
||||||
# Run as a background daemon
|
|
||||||
zesdex --daemon
|
|
||||||
|
|
||||||
# Attach to a running daemon session
|
|
||||||
zesdex --attach <session-id>
|
|
||||||
|
|
||||||
# Set log level
|
|
||||||
RUST_LOG=debug zesdex
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Bindings
|
|
||||||
|
|
||||||
| Binding | Action |
|
|
||||||
|---------|--------|
|
|
||||||
| `Ctrl+Q` | Quit |
|
|
||||||
| `Ctrl+H` | Help overlay |
|
|
||||||
| `Ctrl+P` | Settings overlay |
|
|
||||||
| `Ctrl+B` | Bash panel |
|
|
||||||
| `Ctrl+S` | Session hub |
|
|
||||||
| `Ctrl+T` | Task list |
|
|
||||||
| `Ctrl+W` | Workflow view |
|
|
||||||
| `Ctrl+K` | Key input mode |
|
|
||||||
| `Esc` | Cancel / back |
|
|
||||||
| `Tab` | Autocomplete |
|
|
||||||
| `↑/↓` | History / navigation |
|
|
||||||
| `Scroll` | Mouse scroll in chat |
|
|
||||||
|
|
||||||
### Slash Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `/help` | Show help |
|
|
||||||
| `/clear` | Clear transcript |
|
|
||||||
| `/model` | Select AI model provider |
|
|
||||||
| `/workflow` | Open the workflow panel |
|
|
||||||
| `/workflow run <script>` | Run a JSON-encoded workflow script |
|
|
||||||
| `/mcp` | Open MCP server manager |
|
|
||||||
| `/mcp add <name> <command>` | Add an MCP server |
|
|
||||||
| `/login [provider]` | Authenticate with a provider |
|
|
||||||
| `/edit [path]` | Open a file/dir in the external editor |
|
|
||||||
| `/compact` | Compact the conversation transcript |
|
|
||||||
| `/lesson` | Interactive lesson/memory review |
|
|
||||||
| `/quit` | Exit application |
|
|
||||||
| `Any text` | Sent to the AI assistant as a prompt |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
All configuration lives in `~/.config/zesdex/` (or platform equivalent via the `dirs` crate).
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `settings.json` | Provider selection, model, temperature, max tokens, review settings, workflow concurrency |
|
|
||||||
| `app_config.json` | AI provider definitions (name, API base URL, auth type, default model) |
|
|
||||||
| `memory/` | Persistent lesson and reference storage (Markdown with YAML frontmatter) |
|
|
||||||
| `sessions/` | Per-session transcripts, edit logs, and activity data |
|
|
||||||
| `run/` | Unix domain sockets for daemon mode |
|
|
||||||
|
|
||||||
### Provider Configuration
|
|
||||||
|
|
||||||
Providers are defined in `app_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"providers": {
|
|
||||||
"my-provider": {
|
|
||||||
"api_base": "https://api.example.com/v1",
|
|
||||||
"api_key_env": "MY_API_KEY",
|
|
||||||
"default_model": "model-name"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model_roles": {
|
|
||||||
"default": {
|
|
||||||
"provider": "my-provider",
|
|
||||||
"model": "model-name",
|
|
||||||
"max_tokens": 8192,
|
|
||||||
"temperature": 0.7
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"default_provider": "my-provider",
|
|
||||||
"default_model": "model-name"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
Key settings in `settings.json`:
|
|
||||||
|
|
||||||
| Setting | Default | Description |
|
|
||||||
|---------|---------|-------------|
|
|
||||||
| `review_enabled` | `true` | Enable self-review after tool execution |
|
|
||||||
| `review_max_lessons_per_run` | `5` | Max lessons loaded per review cycle |
|
|
||||||
| `adaptive_review_max_skip` | `3` | Consecutive passes before skipping review |
|
|
||||||
| `verify_command` | `null` | Optional command to verify changes |
|
|
||||||
| `workflow_max_concurrency` | `5` | Max parallel sub-agents in workflows |
|
|
||||||
| `session_archive_enabled` | `true` | Auto-archive completed sessions |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- **Rust** 2021 edition toolchain ([rustup](https://rustup.rs/))
|
|
||||||
|
|
||||||
### Build from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd zesdex
|
|
||||||
cargo build --release
|
|
||||||
./target/release/zesdex
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Commit Convention
|
|
||||||
|
|
||||||
Project ini menggunakan **Conventional Commits** untuk otomatis menentukan versi rilis (melalui semantic-release).
|
|
||||||
|
|
||||||
Format:
|
|
||||||
```
|
|
||||||
<type>(<scope>): <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer]
|
|
||||||
```
|
|
||||||
|
|
||||||
**`<type>` — menentukan bump version:**
|
|
||||||
|
|
||||||
| Type | Bump | Keterangan |
|
|
||||||
|-------------|---------|-------------------------------------------|
|
|
||||||
| `feat` | minor | Fitur baru |
|
|
||||||
| `fix` | patch | Perbaikan bug |
|
|
||||||
| `chore` | patch | Tugas maintenance, refactor ringan |
|
|
||||||
| `docs` | patch | Perubahan dokumentasi |
|
|
||||||
| `refactor` | patch | Refactor kode (tanpa perubahan fungsional)|
|
|
||||||
| `test` | patch | Penambahan atau perbaikan test |
|
|
||||||
| `style` | patch | Perubahan formatting, whitespace, dll |
|
|
||||||
| `perf` | patch | Optimasi performa |
|
|
||||||
| `ci` | patch | Perubahan CI/CD |
|
|
||||||
|
|
||||||
**`BREAKING CHANGE`** pada body commit → **major** (apa pun typenya).
|
|
||||||
|
|
||||||
Contoh:
|
|
||||||
```
|
|
||||||
feat(agent): add workspace-aware file search
|
|
||||||
|
|
||||||
Implement context-aware search scoped to current workspace directory.
|
|
||||||
|
|
||||||
BREAKING CHANGE: search results now filter by workspace scope.
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
fix(ipc): handle partial frame on unix socket reconnect
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
chore: update rustls to 0.23
|
|
||||||
```
|
|
||||||
|
|
||||||
### Release Workflow
|
|
||||||
|
|
||||||
Push ke branch `main` akan memicu:
|
|
||||||
1. **CI** — `cargo build --release` + `cargo test`
|
|
||||||
2. **Semantic Release** — analisis commit → update `Cargo.toml` + `CHANGELOG.md` → git tag → GitHub Release dengan binary
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
See [LICENSE](LICENSE) for details.
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# TODO — Rewrite Zesdex Rust → TypeScript/Bun
|
||||||
|
|
||||||
|
> Plan lengkap migrasi in-place dari Rust (11 crate, clean architecture 4 lapis) ke monorepo
|
||||||
|
> TypeScript/Bun. Bahasa Indonesia, commit pakai Conventional Commits.
|
||||||
|
>
|
||||||
|
> **Status:** Fase 0–6 **semua selesai**. Rust source dihapus, build CI/Docker/release pakai Bun. Sisa: `flake.nix`/deploy infra (Nix), `settings.json` wire-shape check. 74 test hijau.
|
||||||
|
> Base: `bun run check` bersih, 74 test hijau.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prinsip & peta pemetaan
|
||||||
|
|
||||||
|
| Rust | TS/Bun |
|
||||||
|
|---|---|
|
||||||
|
| `serde_json` | `JSON.stringify/parse` + `zod` |
|
||||||
|
| `chrono` | `Date` / `Date.now()` (epoch ms) |
|
||||||
|
| `uuid` | `crypto.randomUUID()` |
|
||||||
|
| `sha2`, `base64 URL_SAFE_NO_PAD` | `node:crypto` / `Buffer.toString('base64url')` |
|
||||||
|
| `reqwest` | `fetch` / bun HTTP |
|
||||||
|
| `rusqlite` | `bun:sqlite` |
|
||||||
|
| `argon2` | `@node-rs/argon2` |
|
||||||
|
| `jsonwebtoken` | `node:crypto` HMAC HS256 |
|
||||||
|
| `rmcp` | `@modelcontextprotocol/sdk` |
|
||||||
|
| `ratatui`/`crossterm` | library TUI Bun / ink |
|
||||||
|
| `tiktoken-rs` | `@dqbd/tiktoken` (cl100k_base) |
|
||||||
|
| `clap` | `Bun.argv` / `commander` |
|
||||||
|
| `tokio` semaphore | Bun async + concurrency pool |
|
||||||
|
| `libc` PID lock | `node:fs` `O_EXCL` + `process.kill` liveness |
|
||||||
|
| Rust generics `S<R: Repo>` | DI via constructor interface |
|
||||||
|
| `Arc<Mutex<T>>` | objek mutable (array/objek di ToolCtx) |
|
||||||
|
|
||||||
|
### Struktur target
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/
|
||||||
|
packages/ # ✅ sudah ada: domain, application, infrastructure
|
||||||
|
interfaces/
|
||||||
|
cli/ # gateway + TUI [baru]
|
||||||
|
api/ # REST API [baru]
|
||||||
|
daemon/ # daemon + IPC [baru]
|
||||||
|
ws/ # WebSocket server [baru]
|
||||||
|
grpc/ # stub [baru]
|
||||||
|
web/ # static server [baru]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Selesai
|
||||||
|
|
||||||
|
### Fase 0 — Bootstrap monorepo
|
||||||
|
- [x] `package.json` root (workspaces), `tsconfig.json` strict, `.gitignore`
|
||||||
|
- [x] Paket `domain`, `application`, `infrastructure` (workspace `@zesdex/*`)
|
||||||
|
- [x] `bun install` + `bun test` jalan
|
||||||
|
|
||||||
|
### Fase 1 — Domain layer
|
||||||
|
- [x] `core`: ChatMessage, Conversation, ChatRequest/Response/StreamEvent, SseParser, ToolCall, repairJson, sanitizeToolArguments, ToolCallResult, UsageStats, Store
|
||||||
|
- [x] `auth`: Session, SessionId (path-traversal), OAuthToken/Config, repo/service trait
|
||||||
|
- [x] `cms`: AppConfig, ProviderConfig, ModelRole, Settings+resolveEffectiveModel, SettingsPatch, Memory, EditLog, repo/service trait
|
||||||
|
- [x] `agent`: Origin, Toast, AgentStatus, TurnEvent, SessionRuntime, AgentTurnParams, AgentProgress, ExecutionModel
|
||||||
|
- [x] `workflow`/`subagent`: WorkflowScript/NodeDirective, AccessTier, prompt builders
|
||||||
|
- [x] 27 test domain
|
||||||
|
|
||||||
|
### Fase 2 — Application layer
|
||||||
|
- [x] `ports`: ProviderService, PasswordService, TokenService, AuthService
|
||||||
|
- [x] `AgentTurnServiceImpl` (loop 50 iter, auto-compact 60k, adaptive token, temp, ErrorTracker, bounded parallel)
|
||||||
|
- [x] `OAuthUseCase` (PKCE S256 + CSRF state)
|
||||||
|
- [x] `SessionServiceImpl`, `MemoryServiceImpl`, `SettingsServiceImpl`, `ConversationServiceImpl`
|
||||||
|
- [x] 11 test application
|
||||||
|
|
||||||
|
### Fase 3a — LLM client
|
||||||
|
- [x] `LlmClient` (fetch, retry 10x backoff+jitter, abort 401/402/403)
|
||||||
|
- [x] `resolve_api_key`, `is_auth_error`, `backoff_for_error`
|
||||||
|
|
||||||
|
### Fase 3b — Tool system (`commit a291dd1`)
|
||||||
|
- [x] `Tool` interface, `ToolCtx`/`ToolCtxBuilder`, `InfrastructureToolExecutor`
|
||||||
|
- [x] Registry: `allTools` (37), `toolDefs`, `toolIsRisky`, `toolIsParallelSafe`
|
||||||
|
- [x] FS: read, write, edit, delete · **Guard**: graduated checks, resolve_path sandbox
|
||||||
|
- [x] Search: grep, glob (matcher ringan)
|
||||||
|
- [x] Shell: bash (timeout), bash_output, bash_kill · **Filter**: checkGitDestructive, checkCredentialRead
|
||||||
|
- [x] Git: git_operator, git_worktree, git_cred
|
||||||
|
- [x] Memory: remember, forget, recall
|
||||||
|
- [x] Util: cd, dir_list, dir_cache_update, pong, todowrite, todofinish
|
||||||
|
- [x] Reasoning/Plan: sequential_think, plan_enter, plan_ready
|
||||||
|
- [x] Web: web_search (async → SearXNG)
|
||||||
|
- [x] Semantic: semantic_search, rebuild_index, list_symbols (index multi-bahasa)
|
||||||
|
- [x] Best-practice: best_practice, commit_convention + BestPracticeEngine
|
||||||
|
- [x] Agent/Workflow: spawn_agents, spawn_pipeline, parallel_delegate, workflow_run, note_finding, read_findings, hive_mind (delegasi ke placeholder 3c/3d)
|
||||||
|
- [x] 16 test tool system
|
||||||
|
|
||||||
|
### Fase 3e — Persistence
|
||||||
|
- [x] File repos: settings, app_config (+ auto-detect kredensial Claude), conversation, edits.jsonl, markdown memory, rewind blob, session, oauth, session lock
|
||||||
|
- [x] `writeJsonAtomic`, `slugify`, `buildWorkspaceTree`, `runCommand`
|
||||||
|
|
||||||
|
### Fase 3c — Subagent engine
|
||||||
|
**Sumber Rust:** `apps/infrastructure/src/subagent/{engine,division,context,provider,spawn}.rs`, `subagent/auto/*`
|
||||||
|
|
||||||
|
- [x] `AccessTier.toolsFor(access)` — `tools/division.ts`: Read / Write / Full yang mem-filter registry
|
||||||
|
- [x] `run_agent` loop (`tools/engine.ts`): `MAX_ITERATIONS=25`, `TOOL_OUTPUT_MAX_CHARS=12_000`,
|
||||||
|
`MAX_CONSECUTIVE_TOOL_ERRORS=3`, `MAX_PARALLEL_TOOLS=8`; recovery note setelah 3 error beruntun
|
||||||
|
- [x] `SubagentContext` (`subagent/context.ts`) — directive, toolCtx, access, baseURL, apiKey, model
|
||||||
|
- [x] `spawn_subagent` — `spawn_tools.ts` — runAgent berbasis HTTP provider + config_resolver
|
||||||
|
- [x] `resolve_subagent_provider` (`subagent/provider.ts`) — pilih provider/model subagent dari settings + app_config
|
||||||
|
- [x] `spawn_tools.ts` — isi body `spawnAgents` (paralel) & `spawnPipeline` (berurutan)
|
||||||
|
- [x] `delegate.ts` — `runParallelDelegation` untuk `parallel_delegate`
|
||||||
|
- [x] `http_provider.ts` — shared HTTP provider service builder
|
||||||
|
- [ ] Auto-reviewer (`subagent/auto/*`) — `spawn_background_review`: git diff → LLM review → perbaiki;
|
||||||
|
wiring `review_enabled` (yang sekarang no-op di `Review` agent)
|
||||||
|
- [x] Placeholder di `apps/packages/infrastructure/src/subagent/*` diisi penuh
|
||||||
|
|
||||||
|
### Fase 3d — Workflow + hive-mind
|
||||||
|
**Sumber Rust:** `apps/infrastructure/src/workflow/{script,mod,docs}.rs`, `workflow/engine/{execution,phases,primitives}.rs`, `workflow/hive_mind/{complexity,cycle,synthesis,live,types}.rs`
|
||||||
|
|
||||||
|
- [x] `parse_workflow_script(yaml)` — parser YAML workflow (`workflow/script.ts`)
|
||||||
|
- [x] `execute_workflow(script, ctx)` — eksekusi multi-phase (`workflow/engine.ts` + `workflow/orchestrate.ts`)
|
||||||
|
- [x] Phase primitives: run sub-agent per node (`executePrimitive`)
|
||||||
|
- [x] `execute_cycle(cycle, ctx)` — hive-mind cycle, concurrency 8, kumpulkan NodeOutput (`workflow/cycle.ts`)
|
||||||
|
- [x] `synthesize_consensus(node_outputs, ctx)` — konsensus via LLM + fallback concat (`workflow/synthesis.ts`)
|
||||||
|
- [x] `complexity_heuristic` — pilih apakah cukup 1 cycle / butuh banyak (`workflow/complexity.ts`)
|
||||||
|
- [x] `docs.ts` — tulis convergence/decision doc (`workflow/docs.ts`)
|
||||||
|
- [x] Isi body `WorkflowRun` & `HiveMind` tool (ganti placeholder) di `tools/workflow.ts`
|
||||||
|
- [x] `orchestrate.ts` — top-level `executeWorkflow` + `executeHiveMind` entry points
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⬜ Yang belum dikerjakan
|
||||||
|
|
||||||
|
### Fase 3f — Auth infrastructure
|
||||||
|
**Sumber Rust:** `apps/infrastructure/src/auth/{password,jwt,oauth_loopback,mod}.rs`
|
||||||
|
|
||||||
|
- [x] `password.ts` — Argon2 via `@node-rs/argon2` (hash + verify), port `PasswordService`
|
||||||
|
- [x] `jwt.ts` — HMAC HS256 via `node:crypto` (access + refresh token, typ, exp), port `TokenService`
|
||||||
|
- [x] `oauth_loopback.ts` — server TCP loopback lokal utk terima redirect OAuth; port `oauth_loopback`
|
||||||
|
- [x] `mod.ts` (`index.ts`) — composisi ke `AuthService`
|
||||||
|
- [x] Tambah dep `@node-rs/argon2` di `infrastructure/package.json`
|
||||||
|
|
||||||
|
### Fase 3g — IPC + bgbash
|
||||||
|
**Sumber Rust:** `apps/infrastructure/src/ipc/{frame,protocol,server,client,conn}.rs`, `apps/infrastructure/src/bgbash/{job,control}.rs`
|
||||||
|
|
||||||
|
- [x] `ipc/frame.ts` — length-prefixed framing via `node:net` (kirim/terima bingkai biner)
|
||||||
|
- [x] `ipc/conn.ts` — `Connection` framed JSON socket; `ipc/protocol.ts` — wire types
|
||||||
|
- [x] `ipc/server.ts` + `ipc/client.ts` — Unix socket server + attach client; protocol message (NLJSON/binary)
|
||||||
|
- [x] `bgbash/job.ts` — `BashJob` (pid, output file, log), `spawnBashJob`
|
||||||
|
- [x] `bgbash/control.ts` — registry job global: `register`, `cancel(job_id)`, `is_running`
|
||||||
|
- [x] Wire `bash` tool `run_in_background` → `spawnBashJob` + `register`; `bash_kill` → `cancel`
|
||||||
|
- [x] Update `bash_output` untuk baca dari registry/output dir
|
||||||
|
|
||||||
|
### Fase 4 — Gateway + CLI + TUI
|
||||||
|
**Sumber Rust:** `apps/gateway/src/{lib,main}.rs`, `apps/interfaces/tui/src/**`
|
||||||
|
|
||||||
|
- [x] `apps/interfaces/cli/` (paket Bun):
|
||||||
|
- [x] Parser arg: `--api/--daemon/--attach/--ws/--grpc/--web`, dispatch mode (`args.ts`)
|
||||||
|
- [x] Logging ke file (`index.ts`)
|
||||||
|
- [x] `run_single_process` — komposisi root: wire semua implementasi konkret sekali (`compose.ts`) + REPL loop
|
||||||
|
- [x] Bootstrap seed (`bootstrap.ts`) — wire-shape settings lama terbaca
|
||||||
|
- [x] CLI: parser arg + dispatch + single-process REPL loop + bootstrap seed (Smoke: `bun run cli --help`, `bun run bootstrap` jalan; loop turn + tool read/write)
|
||||||
|
- [x] TUI mode (`@zesdex/tui`, tanpa ncurses — render garis ANSI):
|
||||||
|
- [x] Event loop raw-mode stdin (run.ts) + key decode (decodeKey)
|
||||||
|
- [x] Key dispatcher `handleKey` → `Action[]` + overlay/autocomplete/editor handling (controller.ts)
|
||||||
|
- [x] `Action` enum + `applyAction` dispatcher (action.ts) + slash-command parse/apply (command.ts)
|
||||||
|
- [x] `InputState` (buffer/cursor/history/autocomplete) + `AppStateRest` state machine (state.ts)
|
||||||
|
- [x] Renderer ringan (render.ts): transcript, input, overlay, status — 20 unit tests hijau
|
||||||
|
- [x] Bootstrap seed (`bootstrap`) — `bun run bootstrap` seed defaults, wire-shape settings lama terbaca
|
||||||
|
|
||||||
|
### Fase 5 — Server interface: api / daemon / ws / grpc / web
|
||||||
|
**Sumber Rust:** `apps/interfaces/{api,daemon,ws,grpc,web}/src/**`, `apps/infrastructure/src/middleware/*`
|
||||||
|
|
||||||
|
- [x] **API** (`apps/interfaces/api/`):
|
||||||
|
- [x] Endpoint: `auth` (login/register/refresh, rate-limited), `sessions` CRUD, `conversations`, `chat/completions` proxy, `health`
|
||||||
|
- [x] JWT middleware, CORS
|
||||||
|
- [x] DTO (dto/)
|
||||||
|
- [x] Smoke `bun run api`: health 200; register/login 200; wrong-pwd 401; protected-no-token 401; create/list/delete session; add conversation msg
|
||||||
|
- [x] **daemon** — IPC agent-driver loop (Submit/Close) over per-session Unix socket; streaming token frames; wire CLI `--daemon`/`--attach`
|
||||||
|
- [x] **ws** — handler `/ws`, protokol token/connected/done/error; smoke: no-token rejected 401, with-token welcome
|
||||||
|
- [x] **grpc** — stub (health endpoint); smoke: `/grpc/health` 200
|
||||||
|
- [x] **web** — static server + proteksi traversal; smoke: `/` serves index, traversal 404
|
||||||
|
- [x] CLI `--api`/`--ws`/`--grpc`/`--web` dispatch → start server masing-masing (smoke: `bun run cli --api --api-port` → health 200)
|
||||||
|
|
||||||
|
### Fase 6 — Bersih-bersih + verifikasi akhir
|
||||||
|
- [x] Hapus semua sisa crate Rust: `apps/{domain,application,infrastructure,gateway,bootstrap,interfaces/*}` file `.rs`, `Cargo.toml`, `Cargo.lock`, `.cargo/`, `lefthook.yml`
|
||||||
|
- [x] Update `install.sh` → Bun
|
||||||
|
- [x] Update CI workflow: `.github/workflows/{ci,release}.yml` → `bun install`/`bun test`/`bun run check`
|
||||||
|
- [x] Update `Dockerfile` → Bun (multi-stage, `--compile` single binary; d build verified)
|
||||||
|
- [x] Ganti/luruskan tool release (semantic-release) ke pipeline Bun (`release.yml` test job → bun; prepareCmd → `bun run check`)
|
||||||
|
- [ ] `flake.nix`/`shell.nix`/`deploy.yml`/`flakehub-publish-rolling.yml` → Nix build masih Cargo (tinggalkan — deploy infra, tinggalkan ke user/ops; build lokal `bun build --compile` sudah diverifikasi)
|
||||||
|
- [x] Berkas jadi: hapus folder Rust yang sudah kosong dari `apps/` + Cargo.toml/lock + .cargo + lefthook
|
||||||
|
- [x] Verifikasi akhir: `tsc --noEmit` exit 0, `bun test` 74 hijau, `bun run cli --help` jalan
|
||||||
|
- [ ] Baca ulang file lama (`settings.json`, `app_config.json`, `conversation.json`, memory `.md`) — pastikan wire-shape terbaca
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verifikasi global (tiap fase)
|
||||||
|
- [ ] `bun run build` / `tsc --noEmit` tanpa error (strict)
|
||||||
|
- [ ] `bun test` — port test Rust ke `*.test.ts` (SseParser, repairJson, resolveEffectiveModel, SessionId, turn_service, tool system, subagent, workflow)
|
||||||
|
- [ ] Smoke: `bun run cli --help`; agent mode TUI dengan model stub/lokal; pastikan loop turn + tool read/write
|
||||||
|
- [ ] Server: `bun run api` + hit health/auth; `bun run ws` + connect
|
||||||
|
- [ ] Commit tiap fase dengan message Bahasa Indonesia + Conventional Commits; test hijau sebelum lanjut
|
||||||
|
|
||||||
|
## Catatan
|
||||||
|
- Registry tool = **38** (terhitung workflow_run, note_finding, read_findings, hive_mind; ExploreCodebase tidak diport).
|
||||||
|
- `Tool.run` boleh return `string | Promise<string>`.
|
||||||
|
- `spawn_agents`/`spawn_pipeline`/`parallel_delegate` sudah terisi penuh via 3c.
|
||||||
|
- Workflow/hive-mind tools sudah terisi penuh via 3d.
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "zesdex-application"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
# Application layer — port traits (interfaces), use cases, DTOs.
|
|
||||||
# Depends ONLY on domain. Application services orchestrate domain objects
|
|
||||||
# through port traits without knowing concrete implementations.
|
|
||||||
[dependencies]
|
|
||||||
zesdex-domain = { path = "../domain" }
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
chrono.workspace = true
|
|
||||||
uuid.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
tracing.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
base64.workspace = true
|
|
||||||
sha2.workspace = true
|
|
||||||
url.workspace = true
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
//! Auth use-case implementations.
|
|
||||||
//!
|
|
||||||
//! Contains concrete service types that implement the domain's
|
|
||||||
//! authentication and session management traits by coordinating
|
|
||||||
//! injected repository and port dependencies.
|
|
||||||
//!
|
|
||||||
//! # Use Cases
|
|
||||||
//!
|
|
||||||
//! - [`oauth_service`] — `OAuthUseCase`: OAuth 2.0 authorization-code + PKCE flow
|
|
||||||
//! - [`session_service`] — `SessionServiceImpl`: session CRUD lifecycle
|
|
||||||
|
|
||||||
pub mod oauth_service;
|
|
||||||
pub mod session_service;
|
|
||||||
|
|
||||||
pub use oauth_service::{OAuthFlowStore, OAuthUseCase, TokenExchanger};
|
|
||||||
pub use session_service::SessionServiceImpl;
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
//! OAuth 2.0 authorization-code + PKCE flow use-case.
|
|
||||||
//!
|
|
||||||
//! `OAuthUseCase` orchestrates the standard PKCE-enhanced OAuth flow:
|
|
||||||
//!
|
|
||||||
//! 1. **`start_flow`** — generates a cryptographic PKCE code verifier,
|
|
||||||
//! derives its S256 challenge, creates a CSRF state token, persists
|
|
||||||
//! the verifier + state via `OAuthFlowStore`, and builds an
|
|
||||||
//! authorization URL with all required parameters.
|
|
||||||
//! 2. **`complete_flow`** — validates the returned `state` against the
|
|
||||||
//! stored value (CSRF check), reads the stored verifier, delegates
|
|
||||||
//! the token-code exchange to an injected `TokenExchanger`, and
|
|
||||||
//! persists the resulting `OAuthToken` via `OAuthRepository`.
|
|
||||||
//! 3. **`get_token`** — loads the stored OAuth token (if any).
|
|
||||||
//!
|
|
||||||
//! # Portability
|
|
||||||
//!
|
|
||||||
//! The service is generic over three injected dependencies:
|
|
||||||
//! - `R: OAuthRepository` — token persistence
|
|
||||||
//! - `S: OAuthFlowStore` — ephemeral flow state (verifier + CSRF state)
|
|
||||||
//! - `E: TokenExchanger` — the HTTP token-endpoint exchange
|
|
||||||
//!
|
|
||||||
//! This keeps all I/O and protocol-level concerns abstracted behind
|
|
||||||
//! port traits; the service itself contains only orchestration logic.
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing;
|
|
||||||
|
|
||||||
use zesdex_domain::auth::{OAuthConfig, OAuthRepository, OAuthToken, ServiceError};
|
|
||||||
|
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
|
||||||
use base64::Engine as _;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Port traits (defined here because they are specific to this use-case)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Persistence contract for ephemeral OAuth flow state.
|
|
||||||
///
|
|
||||||
/// Between `start_flow` and `complete_flow` the verifier and CSRF state
|
|
||||||
/// must survive across process boundaries (the user opens a browser, the
|
|
||||||
/// provider redirects back to a loopback listener on the next invocation).
|
|
||||||
///
|
|
||||||
/// Implementors store key-value pairs to disk or another durable medium
|
|
||||||
/// and clear them after a successful (or failed) flow completion.
|
|
||||||
pub trait OAuthFlowStore: Send + Sync {
|
|
||||||
/// Persist the PKCE code verifier and CSRF state token.
|
|
||||||
fn save_flow_state(
|
|
||||||
&self,
|
|
||||||
verifier: &str,
|
|
||||||
state: &str,
|
|
||||||
) -> Result<(), ServiceError>;
|
|
||||||
|
|
||||||
/// Load the stored PKCE code verifier.
|
|
||||||
fn load_verifier(&self) -> Result<String, ServiceError>;
|
|
||||||
|
|
||||||
/// Load the stored CSRF state token.
|
|
||||||
fn load_state(&self) -> Result<String, ServiceError>;
|
|
||||||
|
|
||||||
/// Clear stored flow state (verifier + state).
|
|
||||||
fn clear(&self) -> Result<(), ServiceError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Abstraction for exchanging an authorization code for tokens.
|
|
||||||
///
|
|
||||||
/// Implementors handle the HTTP POST to the provider's token endpoint
|
|
||||||
/// with the appropriate form-encoded parameters, parse the JSON
|
|
||||||
/// response, and return the extracted `OAuthToken`.
|
|
||||||
pub trait TokenExchanger: Send + Sync {
|
|
||||||
/// Exchange an authorization code for an access token.
|
|
||||||
///
|
|
||||||
/// ## Parameters
|
|
||||||
/// - `token_url` — the provider's token endpoint URL
|
|
||||||
/// - `client_id` — OAuth client identifier
|
|
||||||
/// - `client_secret` — optional client secret
|
|
||||||
/// - `redirect_uri` — must match the URI used in `start_flow`
|
|
||||||
/// - `code` — the authorization code from the provider's redirect
|
|
||||||
/// - `code_verifier` — the PKCE verifier from `start_flow`
|
|
||||||
fn exchange_code(
|
|
||||||
&self,
|
|
||||||
token_url: &str,
|
|
||||||
client_id: &str,
|
|
||||||
client_secret: Option<&str>,
|
|
||||||
redirect_uri: &str,
|
|
||||||
code: &str,
|
|
||||||
code_verifier: &str,
|
|
||||||
) -> Result<OAuthToken, ServiceError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// PKCE helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Generate a PKCE code-verifier and its S256 code-challenge.
|
|
||||||
///
|
|
||||||
/// Uses 32 cryptographically random bytes, base64url-encoded (no padding)
|
|
||||||
/// for the verifier, then SHA-256 hashes the verifier and base64url-encodes
|
|
||||||
/// the digest for the challenge. This satisfies the PKCE `S256` method
|
|
||||||
/// which requires a minimum verifier length of 43 characters.
|
|
||||||
fn generate_pkce_pair() -> (String, String) {
|
|
||||||
// 32 random bytes → 43 base64url chars (well above the 43-char PKCE
|
|
||||||
// minimum).
|
|
||||||
let mut bytes = [0u8; 32];
|
|
||||||
bytes[..16].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
|
|
||||||
bytes[16..].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
|
|
||||||
|
|
||||||
let verifier = URL_SAFE_NO_PAD.encode(&bytes);
|
|
||||||
let challenge = {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(verifier.as_bytes());
|
|
||||||
URL_SAFE_NO_PAD.encode(hasher.finalize())
|
|
||||||
};
|
|
||||||
(verifier, challenge)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a random CSRF state token (UUID-based, 36 chars).
|
|
||||||
fn generate_state_token() -> String {
|
|
||||||
uuid::Uuid::new_v4().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Service
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Concrete OAuth flow use-case.
|
|
||||||
///
|
|
||||||
/// Generic over three dependencies:
|
|
||||||
/// - `R` — token persistence (`OAuthRepository`)
|
|
||||||
/// - `S` — flow-state persistence (`OAuthFlowStore`)
|
|
||||||
/// - `E` — token-endpoint HTTP exchange (`TokenExchanger`)
|
|
||||||
pub struct OAuthUseCase<R, S, E> {
|
|
||||||
/// Repository for persisting / loading OAuth tokens.
|
|
||||||
pub token_repo: R,
|
|
||||||
/// Store for ephemeral flow state (verifier + CSRF state).
|
|
||||||
pub flow_store: S,
|
|
||||||
/// Token-endpoint HTTP exchanger.
|
|
||||||
pub token_exchanger: E,
|
|
||||||
/// File path for the token JSON file.
|
|
||||||
pub token_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S, E> {
|
|
||||||
/// Create a new OAuth use-case.
|
|
||||||
pub fn new(
|
|
||||||
token_repo: R,
|
|
||||||
flow_store: S,
|
|
||||||
token_exchanger: E,
|
|
||||||
token_path: PathBuf,
|
|
||||||
) -> Self {
|
|
||||||
OAuthUseCase {
|
|
||||||
token_repo,
|
|
||||||
flow_store,
|
|
||||||
token_exchanger,
|
|
||||||
token_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
|
|
||||||
zesdex_domain::auth::OAuthService for OAuthUseCase<R, S, E>
|
|
||||||
{
|
|
||||||
fn start_flow(
|
|
||||||
&self,
|
|
||||||
config: &OAuthConfig,
|
|
||||||
redirect_uri: &str,
|
|
||||||
) -> Result<(String, String), ServiceError> {
|
|
||||||
if config.auth_url.is_empty() {
|
|
||||||
return Err(ServiceError::InvalidConfig(
|
|
||||||
"OAuth auth_url is empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (verifier, challenge) = generate_pkce_pair();
|
|
||||||
let state = generate_state_token();
|
|
||||||
|
|
||||||
// Persist verifier + state so `complete_flow` can retrieve them.
|
|
||||||
self.flow_store.save_flow_state(&verifier, &state)?;
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
auth_url = %config.auth_url,
|
|
||||||
redirect_uri = %redirect_uri,
|
|
||||||
state_len = state.len(),
|
|
||||||
"starting OAuth flow",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut url = url::Url::parse(&config.auth_url)
|
|
||||||
.map_err(|e| {
|
|
||||||
ServiceError::InvalidConfig(format!(
|
|
||||||
"invalid auth_url '{}': {e}",
|
|
||||||
config.auth_url
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("response_type", "code")
|
|
||||||
.append_pair("client_id", &config.client_id)
|
|
||||||
.append_pair("redirect_uri", redirect_uri)
|
|
||||||
.append_pair("scope", &config.scopes.join(" "))
|
|
||||||
.append_pair("state", &state)
|
|
||||||
.append_pair("code_challenge_method", "S256")
|
|
||||||
.append_pair("code_challenge", &challenge);
|
|
||||||
|
|
||||||
Ok((url.to_string(), state))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complete_flow(
|
|
||||||
&self,
|
|
||||||
config: &OAuthConfig,
|
|
||||||
redirect_uri: &str,
|
|
||||||
code: &str,
|
|
||||||
state: &str,
|
|
||||||
) -> Result<OAuthToken, ServiceError> {
|
|
||||||
// CSRF check: validate the returned state against the stored value.
|
|
||||||
let expected_state = self.flow_store.load_state()?;
|
|
||||||
if expected_state != state {
|
|
||||||
return Err(ServiceError::StateMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the PKCE verifier that was saved in `start_flow`.
|
|
||||||
let verifier = self.flow_store.load_verifier()?;
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
token_url = %config.token_url,
|
|
||||||
code_len = code.len(),
|
|
||||||
"completing OAuth flow — exchanging code for token",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delegate the HTTP token exchange to the injected exchanger.
|
|
||||||
let token = self.token_exchanger.exchange_code(
|
|
||||||
&config.token_url,
|
|
||||||
&config.client_id,
|
|
||||||
config.client_secret.as_deref(),
|
|
||||||
redirect_uri,
|
|
||||||
code,
|
|
||||||
&verifier,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Persist the token and clean up flow state.
|
|
||||||
self.token_repo.save_token(&self.token_path, &token)?;
|
|
||||||
let _ = self.flow_store.clear();
|
|
||||||
|
|
||||||
Ok(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_token(&self) -> Result<Option<OAuthToken>, ServiceError> {
|
|
||||||
self.token_repo
|
|
||||||
.load_token(&self.token_path)
|
|
||||||
.map_err(ServiceError::Repository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
//! Session management use-case.
|
|
||||||
//!
|
|
||||||
//! `SessionServiceImpl` implements [`SessionService`] from the domain
|
|
||||||
//! layer by delegating CRUD operations to injected repository traits.
|
|
||||||
//!
|
|
||||||
//! # Flow
|
|
||||||
//!
|
|
||||||
//! - **`create_session`** — generates a UUID v4 id, creates a `Session`
|
|
||||||
//! entity with the given title, persists via `SessionRepository`.
|
|
||||||
//! - **`list_all`** — delegates to `SessionRepository::list_sessions`.
|
|
||||||
//! - **`archive_session`** — loads session, sets `archived = true`,
|
|
||||||
//! persists the updated entity.
|
|
||||||
//!
|
|
||||||
//! # Generics
|
|
||||||
//!
|
|
||||||
//! - `R: SessionRepository` — session CRUD persistence
|
|
||||||
//! - `L: SessionLockRepository` — session lock acquire/release
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use zesdex_domain::auth::{
|
|
||||||
ServiceError, Session, SessionId, SessionLockRepository, SessionRepository,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Concrete session service backed by injected repository implementations.
|
|
||||||
pub struct SessionServiceImpl<R: SessionRepository, L: SessionLockRepository> {
|
|
||||||
/// Repository for session CRUD operations.
|
|
||||||
pub session_repo: R,
|
|
||||||
/// Repository for session lock acquire/release.
|
|
||||||
pub lock_repo: L,
|
|
||||||
/// Base data directory passed to repository methods.
|
|
||||||
pub base_dir: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
|
|
||||||
/// Create a new session service with the given repositories and base
|
|
||||||
/// data directory.
|
|
||||||
pub fn new(session_repo: R, lock_repo: L, base_dir: PathBuf) -> Self {
|
|
||||||
SessionServiceImpl {
|
|
||||||
session_repo,
|
|
||||||
lock_repo,
|
|
||||||
base_dir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: SessionRepository, L: SessionLockRepository>
|
|
||||||
zesdex_domain::auth::SessionService for SessionServiceImpl<R, L>
|
|
||||||
{
|
|
||||||
fn create_session(&self, title: &str) -> Result<Session, ServiceError> {
|
|
||||||
let id = SessionId::new(&Uuid::new_v4().to_string())
|
|
||||||
.map_err(|e| ServiceError::Other(e))?;
|
|
||||||
let title_owned = if title.is_empty() {
|
|
||||||
"New Session".to_string()
|
|
||||||
} else {
|
|
||||||
title.to_string()
|
|
||||||
};
|
|
||||||
let session = Session::new(id.into_string(), title_owned);
|
|
||||||
tracing::debug!(session_id = %session.id, title = %session.title, "creating new session");
|
|
||||||
self.session_repo
|
|
||||||
.save_session(&self.base_dir, &session)?;
|
|
||||||
Ok(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_all(&self) -> Result<Vec<Session>, ServiceError> {
|
|
||||||
tracing::debug!("listing all sessions");
|
|
||||||
self.session_repo
|
|
||||||
.list_sessions(&self.base_dir)
|
|
||||||
.map_err(ServiceError::Repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> {
|
|
||||||
tracing::debug!(session_id = %id, "archiving session");
|
|
||||||
let mut session = self
|
|
||||||
.session_repo
|
|
||||||
.load_session(&self.base_dir, &id)?;
|
|
||||||
session.archived = true;
|
|
||||||
let millis = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_millis();
|
|
||||||
session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX);
|
|
||||||
self.session_repo
|
|
||||||
.save_session(&self.base_dir, &session)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
//! Conversation use-case implementation.
|
|
||||||
//!
|
|
||||||
//! `ConversationServiceImpl` implements [`ConversationService`] from the
|
|
||||||
//! domain layer. It is generic over `R: ConversationRepository`, delegating
|
|
||||||
//! all persistence to that adapter.
|
|
||||||
//!
|
|
||||||
//! # Flow
|
|
||||||
//!
|
|
||||||
//! Each method computes the session directory from the session ID, then
|
|
||||||
//! delegates the actual I/O to the injected `repo`. Error context is
|
|
||||||
//! added at this layer to identify which session caused the failure.
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing;
|
|
||||||
|
|
||||||
use zesdex_domain::cms::{Conversation, ConversationRepository, ServiceError};
|
|
||||||
use zesdex_domain::core::ChatMessage;
|
|
||||||
|
|
||||||
/// Service implementation for conversation CRUD operations.
|
|
||||||
///
|
|
||||||
/// Generic over `R: ConversationRepository` so the persistence layer
|
|
||||||
/// can be swapped without changing business logic.
|
|
||||||
pub struct ConversationServiceImpl<R> {
|
|
||||||
pub repo: R,
|
|
||||||
/// Base directory containing session subdirectories.
|
|
||||||
pub sessions_dir: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: ConversationRepository> ConversationServiceImpl<R> {
|
|
||||||
/// Create a new service with the given repository and sessions directory.
|
|
||||||
pub fn new(repo: R, sessions_dir: impl Into<PathBuf>) -> Self {
|
|
||||||
tracing::debug!("creating ConversationServiceImpl");
|
|
||||||
Self {
|
|
||||||
repo,
|
|
||||||
sessions_dir: sessions_dir.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute the session directory for a given session id.
|
|
||||||
fn session_dir(&self, session_id: &str) -> PathBuf {
|
|
||||||
self.sessions_dir.join(session_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
|
|
||||||
for ConversationServiceImpl<R>
|
|
||||||
{
|
|
||||||
fn load_conversation(&self, session_id: &str) -> Result<Conversation, ServiceError> {
|
|
||||||
tracing::debug!("loading conversation for session {session_id}");
|
|
||||||
let dir = self.session_dir(session_id);
|
|
||||||
self.repo.load(&dir).map_err(ServiceError::Repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError> {
|
|
||||||
tracing::debug!("saving conversation for session {}", conv.session_id);
|
|
||||||
let dir = self.session_dir(&conv.session_id);
|
|
||||||
self.repo.save(&dir, conv)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_message(
|
|
||||||
&self,
|
|
||||||
conv: &mut Conversation,
|
|
||||||
msg: ChatMessage,
|
|
||||||
) -> Result<(), ServiceError> {
|
|
||||||
tracing::debug!("adding message to session {}", conv.session_id);
|
|
||||||
conv.push(msg);
|
|
||||||
let dir = self.session_dir(&conv.session_id);
|
|
||||||
self.repo.save(&dir, conv)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
//! Memory use-case implementation.
|
|
||||||
//!
|
|
||||||
//! `MemoryServiceImpl` implements [`MemoryService`] from the domain
|
|
||||||
//! layer. It is generic over `R: MemoryRepository`, delegating all
|
|
||||||
//! persistence to that adapter.
|
|
||||||
//!
|
|
||||||
//! # Flow
|
|
||||||
//!
|
|
||||||
//! Each method delegates to the injected `repo` with the configured
|
|
||||||
//! `memory_dir`. Error context is added at this layer to identify which
|
|
||||||
//! memory operation failed.
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing;
|
|
||||||
|
|
||||||
use zesdex_domain::cms::{Memory, MemoryRepository, ServiceError};
|
|
||||||
|
|
||||||
/// Service implementation for memory CRUD operations.
|
|
||||||
///
|
|
||||||
/// Generic over `R: MemoryRepository` so the persistence layer can be
|
|
||||||
/// swapped without changing business logic.
|
|
||||||
pub struct MemoryServiceImpl<R> {
|
|
||||||
pub repo: R,
|
|
||||||
/// Base directory for memory storage files.
|
|
||||||
pub memory_dir: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: MemoryRepository> MemoryServiceImpl<R> {
|
|
||||||
/// Create a new service with the given repository and memory directory.
|
|
||||||
pub fn new(repo: R, memory_dir: impl Into<PathBuf>) -> Self {
|
|
||||||
tracing::debug!("creating MemoryServiceImpl");
|
|
||||||
Self {
|
|
||||||
repo,
|
|
||||||
memory_dir: memory_dir.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: MemoryRepository> zesdex_domain::cms::MemoryService for MemoryServiceImpl<R> {
|
|
||||||
fn list_memories(&self) -> Result<Vec<String>, ServiceError> {
|
|
||||||
tracing::debug!("listing memories from {:?}", self.memory_dir);
|
|
||||||
self.repo
|
|
||||||
.list(&self.memory_dir)
|
|
||||||
.map_err(ServiceError::Repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_memory(&self, memory: &Memory) -> Result<(), ServiceError> {
|
|
||||||
tracing::debug!("saving memory '{}'", memory.name);
|
|
||||||
self.repo.save(&self.memory_dir, memory)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_memory(&self, name: &str) -> Result<(), ServiceError> {
|
|
||||||
tracing::debug!("deleting memory '{name}'");
|
|
||||||
self.repo
|
|
||||||
.delete(&self.memory_dir, name)
|
|
||||||
.map_err(ServiceError::Repository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
//! CMS use-case implementations.
|
|
||||||
//!
|
|
||||||
//! Contains concrete service types that implement the domain's CMS
|
|
||||||
//! service traits by coordinating injected repository dependencies.
|
|
||||||
//!
|
|
||||||
//! # Use Cases
|
|
||||||
//!
|
|
||||||
//! - [`conversation_service`] — `ConversationServiceImpl`: conversation CRUD
|
|
||||||
//! - [`memory_service`] — `MemoryServiceImpl`: long-term memory management
|
|
||||||
//! - [`settings_service`] — `SettingsServiceImpl`: settings & app-config management
|
|
||||||
|
|
||||||
pub mod conversation_service;
|
|
||||||
pub mod memory_service;
|
|
||||||
pub mod settings_service;
|
|
||||||
|
|
||||||
pub use conversation_service::ConversationServiceImpl;
|
|
||||||
pub use memory_service::MemoryServiceImpl;
|
|
||||||
pub use settings_service::SettingsServiceImpl;
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
//! Settings and app-config use-case implementation.
|
|
||||||
//!
|
|
||||||
//! `SettingsServiceImpl` implements [`SettingsService`] from the domain
|
|
||||||
//! layer. It is generic over `S: SettingsRepository` and `C: AppConfigRepository`,
|
|
||||||
//! delegating persistence to those adapters.
|
|
||||||
//!
|
|
||||||
//! # Flow
|
|
||||||
//!
|
|
||||||
//! Each method delegates to the appropriate injected repository with the
|
|
||||||
//! configured `base_dir`. The `update_provider` method coordinates between
|
|
||||||
//! both repositories: load app config → mutate provider map → save app config.
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing;
|
|
||||||
|
|
||||||
use zesdex_domain::cms::{
|
|
||||||
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings,
|
|
||||||
SettingsRepository,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Service implementation for settings and app-config operations.
|
|
||||||
///
|
|
||||||
/// Generic over `S: SettingsRepository` and `C: AppConfigRepository` so
|
|
||||||
/// the persistence layer can be swapped without changing business logic.
|
|
||||||
pub struct SettingsServiceImpl<S, C> {
|
|
||||||
pub settings_repo: S,
|
|
||||||
pub app_config_repo: C,
|
|
||||||
pub base_dir: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
|
|
||||||
/// Create a new service with the given repositories and base directory.
|
|
||||||
pub fn new(
|
|
||||||
settings_repo: S,
|
|
||||||
app_config_repo: C,
|
|
||||||
base_dir: impl Into<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
tracing::debug!("creating SettingsServiceImpl");
|
|
||||||
Self {
|
|
||||||
settings_repo,
|
|
||||||
app_config_repo,
|
|
||||||
base_dir: base_dir.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: SettingsRepository, C: AppConfigRepository>
|
|
||||||
zesdex_domain::cms::SettingsService for SettingsServiceImpl<S, C>
|
|
||||||
{
|
|
||||||
fn load_settings(&self) -> Result<Settings, ServiceError> {
|
|
||||||
tracing::debug!("loading settings");
|
|
||||||
self.settings_repo
|
|
||||||
.load(&self.base_dir)
|
|
||||||
.map_err(ServiceError::Repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_settings(&self, settings: &Settings) -> Result<(), ServiceError> {
|
|
||||||
tracing::debug!("saving settings");
|
|
||||||
self.settings_repo.save(&self.base_dir, settings)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_provider(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
config: &ProviderConfig,
|
|
||||||
) -> Result<(), ServiceError> {
|
|
||||||
tracing::debug!("updating provider '{name}'");
|
|
||||||
let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?;
|
|
||||||
app_config
|
|
||||||
.providers
|
|
||||||
.insert(name.to_string(), config.clone());
|
|
||||||
self.app_config_repo.save(&self.base_dir, &app_config)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
//! # Zesdex Application Layer
|
|
||||||
//!
|
|
||||||
//! Defines port traits (interfaces) and use-case implementations for the
|
|
||||||
//! Zesdex application. This crate depends **only** on the domain crate;
|
|
||||||
//! it has no knowledge of infrastructure or interface adapters.
|
|
||||||
//!
|
|
||||||
//! ## Architecture
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! apps/application/src/
|
|
||||||
//! ├── lib.rs — crate root, re-exports
|
|
||||||
//! ├── ports/ — Port traits (interfaces to external services)
|
|
||||||
//! │ ├── provider.rs -- ProviderService (LLM chat completion)
|
|
||||||
//! │ ├── password.rs -- PasswordService (hash / verify)
|
|
||||||
//! │ ├── token.rs -- TokenService (JWT create / verify)
|
|
||||||
//! │ └── authentication.rs -- AuthService (combined auth)
|
|
||||||
//! ├── auth/ — Auth use-cases
|
|
||||||
//! │ ├── oauth_service.rs -- OAuth 2.0 PKCE flow
|
|
||||||
//! │ └── session_service.rs -- Session CRUD lifecycle
|
|
||||||
//! └── cms/ — CMS use-cases
|
|
||||||
//! ├── conversation_service.rs -- Conversation CRUD
|
|
||||||
//! ├── memory_service.rs -- Long-term memory management
|
|
||||||
//! └── settings_service.rs -- Settings & app-config management
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## Key Design Principle
|
|
||||||
//!
|
|
||||||
//! Application services are generic over their repository/port dependencies.
|
|
||||||
//! Concrete implementations are injected at the composition root, keeping
|
|
||||||
//! the use-case logic independent of any specific persistence or infrastructure
|
|
||||||
//! technology.
|
|
||||||
|
|
||||||
pub mod auth;
|
|
||||||
pub mod cms;
|
|
||||||
pub mod ports;
|
|
||||||
|
|
||||||
// Re-export port traits for ergonomic access.
|
|
||||||
pub use ports::*;
|
|
||||||
|
|
||||||
// Re-export auth use-cases.
|
|
||||||
pub use auth::{
|
|
||||||
oauth_service::{OAuthFlowStore, OAuthUseCase, TokenExchanger},
|
|
||||||
session_service::SessionServiceImpl,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-export CMS use-cases.
|
|
||||||
pub use cms::{
|
|
||||||
conversation_service::ConversationServiceImpl,
|
|
||||||
memory_service::MemoryServiceImpl,
|
|
||||||
settings_service::SettingsServiceImpl,
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//! AuthService port — combined authentication operations.
|
|
||||||
//!
|
|
||||||
//! Defines a high-level authentication trait that composes password
|
|
||||||
//! verification and token generation into a single use-case boundary.
|
|
||||||
//! Implementations delegate to the injected `PasswordService` and
|
|
||||||
//! `TokenService` adapters.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
/// High-level authentication service combining password verification
|
|
||||||
/// and token issuance (login flow).
|
|
||||||
///
|
|
||||||
/// # Flow
|
|
||||||
///
|
|
||||||
/// 1. **`authenticate`** — verify a subject's password against a stored hash.
|
|
||||||
/// 2. **`issue_tokens`** — generate an access + refresh token pair for a subject.
|
|
||||||
///
|
|
||||||
/// Implementations are generic over `PasswordService` and `TokenService`
|
|
||||||
/// port traits.
|
|
||||||
pub trait AuthService: Send + Sync {
|
|
||||||
/// Authenticate a user by verifying a password against a stored hash.
|
|
||||||
///
|
|
||||||
/// Returns `true` if the password matches, `false` otherwise.
|
|
||||||
fn authenticate(
|
|
||||||
&self,
|
|
||||||
password: &str,
|
|
||||||
hash: &str,
|
|
||||||
) -> impl Future<Output = Result<bool>> + Send;
|
|
||||||
|
|
||||||
/// Issue a new access + refresh token pair for the given subject.
|
|
||||||
///
|
|
||||||
/// Returns `(access_token, refresh_token)`.
|
|
||||||
fn issue_tokens(&self, sub: &str) -> Result<(String, String)>;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
//! Port traits — interfaces for external / infrastructure services.
|
|
||||||
//!
|
|
||||||
//! These traits define the boundaries between the application layer and
|
|
||||||
//! the outside world. Infrastructure adapters implement these traits;
|
|
||||||
//! the application layer depends only on the trait definitions.
|
|
||||||
//!
|
|
||||||
//! # Ports
|
|
||||||
//!
|
|
||||||
//! - [`provider`] — `ProviderService`: LLM chat completion (streaming + non-streaming)
|
|
||||||
//! - [`password`] — `PasswordService`: password hashing and verification
|
|
||||||
//! - [`token`] — `TokenService`: JWT access/refresh token generation and verification
|
|
||||||
//! - [`authentication`] — `AuthService`: combined authentication operations
|
|
||||||
|
|
||||||
pub mod authentication;
|
|
||||||
pub mod password;
|
|
||||||
pub mod provider;
|
|
||||||
pub mod token;
|
|
||||||
|
|
||||||
pub use authentication::AuthService;
|
|
||||||
pub use password::PasswordService;
|
|
||||||
pub use provider::ProviderService;
|
|
||||||
pub use token::TokenService;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//! PasswordService port — password hashing and verification abstraction.
|
|
||||||
//!
|
|
||||||
//! Defines the trait that password-hashing adapters (argon2, bcrypt, etc.)
|
|
||||||
//! implement. The application layer depends only on this trait, never on
|
|
||||||
//! a concrete hashing library.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
/// Abstraction for password hashing and verification.
|
|
||||||
///
|
|
||||||
/// Implementors handle the actual hashing algorithm (argon2, bcrypt, etc.)
|
|
||||||
/// and parameter selection. The trait is `Send + Sync` for use in async
|
|
||||||
/// service layers.
|
|
||||||
pub trait PasswordService: Send + Sync {
|
|
||||||
/// Hash a plaintext password and return the encoded hash string
|
|
||||||
/// (suitable for storage in a credential store).
|
|
||||||
fn hash(&self, password: &str) -> impl Future<Output = Result<String>> + Send;
|
|
||||||
|
|
||||||
/// Verify a plaintext password against a previously-hashed string.
|
|
||||||
///
|
|
||||||
/// Returns `true` if the password matches the hash, `false` otherwise.
|
|
||||||
fn verify(&self, password: &str, hash: &str) -> impl Future<Output = Result<bool>> + Send;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
//! ProviderService port — LLM chat completion provider abstraction.
|
|
||||||
//!
|
|
||||||
//! Defines the trait that HTTP-based provider clients (OpenAI, Anthropic,
|
|
||||||
//! etc.) implement. Supports both non-streaming and SSE-streaming chat
|
|
||||||
//! completion requests.
|
|
||||||
//!
|
|
||||||
//! # Flow
|
|
||||||
//!
|
|
||||||
//! 1. Caller builds a message list and optional tool definitions.
|
|
||||||
//! 2. `chat` sends a non-streaming request and returns the full response.
|
|
||||||
//! 3. `chat_stream` sends a streaming request and invokes `on_event` for
|
|
||||||
//! each parsed `StreamEvent` as it arrives, then returns the assembled
|
|
||||||
//! message and usage.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
|
|
||||||
|
|
||||||
/// Abstraction for an LLM provider chat-completion service.
|
|
||||||
///
|
|
||||||
/// Both methods accept a message list, optional tool definitions, and
|
|
||||||
/// generation parameters. Implementors handle authentication, HTTP
|
|
||||||
/// transport, retry logic, and response parsing internally.
|
|
||||||
///
|
|
||||||
/// # Send + Sync
|
|
||||||
///
|
|
||||||
/// This trait is `Send + Sync` so it can be shared across async tasks
|
|
||||||
/// and injected into service structs that require thread safety.
|
|
||||||
pub trait ProviderService: Send + Sync {
|
|
||||||
/// Send a non-streaming chat completion request.
|
|
||||||
///
|
|
||||||
/// Returns the assistant's `ChatMessage` and optional token usage
|
|
||||||
/// `(prompt_tokens, completion_tokens)`.
|
|
||||||
fn chat(
|
|
||||||
&self,
|
|
||||||
messages: &[ChatMessage],
|
|
||||||
tools: Option<Vec<ToolDef>>,
|
|
||||||
max_tokens: Option<u32>,
|
|
||||||
temperature: Option<f32>,
|
|
||||||
) -> impl Future<Output = Result<(ChatMessage, Option<(u64, u64)>)>> + Send;
|
|
||||||
|
|
||||||
/// Send a streaming chat completion request.
|
|
||||||
///
|
|
||||||
/// `on_event` is called for every parsed SSE event and returns `false`
|
|
||||||
/// to signal abort (caller cancellation). Returns the fully assembled
|
|
||||||
/// assistant message and optional usage once the stream completes.
|
|
||||||
fn chat_stream(
|
|
||||||
&self,
|
|
||||||
messages: &[ChatMessage],
|
|
||||||
tools: Option<Vec<ToolDef>>,
|
|
||||||
max_tokens: Option<u32>,
|
|
||||||
temperature: Option<f32>,
|
|
||||||
on_event: Box<dyn FnMut(&StreamEvent) -> bool + Send>,
|
|
||||||
) -> impl Future<Output = Result<(ChatMessage, Option<(u64, u64)>)>> + Send;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
//! TokenService port — JWT access and refresh token abstraction.
|
|
||||||
//!
|
|
||||||
//! Defines the trait that JWT adapter implementations provide. Covers
|
|
||||||
//! token generation (pair of access + refresh tokens) and access token
|
|
||||||
//! verification (returns the subject claim).
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
/// Abstraction for JWT-based token generation and verification.
|
|
||||||
///
|
|
||||||
/// Implementors handle signing key management, token serialisation,
|
|
||||||
/// and expiry validation. The trait is `Send + Sync` for use across
|
|
||||||
/// thread boundaries.
|
|
||||||
pub trait TokenService: Send + Sync {
|
|
||||||
/// Generate an access + refresh token pair for the given subject
|
|
||||||
/// identifier.
|
|
||||||
///
|
|
||||||
/// Returns `(access_token, refresh_token)`.
|
|
||||||
fn generate_tokens(&self, sub: &str) -> Result<(String, String)>;
|
|
||||||
|
|
||||||
/// Verify an access token and return the embedded subject claim.
|
|
||||||
///
|
|
||||||
/// Returns `Err` if the token is expired, malformed, or has an
|
|
||||||
/// invalid signature.
|
|
||||||
fn verify_access_token(&self, token: &str) -> Result<String>;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "zesdex-bootstrap"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
# Bootstrap binary — seeds initial system data (permissions, roles,
|
|
||||||
# admin user) idempotently. Run once after first deployment.
|
|
||||||
[[bin]]
|
|
||||||
name = "bootstrap"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
zesdex-domain = { path = "../domain" }
|
|
||||||
zesdex-application = { path = "../application" }
|
|
||||||
zesdex-infrastructure = { path = "../infrastructure" }
|
|
||||||
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
chrono.workspace = true
|
|
||||||
uuid.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
tracing.workspace = true
|
|
||||||
dirs.workspace = true
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
//! Bootstrap library — shared utilities for the bootstrap binary.
|
|
||||||
//! The main entry point is in `main.rs`.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
//! Bootstrap binary — seeds initial system data idempotently.
|
|
||||||
//!
|
|
||||||
//! Creates default permissions, roles, and admin user if they don't
|
|
||||||
//! already exist. Run once after first deployment.
|
|
||||||
//!
|
|
||||||
//! Usage: cargo run --bin bootstrap
|
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
|
||||||
println!("Zesdex Bootstrap — seeding initial data...");
|
|
||||||
|
|
||||||
let store = zesdex_domain::core::Store::new();
|
|
||||||
store.ensure_dirs()?;
|
|
||||||
|
|
||||||
// Seed default settings if not present
|
|
||||||
let settings_path = store.base_dir.join("settings.json");
|
|
||||||
if !settings_path.exists() {
|
|
||||||
let settings = zesdex_domain::cms::Settings::default();
|
|
||||||
let content = serde_json::to_string_pretty(&settings)?;
|
|
||||||
std::fs::write(&settings_path, content)?;
|
|
||||||
println!(" ✓ Default settings created");
|
|
||||||
} else {
|
|
||||||
println!(" · Settings already exist, skipping");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed default app config if not present
|
|
||||||
let config_path = store.base_dir.join("app_config.json");
|
|
||||||
if !config_path.exists() {
|
|
||||||
let config = zesdex_domain::cms::AppConfig::default();
|
|
||||||
let content = serde_json::to_string_pretty(&config)?;
|
|
||||||
std::fs::write(&config_path, content)?;
|
|
||||||
println!(" ✓ Default app_config created");
|
|
||||||
} else {
|
|
||||||
println!(" · App config already exists, skipping");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Bootstrap complete.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "zesdex-domain"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
# Domain layer — PURE entities, value objects, repository/service traits.
|
|
||||||
# Zero framework dependencies. Only serde for serialization, chrono for
|
|
||||||
# timestamps, uuid for identity.
|
|
||||||
[dependencies]
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
chrono.workspace = true
|
|
||||||
uuid.workspace = true
|
|
||||||
base64.workspace = true
|
|
||||||
sha2.workspace = true
|
|
||||||
url.workspace = true
|
|
||||||
libc.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
tracing.workspace = true
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//! Command types for IAM domain operations.
|
|
||||||
//!
|
|
||||||
//! Following the `NewXxx` / command pattern from clean architecture,
|
|
||||||
//! these types encapsulate the input data for create/update operations
|
|
||||||
//! on domain entities. They decouple presentation DTOs from the entity
|
|
||||||
//! mutation surface and provide a clear boundary for validation.
|
|
||||||
|
|
||||||
/// Command to create a new session.
|
|
||||||
///
|
|
||||||
/// Carries only the data needed to construct a session entity — the
|
|
||||||
/// service generates the UUID and timestamp internally.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct NewSession {
|
|
||||||
/// Human-readable session title.
|
|
||||||
pub title: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for NewSession {
|
|
||||||
fn from(title: String) -> Self {
|
|
||||||
Self { title }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for NewSession {
|
|
||||||
fn from(title: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
title: title.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
//! Domain error types for the IAM (auth) module.
|
|
||||||
//!
|
|
||||||
//! Typed error enums replace `anyhow::Result` in domain traits and
|
|
||||||
//! application services, enabling callers to match on specific error
|
|
||||||
//! variants (e.g. `NotFound` vs `Conflict`) rather than string-checking.
|
|
||||||
//!
|
|
||||||
//! # Components
|
|
||||||
//!
|
|
||||||
//! - [`RepositoryError`] — persistence-layer errors (not found, conflict, I/O)
|
|
||||||
//! - [`ServiceError`] — use-case / orchestration errors (config, state
|
|
||||||
//! mismatch, provider failures)
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::error::DomainError;
|
|
||||||
|
|
||||||
/// Shared repository error type for IAM persistence operations.
|
|
||||||
pub type RepositoryError = DomainError;
|
|
||||||
|
|
||||||
/// Errors from service / use-case operations in the IAM domain.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ServiceError {
|
|
||||||
/// A repository operation failed.
|
|
||||||
Repository(DomainError),
|
|
||||||
/// The provided configuration is invalid.
|
|
||||||
InvalidConfig(String),
|
|
||||||
/// OAuth state mismatch — possible CSRF attack.
|
|
||||||
StateMismatch,
|
|
||||||
/// The OAuth provider returned an error.
|
|
||||||
OAuthProvider(String),
|
|
||||||
/// A generic error with a message.
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomainError> for ServiceError {
|
|
||||||
fn from(err: DomainError) -> Self {
|
|
||||||
ServiceError::Repository(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ServiceError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
ServiceError::Repository(err) => write!(f, "repository error: {err}"),
|
|
||||||
ServiceError::InvalidConfig(msg) => write!(f, "invalid configuration: {msg}"),
|
|
||||||
ServiceError::StateMismatch => {
|
|
||||||
write!(f, "OAuth state mismatch — possible CSRF attack")
|
|
||||||
}
|
|
||||||
ServiceError::OAuthProvider(msg) => write!(f, "OAuth provider error: {msg}"),
|
|
||||||
ServiceError::Other(msg) => write!(f, "{msg}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for ServiceError {
|
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
||||||
match self {
|
|
||||||
ServiceError::Repository(err) => Some(err),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//! IAM Session re-export.
|
|
||||||
//!
|
|
||||||
//! Re-exports `Session` from the auth module for consistent IAM-boundary
|
|
||||||
//! imports. Consumers of the IAM module import `Session` from here rather
|
|
||||||
//! than from the core session module directly, keeping the dependency
|
|
||||||
//! internal and allowing the IAM crate to own its domain vocabulary.
|
|
||||||
|
|
||||||
pub use super::session::Session;
|
|
||||||
|
|
||||||
/// Alias for `Session` used in IAM contexts to distinguish from other
|
|
||||||
/// session types in the system.
|
|
||||||
pub type IamSession = Session;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
//! Authentication domain entities, commands, errors, and repository/service traits.
|
|
||||||
//!
|
|
||||||
//! Combines the session types from `zesdex-entities` (auth sub-module) with the
|
|
||||||
//! IAM domain types (commands, OAuth, repository/service traits) from `zesdex-iam`.
|
|
||||||
//!
|
|
||||||
//! # Sub-modules
|
|
||||||
//!
|
|
||||||
//! - [`session`] — `Session` entity (session metadata)
|
|
||||||
//! - [`session_id`] — `SessionId` value object (validated newtype)
|
|
||||||
//! - [`session_lock`] — `SessionLock` RAII guard (PID-file lock)
|
|
||||||
//! - [`oauth`] — `OAuthToken`, `OAuthConfig` entities
|
|
||||||
//! - [`iam_session`] — Re-export of `Session` for IAM-boundary consistency
|
|
||||||
//! - [`commands`] — `NewSession` command type
|
|
||||||
//! - [`error`] — `RepositoryError`, `ServiceError` types
|
|
||||||
//! - [`repository`] — `SessionRepository`, `SessionLockRepository`, `OAuthRepository`
|
|
||||||
//! - [`service`] — `SessionService`, `OAuthService` traits
|
|
||||||
|
|
||||||
pub mod commands;
|
|
||||||
pub mod error;
|
|
||||||
pub mod iam_session;
|
|
||||||
pub mod oauth;
|
|
||||||
pub mod repository;
|
|
||||||
pub mod service;
|
|
||||||
pub mod session;
|
|
||||||
pub mod session_id;
|
|
||||||
pub mod session_lock;
|
|
||||||
|
|
||||||
pub use commands::NewSession;
|
|
||||||
pub use error::{RepositoryError, ServiceError};
|
|
||||||
pub use iam_session::IamSession;
|
|
||||||
pub use oauth::{OAuthConfig, OAuthToken};
|
|
||||||
pub use repository::{OAuthRepository, SessionLockRepository, SessionRepository};
|
|
||||||
pub use service::{OAuthService, SessionService};
|
|
||||||
pub use session::Session;
|
|
||||||
pub use session_id::SessionId;
|
|
||||||
pub use session_lock::SessionLock;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
//! Pure OAuth entities — no HTTP or persistence logic.
|
|
||||||
//!
|
|
||||||
//! # Components
|
|
||||||
//!
|
|
||||||
//! - [`OAuthToken`] — access token with optional refresh token, epoch expiry
|
|
||||||
//! - [`OAuthConfig`] — provider configuration (auth URL, token URL, client id,
|
|
||||||
//! optional client secret, scopes)
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// An OAuth 2.0 access token with optional refresh token and absolute
|
|
||||||
/// expiry time (epoch seconds).
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct OAuthToken {
|
|
||||||
/// The OAuth 2.0 access token string.
|
|
||||||
pub access_token: String,
|
|
||||||
/// Optional refresh token for long-lived access.
|
|
||||||
pub refresh_token: Option<String>,
|
|
||||||
/// Absolute expiry timestamp (epoch seconds since UNIX_EPOCH).
|
|
||||||
pub expires_at: u64,
|
|
||||||
/// Token type, e.g. `"Bearer"`.
|
|
||||||
pub token_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Static configuration for an OAuth provider.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct OAuthConfig {
|
|
||||||
/// Authorization endpoint URL.
|
|
||||||
pub auth_url: String,
|
|
||||||
/// Token exchange endpoint URL.
|
|
||||||
pub token_url: String,
|
|
||||||
/// OAuth client identifier.
|
|
||||||
pub client_id: String,
|
|
||||||
/// Optional client secret (not all flows require it).
|
|
||||||
pub client_secret: Option<String>,
|
|
||||||
/// Space-separated list of requested scopes.
|
|
||||||
pub scopes: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OAuthConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
OAuthConfig {
|
|
||||||
auth_url: String::new(),
|
|
||||||
token_url: String::new(),
|
|
||||||
client_id: String::new(),
|
|
||||||
client_secret: None,
|
|
||||||
scopes: vec![
|
|
||||||
"openid".to_string(),
|
|
||||||
"profile".to_string(),
|
|
||||||
"email".to_string(),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
//! Repository trait definitions (pure — no impls, no concrete persistence).
|
|
||||||
//!
|
|
||||||
//! Defines the repository contracts that infrastructure adapters implement.
|
|
||||||
//! Following clean architecture, domain code depends only on these traits,
|
|
||||||
//! not on concrete persistence libraries.
|
|
||||||
//!
|
|
||||||
//! # Traits
|
|
||||||
//!
|
|
||||||
//! - [`SessionRepository`] — CRUD for session metadata
|
|
||||||
//! - [`SessionLockRepository`] — acquire/release/liveness for session locks
|
|
||||||
//! - [`OAuthRepository`] — persist/load OAuth tokens
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::auth::error::RepositoryError;
|
|
||||||
use crate::auth::oauth::OAuthToken;
|
|
||||||
use crate::auth::session::Session;
|
|
||||||
use crate::auth::session_id::SessionId;
|
|
||||||
|
|
||||||
/// Repository for loading, saving, listing, and deleting sessions.
|
|
||||||
pub trait SessionRepository {
|
|
||||||
/// List all loadable sessions under `<base_dir>/sessions/`.
|
|
||||||
fn list_sessions(&self, base_dir: &Path) -> Result<Vec<Session>, RepositoryError>;
|
|
||||||
|
|
||||||
/// Load a single session by id.
|
|
||||||
fn load_session(&self, base_dir: &Path, id: &SessionId) -> Result<Session, RepositoryError>;
|
|
||||||
|
|
||||||
/// Save a session's metadata to disk.
|
|
||||||
fn save_session(&self, base_dir: &Path, session: &Session) -> Result<(), RepositoryError>;
|
|
||||||
|
|
||||||
/// Delete a session directory and all its contents.
|
|
||||||
fn delete_session(&self, base_dir: &Path, id: &SessionId) -> Result<(), RepositoryError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Repository for per-session PID-file advisory locks.
|
|
||||||
pub trait SessionLockRepository {
|
|
||||||
/// Try to acquire the lock for a session directory.
|
|
||||||
/// Returns `true` if the lock was acquired, `false` if another live
|
|
||||||
/// process holds it.
|
|
||||||
fn try_lock(&self, session_dir: &Path) -> Result<bool, RepositoryError>;
|
|
||||||
|
|
||||||
/// Release the lock by removing the lock file.
|
|
||||||
fn unlock(&self, session_dir: &Path) -> Result<(), RepositoryError>;
|
|
||||||
|
|
||||||
/// Check whether a process with the given PID is alive.
|
|
||||||
fn is_alive(&self, pid: u32) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Repository for persisting and loading OAuth tokens.
|
|
||||||
pub trait OAuthRepository {
|
|
||||||
/// Persist an OAuth token to a JSON file.
|
|
||||||
fn save_token(&self, path: &Path, token: &OAuthToken) -> Result<(), RepositoryError>;
|
|
||||||
|
|
||||||
/// Load an OAuth token from a JSON file, returning `None` if the file
|
|
||||||
/// does not exist.
|
|
||||||
fn load_token(&self, path: &Path) -> Result<Option<OAuthToken>, RepositoryError>;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
//! Service trait definitions — use-case interfaces for session management
|
|
||||||
//! and OAuth flows.
|
|
||||||
//!
|
|
||||||
//! These traits define the boundary between the application orchestration
|
|
||||||
//! layer and the domain. Implementations live in the application layer.
|
|
||||||
//!
|
|
||||||
//! # Traits
|
|
||||||
//!
|
|
||||||
//! - [`SessionService`] — create, list, archive sessions
|
|
||||||
//! - [`OAuthService`] — start PKCE flow, complete code exchange, retrieve token
|
|
||||||
|
|
||||||
use crate::auth::error::ServiceError;
|
|
||||||
use crate::auth::oauth::{OAuthConfig, OAuthToken};
|
|
||||||
use crate::auth::session::Session;
|
|
||||||
use crate::auth::session_id::SessionId;
|
|
||||||
|
|
||||||
/// Session management use-case boundary.
|
|
||||||
pub trait SessionService {
|
|
||||||
/// Create a new session with a generated UUID and the given title.
|
|
||||||
fn create_session(&self, title: &str) -> Result<Session, ServiceError>;
|
|
||||||
|
|
||||||
/// List all available sessions.
|
|
||||||
fn list_all(&self) -> Result<Vec<Session>, ServiceError>;
|
|
||||||
|
|
||||||
/// Archive a session by id (sets `archived = true`).
|
|
||||||
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// OAuth flow use-case boundary.
|
|
||||||
pub trait OAuthService {
|
|
||||||
/// Start an OAuth authorization-code + PKCE flow for the given
|
|
||||||
/// `redirect_uri` (the caller is responsible for actually listening on
|
|
||||||
/// it — e.g. a bound `LoopbackServer`). Returns `(auth_url, state)`:
|
|
||||||
/// the URL to send the user to, and the CSRF state token that must be
|
|
||||||
/// passed back into `complete_flow` unchanged.
|
|
||||||
fn start_flow(
|
|
||||||
&self,
|
|
||||||
config: &OAuthConfig,
|
|
||||||
redirect_uri: &str,
|
|
||||||
) -> Result<(String, String), ServiceError>;
|
|
||||||
|
|
||||||
/// Complete the OAuth flow: validates `state` against the value
|
|
||||||
/// persisted during `start_flow` (bailing on mismatch — this is the
|
|
||||||
/// CSRF check), then exchanges `code` for a token using the same
|
|
||||||
/// `redirect_uri` passed to `start_flow`.
|
|
||||||
fn complete_flow(
|
|
||||||
&self,
|
|
||||||
config: &OAuthConfig,
|
|
||||||
redirect_uri: &str,
|
|
||||||
code: &str,
|
|
||||||
state: &str,
|
|
||||||
) -> Result<OAuthToken, ServiceError>;
|
|
||||||
|
|
||||||
/// Retrieve the currently stored OAuth token (if any).
|
|
||||||
fn get_token(&self) -> Result<Option<OAuthToken>, ServiceError>;
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
//! Session metadata: id, title, workspace roots, and message/token counts,
|
|
||||||
//! persisted as `session.json` per session directory.
|
|
||||||
//!
|
|
||||||
//! # Flow
|
|
||||||
//!
|
|
||||||
//! Created via [`Session::new`] → mutated in-memory → persisted via repository.
|
|
||||||
//!
|
|
||||||
//! # Components
|
|
||||||
//!
|
|
||||||
//! - `Session` struct — fields for all session metadata
|
|
||||||
//! - `new` — timestamped constructor
|
|
||||||
//! - `session_dir` / `conversation_path` — pure path computation
|
|
||||||
use chrono::Utc;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
/// Metadata for one conversation session (distinct from the message
|
|
||||||
/// history itself, which lives in `Conversation`/the msglog).
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Session {
|
|
||||||
/// Unique session identifier (validated against path traversal in `load`).
|
|
||||||
pub id: String,
|
|
||||||
/// Epoch-millis timestamp of creation (`Utc::now().timestamp_millis()`).
|
|
||||||
pub created_at: i64,
|
|
||||||
/// Epoch-millis timestamp of last update.
|
|
||||||
pub updated_at: i64,
|
|
||||||
/// Human-readable title for the conversation.
|
|
||||||
pub title: String,
|
|
||||||
/// Model identifier string, e.g. `"anthropic/claude-opus-4-8"`.
|
|
||||||
pub model: String,
|
|
||||||
/// Workspace root directories associated with this session.
|
|
||||||
pub workspace_roots: Vec<PathBuf>,
|
|
||||||
/// Running count of messages in the conversation.
|
|
||||||
pub message_count: u32,
|
|
||||||
/// Running count of tokens consumed.
|
|
||||||
pub token_count: u32,
|
|
||||||
/// Soft-delete flag — archived sessions are hidden from the default list.
|
|
||||||
pub archived: bool,
|
|
||||||
/// Optional AI-generated conversation summary (used for compact context).
|
|
||||||
pub summary: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Session {
|
|
||||||
/// Create a new session with the given id/title, defaulting the
|
|
||||||
/// model, workspace root (current dir), and counters.
|
|
||||||
pub fn new(id: String, title: String) -> Self {
|
|
||||||
let now = Utc::now().timestamp_millis();
|
|
||||||
Session {
|
|
||||||
id,
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
title,
|
|
||||||
model: "anthropic/claude-opus-4-8".to_string(),
|
|
||||||
workspace_roots: vec![std::env::current_dir().unwrap_or_default()],
|
|
||||||
message_count: 0,
|
|
||||||
token_count: 0,
|
|
||||||
archived: false,
|
|
||||||
summary: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute this session's directory under `<base_dir>/sessions/<id>`.
|
|
||||||
pub fn session_dir(&self, base_dir: &Path) -> PathBuf {
|
|
||||||
base_dir.join("sessions").join(&self.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute this session's `conversation.json` path.
|
|
||||||
pub fn conversation_path(&self, base_dir: &Path) -> PathBuf {
|
|
||||||
self.session_dir(base_dir).join("conversation.json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
//! Validated session identifier newtype.
|
|
||||||
//!
|
|
||||||
//! [`SessionId`] wraps a `String` that has been checked for path-traversal
|
|
||||||
//! characters. Construction via `SessionId::new(str)` validates the input
|
|
||||||
//! once; the guarantee is then enforced by the type system for all
|
|
||||||
//! downstream use.
|
|
||||||
//!
|
|
||||||
//! # Validation rules
|
|
||||||
//!
|
|
||||||
//! - Must not be empty
|
|
||||||
//! - Must only contain alphanumeric characters, hyphens, and underscores
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// A validated session identifier.
|
|
||||||
///
|
|
||||||
/// Guarantees the inner string is non-empty and contains no path-traversal
|
|
||||||
/// characters (`/`, `\\`, `..`) or other unsafe delimiters.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
||||||
pub struct SessionId(String);
|
|
||||||
|
|
||||||
impl SessionId {
|
|
||||||
/// Validate and construct a `SessionId`.
|
|
||||||
///
|
|
||||||
/// Returns `Err(msg)` if the input contains path separators, `..`, or
|
|
||||||
/// is empty.
|
|
||||||
pub fn new(id: &str) -> Result<Self, String> {
|
|
||||||
if id.is_empty() {
|
|
||||||
return Err("session id must not be empty".to_string());
|
|
||||||
}
|
|
||||||
if id.contains('/') || id.contains('\\') || id.contains("..") {
|
|
||||||
return Err(format!(
|
|
||||||
"session id '{id}' must not contain path separators"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(SessionId(id.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the underlying string.
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the underlying owned string.
|
|
||||||
pub fn into_string(self) -> String {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append this session id as a component of `base_dir`, yielding
|
|
||||||
/// `base_dir / self.0`.
|
|
||||||
///
|
|
||||||
/// Safe because the id has been validated to contain no path separators.
|
|
||||||
pub fn join_to(&self, base_dir: &Path) -> PathBuf {
|
|
||||||
base_dir.join(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<str> for SessionId {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for SessionId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.write_str(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SessionId> for String {
|
|
||||||
fn from(sid: SessionId) -> Self {
|
|
||||||
sid.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_valid_uuids() {
|
|
||||||
assert!(SessionId::new("550e8400-e29b-41d4-a716-446655440000").is_ok());
|
|
||||||
assert!(SessionId::new("my-session_123").is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rejects_path_traversal() {
|
|
||||||
assert!(SessionId::new("../etc/passwd").is_err());
|
|
||||||
assert!(SessionId::new("foo/../../bar").is_err());
|
|
||||||
assert!(SessionId::new("foo\\..\\bar").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rejects_empty() {
|
|
||||||
assert!(SessionId::new("").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_into_string() {
|
|
||||||
let sid = SessionId::new("abc-123").unwrap();
|
|
||||||
assert_eq!(sid.into_string(), "abc-123");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
//! PID-file based advisory lock preventing two processes from operating on
|
|
||||||
//! the same session directory concurrently.
|
|
||||||
//!
|
|
||||||
//! # Flow
|
|
||||||
//!
|
|
||||||
//! [`SessionLock::new`] creates a handle → [`SessionLock::try_lock`] attempts
|
|
||||||
//! atomic `O_CREAT|O_EXCL` creation. If the lock file already exists, the
|
|
||||||
//! owning PID is checked via liveness verification. Stale locks are
|
|
||||||
//! overwritten atomically (temp-file + rename + fsync). On [`Drop`],
|
|
||||||
//! the lock file is removed automatically.
|
|
||||||
//!
|
|
||||||
//! # Components
|
|
||||||
//!
|
|
||||||
//! - `SessionLock` — RAII guard wrapping a lock file path and PID
|
|
||||||
//! - `try_lock` — three-phase atomic acquire with stale-lock recovery
|
|
||||||
//! - `unlock` / `Drop` — explicit and implicit release
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use tracing;
|
|
||||||
|
|
||||||
/// A PID-file lock (`<session_dir>/.lock`) tied to the current process,
|
|
||||||
/// auto-removed on drop.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SessionLock {
|
|
||||||
/// Path to the `.lock` file inside the session directory.
|
|
||||||
pub(crate) path: PathBuf,
|
|
||||||
/// Process ID that holds (or will hold) this lock.
|
|
||||||
pub(crate) pid: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SessionLock {
|
|
||||||
/// Construct a lock handle for a session directory (does not acquire
|
|
||||||
/// the lock yet — call `try_lock`).
|
|
||||||
pub fn new(session_dir: &Path) -> Self {
|
|
||||||
SessionLock {
|
|
||||||
path: session_dir.join(".lock"),
|
|
||||||
pid: std::process::id(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to acquire the session lock using an atomic file creation.
|
|
||||||
///
|
|
||||||
/// Flow: try `O_CREAT | O_EXCL` via `create_new(true)` → if that
|
|
||||||
/// succeeds, the lock is ours — write our PID and return ok. If the
|
|
||||||
/// file already exists, read the PID inside it and check whether that
|
|
||||||
/// PID is still alive: if the process is still running, fail to acquire;
|
|
||||||
/// otherwise the lock is stale — overwrite it with our own PID and succeed.
|
|
||||||
///
|
|
||||||
/// Return: `Ok(true)` if acquired, `Ok(false)` if another live
|
|
||||||
/// process holds it, `Err` on I/O failure.
|
|
||||||
pub fn try_lock(&self) -> std::io::Result<bool> {
|
|
||||||
// Phase 1: try atomic create. If it succeeds, the lock is ours.
|
|
||||||
match fs::OpenOptions::new()
|
|
||||||
.create_new(true)
|
|
||||||
.write(true)
|
|
||||||
.open(&self.path)
|
|
||||||
{
|
|
||||||
Ok(mut file) => {
|
|
||||||
write!(file, "{}", self.pid)?;
|
|
||||||
file.sync_all()?;
|
|
||||||
tracing::debug!(path = %self.path.display(), pid = self.pid, "session lock acquired");
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
|
||||||
tracing::debug!(path = %self.path.display(), "session lock already exists, checking staleness");
|
|
||||||
// Lock file exists — check if it's stale.
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: lock file exists — check liveness of the owning process.
|
|
||||||
let content = fs::read_to_string(&self.path).unwrap_or_default();
|
|
||||||
if let Ok(pid) = content.trim().parse::<u32>() {
|
|
||||||
if Self::is_alive(pid) {
|
|
||||||
tracing::warn!(stale = pid, path = %self.path.display(), "session lock held by live process");
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
tracing::debug!(stale = pid, "stale lock detected, overwriting");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: stale lock — overwrite it atomically (best-effort).
|
|
||||||
// Use a temp file + rename to avoid partial writes corrupting the lock.
|
|
||||||
let tmp = self.path.with_extension("lock.tmp");
|
|
||||||
{
|
|
||||||
let mut tmp_file = fs::OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.write(true)
|
|
||||||
.open(&tmp)?;
|
|
||||||
write!(tmp_file, "{}", self.pid)?;
|
|
||||||
tmp_file.sync_all()?;
|
|
||||||
}
|
|
||||||
fs::rename(&tmp, &self.path)?;
|
|
||||||
// Sync the parent directory so the rename survives a crash.
|
|
||||||
if let Some(parent) = self.path.parent() {
|
|
||||||
let _ = fs::File::open(parent).and_then(|d| d.sync_all());
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Explicitly release the lock by removing the lock file.
|
|
||||||
pub fn unlock(&self) {
|
|
||||||
let _ = fs::remove_file(&self.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether a process with the given PID is currently alive.
|
|
||||||
///
|
|
||||||
/// Uses `kill(pid, 0)` on Unix via the `nix` or `libc` crate in production;
|
|
||||||
/// here we provide a best-effort check using the process table.
|
|
||||||
/// On non-Unix platforms this always returns `true` (conservative).
|
|
||||||
fn is_alive(pid: u32) -> bool {
|
|
||||||
// On Unix, signal 0 checks process existence without sending a signal.
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
// SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks
|
|
||||||
// whether the process exists and the caller has permission to signal it.
|
|
||||||
// The integer argument is a PID validated by `try_lock`.
|
|
||||||
let pid_signed: i32 = match pid.try_into() {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
if unsafe { libc::kill(pid_signed, 0) != 0 } {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Extra check: verify the PID belongs to a zesdex process via
|
|
||||||
// /proc/<pid>/exe to mitigate the PID-reuse race.
|
|
||||||
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
|
|
||||||
if let Ok(target) = std::fs::read_link(&proc_exe) {
|
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
|
||||||
if target != exe {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
// Fallback: always assume alive (conservative).
|
|
||||||
let _ = pid;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for SessionLock {
|
|
||||||
/// Release the lock automatically when the guard goes out of scope,
|
|
||||||
/// so an ungracefully-exited process doesn't leave a dangling lock.
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = fs::remove_file(&self.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
//! Pure domain entity for application configuration.
|
|
||||||
//!
|
|
||||||
//! Defines `AppConfig`, `ProviderConfig`, and `ModelRole` — the data
|
|
||||||
//! structures that describe which LLM providers are registered, which
|
|
||||||
//! model roles exist, and which provider/model is the default.
|
|
||||||
//!
|
|
||||||
//! # Architecture
|
|
||||||
//! These are pure data structures with **no I/O logic**. Load/save
|
|
||||||
//! responsibilities live in `AppConfigRepository` (domain::repository).
|
|
||||||
//!
|
|
||||||
//! ## Data Flow
|
|
||||||
//! 1. `AppConfig` is deserialised from `app_config.json` at startup
|
|
||||||
//! 2. The HTTP handler layer calls `SettingsService::update_provider()`
|
|
||||||
//! to mutate the provider map
|
|
||||||
//! 3. The modified `AppConfig` is serialised back to `app_config.json`
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Top-level application configuration.
|
|
||||||
///
|
|
||||||
/// Holds the registry of configured LLM providers, named model roles
|
|
||||||
/// (logical profiles mapping to a provider+model pair), and the default
|
|
||||||
/// provider/model selection.
|
|
||||||
///
|
|
||||||
/// ## Fields
|
|
||||||
/// - `providers` — map of provider name → connection details
|
|
||||||
/// - `model_roles` — map of role name → provider/model/temperature
|
|
||||||
/// - `default_provider` — the provider to use when none is specified
|
|
||||||
/// - `default_model` — the model to use when none is specified
|
|
||||||
/// - `default_context_window` — fallback context window size in tokens
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AppConfig {
|
|
||||||
pub providers: HashMap<String, ProviderConfig>,
|
|
||||||
pub model_roles: HashMap<String, ModelRole>,
|
|
||||||
pub default_provider: String,
|
|
||||||
pub default_model: String,
|
|
||||||
pub default_context_window: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connection details for a single LLM provider endpoint.
|
|
||||||
///
|
|
||||||
/// ## Fields
|
|
||||||
/// - `api_base` — base URL for the provider API
|
|
||||||
/// - `api_key_env` — optional environment variable name holding the API key
|
|
||||||
/// - `default_model` — optional default model name for this provider
|
|
||||||
/// - `default_api_key` — optional inline API key (less secure than env var)
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ProviderConfig {
|
|
||||||
pub api_base: String,
|
|
||||||
pub api_key_env: Option<String>,
|
|
||||||
pub default_model: Option<String>,
|
|
||||||
pub default_api_key: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A named model role mapping to a specific provider/model with parameters.
|
|
||||||
///
|
|
||||||
/// Roles allow the UI to present logical profiles (e.g. "fast", "reasoning")
|
|
||||||
/// that abstract over concrete provider+model strings.
|
|
||||||
///
|
|
||||||
/// ## Fields
|
|
||||||
/// - `provider` — which provider serves this role
|
|
||||||
/// - `model` — which model to use for this role
|
|
||||||
/// - `max_tokens` — optional maximum output token limit
|
|
||||||
/// - `context_window` — optional context window override
|
|
||||||
/// - `temperature` — optional generation temperature
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ModelRole {
|
|
||||||
pub provider: String,
|
|
||||||
pub model: String,
|
|
||||||
pub max_tokens: Option<u32>,
|
|
||||||
pub context_window: Option<u32>,
|
|
||||||
pub temperature: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the default AppConfig with built-in "zen" and "router" providers.
|
|
||||||
impl Default for AppConfig {
|
|
||||||
/// Construct an AppConfig with the default "zen" and "router" providers.
|
|
||||||
///
|
|
||||||
/// ## Defaults
|
|
||||||
/// - Zen provider: `deepseek-v4-flash-free` model
|
|
||||||
/// - Router provider: `claude-opus-4-8` model
|
|
||||||
/// - Default role: "default" → zen / deepseek-v4-flash-free, temp 0.7
|
|
||||||
/// - `default_context_window`: 256,000 tokens
|
|
||||||
fn default() -> Self {
|
|
||||||
let mut providers = HashMap::new();
|
|
||||||
providers.insert(
|
|
||||||
"zen".to_string(),
|
|
||||||
ProviderConfig {
|
|
||||||
api_base: "https://opencode.ai/zen/v1".to_string(),
|
|
||||||
api_key_env: Some("API_KEY".to_string()),
|
|
||||||
default_model: Some("deepseek-v4-flash-free".to_string()),
|
|
||||||
default_api_key: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
providers.insert(
|
|
||||||
"router".to_string(),
|
|
||||||
ProviderConfig {
|
|
||||||
api_base: "https://9router.asepharyana.my.id/v1".to_string(),
|
|
||||||
api_key_env: Some("ROUTER_API_KEY".to_string()),
|
|
||||||
default_model: Some("claude-opus-4-8".to_string()),
|
|
||||||
default_api_key: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut model_roles = HashMap::new();
|
|
||||||
model_roles.insert(
|
|
||||||
"default".to_string(),
|
|
||||||
ModelRole {
|
|
||||||
provider: "zen".to_string(),
|
|
||||||
model: "deepseek-v4-flash-free".to_string(),
|
|
||||||
max_tokens: None,
|
|
||||||
context_window: None,
|
|
||||||
temperature: Some(0.7),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
providers,
|
|
||||||
model_roles,
|
|
||||||
default_provider: "zen".to_string(),
|
|
||||||
default_model: "deepseek-v4-flash-free".to_string(),
|
|
||||||
default_context_window: 256_000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
//! Command types for CMS domain operations.
|
|
||||||
//!
|
|
||||||
//! Following the `NewXxx` / `XxxPatch` pattern from clean architecture,
|
|
||||||
//! these types encapsulate the input data for create/update operations
|
|
||||||
//! on domain entities. They decouple presentation DTOs from the entity
|
|
||||||
//! mutation surface and provide a clear boundary for validation.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use super::settings::InternetMode;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Settings
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Partial update command for `Settings`.
|
|
||||||
///
|
|
||||||
/// Every field is `Option`al — only non-`None` fields are applied to the
|
|
||||||
/// existing settings instance. Use `apply_to()` to merge into a `Settings`
|
|
||||||
/// value.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct SettingsPatch {
|
|
||||||
/// Override the internet access mode.
|
|
||||||
pub internet_mode: Option<String>,
|
|
||||||
/// Override the active provider name.
|
|
||||||
pub provider: Option<String>,
|
|
||||||
/// Override the active model name.
|
|
||||||
pub model: Option<String>,
|
|
||||||
/// Replace the entire API-keys map.
|
|
||||||
pub api_keys: Option<HashMap<String, String>>,
|
|
||||||
/// Override the max tokens for completions.
|
|
||||||
pub max_tokens: Option<Option<u32>>,
|
|
||||||
/// Override the temperature for completions.
|
|
||||||
pub temperature: Option<Option<f32>>,
|
|
||||||
/// Override the review max lessons per run.
|
|
||||||
pub review_max_lessons_per_run: Option<usize>,
|
|
||||||
/// Override the adaptive review max skip count.
|
|
||||||
pub adaptive_review_max_skip: Option<u32>,
|
|
||||||
/// Override the verify shell command.
|
|
||||||
pub verify_command: Option<Option<String>>,
|
|
||||||
/// Override the verify timeout in milliseconds.
|
|
||||||
pub verify_timeout_ms: Option<u64>,
|
|
||||||
/// Override the max concurrency for workflow execution.
|
|
||||||
pub workflow_max_concurrency: Option<usize>,
|
|
||||||
/// Override the review-enabled flag.
|
|
||||||
pub review_enabled: Option<bool>,
|
|
||||||
/// Override the session-archive-enabled flag.
|
|
||||||
pub session_archive_enabled: Option<bool>,
|
|
||||||
/// Override the LSP auto-provision flag.
|
|
||||||
pub lsp_auto_provision: Option<bool>,
|
|
||||||
/// Override the list of LSP-managed languages.
|
|
||||||
pub lsp_languages: Option<Vec<String>>,
|
|
||||||
/// Override the hive-mind node timeout in milliseconds.
|
|
||||||
pub hive_mind_node_timeout_ms: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SettingsPatch {
|
|
||||||
/// Merge this patch into `settings`, overwriting each non-`None` field.
|
|
||||||
///
|
|
||||||
/// Flow: for each optional field, if `Some`, assign it to the target.
|
|
||||||
///
|
|
||||||
/// ## Errors
|
|
||||||
/// Returns `Err` with a message if `internet_mode` is set to an
|
|
||||||
/// unrecognised value.
|
|
||||||
pub fn apply_to(&self, settings: &mut super::settings::Settings) -> Result<(), String> {
|
|
||||||
if let Some(ref val) = self.internet_mode {
|
|
||||||
settings.internet_mode = match val.as_str() {
|
|
||||||
"Off" => InternetMode::Off,
|
|
||||||
"ReadOnly" => InternetMode::ReadOnly,
|
|
||||||
"Full" => InternetMode::Full,
|
|
||||||
_ => {
|
|
||||||
return Err(format!(
|
|
||||||
"invalid internet_mode '{val}'; expected Off, ReadOnly, or Full"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if let Some(ref val) = self.provider {
|
|
||||||
settings.provider = val.clone();
|
|
||||||
}
|
|
||||||
if let Some(ref val) = self.model {
|
|
||||||
settings.model = val.clone();
|
|
||||||
}
|
|
||||||
if let Some(ref val) = self.api_keys {
|
|
||||||
settings.api_keys = val.clone();
|
|
||||||
}
|
|
||||||
if let Some(val) = self.max_tokens {
|
|
||||||
settings.max_tokens = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = self.temperature {
|
|
||||||
settings.temperature = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = self.review_max_lessons_per_run {
|
|
||||||
settings.review_max_lessons_per_run = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = self.adaptive_review_max_skip {
|
|
||||||
settings.adaptive_review_max_skip = val;
|
|
||||||
}
|
|
||||||
if let Some(ref val) = self.verify_command {
|
|
||||||
settings.verify_command = val.clone();
|
|
||||||
}
|
|
||||||
if let Some(val) = self.verify_timeout_ms {
|
|
||||||
settings.verify_timeout_ms = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = self.workflow_max_concurrency {
|
|
||||||
settings.workflow_max_concurrency = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = self.review_enabled {
|
|
||||||
settings.flags.review_enabled = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = self.session_archive_enabled {
|
|
||||||
settings.flags.session_archive_enabled = val;
|
|
||||||
}
|
|
||||||
if let Some(val) = self.lsp_auto_provision {
|
|
||||||
settings.flags.lsp_auto_provision = val;
|
|
||||||
}
|
|
||||||
if let Some(ref val) = self.lsp_languages {
|
|
||||||
settings.lsp_languages = val.clone();
|
|
||||||
}
|
|
||||||
if let Some(val) = self.hive_mind_node_timeout_ms {
|
|
||||||
settings.hive_mind_node_timeout_ms = val;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Memory
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Command to create a new memory entry.
|
|
||||||
///
|
|
||||||
/// All required fields are non-optional; optional fields use `Option`
|
|
||||||
/// and default to sensible values (empty or the service default).
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct NewMemory {
|
|
||||||
/// Unique name / slug for the memory.
|
|
||||||
pub name: String,
|
|
||||||
/// One-line summary of what the memory captures.
|
|
||||||
pub description: String,
|
|
||||||
/// The full memory content.
|
|
||||||
pub content: String,
|
|
||||||
/// Category kind (defaults to "reference" in the handler).
|
|
||||||
pub kind: Option<String>,
|
|
||||||
/// Outcome of the remembered action.
|
|
||||||
pub outcome: Option<String>,
|
|
||||||
/// Lifecycle stage (defaults to "new" in the handler).
|
|
||||||
pub lifecycle: Option<String>,
|
|
||||||
/// Scope context for the memory.
|
|
||||||
pub scope: Option<String>,
|
|
||||||
/// Code snippet captured before the action.
|
|
||||||
pub before_snippet: Option<String>,
|
|
||||||
/// Code snippet captured after the action.
|
|
||||||
pub after_snippet: Option<String>,
|
|
||||||
/// Source provenances (files, conversations, etc.).
|
|
||||||
pub provenances: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//! Pure domain entity for conversations and chat messages.
|
|
||||||
//!
|
|
||||||
//! Re-exports the canonical `Conversation`, `ChatMessage`, and `Role`
|
|
||||||
//! types from the core module to provide a consistent domain import
|
|
||||||
//! boundary within the CMS module. All CMS code references conversation
|
|
||||||
//! types through this module rather than depending on the core module
|
|
||||||
//! directly.
|
|
||||||
//!
|
|
||||||
//! ## Re-exports
|
|
||||||
//! - `Conversation` — top-level conversation container with message list
|
|
||||||
//! - `ChatMessage` — a single message with role, content, and tool metadata
|
|
||||||
//! - `Role` — message role enum (User, Assistant, System, Tool)
|
|
||||||
|
|
||||||
pub use crate::core::message::{ChatMessage, Role};
|
|
||||||
pub use crate::core::conversation::Conversation;
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
//! Pure domain entity for the edit log — an append-only log of file mutations.
|
|
||||||
//!
|
|
||||||
//! Records every file mutation made by any tool, enabling audit trails
|
|
||||||
//! and potential undo operations. Each entry captures the tool name,
|
|
||||||
//! target path, reason, content hash, and byte delta.
|
|
||||||
//!
|
|
||||||
//! # Architecture
|
|
||||||
//! This is a pure data structure with **no I/O logic**. Load/save
|
|
||||||
//! responsibilities live in `EditLogRepository` (domain::repository).
|
|
||||||
//!
|
|
||||||
//! ## Data Flow
|
|
||||||
//! 1. Tools call `EditLog::push()` to record each mutation
|
|
||||||
//! 2. The in-memory `EditLog` is periodically flushed to disk by the repo
|
|
||||||
//! 3. Oldest entries are evicted from the in-memory cache when
|
|
||||||
//! `MAX_MEMORY_ENTRIES` is exceeded (prevents unbounded growth)
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// A single recorded file edit event.
|
|
||||||
///
|
|
||||||
/// ## Fields
|
|
||||||
/// - `ts` — Unix timestamp (seconds) when the edit occurred
|
|
||||||
/// - `tool` — name of the tool that performed the edit (e.g. "Bash", "Edit")
|
|
||||||
/// - `path` — absolute file path that was modified
|
|
||||||
/// - `reason` — human-readable explanation of why the edit was made
|
|
||||||
/// - `content_sha256` — SHA-256 hex digest of the content *after* the edit
|
|
||||||
/// - `bytes_delta` — signed byte count change (+added, -removed)
|
|
||||||
/// - `origin` — origin identifier (which agent / session context)
|
|
||||||
/// - `session_id` — session in which this edit was performed
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct EditLogEntry {
|
|
||||||
pub ts: i64,
|
|
||||||
pub tool: String,
|
|
||||||
pub path: String,
|
|
||||||
pub reason: String,
|
|
||||||
pub content_sha256: String,
|
|
||||||
pub bytes_delta: i64,
|
|
||||||
pub origin: String,
|
|
||||||
pub session_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maximum number of edit entries held in memory at once.
|
|
||||||
///
|
|
||||||
/// Beyond this limit, old entries are dropped from the in-memory cache
|
|
||||||
/// to prevent unbounded memory growth in long-running sessions.
|
|
||||||
pub const MAX_MEMORY_ENTRIES: usize = 10_000;
|
|
||||||
|
|
||||||
/// In-memory view of a session's edit log.
|
|
||||||
///
|
|
||||||
/// Wraps a `Vec<EditLogEntry>` and provides basic query helpers.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct EditLog {
|
|
||||||
/// Ordered list of edit entries (newest appended last).
|
|
||||||
pub entries: Vec<EditLogEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EditLog {
|
|
||||||
/// Create an empty edit log with no entries.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
entries: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the number of in-memory entries.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.entries.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return `true` if the log contains no entries.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.entries.is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for EditLog {
|
|
||||||
/// Returns an empty `EditLog` via `EditLog::new()`.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 50 of 472 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user