kibi-cli 0.10.1 → 0.11.0

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/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAuCA,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAwCA,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/dist/cli.js CHANGED
@@ -30,6 +30,7 @@ import { queryCommand } from "./commands/query.js";
30
30
  import { searchCommand } from "./commands/search.js";
31
31
  import { statusCommand } from "./commands/status.js";
32
32
  import { syncCommand } from "./commands/sync.js";
33
+ import { usageMetricsCommand } from "./commands/usage-metrics.js";
33
34
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
34
35
  const VERSION = packageJson.version ?? "0.1.0";
35
36
  // implements REQ-003
@@ -167,6 +168,14 @@ program
167
168
  .command("doctor")
168
169
  .description("Diagnose KB setup and configuration")
169
170
  .action(withExitCode(async () => doctorCommand()));
171
+ program
172
+ .command("usage-metrics")
173
+ .description("Report usage and quality metrics from .kb/usage.log")
174
+ .option("--format <format>", "Output format: json|table", "table")
175
+ .option("--limit <n>", "Limit top zero-result source files", "10")
176
+ .action(withExitCode(async (options) => {
177
+ return usageMetricsCommand(options);
178
+ }));
170
179
  program
171
180
  .command("branch")
172
181
  .description("Manage branch KBs")
@@ -0,0 +1,8 @@
1
+ import type { CommandResult } from "../cli.js";
2
+ interface UsageMetricsOptions {
3
+ format?: "json" | "table";
4
+ limit?: string;
5
+ }
6
+ export declare function usageMetricsCommand(options: UsageMetricsOptions): Promise<CommandResult | undefined>;
7
+ export {};
8
+ //# sourceMappingURL=usage-metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-metrics.d.ts","sourceRoot":"","sources":["../../src/commands/usage-metrics.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,UAAU,mBAAmB;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAuBpC"}
@@ -0,0 +1,323 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import Table from "cli-table3";
4
+ // implements REQ-003
5
+ export async function usageMetricsCommand(options) {
6
+ const limit = Number.parseInt(options.limit || "10", 10);
7
+ if (!Number.isFinite(limit) || limit < 1) {
8
+ console.error("Error: --limit must be a positive integer");
9
+ return { exitCode: 1 };
10
+ }
11
+ const usageLogPath = path.join(process.cwd(), ".kb", "usage.log");
12
+ if (!existsSync(usageLogPath)) {
13
+ console.error(`Error: usage log not found at ${usageLogPath}`);
14
+ return { exitCode: 1 };
15
+ }
16
+ const rows = parseUsageLog(readFileSync(usageLogPath, "utf8"));
17
+ const report = buildUsageMetricsReport(rows, limit);
18
+ if (options.format === "json") {
19
+ console.log(JSON.stringify(report, null, 2));
20
+ return;
21
+ }
22
+ console.log(renderUsageMetricsReport(report));
23
+ return;
24
+ }
25
+ function parseUsageLog(contents) {
26
+ const rows = [];
27
+ for (const [index, line] of contents.split(/\r?\n/).entries()) {
28
+ const trimmed = line.trim();
29
+ if (!trimmed) {
30
+ continue;
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(trimmed);
35
+ }
36
+ catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ throw new Error(`Failed to parse .kb/usage.log line ${index + 1}: ${message}`);
39
+ }
40
+ if (!parsed || typeof parsed !== "object") {
41
+ throw new Error(`Failed to parse .kb/usage.log line ${index + 1}: expected object`);
42
+ }
43
+ rows.push(parsed);
44
+ }
45
+ return rows;
46
+ }
47
+ function buildUsageMetricsReport(rows, limit) {
48
+ const timestamps = rows
49
+ .map((row) => row.timestamp)
50
+ .filter((value) => typeof value === "string")
51
+ .sort((left, right) => left.localeCompare(right));
52
+ const toolCounts = new Map();
53
+ const branchCounts = new Map();
54
+ const zeroResultToolCounts = new Map();
55
+ const zeroResultSourceFileCounts = new Map();
56
+ const upsertErrorCategories = new Map();
57
+ const violationTrend = [];
58
+ let successCount = 0;
59
+ let errorCount = 0;
60
+ let telemetryCompleteCount = 0;
61
+ let telemetryMissingCount = 0;
62
+ let zeroResultCount = 0;
63
+ for (const row of rows) {
64
+ increment(toolCounts, normalizeKey(row.tool, "unknown"));
65
+ increment(branchCounts, normalizeKey(row.active_branch || row.branch, "unknown"));
66
+ const outcome = getOutcome(row);
67
+ if (outcome === "success") {
68
+ successCount += 1;
69
+ }
70
+ if (outcome === "error") {
71
+ errorCount += 1;
72
+ }
73
+ if (hasCompleteTelemetry(row)) {
74
+ telemetryCompleteCount += 1;
75
+ }
76
+ else {
77
+ telemetryMissingCount += 1;
78
+ }
79
+ if (isZeroResult(row)) {
80
+ zeroResultCount += 1;
81
+ increment(zeroResultToolCounts, normalizeKey(row.tool, "unknown"));
82
+ const sourceFile = getSourceFile(row);
83
+ if (sourceFile) {
84
+ increment(zeroResultSourceFileCounts, sourceFile);
85
+ }
86
+ }
87
+ if (row.tool === "kb_check" &&
88
+ typeof row.violation_count === "number" &&
89
+ typeof row.timestamp === "string") {
90
+ violationTrend.push({
91
+ timestamp: row.timestamp,
92
+ violationCount: row.violation_count,
93
+ });
94
+ }
95
+ if (row.tool === "kb_upsert" && outcome === "error") {
96
+ increment(upsertErrorCategories, categorizeUpsertError(row.error_message ?? row.error));
97
+ }
98
+ }
99
+ violationTrend.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
100
+ return {
101
+ rowCount: rows.length,
102
+ dateRange: {
103
+ first: timestamps[0] ?? null,
104
+ last: timestamps.at(-1) ?? null,
105
+ },
106
+ toolCounts: mapToSortedObject(toolCounts),
107
+ branchCounts: mapToSortedObject(branchCounts),
108
+ outcomeCounts: {
109
+ success: successCount,
110
+ error: errorCount,
111
+ },
112
+ telemetry: {
113
+ completeCount: telemetryCompleteCount,
114
+ missingCount: telemetryMissingCount,
115
+ completenessRate: rows.length === 0 ? 0 : telemetryCompleteCount / rows.length,
116
+ },
117
+ zeroResults: {
118
+ count: zeroResultCount,
119
+ rate: rows.length === 0 ? 0 : zeroResultCount / rows.length,
120
+ byTool: mapToSortedObject(zeroResultToolCounts),
121
+ topSourceFiles: sortCountEntries(zeroResultSourceFileCounts)
122
+ .slice(0, limit)
123
+ .map(([sourceFile, count]) => ({ sourceFile, count })),
124
+ },
125
+ kbCheck: {
126
+ violationTrend,
127
+ },
128
+ upsertErrors: {
129
+ categories: mapToSortedObject(upsertErrorCategories),
130
+ },
131
+ };
132
+ }
133
+ function hasCompleteTelemetry(row) {
134
+ if (row.telemetry_status === "provided") {
135
+ return true;
136
+ }
137
+ if (row.telemetry_status === "missing") {
138
+ return false;
139
+ }
140
+ return row.telemetry !== null && row.telemetry !== undefined;
141
+ }
142
+ function getOutcome(row) {
143
+ if (row.status === "success" || row.status === "error") {
144
+ return row.status;
145
+ }
146
+ if (row.success === true) {
147
+ return "success";
148
+ }
149
+ if (row.success === false) {
150
+ return "error";
151
+ }
152
+ return null;
153
+ }
154
+ function isZeroResult(row) {
155
+ if (row.zero_results === true) {
156
+ return true;
157
+ }
158
+ if (row.zero_results === false) {
159
+ return false;
160
+ }
161
+ if (row.result_count === 0) {
162
+ return true;
163
+ }
164
+ return row.result_summary === "0 results";
165
+ }
166
+ function getSourceFile(row) {
167
+ if (typeof row.sourceFile === "string" && row.sourceFile.trim()) {
168
+ return row.sourceFile;
169
+ }
170
+ if (row.args &&
171
+ typeof row.args.sourceFile === "string" &&
172
+ row.args.sourceFile.trim()) {
173
+ return row.args.sourceFile;
174
+ }
175
+ if (row.business_args &&
176
+ typeof row.business_args.sourceFile === "string" &&
177
+ row.business_args.sourceFile.trim()) {
178
+ return row.business_args.sourceFile;
179
+ }
180
+ return null;
181
+ }
182
+ function categorizeUpsertError(errorMessage) {
183
+ if (!errorMessage) {
184
+ return "Unknown error";
185
+ }
186
+ if (errorMessage.startsWith("Entity validation failed:")) {
187
+ return "Entity validation failed";
188
+ }
189
+ if (errorMessage.startsWith("Relationship source must match the upserted entity")) {
190
+ return "Relationship source must match the upserted entity";
191
+ }
192
+ const semicolonIndex = errorMessage.indexOf(";");
193
+ if (semicolonIndex > 0) {
194
+ return errorMessage.slice(0, semicolonIndex).trim();
195
+ }
196
+ const colonIndex = errorMessage.indexOf(":");
197
+ if (colonIndex > 0) {
198
+ return errorMessage.slice(0, colonIndex).trim();
199
+ }
200
+ return errorMessage.trim() || "Unknown error";
201
+ }
202
+ function increment(counts, key) {
203
+ counts.set(key, (counts.get(key) ?? 0) + 1);
204
+ }
205
+ function normalizeKey(value, fallback) {
206
+ return typeof value === "string" && value.trim() ? value : fallback;
207
+ }
208
+ function sortCountEntries(counts) {
209
+ return [...counts.entries()].sort(([leftKey, leftCount], [rightKey, rightCount]) => rightCount - leftCount || leftKey.localeCompare(rightKey));
210
+ }
211
+ function mapToSortedObject(counts) {
212
+ return Object.fromEntries(sortCountEntries(counts));
213
+ }
214
+ function renderUsageMetricsReport(report) {
215
+ const sections = [
216
+ renderSummaryTable(report),
217
+ renderCountsTable("Tool Counts", "Tool", report.toolCounts),
218
+ renderCountsTable("Branch Counts", "Branch", report.branchCounts),
219
+ renderCountsTable("Outcome Counts", "Outcome", report.outcomeCounts),
220
+ renderTelemetryTable(report),
221
+ renderZeroResultsTable(report),
222
+ renderViolationTrendTable(report),
223
+ renderCountsTable("Upsert Error Categories", "Category", report.upsertErrors.categories),
224
+ ].filter(Boolean);
225
+ return sections.join("\n\n");
226
+ }
227
+ function renderSummaryTable(report) {
228
+ const table = new Table({
229
+ head: ["Field", "Value"],
230
+ colWidths: [24, 56],
231
+ wordWrap: true,
232
+ });
233
+ table.push(["Row Count", String(report.rowCount)], ["First Timestamp", report.dateRange.first ?? "-"], ["Last Timestamp", report.dateRange.last ?? "-"]);
234
+ return table.toString();
235
+ }
236
+ function renderCountsTable(title, label, counts) {
237
+ const entries = Object.entries(counts);
238
+ const table = new Table({
239
+ head: [label, "Count"],
240
+ colWidths: [48, 12],
241
+ wordWrap: true,
242
+ });
243
+ if (entries.length === 0) {
244
+ table.push(["-", "0"]);
245
+ }
246
+ else {
247
+ for (const [key, count] of entries) {
248
+ table.push([key, String(count)]);
249
+ }
250
+ }
251
+ return `${title}\n${table.toString()}`;
252
+ }
253
+ function renderTelemetryTable(report) {
254
+ const table = new Table({
255
+ head: ["Metric", "Value"],
256
+ colWidths: [32, 28],
257
+ wordWrap: true,
258
+ });
259
+ table.push(["Complete", String(report.telemetry.completeCount)], ["Missing", String(report.telemetry.missingCount)], ["Completeness Rate", formatRate(report.telemetry.completenessRate)]);
260
+ return `Telemetry\n${table.toString()}`;
261
+ }
262
+ function renderZeroResultsTable(report) {
263
+ const summary = new Table({
264
+ head: ["Metric", "Value"],
265
+ colWidths: [32, 28],
266
+ wordWrap: true,
267
+ });
268
+ summary.push(["Count", String(report.zeroResults.count)], ["Rate", formatRate(report.zeroResults.rate)]);
269
+ const byTool = new Table({
270
+ head: ["Tool", "Count"],
271
+ colWidths: [48, 12],
272
+ wordWrap: true,
273
+ });
274
+ const byToolEntries = Object.entries(report.zeroResults.byTool);
275
+ if (byToolEntries.length === 0) {
276
+ byTool.push(["-", "0"]);
277
+ }
278
+ else {
279
+ for (const [tool, count] of byToolEntries) {
280
+ byTool.push([tool, String(count)]);
281
+ }
282
+ }
283
+ const sourceFiles = new Table({
284
+ head: ["Source File", "Zero Results"],
285
+ colWidths: [48, 14],
286
+ wordWrap: true,
287
+ });
288
+ if (report.zeroResults.topSourceFiles.length === 0) {
289
+ sourceFiles.push(["-", "0"]);
290
+ }
291
+ else {
292
+ for (const entry of report.zeroResults.topSourceFiles) {
293
+ sourceFiles.push([entry.sourceFile, String(entry.count)]);
294
+ }
295
+ }
296
+ return [
297
+ "Zero Results",
298
+ summary.toString(),
299
+ "Zero-Result Counts By Tool",
300
+ byTool.toString(),
301
+ "Zero-Result Source Files",
302
+ sourceFiles.toString(),
303
+ ].join("\n");
304
+ }
305
+ function renderViolationTrendTable(report) {
306
+ const table = new Table({
307
+ head: ["Timestamp", "Violations"],
308
+ colWidths: [32, 12],
309
+ wordWrap: true,
310
+ });
311
+ if (report.kbCheck.violationTrend.length === 0) {
312
+ table.push(["-", "0"]);
313
+ }
314
+ else {
315
+ for (const entry of report.kbCheck.violationTrend) {
316
+ table.push([entry.timestamp, String(entry.violationCount)]);
317
+ }
318
+ }
319
+ return `KB Check Violation Trend\n${table.toString()}`;
320
+ }
321
+ function formatRate(value) {
322
+ return `${(value * 100).toFixed(1)}%`;
323
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {