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
8 changes: 8 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
<PackageVersion Include="Cratis.Chronicle.Connections" Version="16.0.6" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.0.6" />
<PackageVersion Include="Cratis.Fundamentals" Version="7.16.4" />
<!-- Prologue -->
<PackageVersion Include="Cratis.Prologue.Configuration" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpretation" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpreter.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Screenplay" Version="1.1.0" />
<PackageVersion Include="Cratis.Screenplay" Version="1.4.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<!-- CLI -->
<PackageVersion Include="MongoDB.Driver" Version="3.10.0" />
<PackageVersion Include="Spectre.Console" Version="0.57.2" />
Expand Down
2 changes: 2 additions & 0 deletions Documentation/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This section documents the cross-cutting mechanics of the `cratis` CLI: the flag

## Topics

- [LLM](llm.md) — Configuring the language model that Cratis tools like Prologue use.
- [Prologue](prologue.md) — Capturing a running system and interpreting the captures into a Screenplay.
- [Global Options](global-options.md) — Flags such as `--output`, `--quiet`, `--yes`, and `--debug` that apply to every command.
- [Output Formats](output-formats.md) — The four output formats (`table`, `plain`, `json`, `json-compact`) with guidance on when to use each and token cost comparisons.
- [Connection](connection.md) — Connection string format, resolution order, and environment variable configuration.
68 changes: 68 additions & 0 deletions Documentation/reference/llm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# LLM

