@kontextmind/kxm 0.7.5 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,348 @@
1
+ /**
2
+ * KXM Declarative Workflow Modes, Domain Isolation, and Pre-Flight Explain
3
+ *
4
+ * Implements major modes (base role + tools + prompt) and stackable domain
5
+ * toolkits to eliminate prompt bloat and calculate pre-flight token costs.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { parse } from "yaml";
11
+ import { calculateModelCost, type PriceCatalog, loadPriceCatalog } from "./prices.ts";
12
+
13
+ export interface MajorMode {
14
+ description?: string | undefined;
15
+ baseTools: string[];
16
+ contextFiles?: string[] | undefined;
17
+ thinkingLevel?: "low" | "medium" | "high" | "xhigh" | undefined;
18
+ model?: string | undefined;
19
+ }
20
+
21
+ export interface Domain {
22
+ description?: string | undefined;
23
+ tools: string[];
24
+ contextFiles?: string[] | undefined;
25
+ promptSnippet?: string | undefined;
26
+ }
27
+
28
+ export interface ModesConfig {
29
+ schema: "kxm.modes.v1";
30
+ majorModes: Record<string, MajorMode>;
31
+ domains?: Record<string, Domain> | undefined;
32
+ }
33
+
34
+ export interface ResolvedMode {
35
+ majorMode: string;
36
+ enabledDomains: string[];
37
+ tools: string[];
38
+ contextFiles: string[];
39
+ promptSnippets: string[];
40
+ thinkingLevel?: string | undefined;
41
+ model?: string | undefined;
42
+ }
43
+
44
+ export interface ContextBreakdownItem {
45
+ name: string;
46
+ chars: number;
47
+ estimatedTokens: number;
48
+ }
49
+
50
+ export interface PromptFootprint {
51
+ majorMode: string;
52
+ enabledDomains: string[];
53
+ model: string;
54
+ breakdown: ContextBreakdownItem[];
55
+ totalChars: number;
56
+ totalTokens: number;
57
+ contextWindowRatio: number;
58
+ projectedCost: {
59
+ inputCostUsd: number | null;
60
+ cacheReadCostUsd: number | null;
61
+ outputCostEstimateUsd: number | null;
62
+ };
63
+ }
64
+
65
+ export const DEFAULT_MODES_CONFIG: ModesConfig = Object.freeze({
66
+ schema: "kxm.modes.v1",
67
+ majorModes: {
68
+ coder: {
69
+ description: "First-pass implementation, bug fixing, and test authoring",
70
+ baseTools: ["read", "edit", "write", "bash"],
71
+ contextFiles: ["AGENTS.md"],
72
+ thinkingLevel: "medium" as const,
73
+ model: "grok/grok-4.6",
74
+ },
75
+ planner: {
76
+ description: "High-level architectural planning, scoping, and decomposition",
77
+ baseTools: ["read", "grep", "find"],
78
+ contextFiles: ["plans/implementation-plan.md"],
79
+ thinkingLevel: "high" as const,
80
+ model: "claude/fable",
81
+ },
82
+ auditor: {
83
+ description: "Security, compliance, and code quality verification",
84
+ baseTools: ["read", "grep"],
85
+ contextFiles: ["SECURITY.md"],
86
+ thinkingLevel: "high" as const,
87
+ model: "codex/gpt-5.6-sol",
88
+ },
89
+ browser: {
90
+ description: "Web application exploration, screenshotting, and UI testing",
91
+ baseTools: ["read", "bash"],
92
+ contextFiles: ["docs/browser-automation.md"],
93
+ thinkingLevel: "medium" as const,
94
+ model: "grok/grok-4.6",
95
+ },
96
+ },
97
+ domains: {
98
+ git: {
99
+ description: "Git version control operations",
100
+ tools: ["git_status", "git_diff", "git_commit"],
101
+ promptSnippet: "Follow git branch conventions; never commit directly to main.",
102
+ },
103
+ k8s: {
104
+ description: "Kubernetes cluster inspection and deployment",
105
+ tools: ["kubectl_get", "kubectl_describe"],
106
+ promptSnippet: "Target local dev cluster; verify namespaces before mutating.",
107
+ },
108
+ database: {
109
+ description: "Database queries and schema verification",
110
+ tools: ["sqlite_query", "sqlite_schema"],
111
+ promptSnippet: "Database is SQLite at .kxm/state/kxm.db; use read-only queries.",
112
+ },
113
+ browser: {
114
+ description: "Remote Steel browser sessions and visual testing",
115
+ tools: ["steel_session", "steel_scrape", "steel_screenshot"],
116
+ promptSnippet: "Use Steel on DOKS for browser automation; invoke takeover on MFA.",
117
+ },
118
+ },
119
+ });
120
+
121
+ /**
122
+ * Estimate tokens from character count using standard ~4 chars/token heuristic.
123
+ */
124
+ export function estimateTokens(charCount: number): number {
125
+ return Math.ceil(charCount / 3.8);
126
+ }
127
+
128
+ /**
129
+ * Load and validate `.kxm/modes.yaml` from project root or return defaults.
130
+ */
131
+ export function loadModesConfig(projectRoot?: string): ModesConfig {
132
+ if (!projectRoot) {
133
+ return DEFAULT_MODES_CONFIG;
134
+ }
135
+
136
+ const modesPath = join(projectRoot, ".kxm", "modes.yaml");
137
+ if (!existsSync(modesPath)) {
138
+ return DEFAULT_MODES_CONFIG;
139
+ }
140
+
141
+ try {
142
+ const raw = readFileSync(modesPath, "utf8");
143
+ const parsed = parse(raw) as Partial<ModesConfig>;
144
+ if (parsed && parsed.schema === "kxm.modes.v1" && parsed.majorModes) {
145
+ return {
146
+ schema: "kxm.modes.v1",
147
+ majorModes: { ...DEFAULT_MODES_CONFIG.majorModes, ...parsed.majorModes },
148
+ domains: { ...DEFAULT_MODES_CONFIG.domains, ...parsed.domains },
149
+ };
150
+ }
151
+ } catch {
152
+ // Malformed config falls back to default safely
153
+ }
154
+
155
+ return DEFAULT_MODES_CONFIG;
156
+ }
157
+
158
+ /**
159
+ * Resolve active tools, context files, and prompt snippets for a major mode and domain set.
160
+ */
161
+ export function resolveActiveMode(
162
+ config: ModesConfig,
163
+ majorModeName = "coder",
164
+ domainNames: string[] = []
165
+ ): ResolvedMode {
166
+ const major = config.majorModes[majorModeName] || config.majorModes["coder"] || DEFAULT_MODES_CONFIG.majorModes["coder"]!;
167
+ const tools = new Set<string>(major.baseTools);
168
+ const contextFiles = new Set<string>(major.contextFiles || []);
169
+ const promptSnippets: string[] = [];
170
+
171
+ const enabledDomains: string[] = [];
172
+
173
+ if (config.domains) {
174
+ for (const dName of domainNames) {
175
+ const d = config.domains[dName.toLowerCase().trim()];
176
+ if (d) {
177
+ enabledDomains.push(dName.toLowerCase().trim());
178
+ d.tools.forEach((t) => tools.add(t));
179
+ (d.contextFiles || []).forEach((f) => contextFiles.add(f));
180
+ if (d.promptSnippet) {
181
+ promptSnippets.push(d.promptSnippet);
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ return {
188
+ majorMode: majorModeName,
189
+ enabledDomains,
190
+ tools: Array.from(tools),
191
+ contextFiles: Array.from(contextFiles),
192
+ promptSnippets,
193
+ thinkingLevel: major.thinkingLevel,
194
+ model: major.model || "grok/grok-4.6",
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Calculate the exact prompt token footprint and projected costs for a mode configuration.
200
+ */
201
+ export function calculatePromptFootprint(
202
+ resolved: ResolvedMode,
203
+ projectRoot: string = process.cwd(),
204
+ catalog?: PriceCatalog
205
+ ): PromptFootprint {
206
+ const breakdown: ContextBreakdownItem[] = [];
207
+
208
+ // 1. Base System Prompt
209
+ const baseSystemPromptChars = 3200; // ~850 tokens base system prompt
210
+ breakdown.push({
211
+ name: `Base System Prompt (${resolved.majorMode})`,
212
+ chars: baseSystemPromptChars,
213
+ estimatedTokens: estimateTokens(baseSystemPromptChars),
214
+ });
215
+
216
+ // 2. Context Files
217
+ for (const relPath of resolved.contextFiles) {
218
+ const fullPath = join(projectRoot, relPath);
219
+ let chars = 0;
220
+ if (existsSync(fullPath)) {
221
+ try {
222
+ chars = readFileSync(fullPath, "utf8").length;
223
+ } catch {
224
+ chars = 0;
225
+ }
226
+ }
227
+ if (chars === 0) {
228
+ chars = 1500; // Estimated fallback if missing
229
+ }
230
+ breakdown.push({
231
+ name: `Context File: ${relPath}`,
232
+ chars,
233
+ estimatedTokens: estimateTokens(chars),
234
+ });
235
+ }
236
+
237
+ // 3. Domain Prompts
238
+ if (resolved.promptSnippets.length > 0) {
239
+ const snippetChars = resolved.promptSnippets.join("\n").length;
240
+ breakdown.push({
241
+ name: `Domain Prompts (${resolved.enabledDomains.join(", ")})`,
242
+ chars: snippetChars,
243
+ estimatedTokens: estimateTokens(snippetChars),
244
+ });
245
+ }
246
+
247
+ // 4. Tool Schemas
248
+ // Each MCP / standard tool schema consumes ~150-200 tokens
249
+ const toolSchemaChars = resolved.tools.length * 650;
250
+ breakdown.push({
251
+ name: `Tool Schemas (${resolved.tools.length} active tools)`,
252
+ chars: toolSchemaChars,
253
+ estimatedTokens: estimateTokens(toolSchemaChars),
254
+ });
255
+
256
+ const totalChars = breakdown.reduce((sum, item) => sum + item.chars, 0);
257
+ const totalTokens = breakdown.reduce((sum, item) => sum + item.estimatedTokens, 0);
258
+ const maxContextWindow = 200000;
259
+ const contextWindowRatio = Math.round((totalTokens / maxContextWindow) * 1000) / 10;
260
+
261
+ // 5. Projected Costs
262
+ const cat = catalog || loadPriceCatalog(projectRoot);
263
+ const rawModel = resolved.model || "grok/grok-4.6";
264
+ const [prov, mod] = rawModel.includes("/") ? rawModel.split("/", 2) : [undefined, rawModel];
265
+
266
+ const inputCost = cat
267
+ ? calculateModelCost(cat, {
268
+ model: mod || rawModel,
269
+ ...(prov !== undefined ? { provider: prov } : {}),
270
+ tokensIn: totalTokens,
271
+ tokensOut: 0,
272
+ cacheReadTokens: 0,
273
+ cacheWriteTokens: 0,
274
+ })
275
+ : undefined;
276
+
277
+ const cacheReadCost = cat
278
+ ? calculateModelCost(cat, {
279
+ model: mod || rawModel,
280
+ ...(prov !== undefined ? { provider: prov } : {}),
281
+ tokensIn: 0,
282
+ tokensOut: 0,
283
+ cacheReadTokens: totalTokens,
284
+ cacheWriteTokens: 0,
285
+ })
286
+ : undefined;
287
+
288
+ const outputEstimateCost = cat
289
+ ? calculateModelCost(cat, {
290
+ model: mod || rawModel,
291
+ ...(prov !== undefined ? { provider: prov } : {}),
292
+ tokensIn: 0,
293
+ tokensOut: 1000,
294
+ cacheReadTokens: 0,
295
+ cacheWriteTokens: 0,
296
+ })
297
+ : undefined;
298
+
299
+ return {
300
+ majorMode: resolved.majorMode,
301
+ enabledDomains: resolved.enabledDomains,
302
+ model: resolved.model || "grok/grok-4.6",
303
+ breakdown,
304
+ totalChars,
305
+ totalTokens,
306
+ contextWindowRatio,
307
+ projectedCost: {
308
+ inputCostUsd: inputCost?.costUsd ?? null,
309
+ cacheReadCostUsd: cacheReadCost?.costUsd ?? null,
310
+ outputCostEstimateUsd: outputEstimateCost?.costUsd ?? null,
311
+ },
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Format a human-readable ASCII report for `kxm explain`.
317
+ */
318
+ export function formatModesExplainReport(footprint: PromptFootprint): string {
319
+ const divider = "═".repeat(60);
320
+ const subDivider = "─".repeat(60);
321
+
322
+ let out = `\n${divider}\n`;
323
+ out += `KXM PRE-FLIGHT CONTEXT EXPLAIN\n`;
324
+ out += `${divider}\n`;
325
+ out += `Major Mode: ${footprint.majorMode}\n`;
326
+ out += `Enabled Domains: ${footprint.enabledDomains.length > 0 ? footprint.enabledDomains.join(", ") : "(none)"}\n`;
327
+ out += `Target Model: ${footprint.model}\n\n`;
328
+
329
+ out += `CONTEXT BREAKDOWN:\n`;
330
+ for (const item of footprint.breakdown) {
331
+ const padName = item.name.padEnd(38, " ");
332
+ const tokenStr = `${item.estimatedTokens.toLocaleString()} tokens`.padStart(16, " ");
333
+ out += ` • ${padName} ${tokenStr}\n`;
334
+ }
335
+
336
+ out += `${subDivider}\n`;
337
+ const totalPad = `TOTAL PROMPT FOOTPRINT:`.padEnd(38, " ");
338
+ const totalStr = `${footprint.totalTokens.toLocaleString()} tokens`.padStart(16, " ");
339
+ out += ` ${totalPad} ${totalStr} (${footprint.contextWindowRatio}% of 200k window)\n\n`;
340
+
341
+ out += `PROJECTED COSTS (per turn):\n`;
342
+ out += ` • Initial Turn Input Cost: ${footprint.projectedCost.inputCostUsd !== null ? `$${footprint.projectedCost.inputCostUsd.toFixed(4)}` : "unmetered/unknown"}\n`;
343
+ out += ` • Subsequent Cache-Read Cost: ${footprint.projectedCost.cacheReadCostUsd !== null ? `$${footprint.projectedCost.cacheReadCostUsd.toFixed(4)} (approx 90% savings)` : "unmetered/unknown"}\n`;
344
+ out += ` • Output Estimate (1k tokens): ${footprint.projectedCost.outputCostEstimateUsd !== null ? `$${footprint.projectedCost.outputCostEstimateUsd.toFixed(4)}` : "unmetered/unknown"}\n`;
345
+ out += `${divider}\n`;
346
+
347
+ return out;
348
+ }
@@ -8,3 +8,4 @@ export * from "./database.ts";
8
8
  export * from "./logger.ts";
9
9
  export * from "./improve.ts";
10
10
  export * from "./browser.ts";
11
+ export * from "./modes.ts";
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.kxm.dev/vnext/modes.schema.json",
4
+ "title": "KXM Modes Configuration",
5
+ "description": "Declarative major modes and domain modules for selective tool loading and context scoping",
6
+ "type": "object",
7
+ "required": ["schema", "majorModes"],
8
+ "properties": {
9
+ "schema": { "const": "kxm.modes.v1" },
10
+ "majorModes": {
11
+ "type": "object",
12
+ "additionalProperties": {
13
+ "type": "object",
14
+ "required": ["baseTools"],
15
+ "properties": {
16
+ "description": { "type": "string" },
17
+ "baseTools": {
18
+ "type": "array",
19
+ "items": { "type": "string" }
20
+ },
21
+ "contextFiles": {
22
+ "type": "array",
23
+ "items": { "type": "string" }
24
+ },
25
+ "thinkingLevel": {
26
+ "type": "string",
27
+ "enum": ["low", "medium", "high", "xhigh"]
28
+ },
29
+ "model": { "type": "string" }
30
+ },
31
+ "additionalProperties": false
32
+ }
33
+ },
34
+ "domains": {
35
+ "type": "object",
36
+ "additionalProperties": {
37
+ "type": "object",
38
+ "required": ["tools"],
39
+ "properties": {
40
+ "description": { "type": "string" },
41
+ "tools": {
42
+ "type": "array",
43
+ "items": { "type": "string" }
44
+ },
45
+ "contextFiles": {
46
+ "type": "array",
47
+ "items": { "type": "string" }
48
+ },
49
+ "promptSnippet": { "type": "string" }
50
+ },
51
+ "additionalProperties": false
52
+ }
53
+ }
54
+ },
55
+ "additionalProperties": false
56
+ }