cngkit 1.0.0 → 1.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.
package/README.md CHANGED
@@ -1,18 +1,27 @@
1
1
  # cngkit
2
2
 
3
- Opinionated Curly.ng CLI kit for repo sync and website tooling.
3
+ Opinionated Curly.ng CLI kit for repo sync, Harness knowledges, and website tooling.
4
4
 
5
5
  ```bash
6
6
  npx cngkit
7
7
  ```
8
8
 
9
- The first MVP provides a real-time repository sync room:
9
+ The CLI provides a real-time repository sync room and a terminal front door to
10
+ the Cloudflare-backed Harness knowledges catalog:
10
11
 
11
12
  - `cngkit share` starts a room from the current directory.
12
13
  - `cngkit join <room-code>` joins another machine to that room.
13
14
  - `cngkit login` opens Curly.ng login in a browser, or prints the URL in headless environments.
14
15
  - `cngkit scrub [path]` scans a file or directory with TruffleHog and prints a redacted report.
15
16
  - `cngkit scrub [path] --yes` rewrites detected secret values inline with `CNGKIT_SECRET` placeholders.
17
+ - `cngkit knowledges status` prints the remote catalog state.
18
+ - `cngkit knowledges audiences` lists available audience filters.
19
+ - `cngkit knowledges search <query>` runs semantic search against Cloudflare Vectorize.
20
+ - `cngkit knowledges list [query]` lists known subskills.
21
+ - `cngkit knowledges files [query]` lists uploaded catalog files.
22
+ - `cngkit knowledges read <file-path>` reads a catalog file excerpt.
23
+ - `cngkit knowledges grep <pattern>` searches catalog file contents.
24
+ - `cngkit knowledges glob [pattern]` lists catalog files by supported glob pattern.
16
25
  - `.git/` and files ignored by the repo's `.gitignore` are not synced.
17
26
  - Later changes override earlier changes for the MVP conflict rule.
18
27
 
@@ -24,6 +33,25 @@ brew install trufflehog
24
33
 
25
34
  Inline masking is intentionally gated behind `--yes` because it rewrites files in place.
26
35
 
36
+ ## Harness Knowledges
37
+
38
+ The `knowledges` command group reads the Cloudflare-backed Harness catalog from the
39
+ Curly API. These commands are read-only:
40
+
41
+ ```bash
42
+ cngkit knowledges status
43
+ cngkit knowledges audiences
44
+ cngkit knowledges search "cloudflare backend" --limit 3
45
+ cngkit knowledges files "vector search" --audience builders
46
+ cngkit knowledges read /libraries/lib-cloudflare/SUBSKILL.md --limit 80
47
+ cngkit knowledges grep Cloudflare --path /libraries/lib-cloudflare --output-mode files_with_matches
48
+ cngkit knowledges glob "**/*.md" --path /libraries/lib-cloudflare
49
+ ```
50
+
51
+ Add `--json` to any `knowledges` command when another tool should consume the
52
+ raw API response. `grep` also accepts `--output-mode <content|files_with_matches|count>`,
53
+ `--include <glob>`, `--context <n>`, and `--case-insensitive`.
54
+
27
55
  This package is mostly personal tooling for Curly.ng. It is intentionally opinionated,
28
56
  small, and practical rather than a general synchronization platform.
29
57
 
package/dist/cli.cjs CHANGED
@@ -7443,7 +7443,7 @@ var logging;
7443
7443
  // src/config.ts
7444
7444
  var import_node_crypto = require("crypto");
7445
7445
  var import_node_process = __toESM(require("process"), 1);
7446
- var packageVersion = "1.0.0";
7446
+ var packageVersion = "1.1.0";
7447
7447
  var defaultApiBaseUrl = "https://curly.ng";
