@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
480 lines (442 loc) • 18.2 kB
text/typescript
/**
* Owner for the `kxm.config.v1` terminal editor.
*
* The panel publishes this contribution and routes every write back here.
* `listSections` re-reads the merged files on each publish. `set` and `clear`
* validate the field and the value, then call the config owner. A refusal
* throws before either owner call, so the file bytes stay as they were.
*/
import { existsSync, readFileSync } from "node:fs";
import { parse } from "yaml";
import {
KXM_TUI_CLEAR_ACTION,
KXM_TUI_SET_ACTION,
createKxmTuiRegistry,
createKxmTuiPiPanel,
createPlainKxmTuiTheme,
runKxmTuiPanel,
type KxmTuiContribution,
type KxmTuiField,
type KxmTuiSectionView,
} from "@kontextmind/tui";
import {
deleteKxmConfigValue,
getKxmConfigValue,
kxmConfigFileForScope,
loadKxmConfig,
setKxmConfigValue,
userConfigDirForEnv,
type KxmResolvedConfig,
} from "./config.ts";
export const KXM_CONFIG_EDITOR_SOURCE = "kxm/config";
export type KxmConfigEditorScope = "project" | "user";
const INTEGER_TEXT = /^(0|[1-9][0-9]*)$/;
const DECIMAL_TEXT = /^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$/;
/** Workflow and harness ids already in use, including `@`, with no whitespace. */
const TEXT_VALUE = /^[A-Za-z0-9@][A-Za-z0-9@._:/-]{0,127}$/;
interface EnumSpec {
readonly kind: "enum";
readonly choices: readonly string[];
}
interface NumberSpec {
readonly kind: "number";
readonly integer: boolean;
readonly min: number;
readonly max: number;
}
interface TextSpec {
readonly kind: "text";
}
type ValueSpec = EnumSpec | NumberSpec | TextSpec;
interface ConfigEditorFieldSpec {
readonly id: string;
readonly label: string;
readonly keyPath: string;
readonly spec: ValueSpec;
readonly note?: string;
}
interface ConfigEditorSectionSpec {
readonly id: string;
readonly title: string;
readonly order: number;
readonly fields: readonly ConfigEditorFieldSpec[];
}
const SCREEN_CHOICES = ["agents", "tasks", "workflows", "plans", "inbox", "procs", "spend"] as const;
const SECTIONS: readonly ConfigEditorSectionSpec[] = [
{
id: "user",
title: "User",
order: 10,
fields: [
{ id: "user-theme", label: "Theme", keyPath: "user.theme", spec: { kind: "enum", choices: ["dark", "light", "minimal"] } },
{ id: "user-token-budget", label: "Token budget", keyPath: "user.tokenBudget", spec: { kind: "number", integer: true, min: 1, max: 2_000_000 } },
],
},
{
id: "defaults",
title: "Defaults",
order: 20,
fields: [
{ id: "defaults-workflow", label: "Workflow", keyPath: "defaults.workflow", spec: { kind: "text" } },
{ id: "defaults-harness", label: "Harness", keyPath: "defaults.harness", spec: { kind: "text" } },
],
},
{
id: "dash",
title: "Dashboard",
order: 30,
fields: [
{ id: "dash-screen", label: "Default screen", keyPath: "dash.defaultScreen", spec: { kind: "enum", choices: SCREEN_CHOICES } },
{ id: "dash-refresh", label: "Refresh interval", keyPath: "dash.refreshIntervalMs", spec: { kind: "number", integer: true, min: 250, max: 3_600_000 } },
{ id: "dash-auto-open", label: "Auto-open", keyPath: "dash.autoOpen", spec: { kind: "enum", choices: ["true", "false"] } },
],
},
{
id: "sync",
title: "Sync",
order: 40,
fields: [
{ id: "sync-tracker", label: "Default tracker", keyPath: "sync.defaultTracker", spec: { kind: "enum", choices: ["github", "jira", "none"] } },
],
},
{
id: "hub",
title: "Hub",
order: 50,
fields: [
{
id: "hub-auto-start",
label: "Auto-start",
keyPath: "hub.autoStart",
spec: { kind: "enum", choices: ["background", "off"] },
note: "Hub URLs and key references stay out of this panel.",
},
],
},
{
id: "improvement",
title: "Improvement",
order: 60,
fields: [
{ id: "improvement-policy", label: "Promotion policy", keyPath: "improvement.promotionPolicy", spec: { kind: "enum", choices: ["manual_pr", "critic_quorum", "auto_threshold"] } },
{ id: "improvement-half-life", label: "Telemetry half-life", keyPath: "improvement.telemetryHalfLifeDays", spec: { kind: "number", integer: false, min: 0.000001, max: 3650 } },
{ id: "improvement-min-runs", label: "Minimum runs", keyPath: "improvement.autoThreshold.minRuns", spec: { kind: "number", integer: true, min: 1, max: 1_000_000 } },
{ id: "improvement-min-pass-rate", label: "Minimum pass rate", keyPath: "improvement.autoThreshold.minPassRate", spec: { kind: "number", integer: false, min: 0, max: 1 } },
{ id: "improvement-min-cost-savings", label: "Minimum cost savings", keyPath: "improvement.autoThreshold.minCostSavings", spec: { kind: "number", integer: false, min: 0, max: 1_000_000 } },
],
},
{
id: "routing",
title: "Routing",
order: 70,
fields: [
{
id: "routing-shadow",
label: "Shadow execution",
keyPath: "routing.shadowExecution.enabled",
spec: { kind: "enum", choices: ["true", "false"] },
note: "Strategy, fallback, and context limits stay in role and route files.",
},
{ id: "routing-sample-rate", label: "Sample rate", keyPath: "routing.shadowExecution.sampleRate", spec: { kind: "number", integer: false, min: 0, max: 1 } },
{ id: "routing-breaker-mode", label: "Breaker mode", keyPath: "routing.circuitBreaker.mode", spec: { kind: "enum", choices: ["soft_demotion", "quarantine"] } },
{ id: "routing-failure-threshold", label: "Failure threshold", keyPath: "routing.circuitBreaker.failureThreshold", spec: { kind: "number", integer: true, min: 1, max: 10_000 } },
{ id: "routing-window-seconds", label: "Window seconds", keyPath: "routing.circuitBreaker.windowSeconds", spec: { kind: "number", integer: true, min: 1, max: 31_536_000 } },
{ id: "routing-cooldown-seconds", label: "Cooldown seconds", keyPath: "routing.circuitBreaker.cooldownSeconds", spec: { kind: "number", integer: true, min: 0, max: 31_536_000 } },
{ id: "routing-penalty", label: "Penalty multiplier", keyPath: "routing.circuitBreaker.penaltyMultiplier", spec: { kind: "number", integer: false, min: 0.000001, max: 1000 } },
],
},
{
id: "telemetry",
title: "Telemetry",
order: 80,
fields: [
{ id: "telemetry-federated", label: "Federated", keyPath: "telemetry.federated", spec: { kind: "enum", choices: ["true", "false"] } },
{ id: "telemetry-anonymize", label: "Anonymize", keyPath: "telemetry.anonymize", spec: { kind: "enum", choices: ["true", "false"] } },
],
},
];
export class KxmConfigEditorError extends Error {
readonly code = "config_edit_refused";
constructor(message: string) {
super(message);
this.name = "KxmConfigEditorError";
}
}
export interface KxmConfigEditorTarget {
readonly cwd: string;
readonly scope: KxmConfigEditorScope;
readonly userConfigDir?: string | undefined;
}
export function kxmConfigEditorKeyPaths(): readonly string[] {
return SECTIONS.flatMap((section) => section.fields.map((field) => field.keyPath));
}
export function createKxmConfigEditorContribution(target: KxmConfigEditorTarget): KxmTuiContribution {
return {
source: KXM_CONFIG_EDITOR_SOURCE,
republishAfterWrite: true,
listSections: () => listConfigEditorSections(target),
handlers: {
[KXM_TUI_SET_ACTION]: (input) => {
writeConfigEditorValue(target, input.sectionId, input.fieldId, input.value);
},
[KXM_TUI_CLEAR_ACTION]: (input) => {
clearConfigEditorValue(target, input.sectionId, input.fieldId);
},
},
describeError: (error) => (error instanceof Error && error.message ? error.message : "config edit failed"),
};
}
function scopeFile(target: KxmConfigEditorTarget): string {
return kxmConfigFileForScope(target.cwd, target.scope, target.userConfigDir);
}
function scopePhrase(scope: KxmConfigEditorScope): string {
return scope === "project" ? "the project file .kxm/config.yaml" : "the user file config.yaml";
}
function listConfigEditorSections(target: KxmConfigEditorTarget): KxmTuiSectionView[] {
const config = loadKxmConfig(target.cwd, {
...(target.userConfigDir === undefined ? {} : { userConfigDir: target.userConfigDir }),
layers: target.scope === "user" ? "user" : "all",
});
const owned = readScopeDocument(scopeFile(target));
const where = scopePhrase(target.scope);
return SECTIONS.map((section) => {
const fields = section.fields.map((field) => publishField(field, config, owned, where));
const diagnosis = fields.find((field) => field.statusText)?.statusText;
return {
source: KXM_CONFIG_EDITOR_SOURCE,
id: section.id,
title: section.title,
order: section.order,
detail: `kxm.config.v1 · ${target.scope} scope. Re-read from disk after each write.`,
...(diagnosis === undefined ? {} : { notice: diagnosis, noticeLevel: "error" as const }),
fields,
};
});
}
function publishField(
field: ConfigEditorFieldSpec,
config: KxmResolvedConfig,
owned: Record<string, unknown> | undefined,
where: string,
): KxmTuiField {
const effective = getKxmConfigValue(config, field.keyPath);
const stored = ownValue(owned, field.keyPath);
const shown = displayScalar(effective);
const inherited = !stored.present;
const diagnosis = storedDiagnosis(field, stored, effective);
const detail = [
`Writes ${where}.`,
inherited ? "Inherited." : "Set in this scope.",
"Clear removes only this override.",
field.note ?? "",
diagnosis ?? "",
].filter((part) => part.length > 0).join(" ");
const choices = field.spec.kind === "enum"
? field.spec.choices.map((choice) => ({ id: choice, label: choice }))
: undefined;
return {
id: field.id,
label: field.label,
kind: field.spec.kind === "enum" ? "enum" : "text",
keyPath: field.keyPath,
detail,
status: "ready",
...(shown === undefined ? {} : { value: shown }),
...(choices === undefined ? {} : { choices }),
...(diagnosis === undefined ? {} : { statusText: diagnosis }),
};
}
function displayScalar(value: unknown): string | undefined {
if (typeof value === "string" && value.length > 0) return value;
if (typeof value === "boolean") return value ? "true" : "false";
if (typeof value === "number" && Number.isFinite(value)) return String(value);
return undefined;
}
function isBooleanEnum(spec: ValueSpec): boolean {
return spec.kind === "enum"
&& spec.choices.length === 2
&& spec.choices.every((choice) => choice === "true" || choice === "false");
}
/** A display string such as `"true"` is not a boolean. The stored YAML type has to match the descriptor. */
function storedTypeMatches(field: ConfigEditorFieldSpec, value: unknown): boolean {
if (field.spec.kind === "number") {
return typeof value === "number"
&& Number.isFinite(value)
&& (!field.spec.integer || Number.isInteger(value));
}
if (isBooleanEnum(field.spec)) return typeof value === "boolean";
return typeof value === "string";
}
function storedDiagnosis(
field: ConfigEditorFieldSpec,
stored: { present: boolean; value: unknown },
effective: unknown,
): string | undefined {
if (!stored.present) return undefined;
if (!storedTypeMatches(field, stored.value)) return "stored type does not match this field";
if (field.spec.kind === "enum" && !isBooleanEnum(field.spec) && !field.spec.choices.includes(stored.value as string)) {
return "stored value is not a supported scalar";
}
if (field.spec.kind === "number") {
const value = stored.value as number;
if (value < field.spec.min || value > field.spec.max) return "stored value is not a supported scalar";
}
if (stored.value !== effective) return "stored value is not a supported scalar";
return undefined;
}
function writeConfigEditorValue(target: KxmConfigEditorTarget, sectionId: string, fieldId: string, raw: string | undefined): void {
const field = requireField(sectionId, fieldId);
const value = parseEditorValue(field, raw);
setKxmConfigValue(target.cwd, field.keyPath, value, {
scope: target.scope,
...(target.userConfigDir === undefined ? {} : { userConfigDir: target.userConfigDir }),
});
}
function clearConfigEditorValue(target: KxmConfigEditorTarget, sectionId: string, fieldId: string): void {
const field = requireField(sectionId, fieldId);
deleteKxmConfigValue(target.cwd, field.keyPath, {
scope: target.scope,
...(target.userConfigDir === undefined ? {} : { userConfigDir: target.userConfigDir }),
});
}
function requireField(sectionId: string, fieldId: string): ConfigEditorFieldSpec {
const section = SECTIONS.find((item) => item.id === sectionId);
const field = section?.fields.find((item) => item.id === fieldId);
if (!field) throw new KxmConfigEditorError("unknown configuration field");
return field;
}
function parseEditorValue(field: ConfigEditorFieldSpec, raw: string | undefined): string | number | boolean {
if (raw === undefined) throw new KxmConfigEditorError("a value is required");
if (raw.length === 0 || raw.length > 128 || /[\u0000-\u001f\u007f]/u.test(raw)) {
throw new KxmConfigEditorError("value must be a single bounded line");
}
if (field.spec.kind === "enum") {
if (!field.spec.choices.includes(raw)) throw new KxmConfigEditorError("value is not a supported choice");
if (isBooleanEnum(field.spec)) return raw === "true";
return raw;
}
if (field.spec.kind === "text") {
if (!TEXT_VALUE.test(raw)) throw new KxmConfigEditorError("value must be a single bounded line");
return raw;
}
if (!(field.spec.integer ? INTEGER_TEXT : DECIMAL_TEXT).test(raw)) {
throw new KxmConfigEditorError("value must be a finite number in range");
}
const parsed = Number(raw);
if (!Number.isFinite(parsed) || (field.spec.integer && !Number.isInteger(parsed)) || parsed < field.spec.min || parsed > field.spec.max) {
throw new KxmConfigEditorError("value must be a finite number in range");
}
return parsed;
}
function readScopeDocument(file: string): Record<string, unknown> | undefined {
if (!existsSync(file)) return undefined;
const parsed = parse(readFileSync(file, "utf8")) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new KxmConfigEditorError(`config file ${file} must be a mapping`);
}
return parsed as Record<string, unknown>;
}
function ownValue(root: Record<string, unknown> | undefined, keyPath: string): { present: boolean; value: unknown } {
if (!root) return { present: false, value: undefined };
let cursor: unknown = root;
for (const part of keyPath.split(".")) {
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) return { present: false, value: undefined };
const record = cursor as Record<string, unknown>;
if (!Object.prototype.hasOwnProperty.call(record, part)) return { present: false, value: undefined };
cursor = record[part];
}
return { present: true, value: cursor };
}
const EDITOR_HELP = [
"↑↓ move · enter edits · x clears this scope's override · q quits",
"Writes are checked, saved, then re-read. A refused edit leaves the file unchanged.",
] as const;
const FRAME_NOW_MS = Date.UTC(2026, 9, 11);
export async function runKxmConfigEditor(input: {
readonly target: KxmConfigEditorTarget;
readonly stdout: (text: string) => void;
readonly isTty: boolean;
readonly dryRun: boolean;
}): Promise<number> {
const registry = createKxmTuiRegistry();
try {
registry.register(createKxmConfigEditorContribution(input.target));
registry.getSections();
const failed = registry.getIssues()[0];
if (failed) throw new KxmConfigEditorError(failed.message);
const interactive = input.isTty && !input.dryRun;
return await runKxmTuiPanel({
registry,
title: "KXM configuration",
breadcrumb: `kxm.config.v1 · ${input.target.scope} scope`,
helpLines: EDITOR_HELP,
stdout: input.stdout,
isTty: interactive,
...(interactive ? {} : { theme: createPlainKxmTuiTheme() }),
now: () => FRAME_NOW_MS,
});
} finally {
registry.dispose();
}
}
interface ConfigPanelUi {
notify?: ((message: string, type?: "error" | "info" | "warning") => void) | undefined;
custom?: ((
factory: (
tui: { requestRender(): void },
theme: { fg(token: string, text: string): string; bg(token: string, text: string): string },
keybindings: unknown,
done: (result: undefined) => void,
) => { render(width: number): string[]; handleInput(data: string): void; invalidate(): void },
options?: { overlay?: boolean },
) => Promise<unknown>) | undefined;
}
/** Mount the project-scope editor on Pi's generic custom panel. Only a TUI session may open it. */
export async function openKxmProjectConfigPanel(
ui: { notify?: (message: string, type?: "error" | "info" | "warning") => void; custom?: unknown },
cwd: string,
env: NodeJS.ProcessEnv = process.env,
mode?: string,
): Promise<void> {
if (mode !== "tui") {
ui.notify?.(
"kxm: /kxm config opens only in the Pi TUI. Nothing was written. Run kxm config edit --scope project",
"warning",
);
return;
}
if (typeof ui.custom !== "function") {
ui.notify?.(
"kxm: /kxm config needs Pi's custom panel. Open the repository in the Pi TUI, or run kxm config edit --scope project",
"warning",
);
return;
}
const custom = ui.custom as NonNullable<ConfigPanelUi["custom"]>;
const registry = createKxmTuiRegistry();
const userConfigDir = userConfigDirForEnv(env);
try {
registry.register(createKxmConfigEditorContribution({
cwd,
scope: "project",
...(userConfigDir === undefined ? {} : { userConfigDir }),
}));
await custom(
(tui, theme, _keybindings, done) => createKxmTuiPiPanel({
registry,
tui,
theme,
title: "KXM configuration",
breadcrumb: "kxm.config.v1 · project scope",
helpLines: EDITOR_HELP,
done: () => {
registry.dispose();
done(undefined);
},
}),
{ overlay: true },
);
} finally {
registry.dispose();
}
}