kibi-cli 0.1.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.
Files changed (75) hide show
  1. package/bin/kibi +19 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +117 -0
  5. package/dist/commands/branch.d.ts +3 -0
  6. package/dist/commands/branch.d.ts.map +1 -0
  7. package/dist/commands/branch.js +66 -0
  8. package/dist/commands/check.d.ts +12 -0
  9. package/dist/commands/check.d.ts.map +1 -0
  10. package/dist/commands/check.js +439 -0
  11. package/dist/commands/doctor.d.ts +2 -0
  12. package/dist/commands/doctor.d.ts.map +1 -0
  13. package/dist/commands/doctor.js +268 -0
  14. package/dist/commands/gc.d.ts +6 -0
  15. package/dist/commands/gc.d.ts.map +1 -0
  16. package/dist/commands/gc.js +117 -0
  17. package/dist/commands/init-helpers.d.ts +8 -0
  18. package/dist/commands/init-helpers.d.ts.map +1 -0
  19. package/dist/commands/init-helpers.js +150 -0
  20. package/dist/commands/init.d.ts +6 -0
  21. package/dist/commands/init.d.ts.map +1 -0
  22. package/dist/commands/init.js +85 -0
  23. package/dist/commands/query.d.ts +12 -0
  24. package/dist/commands/query.d.ts.map +1 -0
  25. package/dist/commands/query.js +469 -0
  26. package/dist/commands/sync.d.ts +7 -0
  27. package/dist/commands/sync.d.ts.map +1 -0
  28. package/dist/commands/sync.js +587 -0
  29. package/dist/extractors/manifest.d.ts +30 -0
  30. package/dist/extractors/manifest.d.ts.map +1 -0
  31. package/dist/extractors/manifest.js +122 -0
  32. package/dist/extractors/markdown.d.ts +39 -0
  33. package/dist/extractors/markdown.d.ts.map +1 -0
  34. package/dist/extractors/markdown.js +203 -0
  35. package/dist/extractors/symbols-coordinator.d.ts +4 -0
  36. package/dist/extractors/symbols-coordinator.d.ts.map +1 -0
  37. package/dist/extractors/symbols-coordinator.js +131 -0
  38. package/dist/extractors/symbols-ts.d.ts +21 -0
  39. package/dist/extractors/symbols-ts.d.ts.map +1 -0
  40. package/dist/extractors/symbols-ts.js +197 -0
  41. package/dist/prolog.d.ts +35 -0
  42. package/dist/prolog.d.ts.map +1 -0
  43. package/dist/prolog.js +328 -0
  44. package/dist/public/extractors/symbols-coordinator.d.ts +2 -0
  45. package/dist/public/extractors/symbols-coordinator.d.ts.map +1 -0
  46. package/dist/public/extractors/symbols-coordinator.js +46 -0
  47. package/dist/public/prolog/index.d.ts +2 -0
  48. package/dist/public/prolog/index.d.ts.map +1 -0
  49. package/dist/public/prolog/index.js +46 -0
  50. package/dist/public/schemas/entity.d.ts +58 -0
  51. package/dist/public/schemas/entity.d.ts.map +1 -0
  52. package/dist/public/schemas/entity.js +102 -0
  53. package/dist/public/schemas/relationship.d.ts +35 -0
  54. package/dist/public/schemas/relationship.d.ts.map +1 -0
  55. package/dist/public/schemas/relationship.js +81 -0
  56. package/dist/types/changeset.d.ts +22 -0
  57. package/dist/types/changeset.d.ts.map +1 -0
  58. package/dist/types/changeset.js +18 -0
  59. package/dist/types/entities.d.ts +40 -0
  60. package/dist/types/entities.d.ts.map +1 -0
  61. package/dist/types/entities.js +18 -0
  62. package/dist/types/relationships.d.ts +11 -0
  63. package/dist/types/relationships.d.ts.map +1 -0
  64. package/dist/types/relationships.js +18 -0
  65. package/package.json +57 -0
  66. package/schema/entities.pl +50 -0
  67. package/schema/relationships.pl +47 -0
  68. package/schema/validation.pl +49 -0
  69. package/src/public/extractors/symbols-coordinator.ts +50 -0
  70. package/src/public/prolog/index.ts +47 -0
  71. package/src/public/schemas/entity.ts +104 -0
  72. package/src/public/schemas/relationship.ts +83 -0
  73. package/src/schemas/changeset.schema.json +48 -0
  74. package/src/schemas/entity.schema.json +55 -0
  75. package/src/schemas/relationship.schema.json +34 -0
