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
87 changes: 87 additions & 0 deletions architecture/CMD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Cmd Command Architecture

Command: `clai [flags] cmd <text>`

The **cmd** command is a specialized variant of the text querier (`query`) designed to produce shell commands. It reuses the text pipeline end-to-end, but:

- switches the system prompt to a “write only a bash command” prompt
- enables `cmdMode` on the querier, which adds an execute/quit confirmation loop after the model finishes streaming output

## Entry Flow

```text
main.go:run()
→ internal.Setup(ctx, usage, args)
→ parseFlags()
→ getCmdFromArgs() → CMD
→ setupTextQuerierWithConf(..., CMD, ...)
→ Load textConfig.json (or default)
→ tConf.CmdMode = true
→ tConf.SystemPrompt = tConf.CmdModePrompt
→ applyFlagOverridesForText(...)
→ ProfileOverrides()
→ setupToolConfig(...)
→ applyProfileOverridesForText(...)
→ SetupInitialChat(args)
→ CreateTextQuerier(ctx, tConf)
→ querier.Query(ctx)
→ (stream tokens)
→ if cmdMode: handleCmdMode() → optionally execute
```

## Key Files

| File | Purpose |
|------|---------|
| `internal/setup.go` | CMD mode dispatch; sets `CmdMode` and `SystemPrompt` before chat setup |
| `internal/text/conf.go` | Defines `CmdModePrompt` default and config fields |
| `internal/text/conf_profile.go` | Special handling when combining profiles with cmd-mode prompt |
| `internal/text/querier_setup.go` | Propagates `CmdMode` into the runtime querier (`querier.cmdMode`) |
| `internal/text/querier_cmd_mode.go` | Implements the cmd execution confirmation loop |

## Prompting / profiles interaction

In cmd mode, profiles are still allowed, but `internal/text/conf_profile.go` ensures the cmd-mode prompt stays authoritative. It wraps prompts in a pattern roughly like:

```text
|| <cmd-prompt> | <custom guided profile> ||
```

and explicitly warns the model not to disobey the cmd prompt.

Also note: profile tool enabling is restricted in cmd mode:

- `c.UseTools = (profile.UseTools && !c.CmdMode) || (len(profile.McpServers) > 0)`

So a profile cannot force-enable built-in tools in cmd mode, but MCP servers may still enable MCP tools.

## Runtime behavior (`handleCmdMode`)

After streaming completes, `handleCmdMode()`:

1. Prints a newline (streaming may end without `\n`).
2. Enters a loop:

```text
Do you want to [e]xecute cmd, [q]uit?:
```

3. If user selects `e`, it executes the model output as a local process.

### Execution details (`executeLlmCmd`)

`executeLlmCmd()`:

- expands `~` to `$HOME` via `utils.ReplaceTildeWithHome`
- removes all double quotes (`"`) from the output to approximate typical shell expansion behavior
- splits by spaces into `cmd` + `args`
- runs `exec.Command(cmd, args...)` with stdout/stderr wired to the current process

Errors:

- non-zero exit code is wrapped into a formatted `code: <exitCode>, stderr: ''` message
- other exec errors are wrapped with context

## Security notes

Cmd mode can execute arbitrary commands. The safety mechanism is explicit user confirmation before execution. Tool restrictions via profiles/flags can further reduce risk, but cannot make executing a suggested command “safe” by itself.
247 changes: 247 additions & 0 deletions architecture/CONFIG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Configuration Architecture

This document describes **how configuration works** in clai: where config is stored, how files are created, and how the *override cascade* is applied (defaults → mode config → model-specific config → profiles → flags).

It is the “index” doc for understanding why a command behaves the way it does.

## Config directories

A clai install uses two primary directories:

- **Config dir**: `utils.GetClaiConfigDir()` ⇒ typically:

```text
<os.UserConfigDir()>/ .clai
```

- **Cache dir**: `utils.GetClaiCacheDir()` ⇒ typically:

```text
<os.UserCacheDir()>/ .clai
```

On startup, `main.run()` ensures the config dir exists:

- `utils.CreateConfigDir(configDirPath)`

The config dir is also printed in `clai help` (see `main.go` usage template).

## Config file types

There are *three* main axes:

1. **Mode configs** (coarse per-command defaults)
2. **Model-specific vendor request configs** (fine-grained provider settings)
3. **Profiles** (workflow presets that override mode+model config)

Plus chat transcripts and reply pointers, which aren’t “config” but strongly affect behavior.

### 1) Mode configs

Stored at:

