@gobing-ai/knowledge-kit 0.0.3 → 0.0.5

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/index.js CHANGED
@@ -14308,12 +14308,6 @@ var logger = {
14308
14308
  Bun.write(Bun.stdout, encoder.encode(text));
14309
14309
  }
14310
14310
  };
14311
-
14312
- // ../../packages/utils/src/index.ts
14313
- function add(a, b) {
14314
- return a + b;
14315
- }
14316
-
14317
14311
  // ../../node_modules/.bun/commander@15.0.0/node_modules/commander/lib/error.js
14318
14312
  class CommanderError extends Error {
14319
14313
  constructor(exitCode, code, message) {
@@ -16358,15 +16352,6 @@ function useColor() {
16358
16352
  // ../../node_modules/.bun/commander@15.0.0/node_modules/commander/index.js
16359
16353
  var program = new Command;
16360
16354
 
16361
- // src/commands/add.ts
16362
- function registerAddCommand(program2) {
16363
- const parseIntArg = (v) => Number.parseInt(v, 10);
16364
- program2.command("add").description("Add two numbers").argument("<a>", "first number", parseIntArg).argument("<b>", "second number", parseIntArg).action((a, b) => {
16365
- logger.out(`${add(a, b)}
16366
- `);
16367
- });
16368
- }
16369
-
16370
16355
  // src/fanin.ts
16371
16356
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
16372
16357
  import { resolve as resolve2 } from "path";
@@ -16441,9 +16426,11 @@ var PluginManifestSchema = exports_external.object({
16441
16426
  description: exports_external.string().optional()
16442
16427
  }).passthrough();
16443
16428
  // ../../packages/kk-core/src/registry.ts
16444
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
16445
- import { basename, join, resolve } from "path";
16446
- function discoverPlugins(pluginsRoot) {
16429
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
16430
+ import { homedir } from "os";
16431
+ import { basename, dirname, join, resolve } from "path";
16432
+ import { fileURLToPath } from "url";
16433
+ function discoverPlugins(pluginsRoot, origin = "path") {
16447
16434
  const absRoot = resolve(pluginsRoot);
16448
16435
  const plugins = [];
16449
16436
  const diagnostics = [];
@@ -16454,7 +16441,7 @@ function discoverPlugins(pluginsRoot) {
16454
16441
  }
16455
16442
  const rootAsCollection = kindForCollection(basename(absRoot));
16456
16443
  if (rootAsCollection) {
16457
- scanCollection(absRoot, rootAsCollection, plugins, diagnostics, seenNames);
16444
+ scanCollection(absRoot, rootAsCollection, origin, plugins, diagnostics, seenNames);
16458
16445
  return { plugins, diagnostics };
16459
16446
  }
16460
16447
  for (const entryName of entries.names) {
@@ -16467,7 +16454,7 @@ function discoverPlugins(pluginsRoot) {
16467
16454
  }
16468
16455
  const expectedKind = kindForCollection(entryName);
16469
16456
  if (expectedKind) {
16470
- scanCollection(dirPath, expectedKind, plugins, diagnostics, seenNames);
16457
+ scanCollection(dirPath, expectedKind, origin, plugins, diagnostics, seenNames);
16471
16458
  continue;
16472
16459
  }
16473
16460
  const strayManifest = join(dirPath, "plugin.json");
@@ -16481,7 +16468,117 @@ function discoverPlugins(pluginsRoot) {
16481
16468
  }
16482
16469
  return { plugins, diagnostics };
16483
16470
  }
16484
- function scanCollection(collectionDir, expectedKind, plugins, diagnostics, seenNames) {
16471
+ function resolvePluginRoots(opts = {}) {
16472
+ if (opts.pluginsRoot !== undefined) {
16473
+ return [{ origin: "path", root: opts.pluginsRoot }];
16474
+ }
16475
+ const roots = [];
16476
+ const builtinRoot = resolveBuiltinRoot(opts.packagePluginsRoot);
16477
+ if (builtinRoot !== undefined) {
16478
+ roots.push({ origin: "builtin", root: builtinRoot });
16479
+ }
16480
+ const userRoot = join(opts.homeDir ?? homedir(), ".kk", "plugins");
16481
+ if (isDirectory(userRoot)) {
16482
+ roots.push({ origin: "user", root: userRoot });
16483
+ }
16484
+ const projectRoot = join(opts.cwd ?? process.cwd(), "plugins");
16485
+ if (isDirectory(projectRoot) && !(builtinRoot !== undefined && sameRealpath(projectRoot, builtinRoot))) {
16486
+ roots.push({ origin: "project", root: projectRoot });
16487
+ }
16488
+ const env = opts.env ?? process.env;
16489
+ for (const entry of (env.KK_PLUGIN_PATH ?? "").split(":").filter((e) => e !== "")) {
16490
+ if (isDirectory(entry)) {
16491
+ roots.push({ origin: "path", root: entry });
16492
+ }
16493
+ }
16494
+ for (const extra of opts.extraPaths ?? []) {
16495
+ if (extra !== "" && isDirectory(extra)) {
16496
+ roots.push({ origin: "path", root: extra });
16497
+ }
16498
+ }
16499
+ return roots;
16500
+ }
16501
+ function discoverAllPlugins(opts = {}) {
16502
+ const roots = resolvePluginRoots(opts);
16503
+ const diagnostics = [];
16504
+ const byName = new Map;
16505
+ const scannedRealpaths = new Set;
16506
+ for (const { origin, root } of roots) {
16507
+ let real;
16508
+ try {
16509
+ real = realpathSync(root);
16510
+ } catch {
16511
+ continue;
16512
+ }
16513
+ if (scannedRealpaths.has(real)) {
16514
+ continue;
16515
+ }
16516
+ scannedRealpaths.add(real);
16517
+ if (origin === "path" && existsSync(join(root, "plugin.json"))) {
16518
+ const records = [];
16519
+ const rootDiagnostics = [];
16520
+ consumeManifest(root, null, origin, records, rootDiagnostics, new Set);
16521
+ diagnostics.push(...rootDiagnostics);
16522
+ for (const record2 of records) {
16523
+ mergeByName(record2, byName, diagnostics);
16524
+ }
16525
+ continue;
16526
+ }
16527
+ const scan = discoverPlugins(root, origin);
16528
+ diagnostics.push(...scan.diagnostics);
16529
+ for (const record2 of scan.plugins) {
16530
+ mergeByName(record2, byName, diagnostics);
16531
+ }
16532
+ }
16533
+ return { plugins: [...byName.values()], diagnostics };
16534
+ }
16535
+ function mergeByName(record2, byName, diagnostics) {
16536
+ const existing = byName.get(record2.name);
16537
+ if (existing !== undefined) {
16538
+ diagnostics.push({
16539
+ code: "shadowed",
16540
+ path: existing.manifestPath,
16541
+ reason: `Plugin name "${record2.name}" shadowed by ` + `${record2.manifestPath} (origin: ${record2.origin})`
16542
+ });
16543
+ }
16544
+ byName.set(record2.name, record2);
16545
+ }
16546
+ function resolveBuiltinRoot(injected) {
16547
+ if (injected !== undefined) {
16548
+ return injected;
16549
+ }
16550
+ let dir = dirname(fileURLToPath(import.meta.url));
16551
+ for (;; ) {
16552
+ const candidate = join(dir, "plugins");
16553
+ if (isPluginsTree(candidate)) {
16554
+ return candidate;
16555
+ }
16556
+ const parent = dirname(dir);
16557
+ if (parent === dir) {
16558
+ return;
16559
+ }
16560
+ dir = parent;
16561
+ }
16562
+ }
16563
+ function isPluginsTree(dir) {
16564
+ if (!isDirectory(dir)) {
16565
+ return false;
16566
+ }
16567
+ for (const collection of Object.keys(PLUGIN_COLLECTION_KINDS)) {
16568
+ if (isDirectory(join(dir, collection))) {
16569
+ return true;
16570
+ }
16571
+ }
16572
+ return false;
16573
+ }
16574
+ function sameRealpath(a, b) {
16575
+ try {
16576
+ return realpathSync(a) === realpathSync(b);
16577
+ } catch {
16578
+ return false;
16579
+ }
16580
+ }
16581
+ function scanCollection(collectionDir, expectedKind, origin, plugins, diagnostics, seenNames) {
16485
16582
  const entries = listSortedDir(collectionDir);
16486
16583
  if (!entries.ok) {
16487
16584
  diagnostics.push(entries.diagnostic);
@@ -16492,15 +16589,23 @@ function scanCollection(collectionDir, expectedKind, plugins, diagnostics, seenN
16492
16589
  if (!isDirectory(dirPath)) {
16493
16590
  continue;
16494
16591
  }
16495
- consumeManifest(dirPath, expectedKind, plugins, diagnostics, seenNames);
16592
+ consumeManifest(dirPath, expectedKind, origin, plugins, diagnostics, seenNames);
16496
16593
  }
16497
16594
  }
16498
- function consumeManifest(dirPath, expectedKind, plugins, diagnostics, seenNames) {
16595
+ function consumeManifest(dirPath, expectedKind, origin, plugins, diagnostics, seenNames) {
16499
16596
  const manifestPath = join(dirPath, "plugin.json");
16500
16597
  let content;
16501
16598
  try {
16502
16599
  content = readFileSync(manifestPath, "utf-8");
16503
- } catch {
16600
+ } catch (err) {
16601
+ if (err?.code === "ENOENT") {
16602
+ return;
16603
+ }
16604
+ diagnostics.push({
16605
+ code: "unreadable",
16606
+ path: manifestPath,
16607
+ reason: err instanceof Error ? err.message : String(err)
16608
+ });
16504
16609
  return;
16505
16610
  }
16506
16611
  let json2;
@@ -16525,7 +16630,7 @@ function consumeManifest(dirPath, expectedKind, plugins, diagnostics, seenNames)
16525
16630
  return;
16526
16631
  }
16527
16632
  const manifest = parsed.data;
16528
- if (manifest.kind !== expectedKind) {
16633
+ if (expectedKind !== null && manifest.kind !== expectedKind) {
16529
16634
  diagnostics.push({
16530
16635
  code: "kind_mismatch",
16531
16636
  path: manifestPath,
@@ -16548,6 +16653,7 @@ function consumeManifest(dirPath, expectedKind, plugins, diagnostics, seenNames)
16548
16653
  entry: manifest.entry,
16549
16654
  dir: dirPath,
16550
16655
  manifestPath,
16656
+ origin,
16551
16657
  version: manifest.version,
16552
16658
  description: manifest.description
16553
16659
  });
@@ -16573,9 +16679,6 @@ function isDirectory(path2) {
16573
16679
  return false;
16574
16680
  }
16575
16681
  }
16576
- function listByKind(result, kind) {
16577
- return result.plugins.filter((p) => p.kind === kind);
16578
- }
16579
16682
  function getByName(result, name) {
16580
16683
  return result.plugins.find((p) => p.name === name);
16581
16684
  }
@@ -16698,25 +16801,6 @@ async function runFanin(argv) {
16698
16801
  return 0;
16699
16802
  }
16700
16803
 
16701
- // src/commands/fanin.ts
16702
- function registerFaninCommand(program2) {
16703
- const collectIn = (value, acc) => {
16704
- acc.push(value);
16705
- return acc;
16706
- };
16707
- program2.command("fanin").description("Merge N Doc[] JSON inputs into one deduplicated Doc[] output").requiredOption("--in <path>", "Input Doc[] JSON file (repeatable)", collectIn, []).requiredOption("--out <path>", "Output merged Doc[] JSON file path").action(async (options) => {
16708
- const argv = [];
16709
- for (const inPath of options.in) {
16710
- argv.push("--in", inPath);
16711
- }
16712
- argv.push("--out", options.out);
16713
- const code = await runFanin(argv);
16714
- if (code !== 0) {
16715
- throw new CommanderError(code, "kk.fanin.failed", `fanin failed with exit code ${code}`);
16716
- }
16717
- });
16718
- }
16719
-
16720
16804
  // src/fanout.ts
16721
16805
  import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync2 } from "fs";
16722
16806
  import { tmpdir } from "os";
@@ -16740,11 +16824,10 @@ function spawnPluginEntry(opts) {
16740
16824
  };
16741
16825
  }
16742
16826
  async function runInvoke(opts) {
16743
- const pluginsRoot = opts.pluginsRoot ?? "./plugins";
16744
- const discovery = discoverPlugins(pluginsRoot);
16827
+ const discovery = discoverAllPlugins({ pluginsRoot: opts.pluginsRoot, extraPaths: opts.pluginsPath });
16745
16828
  const record2 = getByName(discovery, opts.plugin);
16746
16829
  if (!record2) {
16747
- logger.error(`Plugin '${opts.plugin}' not found under ${resolve3(pluginsRoot)}`);
16830
+ logger.error(`Plugin '${opts.plugin}' not found in the merged plugin registry`);
16748
16831
  if (discovery.diagnostics.length > 0) {
16749
16832
  for (const diag of discovery.diagnostics) {
16750
16833
  logger.error(` [diagnostic] ${diag.code} at ${diag.path}: ${diag.reason}`);
@@ -16981,6 +17064,7 @@ function readAggregateResult(path2) {
16981
17064
  }
16982
17065
  function parseArgs2(argv) {
16983
17066
  const targets = [];
17067
+ const pluginsPath = [];
16984
17068
  let contentPath;
16985
17069
  let outPath;
16986
17070
  let retryFromPath;
@@ -17029,6 +17113,12 @@ function parseArgs2(argv) {
17029
17113
  throw new Error("--plugins-root may be specified only once");
17030
17114
  }
17031
17115
  pluginsRoot = value;
17116
+ } else if (arg === "--plugins-path") {
17117
+ const value = argv[++i];
17118
+ if (value === undefined) {
17119
+ throw new Error("--plugins-path requires a directory path");
17120
+ }
17121
+ pluginsPath.push(value);
17032
17122
  } else {
17033
17123
  throw new Error(`Unknown argument: ${arg}`);
17034
17124
  }
@@ -17047,13 +17137,14 @@ function parseArgs2(argv) {
17047
17137
  targets,
17048
17138
  outPath,
17049
17139
  ...retryFromPath !== undefined ? { retryFromPath } : {},
17050
- ...pluginsRoot !== undefined ? { pluginsRoot } : {}
17140
+ ...pluginsRoot !== undefined ? { pluginsRoot } : {},
17141
+ ...pluginsPath.length > 0 ? { pluginsPath } : {}
17051
17142
  };
17052
17143
  }
17053
- async function pluginDispatch(target, contentPath, pluginsRoot) {
17054
- const record2 = getByName(discoverPlugins(pluginsRoot), target);
17144
+ async function pluginDispatch(target, contentPath, discovery) {
17145
+ const record2 = getByName(discovery, target);
17055
17146
  if (record2?.kind !== "publish") {
17056
- throw new Error(`Publish plugin '${target}' not found under ${resolve4(pluginsRoot)}`);
17147
+ throw new Error(`Publish plugin '${target}' not found in the merged plugin registry`);
17057
17148
  }
17058
17149
  const entryPath = isAbsolute2(record2.entry) ? record2.entry : resolve4(record2.dir, record2.entry);
17059
17150
  if (!existsSync4(entryPath)) {
@@ -17083,8 +17174,7 @@ async function pluginDispatch(target, contentPath, pluginsRoot) {
17083
17174
  rmSync(outTmp, { force: true });
17084
17175
  }
17085
17176
  }
17086
- function validateTargets(targets, pluginsRoot) {
17087
- const discovery = discoverPlugins(pluginsRoot);
17177
+ function validateTargets(targets, discovery) {
17088
17178
  const missing = [];
17089
17179
  for (const target of targets) {
17090
17180
  const record2 = getByName(discovery, target);
@@ -17093,7 +17183,7 @@ function validateTargets(targets, pluginsRoot) {
17093
17183
  }
17094
17184
  }
17095
17185
  if (missing.length > 0) {
17096
- logger.error(`Unknown publish target(s): ${missing.join(", ")} under ${resolve4(pluginsRoot)}`);
17186
+ logger.error(`Unknown publish target(s): ${missing.join(", ")}`);
17097
17187
  for (const diag of discovery.diagnostics) {
17098
17188
  logger.error(` [diagnostic] ${diag.code} at ${diag.path}: ${diag.reason}`);
17099
17189
  }
@@ -17113,7 +17203,12 @@ async function runFanout(argv, deps = {}) {
17113
17203
  if (content === null) {
17114
17204
  return EXIT_INPUT2;
17115
17205
  }
17116
- const pluginsRoot = deps.pluginsRoot ?? opts.pluginsRoot ?? "./plugins";
17206
+ if (opts.pluginsRoot !== undefined && (opts.pluginsPath?.length ?? 0) > 0) {
17207
+ logger.error("--plugins-root and --plugins-path are mutually exclusive (ADR-012 R6)");
17208
+ return EXIT_INPUT2;
17209
+ }
17210
+ const pluginsRoot = deps.pluginsRoot ?? opts.pluginsRoot;
17211
+ const discovery = discoverAllPlugins({ pluginsRoot, extraPaths: opts.pluginsPath });
17117
17212
  let prior;
17118
17213
  let skipped = [];
17119
17214
  let targets = opts.targets;
@@ -17133,8 +17228,8 @@ async function runFanout(argv, deps = {}) {
17133
17228
  const carried = new Set(skipped.map((t) => t.target));
17134
17229
  targets = opts.targets.length > 0 ? opts.targets.filter((t) => !carried.has(t)) : selectRetryTargets(prior);
17135
17230
  }
17136
- const dispatch = deps.dispatch ?? ((t) => pluginDispatch(t, opts.contentPath, pluginsRoot));
17137
- if (deps.dispatch === undefined && !validateTargets(targets, pluginsRoot)) {
17231
+ const dispatch = deps.dispatch ?? ((t) => pluginDispatch(t, opts.contentPath, discovery));
17232
+ if (deps.dispatch === undefined && !validateTargets(targets, discovery)) {
17138
17233
  return EXIT_INPUT2;
17139
17234
  }
17140
17235
  const fresh = await fanout(content, targets, dispatch);
@@ -17149,130 +17244,185 @@ async function runFanout(argv, deps = {}) {
17149
17244
  return result.ok ? 0 : EXIT_FAILURE;
17150
17245
  }
17151
17246
 
17152
- // src/commands/fanout.ts
17153
- function registerFanoutCommand(program2) {
17154
- const collectTarget = (value, acc) => {
17155
- acc.push(value);
17156
- return acc;
17157
- };
17158
- program2.command("fanout").description("Publish one Content JSON to N targets, aggregating per-target results").requiredOption("--content <path>", "Source ContentSchema JSON file path").option("--target <plugin>", "Target publish plugin name (repeatable)", collectTarget, []).requiredOption("--out <path>", "Aggregate Result JSON file path").option("--retry-from <path>", "Prior Result JSON file to retry only previously failed targets").option("--plugins-root <dir>", "Root directory containing plugins").action(async (options) => {
17159
- const argv = ["--content", options.content, "--out", options.out];
17160
- for (const target of options.target) {
17161
- argv.push("--target", target);
17162
- }
17163
- if (options.retryFrom) {
17164
- argv.push("--retry-from", options.retryFrom);
17165
- }
17166
- if (options.pluginsRoot) {
17167
- argv.push("--plugins-root", options.pluginsRoot);
17168
- }
17169
- const code = await runFanout(argv);
17170
- if (code !== 0) {
17171
- throw new CommanderError(code, "kk.fanout.failed", `fanout failed with exit code ${code}`);
17172
- }
17247
+ // src/commands/executor.ts
17248
+ function collectPath(value, acc) {
17249
+ acc.push(value);
17250
+ return acc;
17251
+ }
17252
+ async function runInvokeAction(plugin, options) {
17253
+ if (options.pluginsRoot !== undefined && (options.pluginsPath?.length ?? 0) > 0) {
17254
+ throw new CommanderError(2, "kk.executor.run.root-path-conflict", "--plugins-root and --plugins-path are mutually exclusive");
17255
+ }
17256
+ const code = await runInvoke({
17257
+ plugin,
17258
+ inPath: options.in,
17259
+ outPath: options.out,
17260
+ pluginsRoot: options.pluginsRoot,
17261
+ pluginsPath: options.pluginsPath
17173
17262
  });
17263
+ if (code !== 0) {
17264
+ throw new CommanderError(code, "kk.executor.run.failed", `invoke failed with exit code ${code}`);
17265
+ }
17174
17266
  }
17175
-
17176
- // src/commands/invoke.ts
17177
- function registerInvokeCommand(program2) {
17178
- program2.command("invoke").description("Invoke a plugin with input/output files and contract validation").argument("<plugin>", "Name of the plugin to invoke").requiredOption("--in <path>", "Input file path (or source path for ingestion)").requiredOption("--out <path>", "Output file path").option("--plugins-root <dir>", "Root directory containing plugins").action(async (plugin, options) => {
17179
- const code = await runInvoke({
17180
- plugin,
17181
- inPath: options.in,
17182
- outPath: options.out,
17183
- pluginsRoot: options.pluginsRoot
17184
- });
17185
- if (code !== 0) {
17186
- throw new CommanderError(code, "kk.invoke.failed", `invoke failed with exit code ${code}`);
17187
- }
17188
- });
17267
+ function collectIn(value, acc) {
17268
+ acc.push(value);
17269
+ return acc;
17270
+ }
17271
+ async function runFaninAction(options) {
17272
+ const argv = [];
17273
+ for (const inPath of options.in) {
17274
+ argv.push("--in", inPath);
17275
+ }
17276
+ argv.push("--out", options.out);
17277
+ const code = await runFanin(argv);
17278
+ if (code !== 0) {
17279
+ throw new CommanderError(code, "kk.executor.fanin.failed", `fan-in failed with exit code ${code}`);
17280
+ }
17281
+ }
17282
+ function collectTarget(value, acc) {
17283
+ acc.push(value);
17284
+ return acc;
17285
+ }
17286
+ async function runFanoutAction(options) {
17287
+ const argv = ["--content", options.content, "--out", options.out];
17288
+ for (const target of options.target) {
17289
+ argv.push("--target", target);
17290
+ }
17291
+ if (options.retryFrom) {
17292
+ argv.push("--retry-from", options.retryFrom);
17293
+ }
17294
+ if (options.pluginsRoot) {
17295
+ argv.push("--plugins-root", options.pluginsRoot);
17296
+ }
17297
+ for (const path2 of options.pluginsPath ?? []) {
17298
+ argv.push("--plugins-path", path2);
17299
+ }
17300
+ const code = await runFanout(argv);
17301
+ if (code !== 0) {
17302
+ throw new CommanderError(code, "kk.executor.fanout.failed", `fan-out failed with exit code ${code}`);
17303
+ }
17304
+ }
17305
+ function registerExecutorCommand(program2) {
17306
+ const executor = program2.command("executor").description("Run knowledge-kit plugin steps (invoke, fan-in, fan-out)");
17307
+ executor.command("run").description("Invoke a plugin with input/output files and contract validation").argument("<plugin>", "Name of the plugin to invoke").requiredOption("--in <path>", "Input file path (or source path for ingestion)").requiredOption("--out <path>", "Output file path").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectPath, []).action(runInvokeAction);
17308
+ executor.command("fan-in").alias("fanin").description("Merge N Doc[] JSON inputs into one deduplicated Doc[] output").requiredOption("--in <path>", "Input Doc[] JSON file (repeatable)", collectIn, []).requiredOption("--out <path>", "Output merged Doc[] JSON file path").action(runFaninAction);
17309
+ executor.command("fan-out").alias("fanout").description("Publish one Content JSON to N targets, aggregating per-target results").requiredOption("--content <path>", "Source ContentSchema JSON file path").option("--target <plugin>", "Target publish plugin name (repeatable)", collectTarget, []).requiredOption("--out <path>", "Aggregate Result JSON file path").option("--retry-from <path>", "Prior Result JSON file to retry only previously failed targets").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectPath, []).action(runFanoutAction);
17310
+ program2.command("invoke", { hidden: true }).description("Alias for `executor run` (deprecated)").argument("<plugin>", "Name of the plugin to invoke").requiredOption("--in <path>", "Input file path (or source path for ingestion)").requiredOption("--out <path>", "Output file path").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectPath, []).action(runInvokeAction);
17311
+ program2.command("fanin", { hidden: true }).description("Alias for `executor fan-in` (deprecated)").requiredOption("--in <path>", "Input Doc[] JSON file (repeatable)", collectIn, []).requiredOption("--out <path>", "Output merged Doc[] JSON file path").action(runFaninAction);
17312
+ program2.command("fanout", { hidden: true }).description("Alias for `executor fan-out` (deprecated)").requiredOption("--content <path>", "Source ContentSchema JSON file path").option("--target <plugin>", "Target publish plugin name (repeatable)", collectTarget, []).requiredOption("--out <path>", "Aggregate Result JSON file path").option("--retry-from <path>", "Prior Result JSON file to retry only previously failed targets").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectPath, []).action(runFanoutAction);
17189
17313
  }
17190
17314
 
17191
- // src/commands/plugins.ts
17315
+ // src/commands/plugin.ts
17192
17316
  import { existsSync as existsSync5 } from "fs";
17193
17317
  import { resolve as resolve5 } from "path";
17194
- function registerPluginsCommand(program2) {
17195
- const pluginsCmd = program2.command("plugins").description("Manage and inspect knowledge-kit plugins");
17196
- pluginsCmd.command("list").description("List discovered plugins grouped by kind").option("--kind <kind>", "Filter plugins by kind (ingestion, generator, publish)").option("--json", "Output machine-readable JSON array").option("--plugins-root <dir>", "Root directory containing plugins").action((options) => {
17197
- const root = options.pluginsRoot ?? "./plugins";
17198
- const discovery = discoverPlugins(root);
17199
- let records = discovery.plugins;
17200
- if (options.kind) {
17201
- if (!isPluginKind(options.kind)) {
17202
- logger.error(`Invalid plugin kind: ${options.kind}
17203
- `);
17204
- throw new CommanderError(1, "kk.plugins.invalid-kind", `invalid plugin kind: ${options.kind}`);
17205
- }
17206
- records = listByKind(discovery, options.kind);
17318
+ function collectPath2(value, acc) {
17319
+ acc.push(value);
17320
+ return acc;
17321
+ }
17322
+ function buildStatusReport(opts) {
17323
+ const discovery = discoverAllPlugins({ pluginsRoot: opts.pluginsRoot, extraPaths: opts.pluginsPath });
17324
+ const plugins = discovery.plugins.map((record2) => {
17325
+ const diagnostics = [];
17326
+ const entryPath = resolve5(record2.dir, record2.entry);
17327
+ if (!existsSync5(entryPath)) {
17328
+ diagnostics.push({
17329
+ code: "missing_entry",
17330
+ path: entryPath,
17331
+ reason: `Plugin "${record2.name}" entry file does not exist: ${record2.entry}`
17332
+ });
17207
17333
  }
17208
- if (options.json) {
17209
- logger.out(`${JSON.stringify(records, null, 2)}
17334
+ return { ...record2, healthy: diagnostics.length === 0, diagnostics };
17335
+ });
17336
+ return {
17337
+ healthy: discovery.diagnostics.length === 0 && plugins.every((p) => p.healthy),
17338
+ plugins,
17339
+ diagnostics: [...discovery.diagnostics]
17340
+ };
17341
+ }
17342
+ function printDiagnostics(report) {
17343
+ const all = [...report.diagnostics];
17344
+ for (const p of report.plugins) {
17345
+ all.push(...p.diagnostics);
17346
+ }
17347
+ logger.error(`Found ${all.length} diagnostic issue(s):
17210
17348
  `);
17211
- return;
17212
- }
17213
- if (records.length === 0) {
17214
- logger.out(`No plugins found.
17349
+ for (const d of all) {
17350
+ logger.error(` [${d.code}] ${d.path}: ${d.reason}
17215
17351
  `);
17216
- return;
17217
- }
17218
- const kinds = options.kind ? [options.kind] : ["ingestion", "generator", "publish"];
17219
- for (const k of kinds) {
17220
- const group = records.filter((p) => p.kind === k);
17221
- if (group.length === 0)
17222
- continue;
17223
- logger.out(`
17224
- --- ${k.toUpperCase()} PLUGINS ---
17352
+ }
17353
+ }
17354
+ function statusAction(options) {
17355
+ if (options.pluginsRoot !== undefined && (options.pluginsPath?.length ?? 0) > 0) {
17356
+ logger.error(`--plugins-root and --plugins-path are mutually exclusive (ADR-012 R6)
17225
17357
  `);
17226
- for (const p of group) {
17227
- const ver = p.version ? ` (v${p.version})` : "";
17228
- const desc = p.description ? ` - ${p.description}` : "";
17229
- logger.out(` \u2022 ${p.name}${ver}: ${p.entry}${desc}
17358
+ throw new CommanderError(2, "kk.plugin.root-path-conflict", "--plugins-root and --plugins-path are mutually exclusive");
17359
+ }
17360
+ const report = buildStatusReport({ pluginsRoot: options.pluginsRoot, pluginsPath: options.pluginsPath });
17361
+ let records = report.plugins;
17362
+ if (options.kind) {
17363
+ if (!isPluginKind(options.kind)) {
17364
+ logger.error(`Invalid plugin kind: ${options.kind}
17230
17365
  `);
17231
- }
17232
- }
17233
- });
17234
- pluginsCmd.command("doctor").description("Validate plugin manifests and entries under plugins root").option("--json", "Output machine-readable JSON object").option("--plugins-root <dir>", "Root directory containing plugins").action((options) => {
17235
- const root = options.pluginsRoot ?? "./plugins";
17236
- const discovery = discoverPlugins(root);
17237
- const diagnostics = [...discovery.diagnostics];
17238
- for (const p of discovery.plugins) {
17239
- const entryPath = resolve5(p.dir, p.entry);
17240
- if (!existsSync5(entryPath)) {
17241
- diagnostics.push({
17242
- code: "missing_entry",
17243
- path: entryPath,
17244
- reason: `Plugin "${p.name}" entry file does not exist: ${p.entry}`
17245
- });
17246
- }
17366
+ throw new CommanderError(1, "kk.plugin.invalid-kind", `invalid plugin kind: ${options.kind}`);
17247
17367
  }
17248
- const healthy = diagnostics.length === 0;
17249
- if (options.json) {
17250
- logger.out(`${JSON.stringify({ healthy, diagnostics }, null, 2)}
17368
+ records = report.plugins.filter((p) => p.kind === options.kind);
17369
+ }
17370
+ if (options.json) {
17371
+ logger.out(`${JSON.stringify({ ...report, plugins: records }, null, 2)}
17251
17372
  `);
17252
- if (!healthy) {
17253
- throw new CommanderError(1, "kk.plugins.unhealthy", "plugin diagnostics found");
17254
- }
17255
- return;
17373
+ if (!report.healthy) {
17374
+ throw new CommanderError(1, "kk.plugin.unhealthy", "plugin diagnostics found");
17256
17375
  }
17257
- if (healthy) {
17258
- logger.out(`All plugin manifests and entries are healthy.
17376
+ return;
17377
+ }
17378
+ if (records.length === 0) {
17379
+ logger.out(`No plugins found.
17259
17380
  `);
17260
- return;
17381
+ if (!report.healthy) {
17382
+ printDiagnostics(report);
17383
+ throw new CommanderError(1, "kk.plugin.unhealthy", "plugin diagnostics found");
17261
17384
  }
17262
- logger.error(`Found ${diagnostics.length} diagnostic issue(s):
17385
+ return;
17386
+ }
17387
+ const kinds = options.kind ? [options.kind] : ["ingestion", "generator", "publish"];
17388
+ for (const k of kinds) {
17389
+ const group = records.filter((p) => p.kind === k);
17390
+ if (group.length === 0)
17391
+ continue;
17392
+ logger.out(`
17393
+ --- ${k.toUpperCase()} PLUGINS ---
17263
17394
  `);
17264
- for (const d of diagnostics) {
17265
- logger.error(` [${d.code}] ${d.path}: ${d.reason}
17395
+ for (const p of group) {
17396
+ const ver = p.version ? ` (v${p.version})` : "";
17397
+ const desc = p.description ? ` - ${p.description}` : "";
17398
+ const health = p.healthy ? "healthy" : "unhealthy";
17399
+ logger.out(` \u2022 ${p.name}${ver}: ${p.entry}${desc} [${health}]
17266
17400
  `);
17267
17401
  }
17268
- throw new CommanderError(1, "kk.plugins.unhealthy", "plugin diagnostics found");
17269
- });
17402
+ }
17403
+ if (!report.healthy) {
17404
+ printDiagnostics(report);
17405
+ throw new CommanderError(1, "kk.plugin.unhealthy", "plugin diagnostics found");
17406
+ }
17407
+ }
17408
+ function registerStatusVerb(parent, name, description, hidden) {
17409
+ parent.command(name, { hidden }).description(description).option("--kind <kind>", "Filter plugins by kind (ingestion, generator, publish)").option("--json", "Output machine-readable unified health report").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectPath2, []).action(statusAction);
17410
+ }
17411
+ function registerPluginCommand(program2) {
17412
+ const plugin = program2.command("plugin").description("Inspect and validate knowledge-kit plugins");
17413
+ registerStatusVerb(plugin, "status", "List discovered plugins grouped by kind with a per-plugin health mark", false);
17414
+ registerStatusVerb(plugin, "list", "Alias for `plugin status` (deprecated)", true);
17415
+ registerStatusVerb(plugin, "doctor", "Alias for `plugin status` (deprecated)", true);
17416
+ const plugins = program2.command("plugins", { hidden: true }).description("Alias for `plugin status` (deprecated)");
17417
+ registerStatusVerb(plugins, "status", "Alias for `plugin status` (deprecated)", true);
17418
+ registerStatusVerb(plugins, "list", "Alias for `plugin status` (deprecated)", true);
17419
+ registerStatusVerb(plugins, "doctor", "Alias for `plugin status` (deprecated)", true);
17270
17420
  }
17271
17421
 
17272
17422
  // src/config.ts
17273
17423
  import { readFileSync as readFileSync5 } from "fs";
17274
- import { fileURLToPath } from "url";
17275
- var pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
17424
+ import { fileURLToPath as fileURLToPath2 } from "url";
17425
+ var pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
17276
17426
  var pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
17277
17427
  var CLI_CONFIG = {
17278
17428
  binaryName: "kk",
@@ -17282,11 +17432,8 @@ var CLI_CONFIG = {
17282
17432
  // src/cli.ts
17283
17433
  function createProgram() {
17284
17434
  const program2 = new Command().name(CLI_CONFIG.binaryName).description("knowledge-kit CLI").version(CLI_CONFIG.binaryVersion).option("-v, --verbose", "enable verbose diagnostics");
17285
- registerAddCommand(program2);
17286
- registerInvokeCommand(program2);
17287
- registerFaninCommand(program2);
17288
- registerFanoutCommand(program2);
17289
- registerPluginsCommand(program2);
17435
+ registerExecutorCommand(program2);
17436
+ registerPluginCommand(program2);
17290
17437
  return program2;
17291
17438
  }
17292
17439
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@gobing-ai/web-search",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*"
10
+ },
11
+ "devDependencies": {
12
+ "@types/bun": "1.3.14"
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "web-search",
3
+ "kind": "ingestion",
4
+ "entry": "./src/index.ts",
5
+ "version": "1.0.0",
6
+ "description": "Ingests web search results for a topic into Doc objects (fixture stub — no live Firecrawl in v1)"
7
+ }
@@ -0,0 +1,144 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
6
+
7
+ /** Validated `--in` input per the 0054 contract (`topic` required; `maxResults`/`fixture` optional). */
8
+ interface InInput {
9
+ topic: string;
10
+ maxResults: number;
11
+ fixture: boolean;
12
+ }
13
+
14
+ /** Canned web-search results — the v1 fixture stub. Stable sourceUris → stable Doc ids. */
15
+ interface FixtureEntry {
16
+ sourceUri: string;
17
+ title: string;
18
+ body: string;
19
+ position: number;
20
+ }
21
+
22
+ const FIXTURE_ENTRIES: FixtureEntry[] = [
23
+ {
24
+ sourceUri: 'https://example.org/research/storm-overview',
25
+ title: 'STORM: Topic Research via Composed Plugins — Overview',
26
+ body: '# STORM Overview\n\nSTORM chains ingestion and generation plugins over a single research topic.\n\n- **Ingest**: a web-search plugin materializes a `topic` into `Doc[]`\n- **Generate**: a generator plugin (content-gen) turns the `Doc[]` into grounded Content\n\nThis is fixture content emitted by the `web-search` v1 stub; no live Firecrawl call is made.',
27
+ position: 1,
28
+ },
29
+ {
30
+ sourceUri: 'https://example.org/research/storm-contract',
31
+ title: 'The web-search Ingest Contract (0054)',
32
+ body: '# Ingest Contract\n\nThe web-search `--in` JSON carries `topic` (required, non-empty string), `maxResults` (optional positive integer, default 8), and `fixture` (optional boolean, reserved for the stub).\n\nDoc `id` is the sha256 hex prefix (16 chars) of the canonical `sourceUri`, keeping ids deterministic across re-runs.',
33
+ position: 2,
34
+ },
35
+ {
36
+ sourceUri: 'https://example.org/research/storm-dry-run',
37
+ title: 'Dry-Running kk-storm-research',
38
+ body: '# Dry-Run\n\nThe `kk-storm-research` state machine materializes the topic `--vars` into the ingest `--in` JSON, runs `web-search`, then `content-gen`, and terminates at the Content-written `done` state — no publish.',
39
+ position: 3,
40
+ },
41
+ ];
42
+
43
+ /** Deterministic Doc id: sha256(sourceUri) hex prefix, same idea as karakeep-local (0054 Block 4). */
44
+ function computeId(uri: string): string {
45
+ return createHash('sha256').update(uri).digest('hex').slice(0, 16);
46
+ }
47
+
48
+ /**
49
+ * Build the fixture `Doc[]` for a topic (0054 Block 4 mapping). Always emits
50
+ * canned docs — the v1 plugin has no live Firecrawl path.
51
+ */
52
+ export function buildFixtureDocs(topic: string, maxResults: number): Doc[] {
53
+ // maxResults caps the list per 0054 (cap semantics); `maxResults: 1` deliberately yields a
54
+ // single Doc. The Design's "always ≥2 items" wording applies to the default path (maxResults ≥ 2 → 3 canned entries).
55
+ const entries = FIXTURE_ENTRIES.slice(0, Math.max(0, Math.min(FIXTURE_ENTRIES.length, maxResults)));
56
+ return entries.map((entry, rank) => ({
57
+ id: computeId(entry.sourceUri),
58
+ body: entry.body,
59
+ title: entry.title,
60
+ sourceUri: entry.sourceUri,
61
+ mediaType: 'text/markdown',
62
+ metadata: {
63
+ query: topic,
64
+ rank,
65
+ position: entry.position,
66
+ // Per-run timestamp by design (0054 Block 4): determinism is required only for `id`.
67
+ scraped_at: new Date().toISOString(),
68
+ },
69
+ }));
70
+ }
71
+
72
+ /** Parse and validate the `--in` JSON; missing/invalid topic → throws (fail-loud row 1). */
73
+ export function parseInput(raw: string): InInput {
74
+ let data: unknown;
75
+ try {
76
+ data = JSON.parse(raw);
77
+ } catch {
78
+ throw new Error('--in is not valid JSON');
79
+ }
80
+ if (typeof data !== 'object' || data === null) {
81
+ throw new Error('--in must be a JSON object');
82
+ }
83
+ const input = data as Record<string, unknown>;
84
+ if (typeof input.topic !== 'string' || input.topic.trim() === '') {
85
+ throw new Error('--in "topic" is required and must be a non-empty string');
86
+ }
87
+ let maxResults = 8;
88
+ if (input.maxResults !== undefined) {
89
+ if (typeof input.maxResults !== 'number' || !Number.isInteger(input.maxResults) || input.maxResults <= 0) {
90
+ throw new Error('--in "maxResults" must be a positive integer when present');
91
+ }
92
+ maxResults = input.maxResults;
93
+ }
94
+ return { topic: input.topic, maxResults, fixture: input.fixture === true };
95
+ }
96
+
97
+ export async function main(): Promise<number> {
98
+ const { values } = parseArgs({
99
+ options: {
100
+ in: { type: 'string' },
101
+ out: { type: 'string' },
102
+ },
103
+ });
104
+
105
+ if (!values.in || !values.out) {
106
+ console.error('Error: Missing required arguments --in and --out');
107
+ return 1;
108
+ }
109
+
110
+ let input: InInput;
111
+ try {
112
+ input = parseInput(await readFile(values.in, 'utf-8'));
113
+ } catch (err: unknown) {
114
+ console.error(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
115
+ return 1;
116
+ }
117
+
118
+ if (!input.fixture) {
119
+ // 0054 Block 1/Block 8: `fixture` absent or false selects the LIVE Firecrawl path; v1 has no
120
+ // live client. Fail loud (Block 5 row-2 semantics: exit 1, no --out) instead of silently
121
+ // fabricating research docs — an obvious failure beats a surprising recovery.
122
+ console.error(
123
+ 'web-search failed: live Firecrawl not implemented in v1 (pass "fixture": true to use the fixture stub)',
124
+ );
125
+ return 1;
126
+ }
127
+
128
+ try {
129
+ const docs = DocListSchema.parse(buildFixtureDocs(input.topic, input.maxResults));
130
+ const outDir = dirname(values.out);
131
+ if (outDir && outDir !== '.') {
132
+ await mkdir(outDir, { recursive: true });
133
+ }
134
+ await writeFile(values.out, JSON.stringify(docs, null, 2), 'utf-8');
135
+ return 0;
136
+ } catch (err: unknown) {
137
+ console.error(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
138
+ return 1;
139
+ }
140
+ }
141
+
142
+ if (import.meta.main) {
143
+ process.exit(await main());
144
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -60,7 +60,7 @@ Every evaluation writes a JSON verdict file. The default path is
60
60
  ## Evaluation procedure
61
61
 
62
62
  1. **Read the Content** from the path given in the evaluation request (the `content.json` written
63
- by the preceding `kk invoke <generator>` step).
63
+ by the preceding `kk executor run <generator>` step).
64
64
  2. **Select the rubric** — either named in the request (`tech-accuracy`, `brand-tone`,
65
65
  `compliance`, `general`) or loaded by a thin wrapper. Default: `general`.
66
66
  3. **Evaluate each criterion** in the rubric. For each, produce a `pass` / `fail` / `warn` status
@@ -31,7 +31,7 @@ Research, illustration, platform adaptation, and publishing are deliberately **o
31
31
 
32
32
  Do **not** use this skill for: materials extraction or research, image generation, platform
33
33
  adaptation, publishing, style-profile infrastructure, or anything requiring a runtime
34
- plugin (`kk invoke`, `plugin.json`, `GeneratorInput → Content`).
34
+ plugin (`kk executor run`, `plugin.json`, `GeneratorInput → Content`).
35
35
 
36
36
  ## Invocation contract
37
37