Compare commits

...
7 Commits
Author SHA1 Message Date
semantic-release-bot 23e68dc979 chore(release): 1.23.0 [skip ci]
# [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))
2026-09-03 01:34:43 +00:00
asepharyana 8c99c6b1ca fix(ci): Release workflow install semantic-release tanpa merusak package.json
npm install -D menulis 5 paket semantic-release + 333 dep ke devDependencies
package.json — padahal repo pakai bun.lock. prepareCmd lalu jalankan
'bun install --frozen-lockfile' yang melihat package.json 'terpolusi'
(1561 paket vs 30 di bun.lock) -> 'lockfile had changes, but lockfile is
frozen' -> Release selalu gagal.

Fix: install semantic-release ke prefix terisolasi /tmp/sr-tools + tambah ke
GITHUB_PATH, jalankan 'semantic-release' langsung (bukan npx yang pakai
proses -D). prepareCmd (--frozen-lockfile) dibiarkan benar: bun.lock
version-agnostic, verifikasi lokal lolos.
2026-09-03 08:33:44 +07:00
asepharyana 9b28a829e5 chore: bersihkan flake.lock Rust/Nix mati — deploy infra 100% Bun
Todo Fase 6 final: hapus flake.lock (artefak era Rust/Nix, tidak ada flake.nix
lagi, deploy.yml sudah bun build --compile + scp binary). Verifikasi wire-shape
lama selesai: settings.json (api_keys/provider/model/review_*) diload true di
headless + TUI real agent. Gate: tsc 0 error, bun test 70 pass, TUI render.
2026-09-03 08:30:23 +07:00
asepharyana b5ebd07fa8 refactor: rombak arsitektur file ke modular clean-arch (vertikal per-feature)
Organisasi ulang layer-first (domain/application/infrastructure) menjadi
vertikal per-feature, behavior tidak berubah:

- src/shared/domain/  -> fondasi bersama (ex-domain/core): message, provider,
  tool_call, tool_result, store, error, usage, conversation
- src/features/agent/ -> LLM turn loop: domain (TurnEvent, AgentTurnParams) +
  application (turn_service, ports) + infrastructure (llm LlmClient + tools)
- src/features/cms/   -> settings/conversation/memory persistence: domain +
  application (services) + infrastructure (persistence repos)
- src/features/subagent/ -> orchestration: domain (AccessTier) + infrastructure
  (engine runner, context, provider, delegate, spawn_tools)
- src/features/workflow/ -> multi-phase + hive-mind: domain + infrastructure
- src/interfaces/     -> cli + tui (outer ring) tetap

Subagent-runner (engine.ts runAgent, division.ts toolsFor) dipindah ke
features/subagent (bukan agent tools) utk memutus coupling agent<->subagent.

Alias tsconfig: @zesdex/domain -> shared/domain, + @zesdex/agent,
@zesdex/agent-infra, @zesdex/cms, @zesdex/subagent, @zesdex/workflow, @zesdex/shared.
Bersihkan dead: application/ports PasswordService/TokenService/AuthService.
compose.ts (composition root) & bootstrap repoint ke barrel feature.

Gates: tsc --noEmit 0 error, bun test 70 pass/0 fail, bun build 147 modules,
./dist/zesdex --headless -> real turn OK.
2026-09-03 02:30:12 +07:00
asepharyana 7e6ed34f0d refactor: rombak jadi full CLI/TUI only — hapus semua interface server (api/ws/grpc/web/daemon) + wire TUI ke agent beneran
- Hapus total interfaces: api (REST), ws (WebSocket), grpc, web, daemon (IPC)
- Hapus infra server-only yang jadi dead code: infrastructure/ipc + seluruh
  stack auth (domain/auth, application/auth, persistence/iam, infrastructure/auth jwt/password/oauth_loopback)
- TUI (OpenTUI) jadi interface utama: wire ke runSingleProcess() real runtime,
  ganti mock '(no LLM configured)' dengan turnService.runTurn asli + streaming
  stream_token/reasoning/tool_result/system_note/error ke transcript
- Tampilkan model asli di header (mis. claude-opus-5), abort turn via Ctrl+C
- CLI jadi mode headless tambahan: zesdex --headless '<prompt>' one-shot turn
- Bersihkan package.json (script tui/headless), tsconfig paths, docker-compose,
  AGENTS.md; hapus StartOAuth/login stub
- verified: tsc 0 err, 70 test pass, build dist/zesdex; headless 'pong' real; TUI
  interactive render + real turn 'say pong in one word' -> assistant 'pong'