- `<config>/textConfig.json`
- `<config>/photoConfig.json`
- `<config>/videoConfig.json`

They contain settings that are broadly applicable to that “mode” (text vs image vs video). For text this includes:

- chosen model
- printing options (raw vs glow)
- system prompt
- tool use selection defaults
- globbing selection (via `-g` flag which then modifies prompt building)

Mode config loading happens inside `internal.setupTextQuerierWithConf` / `internal.Setup`:

- `utils.LoadConfigFromFile(confDir, "textConfig.json", migrateOldChatConfig, &text.Default)`
- `utils.LoadConfigFromFile(confDir, "photoConfig.json", migrateOldPhotoConfig, &photo.DEFAULT)`
- `utils.LoadConfigFromFile(confDir, "videoConfig.json", nil, &video.Default)`

`LoadConfigFromFile` is responsible for:

- creating the file from defaults if it doesn’t exist
- `json.Unmarshal` into the provided struct
- optionally running a migration callback

### 2) Model-specific vendor configs

These are JSON files created per *vendor+model*.

They exist because different vendors expose different request options and clai avoids a combinatorial CLI flag explosion.

Location:

- `<config>/<vendor>_<model-type>_<model-name>.json`

Example (illustrative):

- `openai_gpt_gpt-4.1.json`
- `anthropic_chat_claude-sonnet-4-20250514.json`

Creation/loading typically occurs during querier creation (`CreateTextQuerier`, `CreatePhotoQuerier`, etc.) and is vendor-specific.

**Important characteristic**:

> These JSON files are effectively “request templates” that are unmarshaled into whatever request struct the vendor implementation uses.
That is why setup exposes them as “model files” rather than as first-class flags.

### 3) Profiles

Profiles are stored as:

- `<config>/profiles/<name>.json`

Profiles are applied only for text-like modes (query/chat/cmd) and are intended to:

- quickly switch prompts/workflows
- pin a model
- restrict or expand tool choices

Profiles are created/edited via `clai setup` (stage 2), and inspected via `clai profiles list`.

Profiles are applied inside `text.Configurations.ProfileOverrides()` (see `internal/text/conf.go` + `internal/text/profile_overrides.go` if present).

### 4) Conversations and reply pointers (context state)

Stored under:

- `<config>/conversations/*.json`
- `<config>/conversations/prevQuery.json` (global reply context)
- `<config>/conversations/dirs/*` (directory-scoped binding metadata)

These are described in `architecture/CHAT.md`.

They aren’t traditional config, but they influence prompt assembly (`-re`, `-dre`, `chat continue`, etc.).

## The override cascade (text/query/chat/cmd)

Text-like commands are configured in `internal/setup.go:setupTextQuerierWithConf`.

The effective precedence is:

1. **Hard-coded defaults** (`text.Default`) – lowest precedence
2. **Mode config file** (`textConfig.json`)
3. **Profiles** (`-p/-profile` or `-prp/-profile-path`)
4. **Flags** (CLI)

There is also a *model-specific vendor config* layer which is loaded during querier creation.

A more faithful mental model:

```text
text.Default
→ merge textConfig.json
→ apply “early” flag overrides (model/raw/reply/profile pointers)
→ if glob mode: build glob context
→ apply profile overrides (prompt/tools/model/etc)
→ finalize tool selection (flags + profiles + defaults)
→ re-apply “late” overrides (some flags override profile, e.g., -cm)
→ build InitialChat (including reply context)
→ CreateTextQuerier(...) loads vendor model config and produces runtime Model
```

### Where flags apply

Flags are parsed in `internal/setup_flags.go:parseFlags` into `internal.Configurations`.

For **text** the important override functions are:

- `applyFlagOverridesForText(tConf, flagSet, defaultFlags)`
- `applyProfileOverridesForText(tConf, flagSet, defaultFlags)` (currently only ensures `-cm` can override profile model)

Key behaviors:

- default flags should *not* override file values; overrides only happen when the user provided a non-default flag value.
- `-dre` is implemented in `internal.Setup` by copying the directory-scoped conversation into `prevQuery.json` and then turning on reply mode.

### Tool selection configuration

Tool usage is controlled by:

- `-t/-tools` CLI flag (string): `""`, `"*"`, or comma-separated list.
- `text.Configurations.UseTools` boolean (enable tool calling)
- `text.Configurations.RequestedToolGlobs` (names or wildcards)
- profiles can also set tool behavior

`internal/setup.go:setupToolConfig` is the bridge between:

- CLI’s `UseTools` string
- text configuration’s `UseTools` + `RequestedToolGlobs`

