@minhpnq1807/contextos 0.5.8 → 0.5.9

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.9
4
+
5
+ - Formats `ctx report`, `ctx evidence`, `ctx stats`, and `ctx benchmark` with sectioned terminal tables for easier scanning and analysis.
6
+ - Adds a small shared terminal table formatter used by report, evidence, stats, and benchmark output.
7
+
3
8
  ## 0.5.8
4
9
 
5
10
  - Adds explicit `ctx setup` interactive onboarding for installing agents, enabling injection, syncing Ruler rules/MCP servers, and syncing skills through skillshare.
package/README.md CHANGED
@@ -346,10 +346,10 @@ This warning comes from a transitive dependency in the local embedding/WASM stac
346
346
  | `ctx setup --no-skills` | Skips skillshare sync during setup. | You do not want shared skills configured. | Does not run `ctx sync --skills`. |
347
347
  | `ctx setup --quiet` | Runs setup in measurement-only mode. | You want reports/stats without visible injected prompt context. | Installs hooks with prompt context injection disabled. |
348
348
  | `ctx debug -- "task"` | Runs the scheduler locally for a fake prompt. | You want to see which AGENTS.md rules and files ContextOS would inject before using Codex. | Prints rule scores, scoring reasons, suggested files, and final `additionalContext`. |
349
- | `ctx report` | Shows the last Stop-hook compliance report for the current workspace. | An agent task has finished and you want the summary again. | Reads `~/.ctx/contextos/workspaces/<workspace-id>/last-report.json`. |
350
- | `ctx evidence` | Shows detailed evidence behind the last report for the current workspace. | You want to inspect why a rule was marked `followed`, `ignored`, or `unknown`. | Prints rule text, source file, score, status, and evidence reason. |
351
- | `ctx stats` | Shows aggregate runtime metrics for the current workspace. | You want to know whether ContextOS is active and useful over time. | Prints prompt count, report count, injected/quiet ratio, average prompt analysis time, efficiency, rule outcomes, hook events, and last suggested files for the current workspace only. |
352
- | `ctx benchmark -- "task"` | Compares baseline AGENTS.md ordering with ContextOS task-aware scheduling. | You want a before/after signal for lost-in-the-middle risk. | Prints parsed/actionable/filtered rule counts, relevant rules in the middle of the original file, scheduled high/mid rules, and top scored rules. |
349
+ | `ctx report` | Shows the last Stop-hook compliance report for the current workspace. | An agent task has finished and you want the summary again. | Prints sectioned tables for summary, rule outcomes, suggested files, and runtime telemetry from `~/.ctx/contextos/workspaces/<workspace-id>/last-report.json`. |
350
+ | `ctx evidence` | Shows detailed evidence behind the last report for the current workspace. | You want to inspect why a rule was marked `followed`, `ignored`, `unknown`, or `unmeasurable`. | Prints a compact evidence table plus per-rule detail tables. |
351
+ | `ctx stats` | Shows aggregate runtime metrics for the current workspace. | You want to know whether ContextOS is active and useful over time. | Prints sectioned tables for prompt/report counts, injection rate, efficiency, rule outcomes, hook events, last prompt, and last report. |
352
+ | `ctx benchmark -- "task"` | Compares baseline AGENTS.md ordering with ContextOS task-aware scheduling. | You want a before/after signal for lost-in-the-middle risk. | Prints tables for parsed/actionable/filtered rules, baseline middle-risk, scheduled high/mid rules, recency reminder status, and top scored rules. |
353
353
  | `ctx sync --rules` | Syncs project rules and MCP servers through Ruler. | You want Codex, Claude Code, and Antigravity to share one project rule/MCP source of truth. | Ensures `.ruler/ruler.toml`, injects `ctx-mcp`, imports existing MCP servers from Codex and project `.mcp.json`, runs `ruler apply --agents codex,claude,antigravity`, mirrors MCP servers to Antigravity MCP configs, and verifies generated config. |
354
354
  | `ctx sync --rules --agents <list>` | Syncs only selected agents through Ruler. | You want to update one or two agents without touching the others. | Accepts comma-separated values such as `codex`, `claude`, `agy`, `antigravity`, or `codex,claude,agy`; `agy` is normalized to Ruler's `antigravity`. |