2026-09-03 01:19:27 +07:00
asepharyana 34322e492a chore: hapus infra Nix Rust mati; deploy.yml rombak ke Bun native
Semua flake/default/shell merujuk Cargo.lock + crate 'zesdex-gateway' yang
sudah dihapus saat rombak monorepo ke project tunggal Bun, sehingga
nix build .#default pasti gagal. Hapus infra Nix yang tak bisa dipakai:

- hapus flake.nix, default.nix, shell.nix, flakehub-publish-rolling.yaml
- deploy.yml: ganti nix build .#default -> setup-bun + bun build --compile
  + scp dist/zesdex -> /usr/local/bin/zesdex -> systemctl restart zesdex
- docker-compose.yml: RUST_LOG usang -> ZESDEX_API_PORT (env TS)

verify: tsc 0 err, bun test 79 pass, dist/zesdex build OK.
2026-09-03 00:14:22 +07:00
asepharyana 3e260e6077 refactor(tui): rombak renderer ke OpenTUI React bindings
Ganti renderer ANSI line-based (render.ts) dan event loop raw stdin dengan
OpenTUI React bindings (@opentui/react + @opentui/core, React 19.2):

- ui.tsx: komponen ZesdexApp (header/scrollbox transkrip/input/footer),
  adaptor toControllerKey (OpenTUI KeyEvent -> controller KeyEvent),
  bootTui (createCliRenderer + createRoot + mount).
- run.ts: pakai bootTui, jalankan OpenTUI sampai user quit.
- state/action/command/controller tetap logika murni (tidak tersentuh).
- 5 unit test baru untuk toControllerKey; 79 test total hijau.
- tsconfig: jsx react-jsx + jsxImportSource @opentui/react.
- hapus render.ts (tidak terpakai); hapus script format rusak (bun fmt).
- deps: @opentui/core, @opentui/react, react, @types/react.

verify: tsc 0 err, bun test 79 pass, dist/zesdex merender TUI via pty.
2026-09-03 00:10:01 +07:00
199 changed files with 787 additions and 4274 deletions

No files matched your search

