kibi-cli 0.2.8 → 0.3.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.js CHANGED
@@ -19,10 +19,15 @@ import { readFileSync } from "node:fs";
19
19
  import { Command } from "commander";
20
20
  import { branchEnsureCommand } from "./commands/branch.js";
21
21
  import { checkCommand } from "./commands/check.js";
22
+ import { coverageCommand } from "./commands/coverage.js";
22
23
  import { doctorCommand } from "./commands/doctor.js";
24
+ import { gapsCommand } from "./commands/gaps.js";
23
25
  import { gcCommand } from "./commands/gc.js";
26
+ import { graphCommand } from "./commands/graph.js";
24
27
  import { initCommand } from "./commands/init.js";
25
28
  import { queryCommand } from "./commands/query.js";
29
+ import { searchCommand } from "./commands/search.js";
30
+ import { statusCommand } from "./commands/status.js";
26
31
  import { syncCommand } from "./commands/sync.js";
27
32
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
28
33
  const VERSION = packageJson.version ?? "0.1.0";
@@ -59,6 +64,63 @@ program
59
64
  .action(async (type, options) => {
60
65
  await queryCommand(type, options);
61
66
  });
67
+ program
68
+ .command("search [query]")
69
+ .description("Search knowledge base metadata and markdown content")
70
+ .option("--type <type>", "Filter by entity type")
71
+ .option("--format <format>", "Output format: json|table", "table")
72
+ .option("--limit <n>", "Limit results", "20")
73
+ .option("--offset <n>", "Skip results", "0")
74
+ .action(async (query, options) => {
75
+ await searchCommand(query, options);
76
+ });
77
+ program
78
+ .command("status")
79
+ .description("Show KB snapshot and freshness metadata")
80
+ .option("--format <format>", "Output format: json|table", "table")
81
+ .action(async (options) => {
82
+ await statusCommand(options);
83
+ });
84
+ program
85
+ .command("gaps [type]")
86
+ .description("Find entities missing or present on selected relationships")
87
+ .option("--missing-rel <rels>", "Comma-separated missing relationship filters")
88
+ .option("--present-rel <rels>", "Comma-separated present relationship filters")
89
+ .option("--tag <tags>", "Comma-separated tag filter")
90
+ .option("--source <path>", "Source file substring filter")
91
+ .option("--limit <n>", "Limit results", "100")
92
+ .option("--offset <n>", "Skip results", "0")
93
+ .option("--format <format>", "Output format: json|table", "table")
94
+ .action(async (type, options) => {
95
+ await gapsCommand(type, options);
96
+ });
97
+ program
98
+ .command("coverage")
99
+ .description("Generate curated coverage reports")
100
+ .option("--by <group>", "Coverage mode: req|symbol|type", "req")
101
+ .option("--tag <tags>", "Comma-separated tag filter")
102
+ .option("--include-passing", "Include passing rows", false)
103
+ .option("--no-include-transitive", "Disable transitive symbol coverage")
104
+ .option("--limit <n>", "Limit results", "100")
105
+ .option("--offset <n>", "Skip results", "0")
106
+ .option("--format <format>", "Output format: json|table", "table")
107
+ .action(async (options) => {
108
+ await coverageCommand(options);
109
+ });
110
+ program
111
+ .command("graph")
112
+ .description("Traverse the KB graph from one or more seed IDs")
113
+ .option("--from <ids>", "Comma-separated seed IDs")
114
+ .option("--relationships <rels>", "Comma-separated relationship filter")
115
+ .option("--direction <direction>", "Direction: outgoing|incoming|both", "outgoing")
116
+ .option("--depth <n>", "Traversal depth", "1")
117
+ .option("--entity-types <types>", "Comma-separated entity type filter")
118
+ .option("--max-nodes <n>", "Maximum node count", "200")
119
+ .option("--max-edges <n>", "Maximum edge count", "500")
120
+ .option("--format <format>", "Output format: json|table", "table")
121
+ .action(async (options) => {
122
+ await graphCommand(options);
123
+ });
62
124
  program
63
125
  .command("check")
64
126
  .description("Check KB consistency and integrity")