Notable rules:

- if `-t` is provided at all (even a list), it is interpreted as intent to enable tooling.
- `-t=*` clears requested list (meaning “allow all”).
- unknown tools are skipped with warnings.
- if nothing valid remains, tooling is disabled for that run.
- MCP tools are not validated against the local registry; names prefixed with `mcp_` are allowed.

### Reply/dir-reply configuration

- `-re` sets `tConf.ReplyMode`.
- `-dre` is handled before text setup:
- `chat.SaveDirScopedAsPrevQuery(confDir)`
- flips reply mode on

This means the rest of the system only needs to understand one reply mechanism: loading `prevQuery.json`.

## Non-text config flows

### Photo

- Load `photoConfig.json` (with default `photo.DEFAULT`)
- Apply flag overrides: model, output dir/prefix/type, reply and stdin replacement
- Build prompt via `photo.Configurations.SetupPrompts()`
- Create vendor querier via `CreatePhotoQuerier(pConf)`

See `PHOTO.md`.

### Video

Same pattern with `videoConfig.json` + `video.Configurations.SetupPrompts()`.

See `VIDEO.md`.

## Setup wizard and config file editing

`clai setup` is the primary user interface to edit all of these files.

It uses globbing under the config dir to find relevant files and offers actions:

- reconfigure via structured prompts
- open in `$EDITOR`
- delete
- paste or create MCP server definitions

See `SETUP.md`.

## Implementation index

If you need to follow configuration in code, start here:

- `internal/setup_flags.go`
- CLI flags → internal struct
- applies overrides into mode configs
- `internal/setup.go`
- command dispatch
- text setup (`setupTextQuerierWithConf`) and special cases (`-dre`)
- `internal/utils/config.go` + `internal/utils/json.go`
- `LoadConfigFromFile`, `CreateFile`, etc.
- `internal/text/conf.go`
- text defaults, initial chat setup, reply/glob integration
- `internal/create_queriers.go`
- model name → vendor querier routing

## Common debugging tips

- Set `DEBUG=1` to print some config snapshots during setup.
- `DEBUG_PROFILES=1` prints tooling glob selection during setup.
- Most “why isn’t my flag working?” issues are precedence/cascade issues; trace:
1. mode config loaded
2. early flag overrides
3. profile overrides
4. tool selection
5. late overrides
6. initial chat construction
57 changes: 57 additions & 0 deletions architecture/DRE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# DRE (Directory Replay) Command Architecture

Command: `clai [flags] dre`

The **dre** command prints the most recent message from the **directory-scoped conversation** bound to the current working directory (CWD).

This is the directory-scoped analog of `clai replay` / `clai re`.

> Related: `clai -dre query ...` uses the bound chat as context. See `CHAT.md` (dir-scoped bindings) and `QUERY.md`.

## Entry Flow

```text
main.go:run()
→ internal.Setup(ctx, usage, args)
→ parseFlags()
→ getCmdFromArgs() → DIRSCOPED_REPLAY
→ setupDRE(...) → dreQuerier
→ dreQuerier.Query(ctx)
→ chat.Replay(raw, true)
```

## Key Files

| File | Purpose |
|------|---------|
| `internal/setup.go` | Dispatches DIRSCOPED_REPLAY mode |
| `internal/dre.go` | Implements the `dre` command querier (`dreQuerier`) |
| `internal/chat/replay.go` | `Replay(raw, dirScoped)` + `replayDirScoped` |
| `internal/chat/dirscope.go` | Directory binding storage + lookup (`LoadDirScope`) |
| `architecture/CHAT.md` | Background: how conversations and dir bindings work |

## How it finds the conversation

Directory scope is loaded via `ChatHandler.LoadDirScope("")`; empty string means “use current working directory”.

If no binding exists (`ds.ChatID == ""`), `dre` errors with:

- `no directory-scoped conversation bound to current directory`

Bindings are created/updated primarily by:

- `clai query ...` (non-reply queries update the binding to the newly used chat)
- `clai chat continue <id|index>` (binds the selected chat to CWD)

## What it prints

Once `chatID` is resolved:

1. Load `<configDir>/conversations/<chatID>.json`.
2. Select the last message in the transcript.
3. Print via `utils.AttemptPrettyPrint(..., raw)`.

## Error handling / exit codes

- On success, `dre` prints and returns nil; `internal.Setup` does not force exit (it returns a querier), so normal exit code is 0.
- Missing binding or missing conversation file returns an error and results in non-zero exit.
Loading