355
355
  | `ctx sync --rules --dry-run` | Previews Ruler sync without writing files or running apply. | You want to inspect behavior before changing project config. | Prints the same flow with dry-run status. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  import { parseRules, filterActionableRules, scoreRules } from "./analyzer.js";
2
2
  import { readAgentsChain } from "./reader.js";
3
3
  import { scheduleContext } from "./scheduler.js";
4
+ import { section, table, truncateCell } from "./terminal-ui.js";
4
5
 
5
6
  export function benchmarkContext({ markdown, sources = [], task = "", openFiles = [], topK = 8 } = {}) {
6
7
  const parsedRules = parseRules(markdown);
@@ -55,18 +56,24 @@ export function benchmarkWorkspace({ cwd = process.cwd(), task = "", openFiles =
55
56
  export function formatBenchmark(result) {
56
57
  const lines = [];
57
58
  lines.push("ContextOS benchmark");
58
- lines.push(`Task: ${result.task || "(empty)"}`);
59
- lines.push(`Rules: ${result.rulesParsed} parsed, ${result.actionableRules} actionable, ${result.filteredRules} filtered`);
60
- lines.push(`Relevant rules: ${result.relevantRules}`);
61
- lines.push(`Baseline middle-risk: ${result.baseline.relevantRulesInMiddle}/${result.relevantRules} relevant rules (${result.baseline.middleRiskPercent}%)`);
62
- lines.push(`ContextOS scheduled: ${result.contextOS.highRules} high, ${result.contextOS.midRules} mid`);
63
- lines.push(`Recency reminder: ${result.contextOS.repeatsHighRulesAtEnd ? "enabled" : "not needed"}`);
59
+ lines.push(section("Summary"));
60
+ lines.push(table(["Metric", "Value"], [
61
+ ["Task", truncateCell(result.task || "(empty)", 100)],
62
+ ["Rules parsed", result.rulesParsed],
63
+ ["Actionable rules", result.actionableRules],
64
+ ["Filtered rules", result.filteredRules],
65
+ ["Relevant rules", result.relevantRules],
66
+ ["Baseline middle-risk", `${result.baseline.relevantRulesInMiddle}/${result.relevantRules} relevant rules (${result.baseline.middleRiskPercent}%)`],
67
+ ["ContextOS scheduled", `${result.contextOS.highRules} high, ${result.contextOS.midRules} mid`],
68
+ ["Recency reminder", result.contextOS.repeatsHighRulesAtEnd ? "enabled" : "not needed"]
69
+ ]));
64
70
  if (result.contextOS.topRules.length) {
65
- lines.push("Top rules:");
66
- for (const rule of result.contextOS.topRules) {
67
- const reasons = rule.reasons?.length ? ` reasons:${rule.reasons.join(",")}` : "";
68
- lines.push(`- ${Number(rule.score || 0).toFixed(2)} ${rule.content}${reasons}`);
69
- }
71
+ lines.push(section("Top Rules"));
72
+ lines.push(table(["Score", "Rule", "Reasons"], result.contextOS.topRules.map((rule) => [
73
+ Number(rule.score || 0).toFixed(2),
74
+ truncateCell(rule.content, 88),
75
+ truncateCell(rule.reasons?.join(", ") || "", 40)
76
+ ])));
70
77
  }
71
78
  return lines.join("\n");
72
79
  }
@@ -1,4 +1,5 @@
1
1
  import { isSystemUserRule } from "./analyzer.js";
2
+ import { section, table, truncateCell } from "./terminal-ui.js";
2
3
 
3
4
  export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot, compliance, runtimeEvidence }) {
4
5
  const actionableCompliance = compliance.filter((item) => !isSystemUserRule(item.rule));
@@ -33,17 +34,33 @@ export function formatReport(report) {
33
34
  report = sanitizeReport(report);
34
35
  const lines = [];
35
36
  lines.push("ContextOS report");
36
- lines.push(`Efficiency: ${report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`}`);
37
- lines.push(`Injected rules: ${report.injectedRuleCount || 0}`);
38
- lines.push(`Rule outcomes: ${report.followed?.length || 0} followed, ${report.ignored?.length || 0} ignored, ${report.unknown?.length || 0} unknown, ${report.unmeasurable?.length || 0} unmeasurable`);
39
- lines.push(`Measured rules: ${report.measuredRuleCount ?? ((report.followed?.length || 0) + (report.ignored?.length || 0))}`);
40
- lines.push(`Changed files: ${report.changedFiles?.length ? report.changedFiles.join(", ") : "none detected"}`);
37
+ lines.push(section("Summary"));
38
+ lines.push(table(["Metric", "Value"], [
39
+ ["Efficiency", report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`],
40
+ ["Injected rules", report.injectedRuleCount || 0],
41
+ ["Measured rules", report.measuredRuleCount ?? ((report.followed?.length || 0) + (report.ignored?.length || 0))],
42
+ ["Changed files", report.changedFiles?.length ? report.changedFiles.length : "none detected"]
43
+ ]));
44
+
45
+ lines.push(section("Rule Outcomes"));
46
+ lines.push(table(["Status", "Count"], [
47
+ ["followed", report.followed?.length || 0],
48
+ ["ignored", report.ignored?.length || 0],
49
+ ["unknown", report.unknown?.length || 0],
50
+ ["unmeasurable", report.unmeasurable?.length || 0]
51
+ ]));
41
52
 
