Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ The project optimizes for reliable tests, lower API cost, and fast debugging.

## Current Architecture

- CLI entry point: `cmd/server/main.go` only exits through `run`; command wiring lives in `cmd/server/root.go`
- CLI command files: `cmd/server/root.go` wires `serve.go`, `migrate.go`, `db.go`, `config.go`, `doctor.go`, `provider.go`, `models.go`, `tools.go`, `audit.go`, `auth.go`, `analyze.go`, `version.go`, `schema.go`, `completion.go` (`provider_startup_probe.go` holds provider-probe helpers)
- Second binary: the CLI `cmd/trajecta` (build artifact `trajecta`) performs local, server-less operations. Today that is the one-off rename migration (`.env` prefix mapping, legacy SQLite merge into Postgres, cassette magic rewrite and structural validation, SQLite archiving) and cassette vault reorganisation (`layout plan` reads the preludes and reports the move list, `layout apply` renames the files and repoints `logs.path` and `upstream_exchanges.cassette_path` in one transaction per move). Its logic lives in `internal/legacymigrate`; every command is a dry run unless `--apply` is passed, and no legacy `LLM_TRACELAB_*` fallback exists in the binaries
- Management HTTP/MCP wiring shared by serve and tests: `cmd/server/management.go`
- CLI split: `server` is the runtime binary and `trajecta` is the operations CLI. A command belongs to `server` only if it needs the proxy process; that is `serve` (also the bare invocation) plus the per-binary `version` and `completion`
- `cmd/server`: `main.go` only exits through `run`; `root.go` wires `serve`, `version`, `completion` and the `--config`/`--format` flags and nothing else. `management.go` holds the management HTTP/MCP wiring shared by serve and tests, `provider_startup_probe.go` the startup provider probe
- `cmd/trajecta`: the operations CLI (build artifact `trajecta`). Its own local, server-less work is the one-off rename migration (`.env` prefix mapping, legacy SQLite merge into Postgres, cassette magic rewrite and structural validation, SQLite archiving) and cassette vault reorganisation (`layout plan` reads the preludes and reports the move list, `layout apply` renames the files and repoints `logs.path` and `upstream_exchanges.cassette_path` in one transaction per move); that logic lives in `internal/legacymigrate`, every command is a dry run unless `--apply` is passed, and no legacy `LLM_TRACELAB_*` fallback exists in the binaries. It also exposes every server-management command group through `internal/cli`: `analyze`, `audit`, `auth`, `config`, `db`, `doctor`, `migrate`, `models`, `provider`, `schema`, `tools`. `root.go` binds `--config`/`--format` to a viper instance and hands it to `internal/cli.Register`
- `internal/cli`: the CLI contract plus those command groups. The contract is the exit-code table, the JSON envelope, the viper-backed `Runtime` (`--config`/`--format`), the `Execute` top-level error path and the ERROR-recording log handler; both binaries share it, `cmd/server` imports the contract only and registers none of the commands
- `internal/appdb`: the config-driven application-database entry points shared by serve startup and the trajecta `db`/`auth`/`audit`/`analyze` commands - driver normalization, migration mode, `MigrateUp`/`OptimizeIndexes`, `Open`/`Initialize` and the auth store opener
- Reverse proxy: `internal/proxy`
- Recording pipeline: `internal/recorder`
- Application store and metadata index: `internal/store`
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
## [Unreleased]

