context-forgev1.0.0
User guide

Rule files

A rule file is a libconfig document. Each top-level setting is either one of the two built-ins (enable, block) or the name of a loaded plugin, whose value configures one instance of that plugin. The server applies rule files in the order they were loaded, and inside a file in the order the settings appear.

Syntax essentials

name = value;                 // every setting ends with a semicolon
flag = true;                  // booleans: true / false
count = 42;                   // integers
text = "hello\n";             // strings: double quotes, C escapes
list = ["a", "b"];            // array (all elements same type)
group = { a = 1; b = "x"; };  // group of settings
// comments: //, # or /* */

Regular expressions are ECMAScript (std::regex). Remember to double the backslashes inside strings: "^total \\d+".

Files must end in .cfg and sit in the --rules directory (or below it with --recursive). A file that fails to parse or uses an unknown plugin name is reported (visible with -v debug or via check) and skipped; the others still load.

Evaluation order

For one rule file and one wrapped command, the server does the following:

  1. Trigger. Every trigger instance is asked trigger(bin, content). If any says yes — or the file has no trigger at all — the file applies. Otherwise it is skipped.
  2. Block split. If block is present, the content is cut into blocks (every n lines or every n characters), optionally keeping only the first show blocks. Otherwise the whole content is one block.
  3. Pre-rules (ansi) run on each block, in file order.
  4. Rules (ln, drop, dup, replace, insert, …) run on each block, in file order.
  5. Blocks are joined back with block.sep (default: nothing).

Within a file, pre-rules always run before rules regardless of where you write them; among rules (and among pre-rules), order is textual. Between files, order follows directory iteration — name your files 10-ansi.cfg, 20-ls.cfg if you need a deterministic sequence, and do not rely on cross-file ordering otherwise.

The same plugin name may appear only once per file (libconfig rejects duplicate keys). Use several files if you need, say, two replace passes.

enable built-in

enable = false;

Boolean. When false the file is ignored entirely. Default true.

trigger trigger

trigger = {
    bin      = ["ls", "exa"];   // optional: exact match on the wrapped program name
    contains = "error";         // optional: regex_search on the whole output
    match    = "^\\s*$";        // optional: regex_match on the whole output
};

The file applies when any given condition holds: bin equals the wrapped program name (as typed after -c), contains finds a match anywhere in the output, or match matches the entire output. An empty group trigger = {}; always fires. The value must be a group; bin must be an array of strings.

ansi pre-rule

ansi = false;   // strip ANSI escape sequences

Boolean, meaning "keep ANSI sequences". true is a no-op; false removes CSI sequences (ESC [ … final-byte, i.e. colours, cursor moves) and OSC sequences (ESC ] … BEL or ESC \, i.e. hyperlinks, window titles) so that the following regexes see plain text. Run it before line-based rules whenever the wrapped program colours its output — most do when they detect a terminal, and since context-forge captures through a pipe they usually won't, but tools forced with --color=always will.

block built-in

block = {
    ln   = 20;      // split every 20 lines … or:
    char = 4096;    // split every 4096 characters (ln and char are mutually exclusive)
    show = 3;       // optional: keep only the first 3 blocks
    sep  = "\n---\n";  // optional: written between blocks when joining (default "")
};

All integers must be > 0. Each block goes through the pre-rules and rules independently, so ln = { head = 1; } after a block = { ln = 10; } keeps the first line of every 10-line chunk. show truncates before the rules run, which makes it a cheap way to cap huge outputs.

ln rule

ln = {
    head = 10;    // positive: keep the first 10 lines · negative: drop the first |n| lines
    tail = -2;    // positive: keep the last n lines  · negative: drop the last |n| lines
};

Both optional; head is applied before tail. Lines are split on \n; the trailing newline of the original content counts as an empty last line, which is why tail = -1 often just removes that empty line.

drop rule

drop = ["^total \\d+", "^\\s*$"];

Array of regexes. Any line for which at least one pattern searches successfully is removed.

dup rule

dup = {
    match  = ["^Binary file .* matches$", "^warning:"];   // regex groups (regex_search per line)
    eq     = ["----"];                                     // exact-line groups
    keep   = 1;        // how many occurrences of each group survive (default 1)
    invert = false;    // false: keep the first `keep` · true: keep the last `keep`
};

De-duplicates families of lines rather than identical lines: every pattern (and every eq string) defines a group; a line belongs to the first group that matches it; only keep members of each group are retained, first or last ones depending on invert. Lines matching no group are untouched.

replace rule

replace = {
    match = ["\\bTODO\\b"];        // regexes, applied to the whole block, in order
    eq    = ["/home/you"];         // exact substrings, applied after the regexes
    by    = "[<INSERT>]";          // replacement; <INSERT> is substituted by the matched text
};

Every match is replaced by by. The literal token <INSERT> (first occurrence) inside by is replaced with the matched text, which lets you wrap or annotate rather than overwrite: by = "**<INSERT>**". Regex capture groups ($1) are not supported. by is required; use by = ""; to delete matches without removing whole lines (unlike drop).

insert rule

insert = {
    before = "----- output -----\n";
    after  = "\n----- end -----";
};

Prepends and/or appends literal text to each block. Combined with block.sep this is enough to frame chunks for a model.

Complete examples

trigger = { bin = ["ls"]; };
drop = ["^total \\d+"];
// One trigger (bin + contains combined), one pre-rule (ansi),
// and every rule plugin chained on the same block.
// Order matters: drop -> dup -> replace -> ln -> insert.
trigger = {
    bin      = ["grep", "rg"];
    contains = "match";
};

ansi = false;                             // strip colour codes before the rules run

drop = ["^\\s*$"];                        // 1. drop empty lines

dup = {                                   // 2. collapse repeated "Binary file ... matches" noise
    match  = ["^Binary file .* matches$"];
    keep   = 1;
    invert = false;
};

replace = {                               // 3. flag TODOs inline
    match = ["\\bTODO\\b"];
    by    = "[TODO -> <INSERT>]";
};

ln = { tail = 50; };                      // 4. keep only the last 50 lines

insert = {                                // 5. wrap the whole output
    before = "----- grep output -----\n";
    after  = "\n----- end -----\n";
};
trigger = { bin = ["cat", "journalctl"]; };
ansi = false;
block = {
    ln   = 25;
    show = 4;         // at most 100 lines get through
    sep  = "\n· · ·\n";
};
ln = { head = 24; }; // inside each block: drop the 25th line

Need behaviour none of these rules provide (JSON pretty-printing, column alignment, a per-program state…)? That's what plugins are for — a new rule is one class and three C functions.