42
53
  if (report.relevantFiles?.length) {
43
- lines.push(`Suggested files: ${report.relevantFiles.map((file) => file.path).join(", ")}`);
54
+ lines.push(section("Suggested Files"));
55
+ lines.push(table(["#", "Path", "Score"], report.relevantFiles.slice(0, 10).map((file, index) => [
56
+ index + 1,
57
+ truncateCell(file.path, 90),
58
+ typeof file.score === "number" ? file.score.toFixed(2) : ""
59
+ ])));
44
60
  }
45
61
  if (report.runtimeEvidence?.signals?.length) {
46
- lines.push(`Runtime telemetry: ${report.runtimeEvidence.signals.join(", ")}`);
62
+ lines.push(section("Runtime Telemetry"));
63
+ lines.push(table(["#", "Signal"], report.runtimeEvidence.signals.map((signal, index) => [index + 1, signal])));
47
64
  }
48
65
 
49
66
  for (const warning of report.warnings || []) lines.push(`Warning: ${warning}`);
@@ -66,9 +83,12 @@ export function formatEvidence(report) {
66
83
  report = sanitizeReport(report);
67
84
  const lines = [];
68
85
  lines.push("ContextOS evidence");
69
- lines.push(`Prompt: ${report.prompt || "(empty)"}`);
70
- lines.push(`Efficiency: ${report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`}`);
71
- lines.push(`Changed files: ${report.changedFiles?.length ? report.changedFiles.join(", ") : "none detected"}`);
86
+ lines.push(section("Summary"));
87
+ lines.push(table(["Field", "Value"], [
88
+ ["Prompt", truncateCell(report.prompt || "(empty)", 100)],
89
+ ["Efficiency", report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`],
90
+ ["Changed files", report.changedFiles?.length ? report.changedFiles.join(", ") : "none detected"]
91
+ ]));
72
92
 
73
93
  for (const warning of report.warnings || []) lines.push(`Warning: ${warning}`);
74
94
 
@@ -85,15 +105,26 @@ export function formatEvidence(report) {
85
105
  return lines.join("\n");
86
106
  }
87
107
 