7448
7448
  function resolveApiBaseUrl(options) {
7449
7449
  return options.apiBaseUrl ?? import_node_process.default.env.CNGKIT_API_BASE_URL ?? defaultApiBaseUrl;
@@ -24296,6 +24296,31 @@ async function waitForSessionClose(socket, watcher) {
24296
24296
  }
24297
24297
 
24298
24298
  // src/commands.ts
24299
+ async function runKnowledgesCommand(args, options, output, dependencies) {
24300
+ const [subcommand, ...subcommandArgs] = args ?? [];
24301
+ switch (subcommand) {
24302
+ case "status":
24303
+ return runKnowStatusCommand(options, output, dependencies);
24304
+ case "audiences":
24305
+ return runKnowAudiencesCommand(options, output, dependencies);
24306
+ case "search":
24307
+ return runKnowSearchCommand(optionalJoinedArgument(subcommandArgs), options, output, dependencies);
24308
+ case "list":
24309
+ return runKnowListCommand(optionalJoinedArgument(subcommandArgs), options, output, dependencies);
24310
+ case "files":
24311
+ return runKnowFilesCommand(optionalJoinedArgument(subcommandArgs), options, output, dependencies);
24312
+ case "read":
24313
+ return runKnowReadCommand(subcommandArgs[0], options, output, dependencies);
24314
+ case "grep":
24315
+ return runKnowGrepCommand(optionalJoinedArgument(subcommandArgs), options, output, dependencies);
24316
+ case "glob":
24317
+ return runKnowGlobCommand(optionalJoinedArgument(subcommandArgs), options, output, dependencies);
24318
+ default:
24319
+ throw new Error(
24320
+ "Missing knowledges command. Usage: cngkit knowledges <status|audiences|search|list|files|read|grep|glob>"
24321
+ );
24322
+ }
24323
+ }
24299
24324
  async function runLoginCommand(options, output) {
24300
24325
  const loginUrl = new URL("/login", resolveApiBaseUrl(options));
24301
24326
  const loginUrlString = loginUrl.toString();
@@ -24344,6 +24369,172 @@ async function runScrubCommand(targetPath, options, output, dependencies) {
24344
24369
  output.info(`Skipped ${result.skippedFindings} finding(s) outside the scrub root.`);
24345
24370
  }
24346
24371
  }
24372
+ async function runKnowStatusCommand(options, output, dependencies) {
24373
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24374
+ const response = await api.getCatalog();
24375
+ if (options.json) {
24376
+ output.info(formatJson(response.data));
24377
+ return;
24378
+ }
24379
+ const latestRun = response.data.latestRun;
24380
+ const lines = [
24381
+ `Catalog: ${response.data.name}`,
24382
+ `Files: ${response.data.files}`,
24383
+ `Blobs: ${response.data.blobs}`,
24384
+ `Vectorize: ${response.data.vectorize.index} (${response.data.vectorize.binding})`
24385
+ ];
24386
+ if (latestRun) {
24387
+ lines.push(`Latest run: ${latestRun.status} (${latestRun.id})`);
24388
+ if (latestRun.updated_at) {
24389
+ lines.push(`Updated: ${latestRun.updated_at}`);
24390
+ }
24391
+ } else {
24392
+ lines.push("Latest run: none");
24393
+ }
24394
+ output.info(lines.join("\n"));
24395
+ }
24396
+ async function runKnowAudiencesCommand(options, output, dependencies) {
24397
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24398
+ const response = await api.listAudiences();
24399
+ if (options.json) {
24400
+ output.info(formatJson(response.data));
24401
+ return;
24402
+ }
24403
+ const lines = response.data.audiences.flatMap((audience) => [
24404
+ `${audience.id} - ${audience.label}`,
24405
+ ` ${singleLine(audience.help)}`
24406
+ ]);
24407
+ output.info(lines.length > 0 ? lines.join("\n") : "No audiences available.");
24408
+ }
24409
+ async function runKnowSearchCommand(query, options, output, dependencies) {
24410
+ if (!query) {
24411
+ throw new Error("Missing search query. Usage: cngkit knowledges search <query>");
24412
+ }
24413
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24414
+ const response = await api.search(query, coerceLimit(options.limit, 5));
24415
+ if (options.json) {
24416
+ output.info(formatJson(response.data));
24417
+ return;
24418
+ }
24419
+ const lines = response.data.results.flatMap((result, index) => [
24420
+ `${index + 1}. ${result.title} (${result.subskillName})`,
24421
+ ` score ${result.score.toFixed(3)} | ${result.path}`,
24422
+ ` ${singleLine(result.description || result.contentPreview)}`
24423
+ ]);
24424
+ output.info(lines.length > 0 ? lines.join("\n") : `No results for "${query}".`);
24425
+ }
24426
+ async function runKnowListCommand(query, options, output, dependencies) {
24427
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24428
+ const response = await api.listSubskills();
24429
+ const limit = coerceLimit(options.limit, 25);
24430
+ const normalizedQuery = query?.toLowerCase();
24431
+ const subskills = response.data.subskills.filter((subskill) => {
24432
+ if (!normalizedQuery) {
24433
+ return true;
24434
+ }
24435
+ return [subskill.name, subskill.title, subskill.description, subskill.type].join(" ").toLowerCase().includes(normalizedQuery);
24436
+ }).slice(0, limit);
24437
+ if (options.json) {
24438
+ output.info(formatJson({ subskills, total: subskills.length }));
24439
+ return;
24440
+ }
24441
+ const lines = subskills.flatMap((subskill) => [
24442
+ `${subskill.name} [${subskill.type}]`,
24443
+ ` ${subskill.title} | files ${subskill.fileCount} | rating ${subskill.rating}`,
24444
+ ` ${singleLine(subskill.description)}`
24445
+ ]);
24446
+ output.info(lines.length > 0 ? lines.join("\n") : "No matching subskills.");
24447
+ }
24448
+ async function runKnowFilesCommand(query, options, output, dependencies) {
24449
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24450
+ const response = await api.listFiles({
24451
+ query,
24452
+ audience: normalizeAudienceId(options.audience),
24453
+ limit: coerceLimit(options.limit, 25)
24454
+ });
24455
+ if (options.json) {
24456
+ output.info(formatJson(response.data));
24457
+ return;
24458
+ }
24459
+ const lines = response.data.files.map((file2) => {
24460
+ const title = file2.display_title ?? file2.title ?? file2.subskill_name ?? "Untitled";
24461
+ return `${file2.path}
24462
+ ${title}`;
24463
+ });
24464
+ output.info(lines.length > 0 ? lines.join("\n") : "No matching files.");
24465
+ }
24466
+ async function runKnowReadCommand(filePath, options, output, dependencies) {
24467
+ if (!filePath) {
24468
+ throw new Error("Missing file_path. Usage: cngkit knowledges read <file_path>");
24469
+ }
24470
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24471
+ const response = await api.read({
24472
+ filePath: normalizeCatalogPath(filePath),
24473
+ offset: coerceOptionalNumber(options.offset),
24474
+ limit: coerceLimit(options.limit, 200, 2e3)
24475
+ });
24476
+ if (options.json) {
24477
+ output.info(formatJson(response.data));
24478
+ return;
24479
+ }
24480
+ output.info(response.data.content);
24481
+ if (response.data.truncated) {
24482
+ output.info(
24483
+ `[truncated: showing ${response.data.limit} lines from offset ${response.data.offset} of ${response.data.total_lines}]`
24484
+ );
24485
+ }
24486
+ }
24487
+ async function runKnowGrepCommand(pattern, options, output, dependencies) {
24488
+ if (!pattern) {
24489
+ throw new Error("Missing pattern. Usage: cngkit knowledges grep <pattern>");
24490
+ }
24491
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24492
+ const response = await api.grep({
24493
+ pattern,
24494
+ path: normalizeCatalogPath(options.path ?? "/"),
24495
+ include: options.include ?? "*",
24496
+ mode: normalizeGrepMode(options.outputMode),
24497
+ context: coerceLimit(options.context, 0, 20),
24498
+ ignoreCase: options.caseInsensitive === true
24499
+ });
24500
+ if (options.json) {
24501
+ output.info(formatJson(response.data));
24502
+ return;
24503
+ }
24504
+ if (response.data.mode === "files_with_matches") {
24505
+ output.info(
24506
+ response.data.files.length > 0 ? response.data.files.join("\n") : `No files matched "${pattern}".`
24507
+ );
24508
+ return;
24509
+ }
24510
+ if (response.data.mode === "count") {
24511
+ const lines2 = response.data.counts.map((count) => `${count.file_path}: ${count.match_count}`);
24512
+ output.info(lines2.length > 0 ? lines2.join("\n") : `No matches for "${pattern}".`);
24513
+ return;
24514
+ }
24515
+ const lines = response.data.matches.flatMap((match) => [
24516
+ `${match.file_path}:${match.line_number}`,
24517
+ ...match.context_before.map((line) => ` ${line}`),
24518
+ `> ${match.line}`,
24519
+ ...match.context_after.map((line) => ` ${line}`)
24520
+ ]);
24521
+ output.info(lines.length > 0 ? lines.join("\n") : `No matches for "${pattern}".`);
24522
+ }
24523
+ async function runKnowGlobCommand(pattern, options, output, dependencies) {
24524
+ const api = dependencies?.api ?? createKnowledgesApi(options);
24525
+ const response = await api.glob({
24526
+ pattern: pattern ?? "**/*.md",
24527
+ path: normalizeCatalogPath(options.path ?? "/")
24528
+ });
24529
+ if (options.json) {
24530
+ output.info(formatJson(response.data));
24531
+ return;
24532
+ }
24533
+ output.info(response.data.files.length > 0 ? response.data.files.join("\n") : "No matching files.");
24534
+ if (response.data.truncated) {
24535
+ output.info(`[truncated: ${response.data.total_files} total files]`);
24536
+ }
24537
+ }
24347
24538
  async function printBackendStatus(options, output) {
24348
24539
  const health = await readBackendHealth(options);
24349
24540
  output.info(health.ok ? `API: ${health.service} ready` : `API: unavailable (${health.message})`);
@@ -24366,6 +24557,121 @@ function formatScrubReport(findings) {
24366
24557
  }
24367
24558
  return lines.join("\n");
24368
24559
  }
24560
+ function createKnowledgesApi(options) {
24561
+ const client = createCngApiClient(options);
24562
+ return {
24563
+ getCatalog: () => client.harnessKnowledges.getHarnessKnowledgesCatalog(),
24564
+ listAudiences: () => client.harnessKnowledges.listHarnessKnowledgesAudiences(),
24565
+ search: (query, limit) => client.harnessKnowledges.searchHarnessKnowledges({
24566
+ q: query,
24567
+ limit
24568
+ }),
24569
+ listSubskills: () => client.harnessKnowledges.listHarnessKnowledgesSubskills(),
24570
+ listFiles: ({ query, audience, limit }) => client.harnessKnowledges.listHarnessSubskillAssets({
24571
+ ...query ? { q: query } : {},
24572
+ ...audience ? { audience } : {},
24573
+ limit
24574
+ }),
24575
+ read: ({ filePath, offset, limit }) => client.harnessFilesystem.getHarnessFilesystemRead({
24576
+ file_path: filePath,
24577
+ offset,
24578
+ limit
24579
+ }),
24580
+ grep: ({ pattern, path: path5, include, mode, context, ignoreCase }) => client.harnessFilesystem.getHarnessFilesystemGrep({
24581
+ pattern,
24582
+ path: path5,
24583
+ include,
24584
+ output_mode: mode,
24585
+ context,
24586
+ case_insensitive: ignoreCase ? "true" : void 0
24587
+ }),
24588
+ glob: ({ pattern, path: path5 }) => client.harnessFilesystem.getHarnessFilesystemGlob({
24589
+ pattern,
24590
+ path: path5
24591
+ })
24592
+ };
24593
+ }
24594
+ function coerceLimit(value, defaultValue, maxValue = 100) {
24595
+ const normalizedValue = coerceOptionalNumber(value);
24596
+ if (normalizedValue === void 0 || Number.isNaN(normalizedValue)) {
24597
+ return defaultValue;
24598
+ }
24599
+ return Math.max(defaultValue === 0 ? 0 : 1, Math.min(maxValue, Math.trunc(normalizedValue)));
24600
+ }
24601
+ function coerceOptionalNumber(value) {
24602
+ if (value === void 0 || value === "") {
24603
+ return void 0;
24604
+ }
24605
+ return Number(value);
24606
+ }
24607
+ function normalizeAudienceId(value) {
24608
+ if (value === void 0) {
24609
+ return void 0;
24610
+ }
24611
+ switch (value) {
24612
+ case "all":
24613
+ case "operators":
24614
+ case "builders":
24615
+ case "researchers":
24616
+ case "agent-makers":
24617
+ return value;
24618
+ default:
24619
+ throw new Error(
24620
+ `Unknown audience "${value}". Run cngkit knowledges audiences to see supported values.`
24621
+ );
24622
+ }
24623
+ }
24624
+ function normalizeGrepMode(value) {
24625
+ if (value === void 0) {
24626
+ return "content";
24627
+ }
24628
+ switch (value) {
24629
+ case "content":
24630
+ case "files_with_matches":
24631
+ case "count":
24632
+ return value;
24633
+ default:
24634
+ throw new Error("Unknown grep mode. Use one of: content, files_with_matches, count.");
24635
+ }
24636
+ }
24637
+ function singleLine(value) {
24638
+ return value.replace(/\s+/g, " ").trim();
24639
+ }
24640
+ function formatJson(value) {
24641
+ return JSON.stringify(value, null, 2);
24642
+ }
24643
+ function optionalJoinedArgument(values) {
24644
+ return values.length > 0 ? values.join(" ") : void 0;
24645
+ }
24646
+ function normalizeCatalogPath(value) {
24647
+ const trimmed = value.trim();
24648
+ if (trimmed === "/" || trimmed === "") {
24649
+ return "/";
24650
+ }
24651
+ const relativePath = trimmed.replace(/^\/+/, "");
24652
+ if (relativePath.startsWith("skills/knowledges/")) {
24653
+ return relativePath;
24654
+ }
24655
+ const [firstSegment, ...restSegments] = relativePath.split("/");
24656
+ if (!firstSegment || restSegments.length === 0) {
24657
+ return relativePath;
24658
+ }
24659
+ switch (firstSegment) {
24660
+ case "concepts":
24661
+ case "domains":
24662
+ case "formats":
24663
+ case "languages":
24664
+ case "libraries":
24665
+ case "patterns":
24666
+ case "platforms":
24667
+ case "procedures":
24668
+ case "protocols":
24669
+ case "tools":
24670
+ return `skills/knowledges/subskills/${firstSegment}/${restSegments.join("/")}`;
24671
+ default:
24672
+ return relativePath;
24673
+ }
24674
+ }
24369
24675
 
24370
24676
  // src/output.ts
24371
24677
  var consoleOutput = {
@@ -24402,6 +24708,9 @@ cli.command("join <room-code>", "Join a real-time repo sync room in the current
24402
24708
  cli.command("scrub [path]", "Scan for secrets with TruffleHog and optionally mask them inline").option("--mask", "Compatibility alias for inline scrubbing; --yes is still required").option("--yes", "Confirm and run inline scrubbing").action((targetPath, options) => {
24403
24709
  void runScrubCommand(targetPath, options, consoleOutput).catch(handleFatalError);
24404
24710
  });
24711
+ cli.command("knowledges [...args]", "Use the Cloudflare-backed Harness knowledges catalog").option("--limit <n>", "Maximum results").option("--audience <id>", "Audience filter").option("--offset <n>", "Starting line offset").option("--path <path>", "Catalog path prefix").option("--include <glob>", "Filename include filter").option("--output-mode <mode>", "Output mode: content, files_with_matches, count").option("--context <n>", "Context lines around content matches").option("--case-insensitive", "Case-insensitive search").option("--json", "Print raw JSON").action((args, options) => {
24712
+ void runKnowledgesCommand(args, options, consoleOutput).catch(handleFatalError);
24713
+ });
24405
24714
  try {
24406
24715
  cli.parse(import_node_process5.default.argv);
24407
24716
  } catch (error51) {