+19 -17
View File
@@ -1,4 +1,4 @@
name: Build & Deploy (Nix) name: Build & Deploy (Bun)
on: on:
push: push:
@@ -25,26 +25,27 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install Nix - name: Setup Bun
uses: DeterminateSystems/nix-installer-action@v22 uses: oven-sh/setup-bun@v2
with: with:
determinate: false bun-version: 1.3.14
extra-conf: |
sandbox = false
accept-flake-config = true
- name: Cache Nix - name: Install deps
uses: DeterminateSystems/magic-nix-cache-action@v14 run: bun install --frozen-lockfile
- name: Build zesdex - name: Type-check
id: build run: bun run check
- name: Test
run: bun test
- name: Build binary
run: | run: |
nix build .#default --impure --option sandbox false --print-build-logs bun run build
STORE_PATH=$(readlink result) test -x dist/zesdex && echo "binary OK ($(stat -c%s dist/zesdex) bytes)"
echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT"
echo "Build OK: $STORE_PATH"
- name: Setup SSH key - name: Setup SSH key
if: env.VPS_HOST != '' && env.VPS_HOST != 'null'
env: env:
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: | run: |
@@ -56,6 +57,7 @@ jobs:
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
- name: Deploy zesdex to VPS - name: Deploy zesdex to VPS
if: env.VPS_HOST != '' && env.VPS_HOST != 'null'
run: | run: |
STORE_PATH="${{ steps.build.outputs.store-path }}" scp dist/zesdex "$VPS_USER@$VPS_HOST:/usr/local/bin/zesdex"
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" 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
+5 -3
View File
@@ -40,16 +40,18 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install semantic-release - name: Install semantic-release (isolated prefix — do NOT touch repo package.json)
run: | run: |
npm install -D \ mkdir -p /tmp/sr-tools
npm install --prefix /tmp/sr-tools \
semantic-release \ semantic-release \
@semantic-release/exec \ @semantic-release/exec \
@semantic-release/git \ @semantic-release/git \
@semantic-release/changelog \ @semantic-release/changelog \
@semantic-release/github @semantic-release/github
echo "/tmp/sr-tools/node_modules/.bin" >> "$GITHUB_PATH"
- name: Run semantic-release - name: Run semantic-release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release run: semantic-release
+39 -22
View File
@@ -1,38 +1,55 @@
# AGENTS.md # 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 ## 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. 2. **Clean Code** — functions under ~40 lines, one level of abstraction per function,
- Domain has ZERO framework dependencies. descriptive names, no flag arguments, no commented-out dead code.
- 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 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). ## Commands
- `@code-reviewer` — Code review specialist (subagent).
## Available Commands - `bun run check` — `tsc --noEmit` typecheck.
- `bun run test` — run all Bun tests.
- `/check` — Run cargo check, clippy, and tests. - `bun run build` — compile `dist/zesdex` binary.
- `/audit` — Code quality audit against clean-architecture best practices. - `bun run tui` — run the interactive TUI.
- `/doc` — Generate or update doc comments. - `bun run headless "<prompt>"` — one-shot agent turn.
- `bun run bootstrap` — seed default settings/app_config.
+28
View File
@@ -1,3 +1,31 @@
# [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) # [1.22.0](https://github.com/asepharyana/zesdex/compare/v1.21.2...v1.22.0) (2026-08-28)
+3 -4
View File
@@ -3,8 +3,7 @@
> Plan lengkap migrasi in-place dari Rust (11 crate, clean architecture 4 lapis) ke monorepo > Plan lengkap migrasi in-place dari Rust (11 crate, clean architecture 4 lapis) ke monorepo
> TypeScript/Bun. Bahasa Indonesia, commit pakai Conventional Commits. > 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. > **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.
> Base: `bun run check` bersih, 74 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 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] 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`) - [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] 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 - [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`.
--- ---
+60
View File
@@ -6,9 +6,13 @@
"name": "zesdex", "name": "zesdex",
"dependencies": { "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",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.0", "@types/bun": "^1.2.0",
"@types/react": "^19.2.18",
"typescript": "^5.7.0", "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=="], "@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/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/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=="], "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=="], "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=="], "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
View File
@@ -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
View File
@@ -1,5 +1,7 @@
version: "3.9" 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: services:
zesdex: zesdex:
build: build:
@@ -12,12 +14,8 @@ services:
- ./workspace:/workspace:ro - ./workspace:/workspace:ro
environment: environment:
- ZESDEX_DATA_DIR=/data - ZESDEX_DATA_DIR=/data
- RUST_LOG=${RUST_LOG:-info}
stdin_open: true stdin_open: true
tty: true tty: true
# For daemon mode, expose the IPC socket directory
# ports:
# - "127.0.0.1:${ZESDEX_PORT:-0}:${ZESDEX_PORT:-0}"
volumes: volumes:
zesdex-data: zesdex-data:
Generated
-82
View File
@@ -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
}
-60
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{ {
"name": "zesdex", "name": "zesdex",
"version": "1.22.0", "version": "1.23.0",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "bun@1.3.14", "packageManager": "bun@1.3.14",
@@ -10,21 +10,20 @@
"build": "bun build src/interfaces/cli/index.ts --compile --outfile=dist/zesdex", "build": "bun build src/interfaces/cli/index.ts --compile --outfile=dist/zesdex",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"test": "bun test", "test": "bun test",
"format": "bun fmt", "tui": "bun src/interfaces/cli/index.ts",
"cli": "bun src/interfaces/cli/index.ts", "cli": "bun src/interfaces/cli/index.ts --headless",
"bootstrap": "bun src/interfaces/cli/bootstrap.ts", "headless": "bun src/interfaces/cli/index.ts --headless",
"api": "bun src/interfaces/api/main.ts", "bootstrap": "bun src/interfaces/cli/bootstrap.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"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.0", "@types/bun": "^1.2.0",
"@types/react": "^19.2.18",
"typescript": "^5.7.0" "typescript": "^5.7.0"
}, },
"dependencies": { "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"
} }
} }
-17
View File
@@ -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";
}
-3
View File
@@ -1,3 +0,0 @@
/** Auth application module — OAuth PKCE + session management use-cases. */
export * from "./oauth_service.ts";
export * from "./session_service.ts";
-141
View File
@@ -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");
});
});
-121
View File
@@ -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);
}
}
-44
View File
@@ -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);
}
}
-9
View File
@@ -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";
-11
View File
@@ -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 };
}
-47
View File
@@ -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;
}
}
-20
View File
@@ -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";
-40
View File
@@ -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],
};
}
-39
View File
@@ -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;
}
-35
View File
@@ -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;
}
-56
View File
@@ -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");
}
-25
View File
@@ -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");
});
});
-41
View File
@@ -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);
}
-120
View File
@@ -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;
}
}
}
-7
View File
@@ -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";
-10
View File
@@ -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. */ /** Service for running agent turns asynchronously. */
export interface AgentTurnService { export interface AgentTurnService {
runTurn(params: import("@zesdex/domain").AgentTurnParams): Promise<void>; runTurn(params: import("@zesdex/agent").AgentTurnParams): Promise<void>;
} }
export * from "./turn_service.ts"; export * from "./turn_service.ts";
@@ -1,6 +1,7 @@
/** /**
* Port traits (interfaces) to external services. Mirrors `apps/application/src/ports/`. * Port traits (interfaces) that the Agent application layer exposes to
* Concrete implementations live in the infrastructure layer. * its infrastructure. Concrete implementations live in
* `features/agent/infrastructure/`.
*/ */
import type { ChatMessage, StreamEvent, ToolDef } from "@zesdex/domain"; import type { ChatMessage, StreamEvent, ToolDef } from "@zesdex/domain";
@@ -31,23 +32,3 @@ export interface ProviderService {
signal?: AbortSignal, signal?: AbortSignal,
): Promise<{ message: ChatMessage; usage: [number, number] | null }>; ): 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];
}
@@ -3,23 +3,25 @@
* `apps/application/src/agent/turn_service.rs`. * `apps/application/src/agent/turn_service.rs`.
*/ */
import { import {
type AgentTurnParams,
type ChatMessage, type ChatMessage,
type StreamEvent, type StreamEvent,
type ToolCall, type ToolCall,
type ToolDef, type ToolDef,
type TurnEventSink,
type JsonValue, type JsonValue,
systemMessage, systemMessage,
userMessage, userMessage,
assistantMessage, assistantMessage,
toolResultMessage, toolResultMessage,
sanitizeToolArguments,
} from "@zesdex/domain";
import {
type AgentTurnParams,
type TurnEventSink,
mainAgentPromptWithProjectContext, mainAgentPromptWithProjectContext,
compactionPrompt, compactionPrompt,
errorRecoveryNote, errorRecoveryNote,
sanitizeToolArguments, } from "@zesdex/agent";
} from "@zesdex/domain"; import type { ProviderService } from "./ports.ts";
import type { ProviderService } from "../ports/index.ts";
import type { ToolExecutor } from "./index.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. * Domain types for agent lifecycle: turn events, session runtime, progress.
* Mirrors `apps/domain/src/agent/mod.rs`. * 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"; import type { AgentProgress } from "./progress.ts";
/** Which kind of caller (main agent vs subagent vs reviewer) is invoking a tool. */ /** Which kind of caller (main agent vs subagent vs reviewer) is invoking a tool. */
File renamed without changes.
File renamed without changes.
+5
View File
@@ -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";
@@ -8,14 +8,12 @@ import {
type StreamEvent, type StreamEvent,
type ToolCall, type ToolCall,
type ToolDef, type ToolDef,
type Settings,
type AppConfig,
SseParser, SseParser,
Roles, Roles,
DEFAULT_API_BASE,
DEFAULT_MODEL,
} from "@zesdex/domain"; } 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 DEFAULT_BASE_URL = DEFAULT_API_BASE;
const REQUEST_TIMEOUT_MS = 600_000; const REQUEST_TIMEOUT_MS = 600_000;
@@ -7,7 +7,7 @@ import * as path from "node:path";
import type { JsonValue } from "@zesdex/domain"; import type { JsonValue } from "@zesdex/domain";
import { type Tool, type ToolCtx } from "./mod.ts"; import { type Tool, type ToolCtx } from "./mod.ts";
import { argStr } from "./util.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. */ /** Get the output of a background bash job by ID. */
export class BashOutput implements Tool { export class BashOutput implements Tool {
@@ -2,8 +2,8 @@
* Built-in best-practice tools. Mirrors `tools/best_practice.rs`. * Built-in best-practice tools. Mirrors `tools/best_practice.rs`.
*/ */
import type { JsonValue } from "@zesdex/domain"; import type { JsonValue } from "@zesdex/domain";
import { type Tool, type ToolCtx } from "../tools/mod.ts"; import { type Tool, type ToolCtx } from "../mod.ts";
import { argStr, optStr } from "../tools/util.ts"; import { argStr, optStr } from "../util.ts";
import { BestPracticeEngine } from "./engine.ts"; import { BestPracticeEngine } from "./engine.ts";
const engine = new BestPracticeEngine(); const engine = new BestPracticeEngine();
@@ -2,7 +2,7 @@
* Tool execution context — shared state passed to every `Tool.run` call. * Tool execution context — shared state passed to every `Tool.run` call.
* Mirrors `apps/infrastructure/src/tools/context.rs`. * 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. */ /** Graduated check rule from project config. */
export interface GraduatedCheck { export interface GraduatedCheck {
Loaded 50 of 199 files, more files were not shown because too many files have changed in this diff. Show more