108
+ lines.push(section("Evidence Table"));
109
+ lines.push(table(["#", "Status", "Score", "Kind", "Rule", "Evidence"], items.map((item, index) => [
110
+ index + 1,
111
+ item.status.toUpperCase(),
112
+ typeof item.rule?.score === "number" ? item.rule.score.toFixed(2) : "",
113
+ item.kind || "",
114
+ truncateCell(item.rule?.content || "(missing rule)", 46),
115
+ truncateCell(item.evidence || "(none)", 58)
116
+ ])));
117
+
88
118
  items.forEach((item, index) => {
89
- lines.push("");
90
- lines.push(`${index + 1}. ${item.status.toUpperCase()}`);
91
- lines.push(`Rule: ${item.rule?.content || "(missing rule)"}`);
92
- if (item.rule?.sourcePath) lines.push(`Source: ${item.rule.sourcePath}`);
93
- if (typeof item.rule?.score === "number") lines.push(`Score: ${item.rule.score.toFixed(2)}`);
94
- if (item.kind) lines.push(`Kind: ${item.kind}`);
95
- if (item.keywords?.length) lines.push(`Keywords: ${item.keywords.join(", ")}`);
96
- lines.push(`Evidence: ${item.evidence || "(none)"}`);
119
+ lines.push(section(`${index + 1}. ${item.status.toUpperCase()}`));
120
+ lines.push(table(["Field", "Value"], [
121
+ ["Rule", truncateCell(item.rule?.content || "(missing rule)", 120)],
122
+ ["Source", item.rule?.sourcePath || ""],
123
+ ["Score", typeof item.rule?.score === "number" ? item.rule.score.toFixed(2) : ""],
124
+ ["Kind", item.kind || ""],
125
+ ["Keywords", item.keywords?.length ? truncateCell(item.keywords.join(", "), 120) : ""],
126
+ ["Evidence", truncateCell(item.evidence || "(none)", 120)]
127
+ ].filter(([, value]) => value !== "")));
97
128
  for (const line of item.matchedLines || []) {
98
129
  const where = line.file ? `${line.file}${typeof line.line === "number" ? `:${line.line}` : ""}` : "diff";
99
130
  lines.push(`Matched line: ${where} ${truncate(line.content || "", 140)}`);
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  import { safeReadText } from "./fs-utils.js";
5
+ import { section, table, truncateCell } from "./terminal-ui.js";
5
6
 
6
7
  function readJsonLines(filePath) {
7
8
  return safeReadText(filePath)
@@ -81,33 +82,47 @@ export function loadStats(dataDir) {
81
82
  export function formatStats(stats) {
82
83
  const lines = [];
83
84
  lines.push("ContextOS stats");
84
- lines.push(`Data dir: ${stats.dataDir}`);
85
- lines.push(`Prompts analyzed: ${stats.promptCount}`);
86
- lines.push(`Reports generated: ${stats.reportCount}`);
87
- lines.push(`Prompt mode: ${stats.injectedCount} injected, ${stats.quietCount} quiet (${stats.injectionRate}% injected)`);
88
- lines.push(`Average prompt analysis: ${stats.averagePromptMs == null ? "unknown" : `${stats.averagePromptMs}ms`}`);
89
- lines.push(`Average efficiency: ${formatAverageEfficiency(stats)}`);
90
- lines.push(`Rule outcomes: ${stats.followed} followed, ${stats.ignored} ignored, ${stats.unknown} unknown, ${stats.unmeasurable || 0} unmeasurable`);
91
-
92
- const eventSummary = Object.entries(stats.events)
93
- .map(([event, count]) => `${event}:${count}`)
94
- .join(", ");
95
- lines.push(`Hook events: ${eventSummary || "none"}`);
85
+ lines.push(section("Summary"));
86
+ lines.push(table(["Metric", "Value"], [
87
+ ["Data dir", stats.dataDir],
88
+ ["Prompts analyzed", stats.promptCount],
89
+ ["Reports generated", stats.reportCount],
90
+ ["Prompt mode", `${stats.injectedCount} injected, ${stats.quietCount} quiet (${stats.injectionRate}% injected)`],
91
+ ["Average prompt analysis", stats.averagePromptMs == null ? "unknown" : `${stats.averagePromptMs}ms`],
92
+ ["Average efficiency", formatAverageEfficiency(stats)]
93
+ ]));
94
+
95
+ lines.push(section("Rule Outcomes"));
96
+ lines.push(table(["Status", "Count"], [
97
+ ["followed", stats.followed],
98
+ ["ignored", stats.ignored],
99
+ ["unknown", stats.unknown],
100
+ ["unmeasurable", stats.unmeasurable || 0]
101
+ ]));
102
+
103
+ lines.push(section("Hook Events"));
104
+ lines.push(Object.keys(stats.events).length
105
+ ? table(["Event", "Count"], Object.entries(stats.events))
106
+ : "none");
96
107
 
97
108
  if (stats.lastPrompt) {
98
- lines.push(`Last prompt: ${truncateLine(stats.lastPrompt.prompt || "", 100) || "(empty)"}`);
99
- lines.push(`Last scheduled rules: ${scheduledRuleCount(stats.lastPrompt)}`);
100
- const files = (stats.lastPrompt.relevantFiles || []).map((file) => file.path).join(", ");
101
- if (files) lines.push(`Last suggested files: ${files}`);
109
+ lines.push(section("Last Prompt"));
110
+ lines.push(table(["Field", "Value"], [
111
+ ["Prompt", truncateCell(stats.lastPrompt.prompt || "(empty)", 100)],
112
+ ["Scheduled rules", scheduledRuleCount(stats.lastPrompt)],
113
+ ["Suggested files", (stats.lastPrompt.relevantFiles || []).map((file) => file.path).join(", ") || "none"]
114
+ ]));
102
115
  }
103
116
 
104
117
  if (stats.lastReport) {
105
- lines.push(`Last report efficiency: ${stats.lastReport.efficiencyScore == null ? "unknown" : `${stats.lastReport.efficiencyScore}%`}`);
106
- lines.push(`Last report measured rules: ${stats.lastReport.measuredRuleCount ?? ((stats.lastReport.followed?.length || 0) + (stats.lastReport.ignored?.length || 0))}`);
107
- lines.push(`Last report unknown rules: ${stats.lastReport.unknownRuleCount ?? (stats.lastReport.unknown?.length || 0)}`);
108
- lines.push(`Last report unmeasurable rules: ${stats.lastReport.unmeasurableRuleCount ?? (stats.lastReport.unmeasurable?.length || 0)}`);
109
- const changed = stats.lastReport.changedFiles?.join(", ");
110
- if (changed) lines.push(`Last changed files: ${changed}`);
118
+ lines.push(section("Last Report"));
119
+ lines.push(table(["Metric", "Value"], [
120
+ ["Efficiency", stats.lastReport.efficiencyScore == null ? "unknown" : `${stats.lastReport.efficiencyScore}%`],
121
+ ["Measured rules", stats.lastReport.measuredRuleCount ?? ((stats.lastReport.followed?.length || 0) + (stats.lastReport.ignored?.length || 0))],
122
+ ["Unknown rules", stats.lastReport.unknownRuleCount ?? (stats.lastReport.unknown?.length || 0)],
123
+ ["Unmeasurable rules", stats.lastReport.unmeasurableRuleCount ?? (stats.lastReport.unmeasurable?.length || 0)],
124
+ ["Changed files", stats.lastReport.changedFiles?.join(", ") || "none"]
125
+ ]));
111
126
  }
112
127
 
113
128
  return lines.join("\n");
@@ -123,8 +138,3 @@ function formatAverageEfficiency(stats) {
123
138
  function scheduledRuleCount(prompt) {
124
139
  return (prompt.scheduled?.highRules?.length || 0) + (prompt.scheduled?.midRules?.length || 0);
125
140
  }
126
-
127
- function truncateLine(value, max) {
128
- const normalized = String(value).replace(/\s+/g, " ").trim();
129
- return normalized.length > max ? `${normalized.slice(0, max - 3)}...` : normalized;
130
- }
@@ -0,0 +1,22 @@
1
+ export function section(title) {
2
+ return `\n${title}\n${"-".repeat(title.length)}`;
3
+ }
4
+
5
+ export function table(headers, rows) {
6
+ const normalizedRows = rows.map((row) => row.map((cell) => String(cell ?? "")));
7
+ const widths = headers.map((header, index) => Math.max(
8
+ String(header).length,
9
+ ...normalizedRows.map((row) => row[index]?.length || 0)
10
+ ));
11
+ const formatRow = (row) => row.map((cell, index) => String(cell ?? "").padEnd(widths[index])).join(" ");
12
+ return [
13
+ formatRow(headers),
14
+ widths.map((width) => "-".repeat(width)).join(" "),
15
+ ...normalizedRows.map(formatRow)
16
+ ].join("\n");
17
+ }
18
+
19
+ export function truncateCell(value, max = 80) {
20
+ const normalized = String(value || "").replace(/\s+/g, " ").trim();
21
+ return normalized.length > max ? `${normalized.slice(0, max - 3)}...` : normalized;
22
+ }