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
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ You're working on a project called "clai".
## Always read:

- ./main.go - This contains usage which gives a functional overview
- ./main_test.go - This contains goldenfile tests, which explains functionality in further detail
- ./go.mod - This shows which libraries are used, do not add additional third party libraries
- ./architecture - This is a directory with many files explaining the architecture of sub-features. Read the document regarding the feature you wish to know more about.

Expand Down
File renamed without changes.
28 changes: 27 additions & 1 deletion architecture/COLOURS.md → architecture/colours.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The file is automatically created with defaults if missing.

### Theme fields

All fields are raw ANSI escape sequences represented as JSON strings.
All colour fields are raw ANSI escape sequences represented as JSON strings.

| Field | Purpose |
|------:|---------|
Expand All @@ -36,9 +36,27 @@ All fields are raw ANSI escape sequences represented as JSON strings.
| `roleUser` | Colour for `user` role labels. |
| `roleTool` | Colour for `tool` role labels. |
| `roleOther` | Fallback colour for any other/unknown role. |
| `notificationBell` | Whether clai should emit terminal BEL (`\a`) after successful task completion. |

Defaults are chosen to match the existing `AttemptPrettyPrint` role palette (system=blue, user=cyan, tool=magenta).

Example:

```json
{
"primary": "\u001b[38;2;110;130;150m",
"secondary": "\u001b[38;2;140;165;190m",
"breadtext": "\u001b[38;2;200;210;220m",
"roleSystem": "\u001b[34m",
"roleUser": "\u001b[36m",
"roleTool": "\u001b[35m",
"roleOther": "\u001b[34m",
"notificationBell": true
}
```

`notificationBell` is intended for terminal/tmux attention behavior. Depending on terminal and tmux configuration, BEL may produce an audible bell, visual flash, or other attention marker.

## Disabling colour: `NO_COLOR`

clai follows the common `NO_COLOR` convention (see also `main.go` usage text).
Expand Down Expand Up @@ -84,6 +102,14 @@ All colouring is applied via `utils.Colorize(...)`, so it automatically respects
- Table rows: `theme.breadtext`
- Interactive prompt line: `theme.secondary`

### 4) Completion notification

After a successful task/query completes, clai may emit terminal BEL depending on `theme.notificationBell`.

Implementation:
- `main.go:triggerCompletionNotification()`
- `internal/utils.NotificationBellEnabled()`

## Customization

To customize colours, edit:
Expand Down
2 changes: 1 addition & 1 deletion architecture/CONFIG.md → architecture/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Stored under:
- `<config>/conversations/globalScope.json` (global reply context)
- `<config>/conversations/dirs/*` (directory-scoped binding metadata)

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

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

Expand Down
2 changes: 1 addition & 1 deletion architecture/DRE.md → architecture/dre.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ main.go:run()
| `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 |
| `architecture/chat.md` | Background: how conversations and dir bindings work |

## How it finds the conversation

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
60 changes: 37 additions & 23 deletions architecture/SHELL-CONTEXT.md → architecture/shell-context.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Shell Context (ASC) Architecture

This document specifies the **auto-append shell context** feature (ASC): a configurable mechanism to append runtime “shell context” to the **final user prompt** for text queries.
This document specifies the **auto-append shell context** feature (ASC): a configurable mechanism to render runtime “shell context” and inject it into the **system prompt** for text queries.

ASC is enabled by selecting a named shell-context definition file, which:
- defines a **template** (Go `text/template`) used to render the appended context block
- defines a **template** (Go `text/template`) used to render the shell context block
- defines a **vars** map where each variable value is produced by running a command in a subprocess shell
- defines execution settings such as which shell to run and per-variable timeout

Expand All @@ -17,7 +17,7 @@ ASC is enabled by selecting a context **by name**:
- `-asc <name>` (short)
- `-add-shell-context <name>` (long)

If the flag is **not** provided (empty name), no shell context is appended.
If the flag is **not** provided (empty name), no shell context is injected.

Examples:
```bash
Expand Down Expand Up @@ -47,25 +47,42 @@ Meaning:

ASC must **not** change stdin handling or token replacement.

