Compare commits

...
9 Commits
Author SHA1 Message Date
semantic-release-bot 30cc579773 chore(release): 1.23.1 [skip ci]
## [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))
2026-09-03 04:01:28 +00:00
asepharyana a54f1644f5 fix(tui): repair layout overflow + streaming dup; polish UI/state
Two real bugs squashed:
- Layout: giving the header box and scrollbox an explicit width in a
  column-flex root made OpenTUI overflow vertically, pushing the input
  row and status bar entirely off-screen (only header+one message showed).
  Root cause confirmed in a minimal probe; fix = drop the redundant
  width so column children size off the root box.
- Streaming: 9router's OpenAI-compatible layer emits FULL-TEXT chunks
  (the whole transcript so far) instead of incremental deltas, so naive
  content+=token duplicated text ("Hello! 👋 How canHello! 👋 How can...").
  Added reconcileStream() that accepts both incremental and full-text
  modes (shorter-replay → ignore, growing-authoritative → adopt, else
  append), unit-tested for both.

UI/state polish:
- Rich transcript: per-role colour + labelled blocks (YOU/AI/SYS/TOOL),
  word-wrapped to terminal width, reasoning dimmed, no misleading
  "(empty)" on tool-calling assistant messages.
- Header: app · real model · session, spread across full width.
- Status bar: ● running/idle, live token usage (in/out), autocomplete
  hint, active toast.
- Bordered autocomplete dropdown + full-width help/overlay panels.
- Turn runner streams all TurnEvents into state (tool markers, agent
  progress, workflow updates, toasts, usage) — ui.tsx is now display-only.
2026-09-03 11:00:29 +07:00
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 1043 additions and 4276 deletions

No files matched your search

+19 -17
View File
@@ -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
+5 -3
View File
@@ -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
+39 -22
View File
@@ -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.
+35
View File
@@ -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 -4
View File
@@ -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`.
---
+60
View File
@@ -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
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"
# 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
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",
"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"
}
}
-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. */
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];
}
@@ -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.
+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 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;
@@ -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 {
@@ -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();
@@ -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