Compare commits
9
Commits
b7006d1ad8
...
v1.23.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30cc579773 | ||
|
|
a54f1644f5 | ||
|
|
23e68dc979 | ||
|
|
8c99c6b1ca | ||
|
|
9b28a829e5 | ||
|
|
b5ebd07fa8 | ||
|
|
7e6ed34f0d | ||
|
|
34322e492a | ||
|
|
3e260e6077 |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
name: Build & Deploy (Nix)
|
||||
name: Build & Deploy (Bun)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -25,26 +25,27 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
determinate: false
|
||||
extra-conf: |
|
||||
sandbox = false
|
||||
accept-flake-config = true
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Cache Nix
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v14
|
||||
- name: Install deps
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build zesdex
|
||||
id: build
|
||||
- name: Type-check
|
||||
run: bun run check
|
||||
|
||||
- name: Test
|
||||
run: bun test
|
||||
|
||||
- name: Build binary
|
||||
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"
|
||||
bun run build
|
||||
test -x dist/zesdex && echo "binary OK ($(stat -c%s dist/zesdex) bytes)"
|
||||
|
||||
- name: Setup SSH key
|
||||
if: env.VPS_HOST != '' && env.VPS_HOST != 'null'
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Deploy zesdex to VPS
|
||||
if: env.VPS_HOST != '' && env.VPS_HOST != 'null'
|
||||
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"
|
||||
scp dist/zesdex "$VPS_USER@$VPS_HOST:/usr/local/bin/zesdex"
|
||||
ssh "$VPS_USER@$VPS_HOST" "chmod +x /usr/local/bin/zesdex && systemctl restart zesdex"
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
@@ -40,16 +40,18 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install semantic-release
|
||||
- name: Install semantic-release (isolated prefix — do NOT touch repo package.json)
|
||||
run: |
|
||||
npm install -D \
|
||||
mkdir -p /tmp/sr-tools
|
||||
npm install --prefix /tmp/sr-tools \
|
||||
semantic-release \
|
||||
@semantic-release/exec \
|
||||
@semantic-release/git \
|
||||
@semantic-release/changelog \
|
||||
@semantic-release/github
|
||||
echo "/tmp/sr-tools/node_modules/.bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Run semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release
|
||||
run: semantic-release
|
||||
@@ -1,38 +1,55 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Kilo when working with code in the zesdex repository.
|
||||
This file provides guidance to AI agents (Kilo / Hermes) working in the zesdex repository.
|
||||
|
||||
## Overview
|
||||
|
||||
Zesdex is an **AI-native autonomous coding agent**, implemented in **TypeScript / Bun**.
|
||||
It was rebuilt to be a **pure CLI/TUI tool** — there are **no network server interfaces**
|
||||
(REST API / WebSocket / gRPC / web / daemon were all removed). The only two entry points:
|
||||
|
||||
- `zesdex` — interactive **OpenTUI** (primary interface, wired to the real agent).
|
||||
- `zesdex --headless "<prompt>"` — one-shot non-interactive turn (headless mode).
|
||||
|
||||
## Best Practice Conventions
|
||||
|
||||
Zesdex follows Kana Engineering Best Practices:
|
||||
1. **Clean Architecture** — strict `domain` / `application` / `infrastructure` layering
|
||||
under `src/`, with thin `interfaces/` for the TUI + CLI.
|
||||
- `domain` has zero I/O and zero framework dependencies.
|
||||
- `application` depends only on `domain` (ports + turn service).
|
||||
- `infrastructure` implements domain ports (LLM client, tool executor, file repos).
|
||||
- DTOs/value objects cross layer boundaries, not entities.
|
||||
|
||||
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 dead code.
|
||||
|
||||
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 exported `function`, `class`, `interface`, and `type`
|
||||
needs a `/** */` doc comment explaining what, flow, why, and return value.
|
||||
|
||||
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:`.
|
||||
|
||||
4. **Commit Convention** — Conventional Commits in Bahasa Indonesia: `feat(scope):`, `fix(scope):`, `chore:`, `docs:`.
|
||||
5. **Error Handling** — throw `Error` with clear messages; the turn loop wraps LLM
|
||||
errors and recovers. Log with `console.*` (redirected to `~/.local/share/zesdex/zesdex.log`).
|
||||
|
||||
5. **Error Handling** — `anyhow::Result` and `anyhow::bail!` throughout. Log with `tracing` (never stderr).
|
||||
6. **Testing** — Bun tests (`bun test`) using `describe/test/expect`. Tests are
|
||||
F.I.R.S.T. Logic-pure modules (TUI state/action/command/controller) are unit-tested.
|
||||
|
||||
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** — no `@ts-ignore` / `any` where a real type exists.
|
||||
|
||||
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.
|
||||
|
||||
8. **Boy Scout Rule** — Leave every module cleaner than you found it.
|
||||
## Stack
|
||||
|
||||
## Available Agents
|
||||
- Bun 1.3.14 (packageManager pinned in `package.json`).
|
||||
- TypeScript strict mode, `tsconfig.json` uses `paths` aliases (`@zesdex/*`) →
|
||||
`src/*`. `bun build ... --compile` bundles into a single `dist/zesdex` binary.
|
||||
|
||||
- `@rust-engineer` — Rust clean architecture specialist (subagent).
|
||||
- `@code-reviewer` — Code review specialist (subagent).
|
||||
## Commands
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `/check` — Run cargo check, clippy, and tests.
|
||||
- `/audit` — Code quality audit against clean-architecture best practices.
|
||||
- `/doc` — Generate or update doc comments.
|
||||
- `bun run check` — `tsc --noEmit` typecheck.
|
||||
- `bun run test` — run all Bun tests.
|
||||
- `bun run build` — compile `dist/zesdex` binary.
|
||||
- `bun run tui` — run the interactive TUI.
|
||||
- `bun run headless "<prompt>"` — one-shot agent turn.
|
||||
- `bun run bootstrap` — seed default settings/app_config.
|
||||
@@ -1,3 +1,38 @@
|
||||
## [1.23.1](https://github.com/asepharyana/zesdex/compare/v1.23.0...v1.23.1) (2026-09-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tui:** repair layout overflow + streaming dup; polish UI/state ([a54f164](https://github.com/asepharyana/zesdex/commit/a54f1644f5943399568194b4f129185a9d841f1d))
|
||||
|
||||
# [1.23.0](https://github.com/asepharyana/zesdex/compare/v1.22.0...v1.23.0) (2026-09-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** perbaiki base64url encode JWT — string di-base64 dulu sebelum replace chars ([1b130e6](https://github.com/asepharyana/zesdex/commit/1b130e69b36603f894bcd45469b9feef28b18562))
|
||||
* **ci:** Release workflow install semantic-release tanpa merusak package.json ([8c99c6b](https://github.com/asepharyana/zesdex/commit/8c99c6b1ca94e1d2b80dd59bb96eb24b7b3b2dfa))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** port Fase 5 REST API — Bun.serve router + JWT middleware + auth/session/conversation/chat handlers + composition root ([49e7932](https://github.com/asepharyana/zesdex/commit/49e79322b4aa1824467e1c871cf68809c1a424ee))
|
||||
* **auth:** port 3f auth infrastructure — Argon2 passwords + HS256 JWT tokens ([7e8a160](https://github.com/asepharyana/zesdex/commit/7e8a160b0e918d51dc559f55fa19395368a4d84e))
|
||||
* **auth:** tambah OAuth loopback server untuk capture authorization-code redirect ([fd58b46](https://github.com/asepharyana/zesdex/commit/fd58b4661fd0928dd461c74a5cb7bc7870ca9692))
|
||||
* **cli:** port Fase 4 CLI — parser arg, mode dispatch, single-process composition root + REPL loop, bootstrap seed ([c582dc1](https://github.com/asepharyana/zesdex/commit/c582dc1461024d5037b8351a0df6b9b11e967782))
|
||||
* **cli:** wire mode dispatch ke server interfaces — --api/--ws/--grpc/--web start server; add tsconfig paths ([02a5523](https://github.com/asepharyana/zesdex/commit/02a55235cfadb3e5daf3774a04f6fc6abe5da78d))
|
||||
* **daemon:** port Fase 5 daemon — IPC agent-driver loop (Submit/Close) + streaming tokens; wire CLI --daemon/--attach ([14d3556](https://github.com/asepharyana/zesdex/commit/14d3556179685587e62873f81a57125037ac5e5a))
|
||||
* **infra:** port tool system ke TypeScript (37 tools + registry + executor) ([82b51f7](https://github.com/asepharyana/zesdex/commit/82b51f7921e33e912f1244152df509522e44cae3))
|
||||
* **ipc:** port 3g IPC + bgbash — framed Unix-socket server/client + background job registry, wire bash tools ([4b4730c](https://github.com/asepharyana/zesdex/commit/4b4730cff7ee2a5c43f8906d17282d95de84cf7a))
|
||||
* **rewrite:** bootstrap monorepo Bun + port domain layer ke TypeScript ([07e84e4](https://github.com/asepharyana/zesdex/commit/07e84e43b386388f75f8293879e49dff1db461ab))
|
||||
* **rewrite:** tambah application layer — port traits & use cases TypeScript ([03538a4](https://github.com/asepharyana/zesdex/commit/03538a455a93ff554abb45a1e54809c9579763fd))
|
||||
* **rewrite:** tambah infrastructure — LLM client + full file persistence ([a2c7e5d](https://github.com/asepharyana/zesdex/commit/a2c7e5d44c784508b4fe735ec3434df2407f76de))
|
||||
* **subagent:** port 3c subagent engine — run_agent loop + spawn/delegate/parallel ([a14a227](https://github.com/asepharyana/zesdex/commit/a14a2275ff90059874166aec8c03bd44e6c7efee))
|
||||
* **tui:** port Fase 4 TUI logic layer — InputState/Action dispatcher/key controller/slash commands + lightweight ANSI renderer + run loop; 20 test ([8098133](https://github.com/asepharyana/zesdex/commit/8098133ba50135c7a5f9a063221ffdb860e064cb))
|
||||
* **web): port Fase 5 static file server w/ traversal protection; feat(grpc:** port health stub ([1a6268b](https://github.com/asepharyana/zesdex/commit/1a6268bde58c67b9d3cbabce08306d53f0b3791a))
|
||||
* **workflow:** port 3d workflow + hive-mind engine — parse, execute, cycle, synthesis, docs ([edc0076](https://github.com/asepharyana/zesdex/commit/edc007619b63d0d91fdad79a6d534c112b8a3981))
|
||||
* **ws:** port Fase 5 WS server — Bun native WebSocket, ZESDEX_WS_TOKEN guard, prompt turn proxy + streaming token/done/error ([d4d423a](https://github.com/asepharyana/zesdex/commit/d4d423a4cae62562da4ca4f0231f6a4f90481473))
|
||||
|
||||
# [1.22.0](https://github.com/asepharyana/zesdex/compare/v1.21.2...v1.22.0) (2026-08-28)
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
> 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.
|
||||
> **Status:** Fase 0–6 **semua selesai**. Rust source dihapus, build CI/Docker/release pakai Bun, `flake.lock` Rust/Nix dibersihkan, wire-shape lama terbaca. Base: `bun run check` bersih, 70 test hijau.
|
||||
|
||||
---
|
||||
|
||||
@@ -181,10 +180,10 @@ apps/
|
||||
- [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] `flake.lock` dead (Rust/Nix era) dihapus — deploy infra 100% Bun (`deploy.yml` bun build --compile + scp binary)
|
||||
- [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
|
||||
- [x] Wire-shape lama terbaca — `settings.json` (Rust-era shape: api_keys/provider/model/review_*) dimuat true di headless + TUI; repos direct JSON parser. Verified: `--headless "1+1"` → "2", TUI header tunjuk `claude-opus-5`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
"name": "zesdex",
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "^2.2.0",
|
||||
"@opentui/core": "^0.5.10",
|
||||
"@opentui/react": "^0.5.10",
|
||||
"react": "^19.2.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"typescript": "^5.7.0",
|
||||
},
|
||||
},
|
||||
@@ -42,14 +46,70 @@
|
||||
|
||||
"@node-rs/argon2-win32-x64-msvc": ["@node-rs/argon2-win32-x64-msvc@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cfcNjVqLpgOU/9l2mLOFduuXTxvGH5qoV38AhcOz9oQaC7GDmsFbAgcuizVNuOtpSyQr5HoQez3f2X2jYgGMsg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.5.10", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.10", "@opentui/core-darwin-x64": "0.5.10", "@opentui/core-linux-arm64": "0.5.10", "@opentui/core-linux-arm64-musl": "0.5.10", "@opentui/core-linux-x64": "0.5.10", "@opentui/core-linux-x64-musl": "0.5.10", "@opentui/core-win32-arm64": "0.5.10", "@opentuiLine truncated
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Vyb+nTbhab8ZcRy5gg1loEEGwRcIbjAeVRIBfHBcbFDqmITBOg7x2gqJ+x/TnoOy4uwMhCmICUN2wiyREw3r1Q=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tTFLcM7Oj1gTyhm/bUdAt3C6grZdCxPk6+/g2azcZBUlI3/62LwbeRS6HbQKFFmm+1fUmX8cq6kWrtul885mVg=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-ncJXcgudhBf2GdJyF3xVQN/Ec+1F7GOL+pRrURmgBYSj2v1w6EyoDQFAACtPTK2c3R38W6fvZwL4JSLlm4EFXQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-dGMphDKexSdeYqwl0wgoFBP88Ta/cdi1Zc1mk29/ENkSCGz+74zlCHgqTHRNGLmI8W5TfuUtCyktQH11/Z+TBQ=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-5qtYaOgwVycZD1GaGshTRsi0rXPAmVExO03N1JQaHu+NYxK/vXSOc7Bu4QW0sPXx3Sp0SpzpP+FHjXABfoK66g=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Oj4H9hApuvuTKPWxh4SoZAgGJorR7vbvnrZA/cAkSMAk2VGSoHRRcqeXQbcH8IcdjVZ0KFpv8Zkl/D5Ye+2mew=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-A9VhgvTxQoUdZ+8LmUumEng1sQNbj9QQQT3NYG9mSxI54qTANi7vOWNSphMiY6RMVsr22pgm6nUvSSvJXv7Jog=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="],
|
||||
|
||||
"@opentui/react": ["@opentui/react@0.5.10", "", { "dependencies": { "@opentui/core": "0.5.10", "react-reconciler": "^0.33.0" }, "peerDependencies": { "react": ">=19.2.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-76UFqe/ML121Skjche2+mGX56GKTlwG3ftrOi3b7WGLvjGucPqgEDJ0+CmaC/Yk85O8ui/ZTZJ2tay/8foR4FQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
|
||||
|
||||
"@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||
|
||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
|
||||
"react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="],
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
|
||||
|
||||
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"react-devtools-core/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
|
||||
pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "zesdex";
|
||||
version = "1.18.2";
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
cargoBuildFlags = [ "-p" "zesdex-gateway" "--bin" "zesdex" ];
|
||||
|
||||
nativeBuildInputs = with pkgs; [ pkg-config ];
|
||||
buildInputs = with pkgs; [ openssl sqlite ];
|
||||
|
||||
doCheck = false;
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "AI-native coding agent with workflow orchestration";
|
||||
homepage = "https://github.com/asepharyana/zesdex";
|
||||
license = licenses.mit;
|
||||
mainProgram = "zesdex";
|
||||
};
|
||||
}
|
||||
+2
-4
@@ -1,5 +1,7 @@
|
||||
version: "3.9"
|
||||
|
||||
# Zesdex is now a pure CLI/TUI tool (no REST/WS/gRPC/web/daemon servers).
|
||||
# This compose file just provides an interactive TTY shell into the binary.
|
||||
services:
|
||||
zesdex:
|
||||
build:
|
||||
@@ -12,12 +14,8 @@ services:
|
||||
- ./workspace:/workspace:ro
|
||||
environment:
|
||||
- ZESDEX_DATA_DIR=/data
|
||||
- RUST_LOG=${RUST_LOG:-info}
|
||||
stdin_open: true
|
||||
tty: true
|
||||
# For daemon mode, expose the IPC socket directory
|
||||
# ports:
|
||||
# - "127.0.0.1:${ZESDEX_PORT:-0}:${ZESDEX_PORT:-0}"
|
||||
|
||||
volumes:
|
||||
zesdex-data:
|
||||
Generated
-82
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1787135253,
|
||||
"narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787194935,
|
||||
"narHash": "sha256-Gt6JpppReMxPGkbPcQhz+1qp+NUUf4oIwzRy+ptJgTk=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "cb98e96d632d0010a7e56c098c51729cb179cc75",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
description = "Zesdex — AI-native coding agent";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
|
||||
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" "clippy" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
rustToolchain
|
||||
pkg-config
|
||||
openssl
|
||||
sqlite
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
|
||||
RUST_LOG = "info";
|
||||
};
|
||||
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "zesdex";
|
||||
version = "1.18.2";
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
# Build only the main binary
|
||||
cargoBuildFlags = [ "-p" "zesdex-gateway" "--bin" "zesdex" ];
|
||||
|
||||
nativeBuildInputs = with pkgs; [ pkg-config perl ];
|
||||
buildInputs = with pkgs; [ openssl sqlite ];
|
||||
|
||||
doCheck = false; # Tests require filesystem access
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "AI-native coding agent with workflow orchestration";
|
||||
homepage = "https://github.com/asepharyana/zesdex";
|
||||
license = licenses.mit;
|
||||
mainProgram = "zesdex";
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
+10
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zesdex",
|
||||
"version": "1.22.0",
|
||||
"version": "1.23.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.14",
|
||||
@@ -10,21 +10,20 @@
|
||||
"build": "bun build src/interfaces/cli/index.ts --compile --outfile=dist/zesdex",
|
||||
"check": "tsc --noEmit",
|
||||
"test": "bun test",
|
||||
"format": "bun fmt",
|
||||
"cli": "bun src/interfaces/cli/index.ts",
|
||||
"bootstrap": "bun src/interfaces/cli/bootstrap.ts",
|
||||
"api": "bun src/interfaces/api/main.ts",
|
||||
"ws": "bun src/interfaces/ws/main.ts",
|
||||
"grpc": "bun src/interfaces/grpc/main.ts",
|
||||
"web": "bun src/interfaces/web/main.ts",
|
||||
"daemon": "bun src/interfaces/daemon/main.ts",
|
||||
"tui": "bun src/interfaces/tui/main.ts"
|
||||
"tui": "bun src/interfaces/cli/index.ts",
|
||||
"cli": "bun src/interfaces/cli/index.ts --headless",
|
||||
"headless": "bun src/interfaces/cli/index.ts --headless",
|
||||
"bootstrap": "bun src/interfaces/cli/bootstrap.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "^2.2.0"
|
||||
"@node-rs/argon2": "^2.2.0",
|
||||
"@opentui/core": "^0.5.10",
|
||||
"@opentui/react": "^0.5.10",
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
|
||||
with pkgs;
|
||||
|
||||
mkShell {
|
||||
buildInputs = [
|
||||
(rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" "clippy" ];
|
||||
})
|
||||
pkg-config
|
||||
openssl
|
||||
sqlite
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rust-bin.stable.latest.default}/lib/rustlib/src/rust/library";
|
||||
RUST_LOG = "info";
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
/** Auth application module — OAuth PKCE + session management use-cases. */
|
||||
export * from "./oauth_service.ts";
|
||||
export * from "./session_service.ts";
|
||||
@@ -1,141 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { generatePkcePair, OAuthUseCase } from "./oauth_service.ts";
|
||||
import type { OAuthFlowStore } from "./oauth_service.ts";
|
||||
import { createHash } from "node:crypto";
|
||||
import type { OAuthToken } from "@zesdex/domain";
|
||||
|
||||
describe("generatePkcePair", () => {
|
||||
it("produces a verifier ≥43 chars and a valid S256 challenge", () => {
|
||||
const [verifier, challenge] = generatePkcePair();
|
||||
expect(verifier.length).toBeGreaterThanOrEqual(43);
|
||||
// Challenge = base64url(sha256(verifier)).
|
||||
const expected = Buffer.from(createHash("sha256").update(verifier, "utf8").digest()).toString("base64url");
|
||||
expect(challenge).toBe(expected);
|
||||
});
|
||||
|
||||
it("is unique across calls", () => {
|
||||
const [a] = generatePkcePair();
|
||||
const [b] = generatePkcePair();
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
class MemoryFlowStore implements OAuthFlowStore {
|
||||
private verifier = "";
|
||||
private state = "";
|
||||
async saveFlowState(v: string, s: string): Promise<void> {
|
||||
this.verifier = v;
|
||||
this.state = s;
|
||||
}
|
||||
async loadVerifier(): Promise<string> {
|
||||
return this.verifier;
|
||||
}
|
||||
async loadState(): Promise<string> {
|
||||
return this.state;
|
||||
}
|
||||
async clear(): Promise<void> {
|
||||
this.verifier = "";
|
||||
this.state = "";
|
||||
}
|
||||
}
|
||||
|
||||
function makeRepo() {
|
||||
let token: OAuthToken | null = null;
|
||||
return {
|
||||
repo: {
|
||||
async saveToken(_path: string, t: OAuthToken): Promise<void> {
|
||||
token = t;
|
||||
},
|
||||
async loadToken(): Promise<OAuthToken | null> {
|
||||
return token;
|
||||
},
|
||||
},
|
||||
getToken: () => token,
|
||||
};
|
||||
}
|
||||
|
||||
describe("OAuthUseCase", () => {
|
||||
it("builds an auth URL with PKCE params and persists flow state", async () => {
|
||||
const { repo } = makeRepo();
|
||||
const store = new MemoryFlowStore();
|
||||
const exchanger = {
|
||||
async exchangeCode(): Promise<OAuthToken> {
|
||||
return { access_token: "at", refresh_token: "rt", expires_at: 9999, token_type: "Bearer" };
|
||||
},
|
||||
};
|
||||
const useCase = new OAuthUseCase(repo, store, exchanger, "/tmp/token.json");
|
||||
|
||||
const { authUrl, state } = await useCase.startFlow(
|
||||
{ auth_url: "https://provider.example/oauth/authorize", token_url: "https://provider.example/oauth/token", client_id: "cid", scopes: ["openid", "profile"] },
|
||||
"http://localhost:9999/callback",
|
||||
);
|
||||
|
||||
const url = new URL(authUrl);
|
||||
expect(url.searchParams.get("response_type")).toBe("code");
|
||||
expect(url.searchParams.get("client_id")).toBe("cid");
|
||||
expect(url.searchParams.get("scope")).toBe("openid profile");
|
||||
expect(url.searchParams.get("code_challenge_method")).toBe("S256");
|
||||
expect(url.searchParams.get("state")).toBe(state);
|
||||
expect(url.searchParams.get("code_challenge")).toBeTruthy();
|
||||
// Flow state persisted.
|
||||
expect(await store.loadState()).toBe(state);
|
||||
expect(await store.loadVerifier()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("enforces CSRF state match in completeFlow", async () => {
|
||||
const { repo } = makeRepo();
|
||||
const store = new MemoryFlowStore();
|
||||
const exchanger = {
|
||||
async exchangeCode(): Promise<OAuthToken> {
|
||||
return { access_token: "at", refresh_token: "rt", expires_at: 9999, token_type: "Bearer" };
|
||||
},
|
||||
};
|
||||
const useCase = new OAuthUseCase(repo, store, exchanger, "/tmp/token.json");
|
||||
await useCase.startFlow(
|
||||
{ auth_url: "https://provider.example/authorize", token_url: "https://provider.example/token", client_id: "cid", scopes: ["openid"] },
|
||||
"http://localhost:9999/callback",
|
||||
);
|
||||
|
||||
await expect(
|
||||
useCase.completeFlow(
|
||||
{ auth_url: "https://provider.example/authorize", token_url: "https://provider.example/token", client_id: "cid", scopes: ["openid"] },
|
||||
"http://localhost:9999/callback",
|
||||
"code123",
|
||||
"wrong-state",
|
||||
),
|
||||
).rejects.toMatchObject({ kind: "state_mismatch" });
|
||||
});
|
||||
|
||||
it("completes flow successfully with matching state", async () => {
|
||||
const { repo, getToken } = makeRepo();
|
||||
const store = new MemoryFlowStore();
|
||||
let exchangedVerifier = "";
|
||||
const exchanger = {
|
||||
async exchangeCode(_tokenUrl: string, _clientId: string, _secret: string | null, _redirectUri: string, code: string, codeVerifier: string): Promise<OAuthToken> {
|
||||
exchangedVerifier = codeVerifier;
|
||||
return { access_token: `at-${code}`, refresh_token: "rt", expires_at: 9999, token_type: "Bearer" };
|
||||
},
|
||||
};
|
||||
const useCase = new OAuthUseCase(repo, store, exchanger, "/tmp/token.json");
|
||||
const { state } = await useCase.startFlow(
|
||||
{ auth_url: "https://provider.example/authorize", token_url: "https://provider.example/token", client_id: "cid", scopes: ["openid"] },
|
||||
"http://localhost:9999/callback",
|
||||
);
|
||||
const savedVerifier = await store.loadVerifier();
|
||||
|
||||
const token = await useCase.completeFlow(
|
||||
{ auth_url: "https://provider.example/authorize", token_url: "https://provider.example/token", client_id: "cid", scopes: ["openid"] },
|
||||
"http://localhost:9999/callback",
|
||||
"code123",
|
||||
state,
|
||||
);
|
||||
|
||||
expect(token.access_token).toBe("at-code123");
|
||||
// The code verifier passed to the exchanger is the one saved at start.
|
||||
expect(exchangedVerifier).toBe(savedVerifier);
|
||||
// Flow state cleared after completion.
|
||||
expect(await store.loadState()).toBe("");
|
||||
// Token persisted via repo.
|
||||
expect(getToken()?.access_token).toBe("at-code123");
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* OAuth 2.0 authorization-code + PKCE flow use-case. Mirrors
|
||||
* `apps/application/src/auth/oauth_service.rs`.
|
||||
*/
|
||||
import {
|
||||
type OAuthConfig,
|
||||
type OAuthRepository,
|
||||
type OAuthToken,
|
||||
authInvalidConfig,
|
||||
authStateMismatch,
|
||||
} from "@zesdex/domain";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Port traits */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/** Persistence contract for ephemeral OAuth flow state. */
|
||||
export interface OAuthFlowStore {
|
||||
saveFlowState(verifier: string, state: string): Promise<void>;
|
||||
loadVerifier(): Promise<string>;
|
||||
loadState(): Promise<string>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
/** Abstraction for exchanging an authorization code for tokens. */
|
||||
export interface TokenExchanger {
|
||||
exchangeCode(
|
||||
tokenUrl: string,
|
||||
clientId: string,
|
||||
clientSecret: string | null,
|
||||
redirectUri: string,
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
): Promise<OAuthToken>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* PKCE helpers */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
function base64url(input: string | Uint8Array): string {
|
||||
const buf = typeof input === "string" ? new TextEncoder().encode(input) : Buffer.from(input as Uint8Array);
|
||||
return Buffer.from(buf).toString("base64url");
|
||||
}
|
||||
|
||||
/** Generate a PKCE code-verifier and its S256 code-challenge. */
|
||||
export function generatePkcePair(): [string, string] {
|
||||
const bytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(bytes);
|
||||
const verifier = base64url(bytes);
|
||||
const challenge = base64url(createHash("sha256").update(verifier, "utf8").digest());
|
||||
return [verifier, challenge];
|
||||
}
|
||||
|
||||
/** Generate a random CSRF state token (UUID-based). */
|
||||
export function generateStateToken(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* OAuthUseCase */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/** Concrete OAuth flow use-case over injected repositories. */
|
||||
export class OAuthUseCase {
|
||||
constructor(
|
||||
private tokenRepo: OAuthRepository,
|
||||
private flowStore: OAuthFlowStore,
|
||||
private tokenExchanger: TokenExchanger,
|
||||
private tokenPath: string,
|
||||
) {}
|
||||
|
||||
async startFlow(config: OAuthConfig, redirectUri: string): Promise<{ authUrl: string; state: string }> {
|
||||
if (config.auth_url === "") {
|
||||
throw authInvalidConfig("OAuth auth_url is empty");
|
||||
}
|
||||
|
||||
const [verifier, challenge] = generatePkcePair();
|
||||
const state = generateStateToken();
|
||||
|
||||
await this.flowStore.saveFlowState(verifier, state);
|
||||
|
||||
const url = new URL(config.auth_url);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("client_id", config.client_id);
|
||||
url.searchParams.set("redirect_uri", redirectUri);
|
||||
url.searchParams.set("scope", config.scopes.join(" "));
|
||||
url.searchParams.set("state", state);
|
||||
url.searchParams.set("code_challenge_method", "S256");
|
||||
url.searchParams.set("code_challenge", challenge);
|
||||
|
||||
return { authUrl: url.toString(), state };
|
||||
}
|
||||
|
||||
async completeFlow(config: OAuthConfig, redirectUri: string, code: string, state: string): Promise<OAuthToken> {
|
||||
// CSRF check.
|
||||
const expectedState = await this.flowStore.loadState();
|
||||
if (expectedState !== state) throw authStateMismatch();
|
||||
|
||||
// Read the PKCE verifier saved in start_flow.
|
||||
const verifier = await this.flowStore.loadVerifier();
|
||||
|
||||
const token = await this.tokenExchanger.exchangeCode(
|
||||
config.token_url,
|
||||
config.client_id,
|
||||
config.client_secret ?? null,
|
||||
redirectUri,
|
||||
code,
|
||||
verifier,
|
||||
);
|
||||
|
||||
await this.tokenRepo.saveToken(this.tokenPath, token);
|
||||
await this.flowStore.clear();
|
||||
return token;
|
||||
}
|
||||
|
||||
async getToken(): Promise<OAuthToken | null> {
|
||||
return this.tokenRepo.loadToken(this.tokenPath);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Session management use-case. Mirrors `apps/application/src/auth/session_service.rs`.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
type Session,
|
||||
type SessionId,
|
||||
type SessionLockRepository,
|
||||
type SessionRepository,
|
||||
newSession,
|
||||
newSessionId,
|
||||
authOtherError as otherErr,
|
||||
} from "@zesdex/domain";
|
||||
|
||||
/** Concrete session service backed by injected repositories. */
|
||||
export class SessionServiceImpl {
|
||||
constructor(
|
||||
private sessionRepo: SessionRepository,
|
||||
/** Lock repository is injected for future lock acquire/release (matches Rust contract). */
|
||||
// @ts-expect-error -- kept for structural parity with the Rust `SessionServiceImpl<R, L>`
|
||||
private lockRepo: SessionLockRepository,
|
||||
private baseDir: string,
|
||||
) {}
|
||||
|
||||
async createSession(title: string): Promise<Session> {
|
||||
const idRes = newSessionId(randomUUID());
|
||||
if (!idRes.ok) throw otherErr(idRes.error);
|
||||
const titleOwned = title === "" ? "New Session" : title;
|
||||
const session = newSession(idRes.value, titleOwned);
|
||||
await this.sessionRepo.saveSession(this.baseDir, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async listAll(): Promise<Session[]> {
|
||||
return this.sessionRepo.listSessions(this.baseDir);
|
||||
}
|
||||
|
||||
async archiveSession(id: SessionId): Promise<void> {
|
||||
const session = await this.sessionRepo.loadSession(this.baseDir, id);
|
||||
session.archived = true;
|
||||
session.updated_at = Date.now();
|
||||
await this.sessionRepo.saveSession(this.baseDir, session);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Zesdex Application Layer — port traits, use cases, turn service.
|
||||
* Depends only on @zesdex/domain. Higher-level ports implemented by
|
||||
* infrastructure adapters.
|
||||
*/
|
||||
export * from "./ports/index.ts";
|
||||
export * from "./agent/index.ts";
|
||||
export * from "./auth/index.ts";
|
||||
export * from "./cms/index.ts";
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Command types for IAM domain operations. Mirrors `commands.rs`.
|
||||
* Carries only the data needed to construct a session entity.
|
||||
*/
|
||||
export interface NewSession {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function newSessionCommand(title: string): NewSession {
|
||||
return { title };
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Domain error types for the IAM (auth) module.
|
||||
* Mirrors `apps/domain/src/auth/error.rs`.
|
||||
*/
|
||||
import type { DomainError } from "../core/error.ts";
|
||||
|
||||
/** Shared repository error type for IAM persistence. */
|
||||
export type RepositoryError = DomainError;
|
||||
|
||||
/** Errors from service / use-case operations in the IAM domain. */
|
||||
export type ServiceError =
|
||||
| { kind: "repository"; error: DomainError }
|
||||
| { kind: "invalid_config"; message: string }
|
||||
| { kind: "state_mismatch" }
|
||||
| { kind: "oauth_provider"; message: string }
|
||||
| { kind: "other"; message: string };
|
||||
|
||||
export function repositoryErr(err: DomainError): ServiceError {
|
||||
return { kind: "repository", error: err };
|
||||
}
|
||||
export function invalidConfig(message: string): ServiceError {
|
||||
return { kind: "invalid_config", message };
|
||||
}
|
||||
export function stateMismatch(): ServiceError {
|
||||
return { kind: "state_mismatch" };
|
||||
}
|
||||
export function oauthProvider(message: string): ServiceError {
|
||||
return { kind: "oauth_provider", message };
|
||||
}
|
||||
export function authOther(message: string): ServiceError {
|
||||
return { kind: "other", message };
|
||||
}
|
||||
|
||||
export function serviceErrorToString(e: ServiceError): string {
|
||||
switch (e.kind) {
|
||||
case "repository":
|
||||
return `repository error: ${e.error.message}`;
|
||||
case "invalid_config":
|
||||
return `invalid configuration: ${e.message}`;
|
||||
case "state_mismatch":
|
||||
return `OAuth state mismatch — possible CSRF attack`;
|
||||
case "oauth_provider":
|
||||
return `OAuth provider error: ${e.message}`;
|
||||
case "other":
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/** Auth domain module — sessions, locks, OAuth, and their interfaces. */
|
||||
export * from "./session.ts";
|
||||
export * from "./session_id.ts";
|
||||
export * from "./session_lock.ts";
|
||||
export * from "./oauth.ts";
|
||||
export * from "./repository.ts";
|
||||
export * from "./service.ts";
|
||||
export * from "./commands.ts";
|
||||
export {
|
||||
type ServiceError as AuthServiceError,
|
||||
type RepositoryError as AuthRepositoryError,
|
||||
} from "./error.ts";
|
||||
export {
|
||||
repositoryErr as authRepositoryErr,
|
||||
invalidConfig as authInvalidConfig,
|
||||
stateMismatch as authStateMismatch,
|
||||
oauthProvider as authOauthProvider,
|
||||
authOther as authOtherError,
|
||||
serviceErrorToString as authServiceErrorToString,
|
||||
} from "./error.ts";
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* OAuth entities. Mirrors `apps/domain/src/auth/oauth.rs`.
|
||||
*/
|
||||
export interface OAuthToken {
|
||||
/** The OAuth 2.0 access token string. */
|
||||
access_token: string;
|
||||
/** Optional refresh token for long-lived access. */
|
||||
refresh_token?: string;
|
||||
/** Absolute expiry timestamp (epoch seconds). */
|
||||
expires_at: number;
|
||||
/** Token type, e.g. `"Bearer"`. */
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
/** Static configuration for an OAuth provider. */
|
||||
export interface OAuthConfig {
|
||||
/** Authorization endpoint URL. */
|
||||
auth_url: string;
|
||||
/** Token exchange endpoint URL. */
|
||||
token_url: string;
|
||||
/** OAuth client identifier. */
|
||||
client_id: string;
|
||||
/** Optional client secret. */
|
||||
client_secret?: string;
|
||||
/** Space-separated list of requested scopes. */
|
||||
scopes: string[];
|
||||
}
|
||||
|
||||
/** Default scopes for a new OAuthConfig. */
|
||||
export const DEFAULT_OAUTH_SCOPES = ["openid", "profile", "email"];
|
||||
|
||||
export function newOAuthConfig(): OAuthConfig {
|
||||
return {
|
||||
auth_url: "",
|
||||
token_url: "",
|
||||
client_id: "",
|
||||
client_secret: undefined,
|
||||
scopes: [...DEFAULT_OAUTH_SCOPES],
|
||||
};
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Repository trait definitions (interfaces) — pure, no impls.
|
||||
* Mirrors `apps/domain/src/auth/repository.rs`. Infrastructure adapters
|
||||
* implement these.
|
||||
*/
|
||||
import type { OAuthToken } from "./oauth.ts";
|
||||
import type { Session } from "./session.ts";
|
||||
import type { SessionId } from "./session_id.ts";
|
||||
|
||||
/** Repository for loading, saving, listing, and deleting sessions. */
|
||||
export interface SessionRepository {
|
||||
/** List all loadable sessions under `<base_dir>/sessions/`. */
|
||||
listSessions(baseDir: string): Promise<Session[]> | Session[];
|
||||
/** Load a single session by id. */
|
||||
loadSession(baseDir: string, id: SessionId): Promise<Session> | Session;
|
||||
/** Save a session's metadata to disk. */
|
||||
saveSession(baseDir: string, session: Session): Promise<void> | void;
|
||||
/** Delete a session directory and all its contents. */
|
||||
deleteSession(baseDir: string, id: SessionId): Promise<void> | void;
|
||||
}
|
||||
|
||||
/** Repository for per-session PID-file advisory locks. */
|
||||
export interface SessionLockRepository {
|
||||
/** Try to acquire the lock. `true` if acquired, `false` if a live process holds it. */
|
||||
tryLock(sessionDir: string): Promise<boolean> | boolean;
|
||||
/** Release the lock. */
|
||||
unlock(sessionDir: string): Promise<void> | void;
|
||||
/** Check whether a process with the given PID is alive. */
|
||||
isAlive(pid: number): boolean;
|
||||
}
|
||||
|
||||
/** Repository for persisting and loading OAuth tokens. */
|
||||
export interface OAuthRepository {
|
||||
/** Persist an OAuth token to a JSON file. */
|
||||
saveToken(path: string, token: OAuthToken): Promise<void> | void;
|
||||
/** Load an OAuth token, returning `null` if the file does not exist. */
|
||||
loadToken(path: string): Promise<OAuthToken | null> | OAuthToken | null;
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Service trait definitions — use-case boundaries for sessions and OAuth.
|
||||
* Mirrors `apps/domain/src/auth/service.rs`. Implementations live in the
|
||||
* application layer.
|
||||
*/
|
||||
import type { OAuthConfig, OAuthToken } from "./oauth.ts";
|
||||
import type { Session } from "./session.ts";
|
||||
import type { SessionId } from "./session_id.ts";
|
||||
|
||||
/** Session management use-case boundary. */
|
||||
export interface SessionService {
|
||||
/** Create a new session with a generated UUID and the given title. */
|
||||
createSession(title: string): Promise<Session> | Session;
|
||||
/** List all available sessions. */
|
||||
listAll(): Promise<Session[]> | Session[];
|
||||
/** Archive a session by id (sets `archived = true`). */
|
||||
archiveSession(id: SessionId): Promise<void> | void;
|
||||
}
|
||||
|
||||
/** OAuth flow use-case boundary. */
|
||||
export interface OAuthService {
|
||||
/**
|
||||
* Start an OAuth authorization-code + PKCE flow. Returns `{ authUrl, state }`:
|
||||
* the URL to send the user to, and the CSRF state token that must be passed
|
||||
* back into `completeFlow` unchanged.
|
||||
*/
|
||||
startFlow(config: OAuthConfig, redirectUri: string): Promise<{ authUrl: string; state: string }> | { authUrl: string; state: string };
|
||||
/**
|
||||
* Complete the flow: validate `state`, then exchange `code` for a token.
|
||||
* Validates `state` against the persisted value (CSRF check).
|
||||
*/
|
||||
completeFlow(config: OAuthConfig, redirectUri: string, code: string, state: string): Promise<OAuthToken> | OAuthToken;
|
||||
/** Retrieve the currently stored OAuth token (if any). */
|
||||
getToken(): Promise<OAuthToken | null> | OAuthToken | null;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Session metadata. Mirrors `apps/domain/src/auth/session.rs`.
|
||||
*/
|
||||
import * as path from "node:path";
|
||||
|
||||
export interface Session {
|
||||
/** Unique session identifier. */
|
||||
id: string;
|
||||
/** Epoch-millis timestamp of creation. */
|
||||
created_at: number;
|
||||
/** Epoch-millis timestamp of last update. */
|
||||
updated_at: number;
|
||||
/** Human-readable title for the conversation. */
|
||||
title: string;
|
||||
/** Model identifier string. */
|
||||
model: string;
|
||||
/** Workspace root directories associated with this session. */
|
||||
workspace_roots: string[];
|
||||
/** Running count of messages in the conversation. */
|
||||
message_count: number;
|
||||
/** Running count of tokens consumed. */
|
||||
token_count: number;
|
||||
/** Soft-delete flag. */
|
||||
archived: boolean;
|
||||
/** Optional AI-generated conversation summary. */
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-opus-4-8";
|
||||
|
||||
/** Create a new session with the given id/title and current dir as root. */
|
||||
export function newSession(id: string, title: string): Session {
|
||||
const now = Date.now();
|
||||
return {
|
||||
id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
title,
|
||||
model: DEFAULT_MODEL,
|
||||
workspace_roots: [process.cwd()],
|
||||
message_count: 0,
|
||||
token_count: 0,
|
||||
archived: false,
|
||||
summary: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Compute this session's directory under `<base_dir>/sessions/<id>`. */
|
||||
export function sessionDir(session: Session, baseDir: string): string {
|
||||
return path.join(baseDir, "sessions", session.id);
|
||||
}
|
||||
|
||||
/** Compute this session's `conversation.json` path. */
|
||||
export function conversationPath(session: Session, baseDir: string): string {
|
||||
return path.join(sessionDir(session, baseDir), "conversation.json");
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { newSessionId } from "./session_id.ts";
|
||||
|
||||
describe("newSessionId", () => {
|
||||
it("accepts valid UUIDs", () => {
|
||||
expect(newSessionId("550e8400-e29b-41d4-a716-446655440000").ok).toBe(true);
|
||||
expect(newSessionId("my-session_123").ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects path traversal", () => {
|
||||
expect(newSessionId("../etc/passwd").ok).toBe(false);
|
||||
expect(newSessionId("foo/../../bar").ok).toBe(false);
|
||||
expect(newSessionId("foo\\..\\bar").ok).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects empty", () => {
|
||||
expect(newSessionId("").ok).toBe(false);
|
||||
});
|
||||
|
||||
it("returns the id string on success", () => {
|
||||
const res = newSessionId("abc-123");
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) expect(res.value).toBe("abc-123");
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Validated session identifier. Mirrors `apps/domain/src/auth/session_id.rs`.
|
||||
*
|
||||
* Guarantees the inner string is non-empty and contains no path-traversal
|
||||
* characters (`/`, `\`, `..`) or other unsafe delimiters.
|
||||
*/
|
||||
|
||||
const SESSION_ID_RE = /^[A-Za-z0-9_.-]+$/;
|
||||
|
||||
export type SessionId = string & { __sessionId?: true };
|
||||
|
||||
/**
|
||||
* Validate and construct a `SessionId`.
|
||||
* Returns `err` message if the input contains path separators, `..`, or is empty.
|
||||
*/
|
||||
export function newSessionId(id: string): { ok: true; value: SessionId } | { ok: false; error: string } {
|
||||
if (id === "") {
|
||||
return { ok: false, error: "session id must not be empty" };
|
||||
}
|
||||
if (id.includes("/") || id.includes("\\") || id.includes("..") || !SESSION_ID_RE.test(id)) {
|
||||
return { ok: false, error: `session id '${id}' must not contain path separators` };
|
||||
}
|
||||
return { ok: true, value: id as SessionId };
|
||||
}
|
||||
|
||||
/** Assert that a session id is valid (throws if not). */
|
||||
export function assertSessionId(id: string): SessionId {
|
||||
const res = newSessionId(id);
|
||||
if (!res.ok) throw new Error(res.error);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
/** Return the underlying string. */
|
||||
export function sessionIdAsString(id: SessionId): string {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Classic Rust-style UUID format (as used by the Rust app). */
|
||||
export function isUuidLike(id: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* PID-file based advisory lock preventing two processes from operating on
|
||||
* the same session directory concurrently. Mirrors `session_lock.rs`.
|
||||
*
|
||||
* Strategy: atomic `O_CREAT|O_EXCL` acquire → on existing file, liveness-check
|
||||
* the owning PID (Unix: read `/proc/<pid>/exe` and compare to self, plus
|
||||
* `kill(pid, 0)`) → stale locks overwritten atomically (temp + rename + fsync).
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
/** A PID-file lock (`<session_dir>/.lock`) tied to the current process. */
|
||||
export class SessionLock {
|
||||
readonly path: string;
|
||||
readonly pid: number;
|
||||
|
||||
constructor(sessionDir: string) {
|
||||
this.path = path.join(sessionDir, ".lock");
|
||||
this.pid = process.pid;
|
||||
}
|
||||
|
||||
/** Attempt to acquire the lock. Returns `true` on success, `false` if held by a live process. */
|
||||
tryLock(): boolean {
|
||||
// Phase 1: atomic create (O_CREAT|O_EXCL equivalent via flag 'wx').
|
||||
try {
|
||||
const fd = fs.openSync(this.path, "wx");
|
||||
try {
|
||||
fs.writeFileSync(fd, String(this.pid));
|
||||
fs.fsyncSync(fd);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
const code = (e as NodeJS.ErrnoException).code;
|
||||
if (code !== "EEXIST") throw e;
|
||||
// Lock file exists — check staleness.
|
||||
}
|
||||
|
||||
// Phase 2: read owning PID and check liveness.
|
||||
const content = fs.readFileSync(this.path, "utf8").trim();
|
||||
if (content !== "") {
|
||||
const pid = Number(content);
|
||||
if (Number.isFinite(pid) && pid > 0) {
|
||||
if (pidIsAlive(pid)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: stale lock — overwrite atomically.
|
||||
const tmp = this.path + ".tmp";
|
||||
const fd = fs.openSync(tmp, "w");
|
||||
try {
|
||||
fs.writeFileSync(fd, String(this.pid));
|
||||
fs.fsyncSync(fd);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
fs.renameSync(tmp, this.path);
|
||||
// Best-effort parent dir fsync.
|
||||
const parent = path.dirname(this.path);
|
||||
try {
|
||||
const dirFd = fs.openSync(parent, "r");
|
||||
try {
|
||||
fs.fsyncSync(dirFd);
|
||||
} finally {
|
||||
fs.closeSync(dirFd);
|
||||
}
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Explicitly release the lock by removing the lock file. */
|
||||
unlock(): void {
|
||||
try {
|
||||
fs.unlinkSync(this.path);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a process with the given PID is alive. Conservative on
|
||||
* non-Unix: returns true.
|
||||
*/
|
||||
export function pidIsAlive(pid: number): boolean {
|
||||
if (process.platform === "win32") return true;
|
||||
try {
|
||||
const selfExe = fs.readlinkSync("/proc/self/exe");
|
||||
let target: string;
|
||||
try {
|
||||
target = fs.readlinkSync(`/proc/${pid}/exe`);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (target !== selfExe) return false;
|
||||
// kill(pid, 0): throws if the process is gone / not permitted.
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// Re-check to close the TOCTOU window.
|
||||
try {
|
||||
return fs.readlinkSync(`/proc/${pid}/exe`) === selfExe;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
// No /proc (macOS) — fall back to kill(pid,0) only.
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Re-export of conversation types from core (matches Rust `cms/conversation.rs`).
|
||||
*/
|
||||
export type { Conversation } from "../core/conversation.ts";
|
||||
export type { ChatMessage, Role } from "../core/message.ts";
|
||||
export { newConversation, pushMessage, rebuildSystem, toApiMessages } from "../core/conversation.ts";
|
||||
export { Roles } from "../core/message.ts";
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Zesdex Domain Layer — pure types, value objects, and port interfaces.
|
||||
* Zero framework dependencies, zero I/O. Mirrors the Rust `zesdex-domain` crate.
|
||||
*/
|
||||
export * from "./core/index.ts";
|
||||
export * from "./auth/index.ts";
|
||||
export * from "./cms/index.ts";
|
||||
export * from "./agent/index.ts";
|
||||
export * from "./subagent/mod.ts";
|
||||
export * from "./workflow/mod.ts";
|
||||
@@ -14,7 +14,7 @@ export interface ToolExecutor {
|
||||
|
||||
/** Service for running agent turns asynchronously. */
|
||||
export interface AgentTurnService {
|
||||
runTurn(params: import("@zesdex/domain").AgentTurnParams): Promise<void>;
|
||||
runTurn(params: import("@zesdex/agent").AgentTurnParams): Promise<void>;
|
||||
}
|
||||
|
||||
export * from "./turn_service.ts";
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Port traits (interfaces) to external services. Mirrors `apps/application/src/ports/`.
|
||||
* Concrete implementations live in the infrastructure layer.
|
||||
* Port traits (interfaces) that the Agent application layer exposes to
|
||||
* its infrastructure. Concrete implementations live in
|
||||
* `features/agent/infrastructure/`.
|
||||
*/
|
||||
import type { ChatMessage, StreamEvent, ToolDef } from "@zesdex/domain";
|
||||
|
||||
@@ -31,23 +32,3 @@ export interface ProviderService {
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ message: ChatMessage; usage: [number, number] | null }>;
|
||||
}
|
||||
|
||||
/** Abstraction for password hashing and verification. */
|
||||
export interface PasswordService {
|
||||
hash(password: string): Promise<string>;
|
||||
verify(password: string, hash: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/** Abstraction for JWT-based token generation and verification. */
|
||||
export interface TokenService {
|
||||
/** Generate an `[access, refresh]` token pair for the given subject. */
|
||||
generateTokens(sub: string): [string, string];
|
||||
verifyAccessToken(token: string): string;
|
||||
verifyRefreshToken(token: string): string;
|
||||
}
|
||||
|
||||
/** High-level authentication service combining password + token issuance. */
|
||||
export interface AuthService {
|
||||
authenticate(password: string, hash: string): Promise<boolean>;
|
||||
issueTokens(sub: string): [string, string];
|
||||
}
|
||||
File renamed without changes.
+7
-5
@@ -3,23 +3,25 @@
|
||||
* `apps/application/src/agent/turn_service.rs`.
|
||||
*/
|
||||
import {
|
||||
type AgentTurnParams,
|
||||
type ChatMessage,
|
||||
type StreamEvent,
|
||||
type ToolCall,
|
||||
type ToolDef,
|
||||
type TurnEventSink,
|
||||
type JsonValue,
|
||||
systemMessage,
|
||||
userMessage,
|
||||
assistantMessage,
|
||||
toolResultMessage,
|
||||
sanitizeToolArguments,
|
||||
} from "@zesdex/domain";
|
||||
import {
|
||||
type AgentTurnParams,
|
||||
type TurnEventSink,
|
||||
mainAgentPromptWithProjectContext,
|
||||
compactionPrompt,
|
||||
errorRecoveryNote,
|
||||
sanitizeToolArguments,
|
||||
} from "@zesdex/domain";
|
||||
import type { ProviderService } from "../ports/index.ts";
|
||||
} from "@zesdex/agent";
|
||||
import type { ProviderService } from "./ports.ts";
|
||||
import type { ToolExecutor } from "./index.ts";
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
File renamed without changes.
File renamed without changes.
@@ -2,7 +2,7 @@
|
||||
* Domain types for agent lifecycle: turn events, session runtime, progress.
|
||||
* Mirrors `apps/domain/src/agent/mod.rs`.
|
||||
*/
|
||||
import type { ChatMessage, JsonValue, ToolCallResult, UsageStats } from "../core/index.ts";
|
||||
import type { ChatMessage, JsonValue, ToolCallResult, UsageStats } from "@zesdex/domain";
|
||||
import type { AgentProgress } from "./progress.ts";
|
||||
|
||||
/** Which kind of caller (main agent vs subagent vs reviewer) is invoking a tool. */
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,5 @@
|
||||
/** Agent feature — LLM turn loop, tool-execution runtime, subagent-spawning tools.
|
||||
* Vertical slice: domain (turn events/runtime) → application (turn service + ports)
|
||||
* → infrastructure (LLM client + tool registry/executor). */
|
||||
export * from "./domain/index.ts";
|
||||
export * from "./application/index.ts";
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Agent infrastructure — concrete LLM client + tool-execution runtime.
|
||||
* Consumed by the composition root. */
|
||||
export { LlmClient, resolveApiKey } from "./llm/provider.ts";
|
||||
export { InfrastructureToolExecutor } from "./tools/executor.ts";
|
||||
export { allTools, toolDefs } from "./tools/registry.ts";
|
||||
export { type ToolCtx } from "./tools/context.ts";
|
||||
File renamed without changes.
+3
-5
@@ -8,14 +8,12 @@ import {
|
||||
type StreamEvent,
|
||||
type ToolCall,
|
||||
type ToolDef,
|
||||
type Settings,
|
||||
type AppConfig,
|
||||
SseParser,
|
||||
Roles,
|
||||
DEFAULT_API_BASE,
|
||||
DEFAULT_MODEL,
|
||||
} from "@zesdex/domain";
|
||||
import type { ProviderService } from "@zesdex/application";
|
||||
import type { Settings, AppConfig } from "@zesdex/cms";
|
||||
import { DEFAULT_API_BASE, DEFAULT_MODEL } from "@zesdex/agent";
|
||||
import type { ProviderService } from "../../application/ports.ts";
|
||||
|
||||
const DEFAULT_BASE_URL = DEFAULT_API_BASE;
|
||||
const REQUEST_TIMEOUT_MS = 600_000;
|
||||
+1
-1
@@ -7,7 +7,7 @@ import * as path from "node:path";
|
||||
import type { JsonValue } from "@zesdex/domain";
|
||||
import { type Tool, type ToolCtx } from "./mod.ts";
|
||||
import { argStr } from "./util.ts";
|
||||
import { bashControl } from "../bgbash/control.ts";
|
||||
import { bashControl } from "./bgbash/control.ts";
|
||||
|
||||
/** Get the output of a background bash job by ID. */
|
||||
export class BashOutput implements Tool {
|
||||
File renamed without changes.
+2
-2
@@ -2,8 +2,8 @@
|
||||
* Built-in best-practice tools. Mirrors `tools/best_practice.rs`.
|
||||
*/
|
||||
import type { JsonValue } from "@zesdex/domain";
|
||||
import { type Tool, type ToolCtx } from "../tools/mod.ts";
|
||||
import { argStr, optStr } from "../tools/util.ts";
|
||||
import { type Tool, type ToolCtx } from "../mod.ts";
|
||||
import { argStr, optStr } from "../util.ts";
|
||||
import { BestPracticeEngine } from "./engine.ts";
|
||||
|
||||
const engine = new BestPracticeEngine();
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -2,7 +2,7 @@
|
||||
* Tool execution context — shared state passed to every `Tool.run` call.
|
||||
* Mirrors `apps/infrastructure/src/tools/context.rs`.
|
||||
*/
|
||||
import type { Origin, TurnEvent } from "@zesdex/domain";
|
||||
import type { Origin, TurnEvent } from "@zesdex/agent";
|
||||
|
||||
/** Graduated check rule from project config. */
|
||||
export interface GraduatedCheck {
|
||||
Loaded 50 of 199 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user