Prompt assembly currently happens in `text.Configurations.SetupInitialChat(...)` (see `architecture/QUERY.md`). The flow becomes:
Prompt assembly currently happens in `text.Configurations.SetupInitialChat(...)` (see `architecture/query.md`). The flow is:

1. Build the user prompt normally:
- `prompt := utils.Prompt(tConf.StdinReplace, args)`
- (glob/reply context logic remains unchanged)
1. Build the system prompt normally from `tConf.SystemPrompt`
2. If `tConf.ShellContext` (string name) is non-empty:
- load the selected shell context definition (see below)
- evaluate its variables by running subprocess commands
- render the template into a text block
- append to the prompt:
- `prompt = prompt + "\n\n" + renderedBlock`
3. Continue existing image detection, message append, chat ID creation, etc.
- inject that block into the **system prompt**
3. Build the user prompt normally:
- `prompt := utils.Prompt(tConf.StdinReplace, args)`
- (glob/reply context logic remains unchanged)
4. Continue existing image detection, message append, chat ID creation, etc.

This keeps ASC orthogonal to:
- stdin piping
- `{}` / `-I` replacement
- globbing
- reply/dir-reply context

### Current injection format
The rendered shell context is inserted using the existing helper wrapper format:

```text
<shell context>
...rendered template output...
</shell context>
...system prompt...
```

This means the shell context is visible to the model as part of the system message, while the user message remains unchanged.

### Failure behavior
ASC is **best-effort**:
- if loading or rendering the shell context fails during system-prompt setup, clai prints a warning and continues without shell context
- user prompt assembly and query execution continue normally

---

## On-disk layout
Expand Down Expand Up @@ -117,7 +134,7 @@ Each file `shellContexts/<name>.json` describes:
- which shell to spawn for commands
- per-variable timeout
- placeholder values for timeouts and errors
- a Go template used to format the final appended block
- a Go template used to format the rendered shell context block
- a set of variables (command map)

Proposed schema:
Expand Down Expand Up @@ -151,7 +168,7 @@ Proposed schema:
- `error_value` (string): Value substituted for a variable when the command fails (non-zero exit, missing binary, etc.).
- Default: `"<error>"` (may be empty string if desired)

- `template` (string): Go `text/template` template used to render the final appended block.
- `template` (string): Go `text/template` template used to render the final shell context block.
- Variables are accessed as `{{.varName}}`.
- Templates may use conditionals (`if`, `with`), loops (`range`), etc.

Expand Down Expand Up @@ -204,18 +221,14 @@ This enables user-authored rich contexts, e.g.:
```gotemplate
[Shell context]
wd: {{.cwd}}
{{- if (contains .cwd "/prod/") }}
mode: production
{{- end }}
{{- if .git_branch }}
git: {{.git_branch}}
{{- end }}
```

Implementations may provide a small safe `FuncMap` (e.g. `contains`, `hasPrefix`, `trim`, etc.) to make templates ergonomic.

If template parsing or execution fails:
- the shell context should be omitted (ASC is best-effort)
- the shell context should be omitted from the system prompt
- clai should continue best-effort
- optionally warn under `DEBUG`

---
Expand All @@ -238,10 +251,11 @@ If template parsing or execution fails:
- apply during profile override step

### Prompt assembly
- in `internal/text/conf.go`, `SetupInitialChat(...)`:
- after `utils.Prompt(...)`, if `ShellContext != ""`:
- in `internal/text/conf.go`:
- during `setupSystemPrompt(...)`, if `ShellContext != ""`:
- render shell context block
- append `\n\n` + block to prompt
- inject it into the system prompt
- user prompt assembly via `utils.Prompt(...)` remains unchanged

---

Expand All @@ -253,9 +267,9 @@ Recommended approach:
- implement command execution via an injected runner (func/interface), allowing tests to:
- return fixed stdout per var
- simulate timeouts
- add an end-to-end-ish contract test in `main_query_goldenfile_test.go`:
- add an end-to-end-ish contract test:
- run `clai -r -cm test -add-shell-context minimal q hello`
- assert stdout includes the appended rendered block
- assert stdout remains just the user-visible prompt/response path, i.e. shell context is **not** echoed as part of the user message
- add a test verifying timeout behavior:
- a var exceeding `timeout_ms` yields `timed_out_value`
- a warning is printed (via `ancli`) for each timed-out var
Expand Down
4 changes: 2 additions & 2 deletions architecture/STREAMING.md → architecture/streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This document explains how **streaming** works in clai when calling LLM chat-completions APIs.