@@ -0,0 +1,469 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ /*
19
+ How to apply this header to source files (examples)
20
+
21
+ 1) Prepend header to a single file (POSIX shells):
22
+
23
+ cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
24
+
25
+ 2) Apply to multiple files (example: the project's main entry files):
26
+
27
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
28
+ if [ -f "$f" ]; then
29
+ cp "$f" "$f".bak
30
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
31
+ fi
32
+ done
33
+
34
+ 3) Avoid duplicating the header: run a quick guard to only add if missing
35
+
36
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
37
+ if [ -f "$f" ]; then
38
+ if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
39
+ cp "$f" "$f".bak
40
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
41
+ fi
42
+ fi
43
+ done
44
+ */
45
+ import * as path from "node:path";
46
+ import Table from "cli-table3";
47
+ import { PrologProcess } from "../prolog.js";
48
+ export async function queryCommand(type, options) {
49
+ try {
50
+ const prolog = new PrologProcess();
51
+ await prolog.start();
52
+ await prolog.query("set_prolog_flag(answer_write_options, [max_depth(0), spacing(next_argument)])");
53
+ let currentBranch = "main";
54
+ try {
55
+ const { execSync } = await import("node:child_process");
56
+ currentBranch = execSync("git branch --show-current", {
57
+ cwd: process.cwd(),
58
+ encoding: "utf8",
59
+ }).trim();
60
+ if (!currentBranch || currentBranch === "master")
61
+ currentBranch = "main";
62
+ }
63
+ catch {
64
+ currentBranch = "main";
65
+ }
66
+ const kbPath = path.join(process.cwd(), `.kb/branches/${currentBranch}`);
67
+ const attachResult = await prolog.query(`kb_attach('${kbPath}')`);
68
+ if (!attachResult.success) {
69
+ await prolog.terminate();
70
+ console.error(`Error: Failed to attach KB: ${attachResult.error || "Unknown error"}`);
71
+ process.exit(1);
72
+ }
73
+ let results = [];
74
+ // Query relationships mode
75
+ if (options.relationships) {
76
+ const goal = `findall([Type,From,To], kb_relationship(Type, ${options.relationships}, To), Results)`;
77
+ const queryResult = await prolog.query(goal);
78
+ if (queryResult.success && queryResult.bindings.Results) {
79
+ const relationshipsData = parseListOfLists(queryResult.bindings.Results);
80
+ results = relationshipsData.map((rel) => ({
81
+ type: rel[0],
82
+ from: options.relationships,
83
+ to: rel[1],
84
+ }));
85
+ }
86
+ }
87
+ // Query entities mode
88
+ else if (type || options.source) {
89
+ // Validate type if provided
90
+ if (type) {
91
+ const validTypes = [
92
+ "req",
93
+ "scenario",
94
+ "test",
95
+ "adr",
96
+ "flag",
97
+ "event",
98
+ "symbol",
99
+ "fact",
100
+ ];
101
+ if (!validTypes.includes(type)) {
102
+ await prolog.query("kb_detach");
103
+ await prolog.terminate();
104
+ console.error(`Error: Invalid type '${type}'. Valid types: ${validTypes.join(", ")}`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+ let goal;
109
+ if (options.source) {
110
+ // Query by source path (substring match)
111
+ const safeSource = String(options.source).replace(/'/g, "\\'");
112
+ if (type) {
113
+ goal = `findall([Id,'${type}',Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, '${type}', Props)), Results)`;
114
+ }
115
+ else {
116
+ goal = `findall([Id,Type,Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, Type, Props)), Results)`;
117
+ }
118
+ }
119
+ else if (options.id) {
120
+ const safeId = String(options.id).replace(/'/g, "''");
121
+ goal = `kb_entity('${safeId}', '${type}', Props), Id = '${safeId}', Type = '${type}', Result = [Id, Type, Props]`;
122
+ }
123
+ else if (options.tag) {
124
+ const safeTag = String(options.tag).replace(/'/g, "''");
125
+ goal = `findall([Id,'${type}',Props], (kb_entity(Id, '${type}', Props), memberchk(tags=Tags, Props), member('${safeTag}', Tags)), Results)`;
126
+ }
127
+ else {
128
+ goal = `findall([Id,'${type}',Props], kb_entity(Id, '${type}', Props), Results)`;
129
+ }
130
+ const queryResult = await prolog.query(goal);
131
+ if (queryResult.success) {
132
+ if (options.id) {
133
+ // Single entity query
134
+ if (queryResult.bindings.Result) {
135
+ const entity = parseEntityFromBinding(queryResult.bindings.Result);
136
+ results = [entity];
137
+ }
138
+ }
139
+ else {
140
+ // Multiple entities query
141
+ if (queryResult.bindings.Results) {
142
+ const entitiesData = parseListOfLists(queryResult.bindings.Results);
143
+ for (const data of entitiesData) {
144
+ const entity = parseEntityFromList(data);
145
+ results.push(entity);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ else {
152
+ await prolog.query("kb_detach");
153
+ await prolog.terminate();
154
+ console.error("Error: Must specify entity type, --source, or --relationships option");
155
+ process.exit(1);
156
+ }
157
+ await prolog.query("kb_detach");
158
+ await prolog.terminate();
159
+ // Apply pagination
160
+ const limit = Number.parseInt(options.limit || "100");
161
+ const offset = Number.parseInt(options.offset || "0");
162
+ const paginated = results.slice(offset, offset + limit);
163
+ if (!paginated || paginated.length === 0) {
164
+ if (options.format === "json") {
165
+ console.log("[]");
166
+ }
167
+ else {
168
+ console.log("No entities found");
169
+ }
170
+ process.exit(0);
171
+ }
172
+ // Format output
173
+ if (options.format === "table") {
174
+ outputTable(paginated, Boolean(options.relationships));
175
+ }
176
+ else {
177
+ console.log(JSON.stringify(paginated, null, 2));
178
+ }
179
+ process.exit(0);
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ console.error(`Error: ${message}`);
184
+ process.exit(1);
185
+ }
186
+ }
187
+ /**
188
+ * Parse a Prolog list of lists into a JavaScript array.
189
+ * Input: "[[a,b,c],[d,e,f]]"
190
+ * Output: [["a", "b", "c"], ["d", "e", "f"]]
191
+ */
192
+ function parseListOfLists(listStr) {
193
+ // Clean input
194
+ const cleaned = listStr.trim().replace(/^\[/, "").replace(/\]$/, "");
195
+ if (cleaned === "") {
196
+ return [];
197
+ }
198
+ const results = [];
199
+ let depth = 0;
200
+ let inQuotes = false;
201
+ let current = "";
202
+ let currentList = [];
203
+ for (let i = 0; i < cleaned.length; i++) {
204
+ const char = cleaned[i];
205
+ const prevChar = i > 0 ? cleaned[i - 1] : "";
206
+ if (char === '"' && prevChar !== "\\") {
207
+ inQuotes = !inQuotes;
208
+ current += char;
209
+ continue;
210
+ }
211
+ if (inQuotes) {
212
+ current += char;
213
+ continue;
214
+ }
215
+ if (char === "[") {
216
+ depth++;
217
+ if (depth > 1)
218
+ current += char;
219
+ }
220
+ else if (char === "]") {
221
+ depth--;
222
+ if (depth === 0) {
223
+ if (current) {
224
+ currentList.push(current.trim());
225
+ current = "";
226
+ }
227
+ if (currentList.length > 0) {
228
+ results.push(currentList);
229
+ currentList = [];
230
+ }
231
+ }
232
+ else {
233
+ current += char;
234
+ }
235
+ }
236
+ else if (char === "," && depth === 1) {
237
+ if (current) {
238
+ currentList.push(current.trim());
239
+ current = "";
240
+ }
241
+ }
242
+ else if (char === "," && depth === 0) {
243
+ // Skip comma between lists
244
+ }
245
+ else {
246
+ current += char;
247
+ }
248
+ }
249
+ return results;
250
+ }
251
+ /**
252
+ * Parse a single entity from Prolog binding format.
253
+ * Input: "[abc123, req, [id=abc123, title=\"Test\", ...]]"
254
+ */
255
+ function parseEntityFromBinding(bindingStr) {
256
+ const cleaned = bindingStr.trim().replace(/^\[/, "").replace(/\]$/, "");
257
+ const parts = splitTopLevel(cleaned, ",");
258
+ if (parts.length < 3) {
259
+ return {};
260
+ }
261
+ const id = parts[0].trim();
262
+ const type = parts[1].trim();
263
+ const propsStr = parts.slice(2).join(",").trim();
264
+ const props = parsePropertyList(propsStr);
265
+ return { id, type, ...props };
266
+ }
267
+ /**
268
+ * Parse entity from array returned by parseListOfLists.
269
+ * Input: ["abc123", "req", "[id=abc123, title=\"Test\", ...]"]
270
+ */
271
+ function parseEntityFromList(data) {
272
+ if (data.length < 3) {
273
+ return {};
274
+ }
275
+ const id = data[0].trim();
276
+ const type = data[1].trim();
277
+ const propsStr = data[2].trim();
278
+ const props = parsePropertyList(propsStr);
279
+ return { id, type, ...props };
280
+ }
281
+ /**
282
+ * Parse Prolog property list into JavaScript object.
283
+ * Input: "[id=abc123, title=^^(\"User Auth\", xsd:string), status='file:///path/approved', tags=^^(\"[security,auth]\", xsd:string)]"
284
+ * Output: { id: "abc123", title: "User Auth", status: "approved", tags: ["security", "auth"] }
285
+ */
286
+ function parsePropertyList(propsStr) {
287
+ const props = {};
288
+ // Remove outer brackets
289
+ let cleaned = propsStr.trim();
290
+ if (cleaned.startsWith("[")) {
291
+ cleaned = cleaned.substring(1);
292
+ }
293
+ if (cleaned.endsWith("]")) {
294
+ cleaned = cleaned.substring(0, cleaned.length - 1);
295
+ }
296
+ // Split by top-level commas
297
+ const pairs = splitTopLevel(cleaned, ",");
298
+ for (const pair of pairs) {
299
+ const eqIndex = pair.indexOf("=");
300
+ if (eqIndex === -1)
301
+ continue;
302
+ const key = pair.substring(0, eqIndex).trim();
303
+ const value = pair.substring(eqIndex + 1).trim();
304
+ if (key === "..." || value === "..." || value === "...|...") {
305
+ continue;
306
+ }
307
+ const parsed = parsePrologValue(value);
308
+ props[key] = parsed;
309
+ }
310
+ return props;
311
+ }
312
+ /**
313
+ * Parse a single Prolog value, handling typed literals and URIs.
314
+ * Examples:
315
+ * - ^^("value", 'http://...#string') -> "value"
316
+ * - 'file:///path/to/id' -> "id" (extract last segment)
317
+ * - "string" -> "string"
318
+ * - atom -> "atom"
319
+ * - [a,b,c] -> ["a", "b", "c"]
320
+ */
321
+ function parsePrologValue(value) {
322
+ value = value.trim();
323
+ if (value.startsWith("^^(")) {
324
+ const innerStart = value.indexOf("(") + 1;
325
+ let depth = 1;
326
+ let innerEnd = innerStart;
327
+ for (let i = innerStart; i < value.length; i++) {
328
+ if (value[i] === "(")
329
+ depth++;
330
+ if (value[i] === ")") {
331
+ depth--;
332
+ if (depth === 0) {
333
+ innerEnd = i;
334
+ break;
335
+ }
336
+ }
337
+ }
338
+ const innerContent = value.substring(innerStart, innerEnd);
339
+ const parts = splitTopLevel(innerContent, ",");
340
+ if (parts.length >= 2) {
341
+ let literalValue = parts[0].trim();
342
+ if (literalValue.startsWith('"') && literalValue.endsWith('"')) {
343
+ literalValue = literalValue.substring(1, literalValue.length - 1);
344
+ }
345
+ if (literalValue.startsWith("[") && literalValue.endsWith("]")) {
346
+ const listContent = literalValue.substring(1, literalValue.length - 1);
347
+ if (listContent === "") {
348
+ return [];
349
+ }
350
+ return listContent.split(",").map((item) => item.trim());
351
+ }
352
+ return literalValue;
353
+ }
354
+ }
355
+ if (value.startsWith("file:///")) {
356
+ const cleaned = value;
357
+ const lastSlash = cleaned.lastIndexOf("/");
358
+ if (lastSlash !== -1) {
359
+ return cleaned.substring(lastSlash + 1);
360
+ }
361
+ return cleaned;
362
+ }
363
+ // Handle quoted string
364
+ if (value.startsWith('"') && value.endsWith('"')) {
365
+ return value.substring(1, value.length - 1);
366
+ }
367
+ // Handle quoted atom (may contain file URLs that need extraction)
368
+ if (value.startsWith("'") && value.endsWith("'")) {
369
+ const unquoted = value.substring(1, value.length - 1);
370
+ // Check if unquoted value is a file URL
371
+ if (unquoted.startsWith("file:///")) {
372
+ const lastSlash = unquoted.lastIndexOf("/");
373
+ if (lastSlash !== -1) {
374
+ return unquoted.substring(lastSlash + 1);
375
+ }
376
+ }
377
+ return unquoted;
378
+ }
379
+ // Handle list
380
+ if (value.startsWith("[") && value.endsWith("]")) {
381
+ const listContent = value.substring(1, value.length - 1);
382
+ if (listContent === "") {
383
+ return [];
384
+ }
385
+ const items = listContent.split(",").map((item) => {
386
+ return parsePrologValue(item.trim());
387
+ });
388
+ return items;
389
+ }
390
+ // Return as-is
391
+ return value;
392
+ }
393
+ /**
394
+ * Split a string by delimiter at the top level (not inside brackets or quotes).
395
+ */
396
+ function splitTopLevel(str, delimiter) {
397
+ const results = [];
398
+ let current = "";
399
+ let depth = 0;
400
+ let inQuotes = false;
401
+ for (let i = 0; i < str.length; i++) {
402
+ const char = str[i];
403
+ const prevChar = i > 0 ? str[i - 1] : "";
404
+ if (char === '"' && prevChar !== "\\") {
405
+ inQuotes = !inQuotes;
406
+ current += char;
407
+ }
408
+ else if (!inQuotes && (char === "[" || char === "(")) {
409
+ depth++;
410
+ current += char;
411
+ }
412
+ else if (!inQuotes && (char === "]" || char === ")")) {
413
+ depth--;
414
+ current += char;
415
+ }
416
+ else if (!inQuotes && depth === 0 && char === delimiter) {
417
+ if (current) {
418
+ results.push(current);
419
+ current = "";
420
+ }
421
+ }
422
+ else {
423
+ current += char;
424
+ }
425
+ }
426
+ if (current) {
427
+ results.push(current);
428
+ }
429
+ return results;
430
+ }
431
+ /**
432
+ * Output results as a formatted table.
433
+ */
434
+ function outputTable(items, isRelationships) {
435
+ if (items.length === 0) {
436
+ console.log("No entities found.");
437
+ return;
438
+ }
439
+ if (isRelationships) {
440
+ const table = new Table({
441
+ head: ["Type", "From", "To"],
442
+ colWidths: [20, 18, 18],
443
+ });
444
+ for (const item of items) {
445
+ table.push([
446
+ item.type || "N/A",
447
+ item.from?.substring(0, 16) || "N/A",
448
+ item.to?.substring(0, 16) || "N/A",
449
+ ]);
450
+ }
451
+ console.log(table.toString());
452
+ }
453
+ else {
454
+ const table = new Table({
455
+ head: ["ID", "Type", "Title", "Status", "Tags"],
456
+ colWidths: [18, 10, 40, 12, 30],
457
+ });
458
+ for (const entity of items) {
459
+ table.push([
460
+ entity.id?.substring(0, 16) || "N/A",
461
+ entity.type || "N/A",
462
+ (entity.title || "N/A").substring(0, 38),
463
+ entity.status || "N/A",
464
+ (entity.tags || []).join(", ").substring(0, 28) || "",
465
+ ]);
466
+ }
467
+ console.log(table.toString());
468
+ }
469
+ }
@@ -0,0 +1,7 @@
1
+ export declare class SyncError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare function syncCommand(options?: {
5
+ validateOnly?: boolean;
6
+ }): Promise<void>;
7
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAgEA,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AA6FD,wBAAsB,WAAW,CAC/B,OAAO,GAAE;IACP,YAAY,CAAC,EAAE,OAAO,CAAC;CACnB,GACL,OAAO,CAAC,IAAI,CAAC,CAkaf"}