@@ -0,0 +1,12 @@
1
+ interface CoverageOptions {
2
+ by?: "req" | "symbol" | "type";
3
+ tag?: string;
4
+ includePassing?: boolean;
5
+ includeTransitive?: boolean;
6
+ limit?: string;
7
+ offset?: string;
8
+ format?: "json" | "table";
9
+ }
10
+ export declare function coverageCommand(options: CoverageOptions): Promise<void>;
11
+ export {};
12
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/commands/coverage.ts"],"names":[],"mappings":"AAQA,UAAU,eAAe;IACvB,EAAE,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC7E"}
@@ -0,0 +1,24 @@
1
+ import { escapeAtom } from "../prolog/codec.js";
2
+ import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
3
+ // implements REQ-002, REQ-003
4
+ export async function coverageCommand(options) {
5
+ await withPrologProcess(async (prolog) => {
6
+ const kbPath = await resolveCurrentKbPath();
7
+ const by = options.by || "req";
8
+ const tags = options.tag
9
+ ? `[${options.tag
10
+ .split(",")
11
+ .map((item) => item.trim())
12
+ .filter(Boolean)
13
+ .map((item) => `'${escapeAtom(item)}'`)
14
+ .join(",")}]`
15
+ : "[]";
16
+ const includePassing = options.includePassing ?? false;
17
+ const includeTransitive = options.includeTransitive ?? true;
18
+ const limit = Number.parseInt(options.limit || "100", 10);
19
+ const offset = Number.parseInt(options.offset || "0", 10);
20
+ const result = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:coverage_report_json('${by}', ${tags}, ${includePassing}, ${includeTransitive}, ${limit}, ${offset}, JsonString)`, "coverage query failed", kbPath);
21
+ const summary = (result.summary ?? {});
22
+ printDiscoveryResult(options.format, result, `Coverage summary: ${summary.fullyCovered ?? 0} fully covered out of ${summary.total ?? 0}.`);
23
+ });
24
+ }
@@ -0,0 +1,11 @@
1
+ import { PrologProcess } from "../prolog.js";
2
+ export interface DiscoveryCommandOptions {
3
+ format?: "json" | "table";
4
+ }
5
+ export declare function withAttachedBranchProlog<T>(callback: (prolog: PrologProcess) => Promise<T>): Promise<T>;
6
+ export declare function withPrologProcess<T>(callback: (prolog: PrologProcess) => Promise<T>): Promise<T>;
7
+ export declare function resolveCurrentKbPath(): Promise<string>;
8
+ export declare function resolveCoreModulePath(fileName: string): string;
9
+ export declare function runJsonModuleQuery<T>(prolog: PrologProcess, fileName: string, goal: string, errorLabel: string, kbPath?: string): Promise<T>;
10
+ export declare function printDiscoveryResult(format: "json" | "table" | undefined, structured: unknown, fallbackText: string): void;
11
+ //# sourceMappingURL=discovery-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery-shared.d.ts","sourceRoot":"","sources":["../../src/commands/discovery-shared.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAmB,MAAM,cAAc,CAAC;AAI9D,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,CAAC,CAAC,CAsCZ;AAGD,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,CAAC,CAAC,CAcZ;AAGD,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS5D;AAGD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE9D;AAGD,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,CAsBZ;AAGD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,EACpC,UAAU,EAAE,OAAO,EACnB,YAAY,EAAE,MAAM,GACnB,IAAI,CAQN"}
@@ -0,0 +1,280 @@
1
+ import path from "node:path";
2
+ import Table from "cli-table3";
3
+ import { PrologProcess, resolveKbPlPath } from "../prolog.js";
4
+ import { escapeAtom } from "../prolog/codec.js";
5
+ import { getCurrentBranch } from "./init-helpers.js";
6
+ // implements REQ-003
7
+ export async function withAttachedBranchProlog(callback) {
8
+ let prolog = null;
9
+ let attached = false;
10
+ try {
11
+ prolog = new PrologProcess({ timeout: 120000 });
12
+ await prolog.start();
13
+ await prolog.query("set_prolog_flag(answer_write_options, [max_depth(0), spacing(next_argument)])");
14
+ let branch;
15
+ try {
16
+ branch = process.env.KIBI_BRANCH || (await getCurrentBranch(process.cwd()));
17
+ }
18
+ catch {
19
+ branch = process.env.KIBI_BRANCH || "main";
20
+ }
21
+ const kbPath = path.join(process.cwd(), ".kb/branches", branch);
22
+ const attachResult = await prolog.query(`kb_attach('${escapeAtom(kbPath)}')`);
23
+ if (!attachResult.success) {
24
+ throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
25
+ }
26
+ attached = true;
27
+ return await callback(prolog);
28
+ }
29
+ finally {
30
+ if (prolog) {
31
+ if (attached) {
32
+ try {
33
+ await prolog.query("kb_detach");
34
+ }
35
+ catch { }
36
+ }
37
+ try {
38
+ await prolog.terminate();
39
+ }
40
+ catch { }
41
+ }
42
+ }
43
+ }
44
+ // implements REQ-003
45
+ export async function withPrologProcess(callback) {
46
+ const prolog = new PrologProcess({ timeout: 120000 });
47
+ try {
48
+ await prolog.start();
49
+ ;
50
+ prolog.useOneShotMode = true;
51
+ await prolog.query("set_prolog_flag(answer_write_options, [max_depth(0), spacing(next_argument)])");
52
+ return await callback(prolog);
53
+ }
54
+ finally {
55
+ try {
56
+ await prolog.terminate();
57
+ }
58
+ catch { }
59
+ }
60
+ }
61
+ // implements REQ-003
62
+ export async function resolveCurrentKbPath() {
63
+ let branch;
64
+ try {
65
+ branch = process.env.KIBI_BRANCH || (await getCurrentBranch(process.cwd()));
66
+ }
67
+ catch {
68
+ branch = process.env.KIBI_BRANCH || "main";
69
+ }
70
+ return path.join(process.cwd(), ".kb/branches", branch);
71
+ }
72
+ // implements REQ-003
73
+ export function resolveCoreModulePath(fileName) {
74
+ return path.join(path.dirname(resolveKbPlPath()), fileName);
75
+ }
76
+ // implements REQ-003
77
+ export async function runJsonModuleQuery(prolog, fileName, goal, errorLabel, kbPath) {
78
+ const modulePath = escapeAtom(resolveCoreModulePath(fileName).replace(/\\/g, "/"));
79
+ const wrappedGoal = kbPath
80
+ ? `(use_module('${modulePath}'), kb_attach('${escapeAtom(kbPath)}'), ${goal}, kb_detach)`
81
+ : `(use_module('${modulePath}'), ${goal})`;
82
+ const result = await prolog.query(wrappedGoal);
83
+ if (!result.success) {
84
+ throw new Error(`${errorLabel}: ${result.error || "Unknown error"}`);
85
+ }
86
+ const rawJson = result.bindings.JsonString;
87
+ if (!rawJson) {
88
+ throw new Error(`${errorLabel}: missing JsonString binding`);
89
+ }
90
+ let parsed = JSON.parse(rawJson);
91
+ if (typeof parsed === "string") {
92
+ parsed = JSON.parse(parsed);
93
+ }
94
+ return parsed;
95
+ }
96
+ // implements REQ-003
97
+ export function printDiscoveryResult(format, structured, fallbackText) {
98
+ if (format === "json") {
99
+ console.log(JSON.stringify(structured, null, 2));
100
+ return;
101
+ }
102
+ const rendered = renderDiscoveryTable(structured);
103
+ console.log(rendered || fallbackText);
104
+ }
105
+ function renderDiscoveryTable(structured) {
106
+ if (!structured || typeof structured !== "object") {
107
+ return null;
108
+ }
109
+ const payload = structured;
110
+ if (Array.isArray(payload.results)) {
111
+ return renderSearchTable(payload);
112
+ }
113
+ if (typeof payload.branch === "string" && typeof payload.syncState === "string") {
114
+ return renderStatusTable(payload);
115
+ }
116
+ if (Array.isArray(payload.nodes) && Array.isArray(payload.edges)) {
117
+ return renderGraphTable(payload);
118
+ }
119
+ if (Array.isArray(payload.rows) && payload.summary && typeof payload.summary === "object") {
120
+ return renderCoverageTable(payload);
121
+ }
122
+ if (Array.isArray(payload.rows)) {
123
+ return renderGapsTable(payload);
124
+ }
125
+ return null;
126
+ }
127
+ function renderSearchTable(payload) {
128
+ const rows = Array.isArray(payload.results) ? payload.results : [];
129
+ const table = new Table({
130
+ head: ["ID", "Type", "Title", "Score", "Reasons", "Snippet"],
131
+ wordWrap: true,
132
+ colWidths: [20, 10, 32, 8, 28, 44],
133
+ });
134
+ for (const row of rows) {
135
+ const match = row;
136
+ const entity = (match.entity ?? {});
137
+ const reasons = Array.isArray(match.reasons) ? match.reasons.join(", ") : "";
138
+ table.push([
139
+ stringifyCell(entity.id),
140
+ stringifyCell(entity.type),
141
+ stringifyCell(entity.title),
142
+ stringifyCell(match.score),
143
+ reasons,
144
+ stringifyCell(match.snippet),
145
+ ]);
146
+ }
147
+ return [
148
+ `Search results: ${stringifyCell(payload.count)} total`,
149
+ table.toString(),
150
+ ].join("\n");
151
+ }
152
+ function renderStatusTable(payload) {
153
+ const table = new Table({
154
+ head: ["Field", "Value"],
155
+ colWidths: [18, 72],
156
+ wordWrap: true,
157
+ });
158
+ table.push(["Branch", stringifyCell(payload.branch)], ["Sync State", stringifyCell(payload.syncState)], ["Dirty", stringifyCell(payload.dirty)], ["Snapshot", stringifyCell(payload.snapshotId)], ["Synced At", stringifyCell(payload.syncedAt)], ["KB Path", stringifyCell(payload.kbPath)]);
159
+ return table.toString();
160
+ }
161
+ function renderGapsTable(payload) {
162
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
163
+ const table = new Table({
164
+ head: ["ID", "Type", "Status", "Missing", "Present", "Source"],
165
+ colWidths: [20, 10, 12, 24, 24, 40],
166
+ wordWrap: true,
167
+ });
168
+ for (const row of rows) {
169
+ const item = row;
170
+ table.push([
171
+ stringifyCell(item.id),
172
+ stringifyCell(item.type),
173
+ stringifyCell(item.status),
174
+ joinCells(item.missingRelationships),
175
+ joinCells(item.presentRelationships),
176
+ stringifyCell(item.source),
177
+ ]);
178
+ }
179
+ return [`Gap rows: ${stringifyCell(payload.count)}`, table.toString()].join("\n");
180
+ }
181
+ function renderCoverageTable(payload) {
182
+ const summary = (payload.summary ?? {});
183
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
184
+ const summaryTable = new Table({
185
+ head: ["Metric", "Value"],
186
+ colWidths: [24, 16],
187
+ });
188
+ for (const [key, value] of Object.entries(summary)) {
189
+ summaryTable.push([key, stringifyCell(value)]);
190
+ }
191
+ const firstRow = rows[0];
192
+ const isRequirementCoverage = firstRow && Object.hasOwn(firstRow, "scenarioCount");
193
+ const table = isRequirementCoverage
194
+ ? new Table({
195
+ head: ["ID", "Status", "Priority", "Coverage", "Scen", "Tests", "Symbols", "Gaps"],
196
+ colWidths: [20, 12, 12, 18, 8, 8, 10, 28],
197
+ wordWrap: true,
198
+ })
199
+ : new Table({
200
+ head: ["ID", "Type", "Coverage", "Details", "Gaps"],
201
+ colWidths: [20, 10, 18, 28, 28],
202
+ wordWrap: true,
203
+ });
204
+ for (const row of rows) {
205
+ const item = row;
206
+ if (isRequirementCoverage) {
207
+ table.push([
208
+ stringifyCell(item.id),
209
+ stringifyCell(item.status),
210
+ stringifyCell(item.priority),
211
+ stringifyCell(item.coverageStatus),
212
+ stringifyCell(item.scenarioCount),
213
+ stringifyCell(item.testCount),
214
+ stringifyCell(item.transitiveSymbolCount),
215
+ joinCells(item.gaps),
216
+ ]);
217
+ }
218
+ else {
219
+ table.push([
220
+ stringifyCell(item.id),
221
+ stringifyCell(item.type),
222
+ stringifyCell(item.coverageStatus),
223
+ `req=${stringifyCell(item.directRequirementCount)} test=${stringifyCell(item.testCount)} count=${stringifyCell(item.count)}`,
224
+ joinCells(item.gaps),
225
+ ]);
226
+ }
227
+ }
228
+ return [summaryTable.toString(), table.toString()].join("\n\n");
229
+ }
230
+ function renderGraphTable(payload) {
231
+ const nodes = Array.isArray(payload.nodes) ? payload.nodes : [];
232
+ const edges = Array.isArray(payload.edges) ? payload.edges : [];
233
+ const nodeTable = new Table({
234
+ head: ["Node ID", "Type", "Title", "Status"],
235
+ colWidths: [22, 10, 36, 12],
236
+ wordWrap: true,
237
+ });
238
+ for (const row of nodes) {
239
+ const item = row;
240
+ nodeTable.push([
241
+ stringifyCell(item.id),
242
+ stringifyCell(item.type),
243
+ stringifyCell(item.title),
244
+ stringifyCell(item.status),
245
+ ]);
246
+ }
247
+ const edgeTable = new Table({
248
+ head: ["Relationship", "From", "To"],
249
+ colWidths: [18, 22, 22],
250
+ wordWrap: true,
251
+ });
252
+ for (const row of edges) {
253
+ const item = row;
254
+ edgeTable.push([
255
+ stringifyCell(item.type),
256
+ stringifyCell(item.from),
257
+ stringifyCell(item.to),
258
+ ]);
259
+ }
260
+ return [
261
+ `Nodes: ${nodes.length} Edges: ${edges.length} Truncated: ${stringifyCell(payload.truncated)}`,
262
+ nodeTable.toString(),
263
+ edgeTable.toString(),
264
+ ].join("\n\n");
265
+ }
266
+ function stringifyCell(value) {
267
+ if (value === null || value === undefined || value === "") {
268
+ return "-";
269
+ }
270
+ if (typeof value === "string") {
271
+ return value;
272
+ }
273
+ return String(value);
274
+ }
275
+ function joinCells(value) {
276
+ if (!Array.isArray(value) || value.length === 0) {
277
+ return "-";
278
+ }
279
+ return value.map((item) => stringifyCell(item)).join(", ");
280
+ }
@@ -198,7 +198,7 @@ function checkGitHooks() {
198
198
  return {
199
199
  passed: false,
200
200
  message: "Partially installed",
201
- remediation: "Run: kibi init --hooks",
201
+ remediation: "Run: kibi init",
202
202
  };
203
203
  }
204
204
  function checkPreCommitHook() {
@@ -218,7 +218,7 @@ function checkPreCommitHook() {
218
218
  return {
219
219
  passed: false,
220
220
  message: "Not installed",
221
- remediation: "Run: kibi init --hooks",
221
+ remediation: "Run: kibi init",
222
222
  };
223
223
  }
224
224
  try {
@@ -233,7 +233,7 @@ function checkPreCommitHook() {
233
233
  return {
234
234
  passed: false,
235
235
  message: "pre-commit hook installed but does not invoke kibi",
236
- remediation: "Run: kibi init --hooks to install recommended hooks",
236
+ remediation: "Run: kibi init to install recommended hooks",
237
237
  };
238
238
  }
239
239
  if (preCommitExecutable) {
@@ -247,7 +247,7 @@ function checkPreCommitHook() {
247
247
  return {
248
248
  passed: true,
249
249
  message: "Installed and executable (uses legacy 'kibi check' — consider running 'kibi init' to update hooks to use '--staged')",
250
- remediation: "Run: kibi init --hooks to update git hooks to the latest template",
250
+ remediation: "Run: kibi init to update git hooks to the latest template",
251
251
  };
252
252
  }
253
253
  return {
@@ -260,7 +260,7 @@ function checkPreCommitHook() {
260
260
  return {
261
261
  passed: false,
262
262
  message: "Unable to check hook permissions or read content",
263
- remediation: "Run: kibi init --hooks",
263
+ remediation: "Run: kibi init",
264
264
  };
265
265
  }
266
266
  }
@@ -281,7 +281,7 @@ function checkPostRewriteHook() {
281
281
  return {
282
282
  passed: false,
283
283
  message: "Not installed",
284
- remediation: "Run: kibi init --hooks",
284
+ remediation: "Run: kibi init",
285
285
  };
286
286
  }
287
287
  try {
@@ -294,7 +294,7 @@ function checkPostRewriteHook() {
294
294
  return {
295
295
  passed: false,
296
296
  message: "post-rewrite hook installed but does not invoke kibi",
297
- remediation: "Run: kibi init --hooks to install recommended hooks",
297
+ remediation: "Run: kibi init to install recommended hooks",
298
298
  };
299
299
  }
300
300
  if (postRewriteExecutable) {
@@ -313,7 +313,7 @@ function checkPostRewriteHook() {
313
313
  return {
314
314
  passed: false,
315
315
  message: "Unable to check hook permissions or read content",
316
- remediation: "Run: kibi init --hooks",
316
+ remediation: "Run: kibi init",
317
317
  };
318
318
  }
319
319
  }
@@ -0,0 +1,12 @@
1
+ interface GapsOptions {
2
+ missingRel?: string;
3
+ presentRel?: string;
4
+ tag?: string;
5
+ source?: string;
6
+ limit?: string;
7
+ offset?: string;
8
+ format?: "json" | "table";
9
+ }
10
+ export declare function gapsCommand(type: string | undefined, options: GapsOptions): Promise<void>;
11
+ export {};
12
+ //# sourceMappingURL=gaps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gaps.d.ts","sourceRoot":"","sources":["../../src/commands/gaps.ts"],"names":[],"mappings":"AAQA,UAAU,WAAW;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -0,0 +1,28 @@
1
+ import { escapeAtom } from "../prolog/codec.js";
2
+ import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
3
+ // implements REQ-002, REQ-003
4
+ export async function gapsCommand(type, options) {
5
+ await withPrologProcess(async (prolog) => {
6
+ const kbPath = await resolveCurrentKbPath();
7
+ const missing = csvToPrologList(options.missingRel);
8
+ const present = csvToPrologList(options.presentRel);
9
+ const tags = csvToPrologList(options.tag);
10
+ const source = options.source ? `'${escapeAtom(options.source)}'` : "none";
11
+ const limit = Number.parseInt(options.limit || "100", 10);
12
+ const offset = Number.parseInt(options.offset || "0", 10);
13
+ const typeArg = type ? `'${escapeAtom(type)}'` : "none";
14
+ const result = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:find_gaps_json(${typeArg}, ${missing}, ${present}, ${tags}, ${source}, ${limit}, ${offset}, JsonString)`, "gaps query failed", kbPath);
15
+ printDiscoveryResult(options.format, result, `Found ${result.count ?? 0} gap rows.`);
16
+ });
17
+ }
18
+ function csvToPrologList(value) {
19
+ if (!value?.trim()) {
20
+ return "[]";
21
+ }
22
+ return `[${value
23
+ .split(",")
24
+ .map((item) => item.trim())
25
+ .filter(Boolean)
26
+ .map((item) => `'${escapeAtom(item)}'`)
27
+ .join(",")}]`;
28
+ }
@@ -0,0 +1,13 @@
1
+ interface GraphOptions {
2
+ from?: string;
3
+ relationships?: string;
4
+ direction?: "outgoing" | "incoming" | "both";
5
+ depth?: string;
6
+ entityTypes?: string;
7
+ maxNodes?: string;
8
+ maxEdges?: string;
9
+ format?: "json" | "table";
10
+ }
11
+ export declare function graphCommand(options: GraphOptions): Promise<void>;
12
+ export {};
13
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/commands/graph.ts"],"names":[],"mappings":"AAQA,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
@@ -0,0 +1,35 @@
1
+ import { escapeAtom } from "../prolog/codec.js";
2
+ import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
3
+ // implements REQ-002, REQ-003
4
+ export async function graphCommand(options) {
5
+ if (!options.from?.trim()) {
6
+ console.error("Error: --from is required");
7
+ process.exitCode = 1;
8
+ return;
9
+ }
10
+ await withPrologProcess(async (prolog) => {
11
+ const kbPath = await resolveCurrentKbPath();
12
+ const seedIds = csvToPrologList(options.from);
13
+ const relationships = csvToPrologList(options.relationships);
14
+ const direction = options.direction || "outgoing";
15
+ const depth = Number.parseInt(options.depth || "1", 10);
16
+ const entityTypes = csvToPrologList(options.entityTypes);
17
+ const maxNodes = Number.parseInt(options.maxNodes || "200", 10);
18
+ const maxEdges = Number.parseInt(options.maxEdges || "500", 10);
19
+ const result = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:graph_expand_json(${seedIds}, ${relationships}, '${direction}', ${depth}, ${entityTypes}, ${maxNodes}, ${maxEdges}, JsonString)`, "graph query failed", kbPath);
20
+ const nodes = Array.isArray(result.nodes) ? result.nodes.length : 0;
21
+ const edges = Array.isArray(result.edges) ? result.edges.length : 0;
22
+ printDiscoveryResult(options.format, result, `Graph traversal returned ${nodes} nodes and ${edges} edges.`);
23
+ });
24
+ }
25
+ function csvToPrologList(value) {
26
+ if (!value?.trim()) {
27
+ return "[]";
28
+ }
29
+ return `[${value
30
+ .split(",")
31
+ .map((item) => item.trim())
32
+ .filter(Boolean)
33
+ .map((item) => `'${escapeAtom(item)}'`)
34
+ .join(",")}]`;
35
+ }
@@ -0,0 +1,9 @@
1
+ interface SearchOptions {
2
+ type?: string;
3
+ format?: "json" | "table";
4
+ limit?: string;
5
+ offset?: string;
6
+ }
7
+ export declare function searchCommand(query: string | undefined, options: SearchOptions): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/commands/search.ts"],"names":[],"mappings":"AAYA,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAGD,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -0,0 +1,38 @@
1
+ import { rankEntities } from "../search-ranking.js";
2
+ import { VALID_ENTITY_TYPES, queryEntities, } from "../query/service.js";
3
+ import { printDiscoveryResult, withAttachedBranchProlog, } from "./discovery-shared.js";
4
+ // implements REQ-mcp-search-discovery, REQ-003
5
+ export async function searchCommand(query, options) {
6
+ if (!query?.trim()) {
7
+ console.error("Error: search query is required");
8
+ process.exitCode = 1;
9
+ return;
10
+ }
11
+ if (options.type && !VALID_ENTITY_TYPES.includes(options.type)) {
12
+ console.error(`Error: invalid type '${options.type}'. Valid types: ${VALID_ENTITY_TYPES.join(", ")}`);
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ await withAttachedBranchProlog(async (prolog) => {
17
+ const limit = Number.parseInt(options.limit || "20", 10);
18
+ const offset = Number.parseInt(options.offset || "0", 10);
19
+ const result = await executeSearch(prolog, query, options.type, limit, offset);
20
+ printDiscoveryResult(options.format, result, buildSearchText(query, result));
21
+ });
22
+ }
23
+ async function executeSearch(prolog, query, type, limit, offset) {
24
+ const entitiesResult = await queryEntities(prolog, {
25
+ type,
26
+ limit: 100000,
27
+ offset: 0,
28
+ });
29
+ const matches = await rankEntities(entitiesResult.entities, query, process.cwd());
30
+ const paginated = matches.slice(offset, offset + limit);
31
+ return { results: paginated, count: matches.length };
32
+ }
33
+ function buildSearchText(query, result) {
34
+ if (result.count === 0) {
35
+ return `No search results for '${query}'.`;
36
+ }
37
+ return `Found ${result.count} search results for '${query}'. Showing ${result.results.length}: ${result.results.map((match) => match.entity.id).join(", ")}`;
38
+ }
@@ -0,0 +1,6 @@
1
+ interface StatusOptions {
2
+ format?: "json" | "table";
3
+ }
4
+ export declare function statusCommand(options: StatusOptions): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAOA,UAAU,aAAa;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBzE"}
@@ -0,0 +1,9 @@
1
+ import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
2
+ // implements REQ-002, REQ-003
3
+ export async function statusCommand(options) {
4
+ await withPrologProcess(async (prolog) => {
5
+ const kbPath = await resolveCurrentKbPath();
6
+ const result = await runJsonModuleQuery(prolog, "status.pl", "status:kb_status_json(JsonString)", "status query failed", kbPath);
7
+ printDiscoveryResult(options.format, result, `Branch ${result.branch} is ${result.syncState} (dirty=${result.dirty})`);
8
+ });
9
+ }
@@ -0,0 +1,9 @@
1
+ export interface SearchMatch {
2
+ entity: Record<string, unknown>;
3
+ score: number;
4
+ reasons: string[];
5
+ snippet?: string;
6
+ }
7
+ export declare function rankEntities(entities: Record<string, unknown>[], query: string, workspaceRoot: string): Promise<SearchMatch[]>;
8
+ export declare function loadMarkdownBody(source: string, workspaceRoot: string): Promise<string | null>;
9
+ //# sourceMappingURL=search-ranking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-ranking.d.ts","sourceRoot":"","sources":["../src/search-ranking.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAuBxB;AA2FD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2BxB"}
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ // implements REQ-mcp-search-discovery, REQ-002, REQ-003
5
+ export async function rankEntities(entities, query, workspaceRoot) {
6
+ const matches = [];
7
+ for (const entity of entities) {
8
+ const match = await rankEntity(entity, query, workspaceRoot);
9
+ if (match) {
10
+ matches.push(match);
11
+ }
12
+ }
13
+ matches.sort((left, right) => {
14
+ if (right.score !== left.score) {
15
+ return right.score - left.score;
16
+ }
17
+ const leftType = String(left.entity.type ?? "");
18
+ const rightType = String(right.entity.type ?? "");
19
+ if (leftType !== rightType) {
20
+ return leftType.localeCompare(rightType);
21
+ }
22
+ return String(left.entity.id ?? "").localeCompare(String(right.entity.id ?? ""));
23
+ });
24
+ return matches;
25
+ }
26
+ async function rankEntity(entity, query, workspaceRoot) {
27
+ const normalizedQuery = normalize(query);
28
+ const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
29
+ const reasons = [];
30
+ let score = 0;
31
+ const id = String(entity.id ?? "");
32
+ const title = String(entity.title ?? "");
33
+ const type = String(entity.type ?? "");
34
+ const source = String(entity.source ?? "");
35
+ const owner = String(entity.owner ?? "");
36
+ const priority = String(entity.priority ?? "");
37
+ const severity = String(entity.severity ?? "");
38
+ const tags = Array.isArray(entity.tags)
39
+ ? entity.tags.map((tag) => String(tag))
40
+ : [];
41
+ const normalizedTitle = normalize(title);
42
+ const normalizedId = normalize(id);
43
+ if (normalizedTitle === normalizedQuery) {
44
+ score += 100;
45
+ reasons.push("exact title match");
46
+ }
47
+ else if (normalizedTitle.includes(normalizedQuery)) {
48
+ score += 60;
49
+ reasons.push("title phrase match");
50
+ }
51
+ if (normalizedId === normalizedQuery) {
52
+ score += 90;
53
+ reasons.push("exact ID match");
54
+ }
55
+ else if (normalizedId.includes(normalizedQuery)) {
56
+ score += 55;
57
+ reasons.push("ID match");
58
+ }
59
+ const metadataFields = [type, source, owner, priority, severity];
60
+ const metadataMatched = metadataFields.some((field) => normalize(field).includes(normalizedQuery));
61
+ if (metadataMatched) {
62
+ score += 20;
63
+ reasons.push("metadata match");
64
+ }
65
+ const matchingTags = tags.filter((tag) => normalize(tag).includes(normalizedQuery));
66
+ if (matchingTags.length > 0) {
67
+ score += 30;
68
+ reasons.push("tag match");
69
+ }
70
+ const titleTokenMatches = countTokenMatches(normalizedTitle, tokens);
71
+ if (titleTokenMatches > 0) {
72
+ score += titleTokenMatches * 8;
73
+ reasons.push("title token coverage");
74
+ }
75
+ const bodyText = await loadMarkdownBody(source, workspaceRoot);
76
+ let snippet;
77
+ if (bodyText) {
78
+ const normalizedBody = normalize(bodyText);
79
+ if (normalizedBody.includes(normalizedQuery)) {
80
+ score += 15;
81
+ reasons.push("markdown body match");
82
+ snippet = buildSnippet(bodyText, query);
83
+ }
84
+ else {
85
+ const bodyTokenMatches = countTokenMatches(normalizedBody, tokens);
86
+ if (bodyTokenMatches > 0) {
87
+ score += bodyTokenMatches * 3;
88
+ reasons.push("markdown body token coverage");
89
+ snippet = buildSnippet(bodyText, query);
90
+ }
91
+ }
92
+ }
93
+ if (score === 0) {
94
+ return null;
95
+ }
96
+ return {
97
+ entity,
98
+ score,
99
+ reasons: Array.from(new Set(reasons)),
100
+ snippet,
101
+ };
102
+ }
103
+ export async function loadMarkdownBody(source, workspaceRoot) {
104
+ if (!source) {
105
+ return null;
106
+ }
107
+ const normalizedSource = source.split("#", 1)[0]?.trim() ?? "";
108
+ if (!normalizedSource.endsWith(".md")) {
109
+ return null;
110
+ }
111
+ // Resolve to absolute path; relative paths are resolved against workspaceRoot.
112
+ const resolved = path.resolve(path.isAbsolute(normalizedSource) ? normalizedSource : path.join(workspaceRoot, normalizedSource));
113
+ // Reject paths that escape the workspace root to prevent path traversal.
114
+ const normalizedRoot = path.resolve(workspaceRoot);
115
+ if (!resolved.startsWith(normalizedRoot + path.sep)) {
116
+ return null;
117
+ }
118
+ try {
119
+ const fileContent = await fs.readFile(resolved, "utf8");
120
+ return matter(fileContent).content;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ function normalize(value) {
127
+ return value.trim().toLowerCase();
128
+ }
129
+ function countTokenMatches(haystack, tokens) {
130
+ return tokens.filter((token) => haystack.includes(token)).length;
131
+ }
132
+ function buildSnippet(bodyText, query) {
133
+ const lines = bodyText
134
+ .split(/\r?\n/)
135
+ .map((line) => line.trim())
136
+ .filter(Boolean);
137
+ const normalizedQuery = normalize(query);
138
+ const matchedLine = lines.find((line) => normalize(line).includes(normalizedQuery)) ?? lines[0];
139
+ if (!matchedLine) {
140
+ return undefined;
141
+ }
142
+ return matchedLine.length > 160 ? `${matchedLine.slice(0, 157)}...` : matchedLine;
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -58,6 +58,10 @@
58
58
  "./diagnostics": {
59
59
  "types": "./dist/diagnostics.d.ts",
60
60
  "default": "./dist/diagnostics.js"
61
+ },
62
+ "./search-ranking": {
63
+ "types": "./dist/search-ranking.d.ts",
64
+ "default": "./dist/search-ranking.js"
61
65
  }
62
66
  },
63
67
  "types": "./dist/cli.d.ts",
@@ -68,7 +72,7 @@
68
72
  "fast-glob": "^3.2.12",
69
73
  "gray-matter": "^4.0.3",
70
74
  "js-yaml": "^4.1.0",
71
- "kibi-core": "^0.1.10",
75
+ "kibi-core": "^0.2.0",
72
76
  "ts-morph": "^23.0.0"
73
77
  },
74
78
  "devDependencies": {