It is an extension of `architecture/QUERY.md`: QUERY describes **when** a streaming request is executed; this document describes **how the streamed response is represented, normalized, and consumed**, independent of vendor.
It is an extension of `architecture/query.md`: QUERY describes **when** a streaming request is executed; this document describes **how the streamed response is represented, normalized, and consumed**, independent of vendor.

## Scope

Expand All @@ -27,7 +27,7 @@ In code, the contract is:

### Normalized event types

Across vendors, clai reduces streaming to a small set of event shapes (as described in `architecture/QUERY.md`):
Across vendors, clai reduces streaming to a small set of event shapes (as described in `architecture/query.md`):

- `string` chunks: plain assistant text deltas
- `pub_models.Call`: a tool/function call request (name + JSON args)
Expand Down
6 changes: 3 additions & 3 deletions architecture/TOOLING.md → architecture/tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ This document describes **how clai’s tooling system works end-to-end**, includ

> Related docs:
>
>- `architecture/TOOLS.md` describes the **`clai tools` inspection command**.
>- `architecture/QUERY.md` describes query/chat runtime behavior.
>- `architecture/CONFIG.md` documents config layout and flags.
>- `architecture/tools.md` describes the **`clai tools` inspection command**.
>- `architecture/query.md` describes query/chat runtime behavior.
>- `architecture/config.md` documents config layout and flags.

## Terminology

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
33 changes: 24 additions & 9 deletions EXAMPLES.md → examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

Dive deeper into the subject you're interested in by seeing the different files in the [./architecture](./architecture), see links below.

## Find your config dir

```bash
clai confdir
cd "$(clai confdir)"
clai confdir mcpServers
clai confdir conversations dirs
```

- `confdir` prints the absolute clai config dir path.
- You may also pass a registered config subpath, such as `mcpServers` or `conversations dirs`.
- This is useful for quickly jumping into config-managed files.

See: [`config.md`](./architecture/config.md).

## Query + reply + directory reply