`cratis llm` configures the language model that Cratis tools use. The CLI does not talk to a language model itself — it stores the provider settings in `~/.cratis/config.json`, where tools such as [Prologue](https://github.com/Cratis/Prologue) pick them up, for example when interpreting a captured system.

```bash
cratis llm use <KIND>
cratis llm show
cratis llm clear
```

## Providers

| Kind | Description | Default model hint |
|---|---|---|
| `anthropic` | Anthropic's hosted API | `claude-opus-4-6` |
| `openai` | OpenAI's hosted API | `gpt-4o-mini` |
| `local` | Any OpenAI-compatible endpoint running on your own machine (e.g. Ollama, LM Studio) | none |

## `cratis llm use <KIND>`

Configures the provider and saves it to the configuration file. Values not passed as options are prompted for interactively when running in a terminal — the API key with hidden input, the endpoint only for `local` (where it is required), and the model with the provider's default pre-filled.

| Option | Description |
|---|---|
| `--api-key <KEY>` | API key for the provider. Required for `anthropic` and `openai`; optional for `local`. |
| `--endpoint <URL>` | Endpoint URL. Required for `local` (e.g. `http://localhost:11434/v1`); the hosted providers use their own default endpoints. |
| `--model <NAME>` | Model to use. Optional — each hosted provider has a sensible default. |

```bash
cratis llm use anthropic
cratis llm use openai --api-key sk-... --model gpt-4o-mini
cratis llm use local --endpoint http://localhost:11434/v1 --model llama3
```

When the terminal is not interactive (CI, piped input), all required values must be passed as options — a missing required value fails with a validation error and a non-zero exit code.

## `cratis llm show`

Displays the configured provider. The API key is always masked — only the first and last four characters are shown, and keys too short to mask partially are hidden entirely. Global options such as `-o/--output` select the output format — see [Global Options](global-options.md).

## `cratis llm clear`

Removes the language model configuration, including the stored API key. Prompts for confirmation; pass `-y/--yes` to skip the prompt.

## What is stored

The settings live in the `llm` section of `~/.cratis/config.json`:

```json
{
"llm": {
"kind": "anthropic",
"apiKey": "sk-ant-...",
"model": "claude-opus-4-6"
}
}
```

> [!WARNING]
> The API key is stored in plain text in your user profile. Use a key you can rotate, and prefer per-developer keys over shared ones.

## Errors

| Condition | Result |
|---|---|
| Unknown `<KIND>` | Validation error listing the valid kinds. |
| Missing API key for `anthropic`/`openai` in a non-interactive terminal | Validation error. |
| Missing `--endpoint` for `local` in a non-interactive terminal | Validation error. |
73 changes: 73 additions & 0 deletions Documentation/reference/prologue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Prologue

`cratis prologue` captures what a running system *does* and interprets it into a Cratis Screenplay. [Prologue](https://github.com/Cratis/Prologue) points its extractor at an ordinary system — no Cratis constructs required — observes the HTTP commands, database changes, and telemetry flowing through it, and the CLI turns those captures into a `.play` file you continue authoring from.

```bash
cratis prologue start
cratis prologue interpret [PATH]
```

The typical flow: `start` writes a `cratis-prologue.json` capture configuration, you run the Prologue extractor container next to your system while exercising it, and `interpret` reads the captured `.jsonl` files and produces the Screenplay.

## `cratis prologue start`

An interactive wizard that produces the `cratis-prologue.json` configuration the Prologue extractor reads. It generates a fresh Prologue id and walks through:

1. **Sources** — a multi-select of what to capture:
- **SQL Server** — one or more databases, each with a name, connection string, and an optional table allowlist (comma separated, empty captures every user table). The extractor enables Change Data Capture itself.
- **PostgreSQL** — one or more databases, each with a name and connection string. Captured through logical replication with default slot and publication names.
- **API** — the extractor sits in front of your system as a reverse proxy and observes the state-changing HTTP commands (`POST`, `PUT`, `DELETE`) flowing through it. You provide the base path (default `/api`) and the address of your system.
- **OpenTelemetry** — the extractor acts as an OTLP collector (HTTP and gRPC). You provide service names to capture (empty captures all), attribute keys whose values are captured, and optional upstream collectors to forward telemetry to (empty makes it a terminal collector).
2. **Output** — where captured data goes: rolling JSON capture files (default directory `./captures`, ready for `cratis prologue interpret`) or the Prologue Receiver API (default endpoint `http://localhost:5005`).

When done it writes the file, prints a summary table, and shows how to run the extractor container.

| Option | Description |
|---|---|
| `--file <PATH>` | Where to write the configuration — a file path or a directory. Defaults to `cratis-prologue.json` in the current directory. |

```bash
cratis prologue start
cratis prologue start --file ./my-system
```

The wizard requires an interactive terminal. In CI, with piped input, or with `-y/--yes` it fails with a validation error — write the configuration by hand instead.

## `cratis prologue interpret [PATH]`

Reads Prologue capture (`.jsonl`) files and interprets them into a Screenplay. Deterministic heuristics build the event model's structure from the evidence — commands, events, read models, projections, and constraints per module, feature, and slice. When a language model is configured it refines the names into domain language, derives the system name, and may ask you clarifying questions.

| Argument | Description |
|---|---|
| `PATH` | Folder holding the capture (`.jsonl`) files. Defaults to the configured JSON output directory when a `cratis-prologue.json` is found in `PATH` or the current directory, otherwise the current directory. |

| Option | Description |
|---|---|
| `--file <FILE>` | File to write the generated Screenplay to. Defaults to `<SystemName>.play` in the current directory. |
| `--prologue-id <ID>` | The Prologue the captures belong to. Defaults from `cratis-prologue.json` when one is present. |

```bash
cratis prologue interpret
cratis prologue interpret ./captures
cratis prologue interpret ./captures --file MySystem.play
```

On success it prints a panel with the written path, the derived system name, and the module/feature/slice counts — and the natural next step is `cratis run` to boot the Screenplay in a local [Stage](run.md) sandbox.

### Language model refinement

The language model is resolved in this order:

1. The `llm` section of a found `cratis-prologue.json`, when it is enabled there.
2. The `llm` section of `~/.cratis/config.json`, written by [`cratis llm use`](llm.md) — `anthropic`, `openai`, or `local` (OpenAI-compatible).
3. Neither configured — interpretation runs with heuristics only and an info line points at `cratis llm use`.

When the language model is genuinely uncertain about a decision that materially changes the model, it asks questions — one at a time, each with its background context, a list of choices, and always an "Other" entry for typing your own answer. Questions are only asked in an interactive terminal; non-interactive runs (CI, piped output, `-y/--yes`) never ask and finalize with the model's best effort.

## Errors

| Condition | Result |
|---|---|
| `start` without an interactive terminal, or with `--yes` | Validation error — the wizard needs a terminal. |
| `interpret` finds no capture (`.jsonl`) files in the folder | Not-found error with a hint to run the extractor with JSON output. |
| Interpretation fails | Server error carrying the session's error message. |
4 changes: 4 additions & 0 deletions Documentation/reference/toc.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
- name: Run
href: run.md
- name: LLM
href: llm.md
- name: Prologue
href: prologue.md
- name: Global Options
href: global-options.md
- name: Output Formats
Expand Down
2 changes: 2 additions & 0 deletions Source/Cli.Specs/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
global using Cratis.Cli.Commands.Chronicle.Observers;
global using Cratis.Cli.Commands.Completions;
global using Cratis.Cli.Commands.Init;
global using Cratis.Cli.Commands.Llm;
global using Cratis.Cli.Commands.Prologue;
global using Cratis.Cli.Commands.Run;
global using Cratis.Specifications;
global using Xunit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_ApiKeyMask.when_masking;

public class with_key_longer_than_eight_characters : Specification
{
string _result;

void Because() => _result = ApiKeyMask.Mask("sk-ant-api03-secret");

[Fact] void should_reveal_only_the_first_and_last_four_characters() => _result.ShouldEqual("sk-a...cret");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_ApiKeyMask.when_masking;

public class with_key_of_exactly_eight_characters : Specification
{
string _result;

void Because() => _result = ApiKeyMask.Mask("12345678");

[Fact] void should_fully_mask_the_key() => _result.ShouldEqual("********");
}
14 changes: 14 additions & 0 deletions Source/Cli.Specs/for_ApiKeyMask/when_masking/with_short_key.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_ApiKeyMask.when_masking;

public class with_short_key : Specification
{
string _result;

void Because() => _result = ApiKeyMask.Mask("abc");

[Fact] void should_fully_mask_the_key() => _result.ShouldEqual("********");
[Fact] void should_not_reveal_the_key_length() => _result.Length.ShouldEqual(8);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Prologue.Configuration;

namespace Cratis.Cli.for_CaptureFolders.when_resolving;

public class with_api_output_configuration : Specification
{
string _result;

void Because() => _result = CaptureFolders.Resolve(
null,
new PrologueConfiguration { Prologue = new PrologueOptions { Output = new OutputOptions { Kind = OutputKind.Api } } },
"/config/cratis-prologue.json",
"/work");

[Fact] void should_use_the_current_directory() => _result.ShouldEqual("/work");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Prologue.Configuration;

namespace Cratis.Cli.for_CaptureFolders.when_resolving;

public class with_explicit_path : Specification
{
string _result;

void Because() => _result = CaptureFolders.Resolve(
"captures",
new PrologueConfiguration(),
"/config/cratis-prologue.json",
"/work");

[Fact] void should_resolve_the_path_against_the_current_directory() => _result.ShouldEqual(Path.Combine("/work", "captures"));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Prologue.Configuration;

namespace Cratis.Cli.for_CaptureFolders.when_resolving;

public class with_json_output_configuration : Specification
{
PrologueConfiguration _configuration;
string _result;

void Establish() => _configuration = new PrologueConfiguration
{
Prologue = new PrologueOptions
{
Output = new OutputOptions
{
Kind = OutputKind.Json,
Json = new JsonFileOptions { Directory = "./captures" }
}
}
};

void Because() => _result = CaptureFolders.Resolve(null, _configuration, "/config/cratis-prologue.json", "/work");

[Fact] void should_resolve_the_configured_directory_against_the_configuration_folder() => _result.ShouldEqual(Path.Combine("/config", "captures"));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_CaptureFolders.when_resolving;

public class without_configuration : Specification
{
string _result;

void Because() => _result = CaptureFolders.Resolve(null, null, null, "/work");

[Fact] void should_use_the_current_directory() => _result.ShouldEqual("/work");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_CliConfiguration;

[Collection(CliSpecsCollection.Name)]
public class when_saving_config_after_clearing_llm_section : given.a_temp_config_directory
{
CliConfiguration _loaded;
string _json;

void Establish()
{
var config = new CliConfiguration
{
Llm = new LlmConfiguration { Kind = "anthropic", ApiKey = "sk-ant-test-key" }
};
config.Save();

config.Llm = null;
config.Save();
}

void Because()
{
_json = File.ReadAllText(CliConfiguration.GetConfigPath());
_loaded = CliConfiguration.Load();
}

[Fact] void should_not_have_an_llm_section() => _loaded.Llm.ShouldBeNull();
[Fact] void should_not_write_the_section_to_the_file() => _json.ShouldNotContain("\"llm\"");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_CliConfiguration;

[Collection(CliSpecsCollection.Name)]
public class when_saving_config_with_llm_section : given.a_temp_config_directory
{
const string ExpectedKind = "anthropic";
const string ExpectedEndpoint = "http://localhost:11434/v1";
const string ExpectedApiKey = "sk-ant-test-key";
const string ExpectedModel = "claude-opus-4-6";

CliConfiguration _loaded;
string _json;

void Establish()
{
var config = new CliConfiguration
{
Llm = new LlmConfiguration
{
Kind = ExpectedKind,
Endpoint = ExpectedEndpoint,
ApiKey = ExpectedApiKey,
Model = ExpectedModel
}
};
config.Save();
}

void Because()
{
_json = File.ReadAllText(CliConfiguration.GetConfigPath());
_loaded = CliConfiguration.Load();
}

[Fact] void should_roundtrip_the_kind() => _loaded.Llm.Kind.ShouldEqual(ExpectedKind);
[Fact] void should_roundtrip_the_endpoint() => _loaded.Llm.Endpoint.ShouldEqual(ExpectedEndpoint);
[Fact] void should_roundtrip_the_api_key() => _loaded.Llm.ApiKey.ShouldEqual(ExpectedApiKey);
[Fact] void should_roundtrip_the_model() => _loaded.Llm.Model.ShouldEqual(ExpectedModel);
[Fact] void should_serialize_the_section_with_camel_case() => _json.ShouldContain("\"llm\"");
}
Loading
Loading