### Changed
- The `server` binary no longer carries the commands that never needed a proxy process. Every management group - `analyze`, `audit`, `auth`, `config`, `db`, `doctor`, `migrate`, `models`, `provider`, `schema`, `tools` - moved to the `trajecta` CLI, and `server` keeps only what is about running or describing the running binary: `serve` (also the bare `server -c config.yaml` invocation), `version` and `completion`. The command bodies and the CLI contract they were written against now live together in `internal/cli`: the exit-code table, the JSON envelope (`ok`/`command`/`result`/`warnings`/`error`), the viper-backed `Runtime` that owns `-c/--config` and `--format`, and the `Execute` top-level path that classifies an error, records it and writes the envelope to stderr. `cmd/server` imports that contract for its own three commands and registers none of the migrated ones; `cmd/trajecta` binds the same two flags and calls `cli.Register`, so a command that used to be spelled `server db migrate status` is now `trajecta db migrate status` with the same subcommands, flags, exit codes and JSON shape. The application-database entry points the two binaries now share - driver normalization, the migration-mode decision, `MigrateUp`/`OptimizeIndexes`, `Open`/`Initialize` and the auth store opener - moved to `internal/appdb`, which is also where serve startup reads them from, so the startup path and the `db`/`auth` commands can no longer drift apart; `NormalizeMCPPath`/`ValidateServe` moved to `internal/config` and the provider-probe target helpers to `internal/providerprobe` for the same reason. `Taskfile.yml` runs `migrate`, `migrate:db:up`, `auth:init-user` and `auth:create-token` through `go run ./cmd/trajecta` now, and `task test:e2e` covers `./cmd/trajecta` alongside `./cmd/server`.
- `trajecta` reports failures through the shared contract instead of its own copy of it. An error envelope is now written to stderr rather than stdout, which is what lets `trajecta db migrate status --format json > state.json` keep the result parseable, and an internal failure exits `8` instead of `1`, matching `server`. Commands that emit an `ok: false` envelope and then return an error still write exactly one JSON object: `cli.Execute` records that a result was already written and does not add a second one.
- The 协议 tab draws each half of the parsed exchange as a collapsed tree instead of an expanded one. Every node used to be opened at once with its own raw JSON block, so a request of forty messages pushed the response half of the page past the end of the scroll; a row is now one value, opened on demand, labelled with the parser's name for it (`message`, `tool_call`, `tool_result`, with the role beside it), showing the node's own text when it has one, and falling back to the shape of the value (`future_item`, `object`, `array`) rather than printing `unknown`. Clicking a row opens that value in a dialog named by its JSON path, where drilling further stays in the dialog, and a payload the parser could not read as JSON - an error body, a stream fragment - is shown as the text it is.
- The trace detail tab strip is pinned under the page header. The 会话 tab lands on the newest turn of a long conversation, which is the bottom of the page, and the tab row that scrolled away left the reader no way to change panels except going back to the top. `AppShell` now measures every element marked `data-sticky-chrome`, hands each the offset it pins at through `--sticky-offset`, and keeps `--sticky-chrome-h` as the total for `scroll-padding-top`; the conversation rail's height is capped against that total so it still fits below the taller chrome.
- The 工具调用 tab's definition dialog draws the declaration as the form it describes. A JSON Schema read as JSON puts every parameter two levels down and answers "what is the declaration" rather than "what can I pass", so parameters are now one row each with their type, requiredness, description, enumeration values, default, format, range and `additionalProperties`, nested objects and array elements indented under the parameter that declares them, and the raw schema kept behind a fold for the keywords a form cannot draw. The dialog's summary tag came from `buildToolSchemaSummary`, which spoke English inside a localized dialog, and is gone with its unit tests.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ func TestChat(t *testing.T) {

| 目录 | 职责 |
| --- | --- |
| `cmd/server` | CLI 入口与全部子命令(serve、db、auth、provider、models、audit、analyze、doctor…) |
| `cmd/server` | 运行时二进制入口:`serve`(以及 `version`、`completion`) |
| `cmd/trajecta`、`internal/cli` | 运维 CLI 与两个二进制共享的命令契约:upgrade、layout、db、auth、provider、models、audit、analyze、doctor… |
| `internal/proxy` | 反向代理、协议入口归一化、流式响应拦截 |
| `internal/recorder` | cassette 录制与落盘 |
| `pkg/recordfile` | 录制格式解析与写入(V3 写入,V2 / 旧 magic 兼容读取) |
Expand Down
3 changes: 2 additions & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ The complete preset list (25 registry keys, 20 distinct providers after alias de

| Directory | Responsibility |
| --- | --- |
| `cmd/server` | CLI entry point and all subcommands (serve, db, auth, provider, models, audit, analyze, doctor…) |
| `cmd/server` | Runtime binary entry point: `serve` (plus `version` and `completion`) |
| `cmd/trajecta`, `internal/cli` | Operations CLI and the command contract both binaries share: upgrade, layout, db, auth, provider, models, audit, analyze, doctor… |
| `internal/proxy` | Reverse proxy, protocol entry normalization, streaming interception |
| `internal/recorder` | Cassette recording and persistence |
| `pkg/recordfile` | Record format parsing and writing (writes V3, reads V2 and the legacy magic) |
Expand Down
10 changes: 5 additions & 5 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ tasks:
test:e2e:
desc: Run local end-to-end tests without external network dependencies
cmds:
- go test ./internal/proxy ./internal/monitor ./cmd/server ./unittest
- go test ./internal/proxy ./internal/monitor ./cmd/server ./cmd/trajecta ./unittest

atif:validate:
desc: Validate ATIF-v1.8 JSONL with pinned Harbor models (install scripts/atif-requirements.txt first)
Expand Down Expand Up @@ -210,12 +210,12 @@ tasks:
migrate:
desc: Rewrite legacy V2 cassettes to V3 and rebuild SQLite index
cmds:
- go run ./cmd/server migrate -c {{.CONFIG}}
- go run ./cmd/trajecta migrate -c {{.CONFIG}}

migrate:db:up:
desc: Apply application database migrations
cmds:
- go run ./cmd/server db migrate up -c {{.CONFIG}}
- go run ./cmd/trajecta db migrate up -c {{.CONFIG}}

migrate:legacy:
desc: Inspect a pre-rename deployment; pass ARGS="--apply" to write changes
Expand All @@ -225,12 +225,12 @@ tasks:
auth:init-user:
desc: Initialize an auth user, pass USER and PASSWORD
cmds:
- go run ./cmd/server auth init-user -c {{.CONFIG}} --username "{{.USER}}" --password "{{.PASSWORD}}"
- go run ./cmd/trajecta auth init-user -c {{.CONFIG}} --username "{{.USER}}" --password "{{.PASSWORD}}"

auth:create-token:
desc: Create an API token for a user, pass USER and NAME
cmds:
- go run ./cmd/server auth create-token -c {{.CONFIG}} --username "{{.USER}}" --name "{{.NAME}}"
- go run ./cmd/trajecta auth create-token -c {{.CONFIG}} --username "{{.USER}}" --name "{{.NAME}}"

run:
desc: Start proxy and monitor with local config
Expand Down
9 changes: 6 additions & 3 deletions cmd/server/completion.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package main

import "github.com/spf13/cobra"
import (
"github.com/kingfs/Trajecta/internal/cli"
"github.com/spf13/cobra"
)

func newCompletionCommand(root *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "completion bash|zsh|fish|powershell",
Short: "Generate shell completion scripts",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return cliUsageError("completion requires one shell: bash, zsh, fish, or powershell", "shell")
return cli.UsageError("completion requires one shell: bash, zsh, fish, or powershell", "shell")
}
switch args[0] {
case "bash":
Expand All @@ -20,7 +23,7 @@ func newCompletionCommand(root *cobra.Command) *cobra.Command {
case "powershell":
return root.GenPowerShellCompletion(cmd.OutOrStdout())
default:
return cliUsageError("unsupported shell "+args[0], "shell")
return cli.UsageError("unsupported shell "+args[0], "shell")
}
},
}
Expand Down
33 changes: 2 additions & 31 deletions cmd/server/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ package main

import (
"crypto/sha256"
"errors"

"log/slog"
"net/http"
"strings"
"time"

"github.com/kingfs/Trajecta/internal/auth"
Expand Down Expand Up @@ -60,7 +57,7 @@ func newManagementMuxWithFunctionExecutorManager(
// endpoint for older clients.
Stateless: true,
})
mux.Handle(normalizeMCPPathMust(cfg.MCP.Path), auth.Middleware(mcpHandler, "trajecta-mcp", verifier))
mux.Handle(config.EffectiveMCPPath(cfg), auth.Middleware(mcpHandler, "trajecta-mcp", verifier))
}
functionExecutorConfig := cfg.ResponsesFunctionExecutorsConfig()
if functionExecutorManager != nil {
Expand Down Expand Up @@ -102,33 +99,7 @@ func effectiveMCPPath(cfg *config.Config) string {
if !cfg.MCP.Enabled {
return ""
}
return normalizeMCPPathMust(cfg.MCP.Path)
}

func normalizeMCPPathMust(path string) string {
normalized, err := normalizeMCPPath(path)
if err != nil {
panic(err)
}
return normalized
}

func normalizeMCPPath(path string) (string, error) {
path = strings.TrimSpace(path)
if path == "" {
return "/mcp", nil
}
if path == "/" {
return "", errors.New("mcp.path must not be /")
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
path = strings.TrimRight(path, "/")
if path == "" {
return "", errors.New("mcp.path must not be empty")
}
return path, nil
return config.EffectiveMCPPath(cfg)
}

func logResolvedTargets(rtr *router.Router) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/server/provider_startup_probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func applyStartupProviderProbeToTarget(ctx context.Context, target *config.Upstr
}
probeTarget := *target
probeTarget.Upstream = *upstreamCfg
report, err := providerprobe.Probe(ctx, providerProbeTargetFromConfig(probeTarget), client)
report, err := providerprobe.Probe(ctx, providerprobe.TargetFromConfig(probeTarget), client)
if err != nil {
slog.Warn("Startup provider probe failed; continuing with explicit configuration",
"provider_id", strings.TrimSpace(probeTarget.ID),
Expand Down
Loading
Loading