```bash
Expand All @@ -15,7 +30,7 @@ clai -dre query "Apply it to this repo"
- `-re` loads `globalScope.json` as context.
- `-dre` first copies the directory-bound chat into `globalScope.json`, then uses normal `-re` plumbing.

See: [`QUERY.md`](./architecture/QUERY.md), [`CHAT.md`](./architecture/CHAT.md), [`DRE.md`](./architecture/DRE.md).
See: [`query.md`](./architecture/query.md), [`chat.md`](./architecture/chat.md), [`dre.md`](./architecture/dre.md).

## Auto-append shell context

Expand All @@ -28,7 +43,7 @@ clai -append-shell-context git query "Summarize the current repo state before an
- The selected context runs shell commands, renders a template, and appends the result to your final query prompt.
- Use this when you want runtime context like cwd, branch name, or git status included automatically.

See: [`SHELL-CONTEXT.md`](./architecture/SHELL-CONTEXT.md).
See: [`shell-context.md`](./architecture/shell-context.md).

## Inspect “what did it say last time?”

Expand All @@ -41,7 +56,7 @@ clai -r replay # raw (no pretty/glow)
- `replay` and `dre` do not call any LLM.
- They load a chat transcript and pretty-print the last message.

See: [`REPLAY.md`](./architecture/REPLAY.md), [`DRE.md`](./architecture/DRE.md).
See: [`replay.md`](./architecture/replay.md), [`dre.md`](./architecture/dre.md).

## Bind a previous conversation to the current directory

Expand All @@ -54,7 +69,7 @@ clai -dre query "Continue from that context"
- `chat continue <index|id>` selects an existing transcript and stores a directory binding.
- After that, `-dre` in this directory uses that conversation as context.

See: [`CHAT.md`](./architecture/CHAT.md).
See: [`chat.md`](./architecture/chat.md).

## Profiles = workflow presets

Expand All @@ -67,7 +82,7 @@ clai -p ops query "Find the owners of this subsystem"
- They can override model, prompts, and requested tools.
- These are colliqualy "agent configurations"

See: [`PROFILES.md`](./architecture/PROFILES.md), [`CONFIG.md`](./architecture/CONFIG.md).
See: [`profiles.md`](./architecture/profiles.md), [`config.md`](./architecture/config.md).

Also see examples:

Expand All @@ -86,7 +101,7 @@ clai -t "rg,cat" query "Search for parsing logic and show me the file"
- `-t` enables tool calling for that _run_; without it, tool calls are disabled.
- `-t "*"` allows all registered tools.

See: [`TOOLS.md`](./architecture/TOOLS.md), [`TOOLING.md`](./architecture/TOOLING.md), [`CONFIG.md`](./architecture/CONFIG.md).
See: [`tools.md`](./architecture/tools.md), [`tooling.md`](./architecture/tooling.md), [`config.md`](./architecture/config.md).

## MCP tools (external tool servers)

Expand All @@ -98,7 +113,7 @@ clai -t "mcp_linear*" query "List open incidents assigned to me"
- MCP server configs are stored in `<clai-config>/mcpServers/*.json`.
- MCP tool names are typically `mcp_<server>_<tool>` (and can be globbed).

See: [`TOOLING.md`](./architecture/TOOLING.md), [`SETUP.md`](./architecture/SETUP.md).
See: [`tooling.md`](./architecture/tooling.md), [`setup.md`](./architecture/setup.md).

## Multimodal: photo + video

Expand All @@ -112,7 +127,7 @@ clai video "A slow pan across a terminal showing streaming output"
- `photo`/`video` have separate mode configs: `photoConfig.json`, `videoConfig.json`.
- Output can be saved locally or printed as a URL, depending on config.

See: [`PHOTO.md`](./architecture/PHOTO.md), [`VIDEO.md`](./architecture/VIDEO.md), [`CONFIG.md`](./architecture/CONFIG.md).
See: [`photo.md`](./architecture/photo.md), [`video.md`](./architecture/video.md), [`config.md`](./architecture/config.md).

## Streaming: one normalized event loop

Expand All @@ -122,4 +137,4 @@ See: [`PHOTO.md`](./architecture/PHOTO.md), [`VIDEO.md`](./architecture/VIDEO.md
- stop/no-op/error events
- The querier loop is vendor-agnostic: it prints deltas, executes tools, and finalizes output.

See: [`STREAMING.md`](./architecture/STREAMING.md), [`QUERY.md`](./architecture/QUERY.md).
See: [`streaming.md`](./architecture/streaming.md), [`query.md`](./architecture/query.md).
24 changes: 24 additions & 0 deletions internal/confdir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package internal

import (
"context"
"fmt"

"github.com/baalimago/clai/internal/models"
"github.com/baalimago/clai/internal/utils"
)

func printConfDir(_ context.Context, args []string) (models.Querier, error) {
configDir, err := utils.GetClaiConfigDir()
if err != nil {
return nil, fmt.Errorf("failed to get clai config dir: %w", err)
}

resolved, err := utils.ResolveConfigDirPath(configDir, args[1:])
if err != nil {
return nil, fmt.Errorf("failed to resolve config dir path: %w", err)
}

fmt.Println(resolved)
return nil, utils.ErrUserInitiatedExit
}
5 changes: 5 additions & 0 deletions internal/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
DIRSCOPED_REPLAY
TOOLS
PROFILES
CONFDIR
)

var defaultFlags = Configurations{
Expand Down Expand Up @@ -107,6 +108,8 @@ func getCmdFromArgs(args []string) (Mode, error) {
return TOOLS, nil
case "profiles":
return PROFILES, nil
case "confdir":
return CONFDIR, nil
default:
return HELP, fmt.Errorf("unknown command: '%s' all args: '%s'", cmd, args)
}
Expand Down Expand Up @@ -389,6 +392,8 @@ func Setup(ctx context.Context, usage string, allArgs []string) (models.Querier,
return nil, tools.SubCmd(ctx, allArgs)
case PROFILES:
return nil, profiles.SubCmd(ctx, allArgs)
case CONFDIR:
return printConfDir(ctx, postFlagArgs)
default:
return nil, fmt.Errorf("unknown mode: %v", mode)
}
Expand Down
Loading