@aikdna/kdna-cli 0.17.0 → 0.18.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/src/agent.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * not treat as a fit decision; many false positives expected)
17
17
  * The agent makes the final call using its own language understanding.
18
18
  *
19
- * kdna load <name> [--as=prompt|json|raw]
19
+ * kdna load <name|file.kdna> [--as=prompt|json|raw]
20
20
  * Read the domain's Core + Patterns and emit:
21
21
  * --as=prompt (default): compact text suitable for system-prompt
22
22
  * injection (axioms one-liners + stances +
@@ -30,44 +30,44 @@
30
30
  */
31
31
 
32
32
  const fs = require('fs');
33
- const path = require('path');
34
33
  const { parseName } = require('./registry');
35
34
  const { recordTrace } = require('./cmds/trace');
35
+ const {
36
+ getInstalled,
37
+ listInstalled: listInstalledAssets,
38
+ readContainer,
39
+ readContainerEntry,
40
+ readContainerJson,
41
+ resolveAsset,
42
+ } = require('./package-store');
43
+ const { licenseDecryptOptionsForManifest } = require('./cmds/license');
36
44
 
37
45
  function detectAgent() {
38
46
  return process.env.KDNA_AGENT || 'cli';
39
47
  }
40
48
 
41
- const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
42
- const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
49
+ function listInstalled() {
50
+ return listInstalledAssets().map((entry) => {
51
+ const parsed = parseName(entry.full);
52
+ return { ...entry, scope: parsed.scope, ident: parsed.ident };
53
+ });
54
+ }
43
55
 
44
- function readJson(p) {
45
- try {
46
- return JSON.parse(fs.readFileSync(p, 'utf8'));
47
- } catch {
48
- return null;
49
- }
56
+ function assetLabel(asset, fallback) {
57
+ return asset.name || asset.parsed?.full || fallback;
50
58
  }
51
59
 
52
- function listInstalled() {
53
- if (!fs.existsSync(INSTALL_DIR)) return [];
54
- const out = [];
55
- for (const scopeName of fs.readdirSync(INSTALL_DIR)) {
56
- if (!scopeName.startsWith('@')) continue;
57
- const scopeDir = path.join(INSTALL_DIR, scopeName);
58
- try {
59
- if (!fs.statSync(scopeDir).isDirectory()) continue;
60
- for (const ident of fs.readdirSync(scopeDir)) {
61
- if (ident.startsWith('.')) continue;
62
- const dir = path.join(scopeDir, ident);
63
- if (!fs.statSync(dir).isDirectory()) continue;
64
- out.push({ scope: scopeName, ident, dir, full: `${scopeName}/${ident}` });
65
- }
66
- } catch {
67
- /* skip */
68
- }
69
- }
70
- return out;
60
+ function traceAssetFields(asset, manifest = {}, license = null) {
61
+ const fields = {
62
+ asset_path: asset.asset_path,
63
+ asset_digest: asset.asset_digest || null,
64
+ content_digest: asset.content_digest || null,
65
+ version: manifest.version || asset.version || null,
66
+ judgment_version: manifest.judgment_version || asset.judgment_version || null,
67
+ access: manifest.access || asset.access || null,
68
+ };
69
+ if (license?.license_id) fields.license_id = license.license_id;
70
+ return fields;
71
71
  }
72
72
 
73
73
  // ─── kdna available ────────────────────────────────────────────────────
@@ -78,11 +78,9 @@ function cmdAvailable(args = []) {
78
78
 
79
79
  const out = [];
80
80
  for (const e of installed) {
81
- const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
81
+ const { manifest = {}, core = {} } = readContainer(e.asset_path);
82
82
  if (manifest.yanked === true) continue;
83
83
 
84
- const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
85
-
86
84
  // Pull applies_when across all axioms (this is what the agent needs
87
85
  // for fit-check). Collapsing per-axiom into one set makes the agent's
88
86
  // matching decision much cheaper.
@@ -110,14 +108,7 @@ function cmdAvailable(args = []) {
110
108
  }
111
109
 
112
110
  if (wantJson) {
113
- const result = out.length
114
- ? out
115
- : {
116
- count: 0,
117
- domains: [],
118
- note: 'No domains installed. Run: kdna install <name> See: kdna list --available',
119
- };
120
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
111
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
121
112
  return;
122
113
  }
123
114
 
@@ -195,12 +186,11 @@ function cmdMatch(taskText, args = []) {
195
186
  const hints = [];
196
187
 
197
188
  for (const e of installed) {
198
- const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
189
+ const { manifest = {}, core = {} } = readContainer(e.asset_path);
199
190
  if (manifest.yanked === true) {
200
191
  dropped.push({ name: manifest.name || e.full, reason: 'yanked' });
201
192
  continue;
202
193
  }
203
- const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
204
194
 
205
195
  // does_not_apply_when disqualification (HARD signal)
206
196
  let disqualified = null;
@@ -318,7 +308,7 @@ function cmdMatch(taskText, args = []) {
318
308
  }
319
309
  }
320
310
  console.log('');
321
- console.log('To consider any of these, read its full data: kdna load <name> --as=json');
311
+ console.log('To consider any of these, read its full data: kdna load <name|file.kdna> --as=json');
322
312
  }
323
313
  }
324
314
 
@@ -348,20 +338,38 @@ function cmdLoad(input, args = []) {
348
338
  profileInput = args[inputIdx + 1] || null;
349
339
  }
350
340
 
351
- const parsed = parseName(input);
352
- if (!parsed) {
353
- console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
341
+ const asset = resolveAsset(input);
342
+ if (!asset) {
343
+ console.error(`KDNA asset not found: ${input}. Use an installed name or a .kdna file.`);
354
344
  process.exit(2);
355
345
  }
356
- const dir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
357
- if (!fs.existsSync(dir)) {
358
- console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
359
- process.exit(2);
346
+
347
+ const manifest = readContainerJson(asset.asset_path, 'kdna.json') || {};
348
+ const encryptedEntries = manifest.encryption?.encrypted_entries || [];
349
+ let decryptOptions = {};
350
+ let licenseActivation = null;
351
+ if (manifest.access === 'licensed' || encryptedEntries.length > 0) {
352
+ const activation = licenseDecryptOptionsForManifest(manifest);
353
+ if (!activation.ok) {
354
+ console.error(`KDNA license required for ${manifest.name || input}: ${activation.error}`);
355
+ console.error(`Install a license with: kdna license install <license.json>`);
356
+ process.exit(3);
357
+ }
358
+ decryptOptions = { decryptEntry: activation.decryptEntry };
359
+ licenseActivation = activation.license;
360
360
  }
361
361
 
362
- const manifest = readJson(path.join(dir, 'kdna.json')) || {};
362
+ let container;
363
+ try {
364
+ container = readContainer(asset.asset_path, decryptOptions);
365
+ } catch (e) {
366
+ console.error(`Failed to load KDNA asset: ${e.message}`);
367
+ process.exit(3);
368
+ }
369
+ const parsed = asset.parsed || parseName(manifest.name || '');
370
+ const label = assetLabel(asset, input);
363
371
  if (manifest.yanked === true) {
364
- console.error(`${parsed.full}@${manifest.version} has been yanked.`);
372
+ console.error(`${label}@${manifest.version} has been yanked.`);
365
373
  if (manifest.replaced_by) console.error(`Try: ${manifest.replaced_by}`);
366
374
  process.exit(2);
367
375
  }
@@ -386,8 +394,8 @@ function cmdLoad(input, args = []) {
386
394
  if (loadWarnings.length > 0) {
387
395
  console.error(loadWarnings.join('\n'));
388
396
  }
389
- const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
390
- const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
397
+ const core = container.core || {};
398
+ const pat = container.patterns || {};
391
399
 
392
400
  // JSON format
393
401
  if (format === 'json') {
@@ -401,13 +409,17 @@ function cmdLoad(input, args = []) {
401
409
  deprecated: manifest.status === 'deprecated',
402
410
  yanked: false,
403
411
  warnings: loadWarnings,
412
+ asset_digest: asset.asset_digest || null,
413
+ content_digest: asset.content_digest || null,
414
+ license_id: licenseActivation?.license_id || null,
404
415
  },
405
416
  }, null, 2) + '\n');
406
417
  recordTrace({
407
418
  timestamp: new Date().toISOString(),
408
419
  agent: detectAgent(),
409
- domain: parsed.full,
420
+ domain: label,
410
421
  format: 'json',
422
+ asset: traceAssetFields(asset, manifest, licenseActivation),
411
423
  });
412
424
  return;
413
425
  }
@@ -415,40 +427,46 @@ function cmdLoad(input, args = []) {
415
427
  // Raw format
416
428
  if (format === 'raw') {
417
429
  for (const f of ['KDNA_Core.json', 'KDNA_Patterns.json']) {
418
- const p = path.join(dir, f);
419
- if (fs.existsSync(p)) {
430
+ const encrypted = encryptedEntries.includes(f);
431
+ const buf = encrypted
432
+ ? Buffer.from(JSON.stringify(container[f === 'KDNA_Core.json' ? 'core' : 'patterns'], null, 2))
433
+ : readContainerEntry(asset.asset_path, f);
434
+ if (buf) {
420
435
  process.stdout.write(`\n=== ${f} ===\n`);
421
- process.stdout.write(fs.readFileSync(p, 'utf8'));
436
+ process.stdout.write(buf.toString('utf8'));
422
437
  }
423
438
  }
424
439
  recordTrace({
425
440
  timestamp: new Date().toISOString(),
426
441
  agent: detectAgent(),
427
- domain: parsed.full,
442
+ domain: label,
428
443
  format: 'raw',
444
+ asset: traceAssetFields(asset, manifest, licenseActivation),
429
445
  });
430
446
  return;
431
447
  }
432
448
 
433
449
  // Load profiles
434
450
  if (profile) {
435
- emitProfile(parsed, manifest, core, pat, profile, profileInput);
451
+ emitProfile(parsed || { full: label }, manifest, core, pat, profile, profileInput);
436
452
  recordTrace({
437
453
  timestamp: new Date().toISOString(),
438
454
  agent: detectAgent(),
439
- domain: parsed.full,
455
+ domain: label,
440
456
  format: `profile:${profile}`,
457
+ asset: traceAssetFields(asset, manifest, licenseActivation),
441
458
  });
442
459
  return;
443
460
  }
444
461
 
445
462
  // Default: --as=prompt — compact text optimized for system-prompt injection.
446
- emitCompact(parsed, manifest, core, pat);
463
+ emitCompact(parsed || { full: label }, manifest, core, pat);
447
464
  recordTrace({
448
465
  timestamp: new Date().toISOString(),
449
466
  agent: detectAgent(),
450
- domain: parsed.full,
467
+ domain: label,
451
468
  format: 'prompt',
469
+ asset: traceAssetFields(asset, manifest, licenseActivation),
452
470
  });
453
471
  }
454
472
 
@@ -461,6 +479,7 @@ function emitProfile(parsed, manifest, core, pat, profile, input) {
461
479
  lines.push('');
462
480
 
463
481
  const axioms = core.axioms || [];
482
+ emitRequiredOutput(lines, manifest, core, pat);
464
483
 
465
484
  switch (profile) {
466
485
  case 'index':
@@ -546,11 +565,11 @@ function emitProfile(parsed, manifest, core, pat, profile, input) {
546
565
  }
547
566
 
548
567
  if (pat.terminology?.banned_terms?.length) {
549
- lines.push('## Banned terms');
568
+ lines.push('## MUST NOT SAY');
550
569
  for (const t of pat.terminology.banned_terms) {
551
570
  const term = typeof t === 'string' ? t : t.term;
552
571
  const replace = typeof t === 'object' ? t.replace_with : null;
553
- lines.push(`- "${term}"${replace ? ` use: ${replace}` : ''}`);
572
+ lines.push(`- "${term}"${replace ? ` -> use: ${replace}` : ''}`);
554
573
  }
555
574
  lines.push('');
556
575
  }
@@ -589,8 +608,11 @@ function emitCompact(parsed, manifest, core, pat) {
589
608
  if (manifest.core_insight) lines.push(`# core insight: ${manifest.core_insight}`);
590
609
  lines.push('');
591
610
 
611
+ emitRequiredOutput(lines, manifest, core, pat);
612
+
592
613
  if (core.axioms?.length) {
593
- lines.push('## Axioms (reason from these)');
614
+ lines.push('## JUDGMENT GUIDANCE');
615
+ lines.push('### Axioms (reason from these)');
594
616
  for (const a of core.axioms) {
595
617
  lines.push(`- ${a.one_sentence}`);
596
618
  if (a.applies_when?.length) {
@@ -605,7 +627,7 @@ function emitCompact(parsed, manifest, core, pat) {
605
627
  }
606
628
 
607
629
  if (core.stances?.length) {
608
- lines.push('## Stances');
630
+ lines.push('### Stances');
609
631
  for (const s of core.stances) {
610
632
  const text = typeof s === 'string' ? s : s.stance;
611
633
  if (text) lines.push(`- ${text}`);
@@ -614,17 +636,18 @@ function emitCompact(parsed, manifest, core, pat) {
614
636
  }
615
637
 
616
638
  if (pat.terminology?.banned_terms?.length) {
617
- lines.push('## Banned terms (do not use even if user uses them)');
639
+ lines.push('## MUST NOT SAY');
618
640
  for (const t of pat.terminology.banned_terms) {
619
641
  const term = typeof t === 'string' ? t : t.term;
620
642
  const replace = typeof t === 'object' ? t.replace_with : null;
621
- lines.push(`- "${term}"${replace ? ` use: ${replace}` : ''}`);
643
+ lines.push(`- "${term}"${replace ? ` -> use: ${replace}` : ''}`);
622
644
  }
623
645
  lines.push('');
624
646
  }
625
647
 
626
648
  if (pat.misunderstandings?.length) {
627
- lines.push('## Misunderstandings to detect and avoid');
649
+ if (!core.axioms?.length) lines.push('## JUDGMENT GUIDANCE');
650
+ lines.push('### Misunderstandings to detect and avoid');
628
651
  for (const m of pat.misunderstandings) {
629
652
  lines.push(`- WRONG: ${m.wrong}`);
630
653
  lines.push(` CORRECT: ${m.correct}`);
@@ -634,7 +657,8 @@ function emitCompact(parsed, manifest, core, pat) {
634
657
  }
635
658
 
636
659
  if (pat.self_check?.length) {
637
- lines.push('## Self-checks (answer before final output)');
660
+ lines.push('## SELF-CHECK');
661
+ lines.push('Answer before final output.');
638
662
  for (const q of pat.self_check) {
639
663
  const text = typeof q === 'string' ? q : q.question;
640
664
  if (text) lines.push(`- ${text}`);
@@ -649,6 +673,37 @@ function emitCompact(parsed, manifest, core, pat) {
649
673
  process.stdout.write(lines.join('\n') + '\n');
650
674
  }
651
675
 
676
+ function emitRequiredOutput(lines, manifest, core, pat) {
677
+ const required = uniqueStrings([
678
+ ...asStringArray(manifest.required_output),
679
+ ...asStringArray(manifest.must_include),
680
+ ...asStringArray(core.required_output),
681
+ ...asStringArray(core.must_include),
682
+ ...asStringArray(pat.required_output),
683
+ ...asStringArray(pat.must_include),
684
+ ...asStringArray(pat.output_constraints?.required_output),
685
+ ...asStringArray(pat.output_constraints?.must_include),
686
+ ]);
687
+
688
+ if (!required.length) return;
689
+
690
+ lines.push('## REQUIRED OUTPUT');
691
+ lines.push('Include these statements when they are relevant to the user request.');
692
+ for (const item of required) lines.push(`- ${item}`);
693
+ lines.push('');
694
+ }
695
+
696
+ function asStringArray(value) {
697
+ if (!value) return [];
698
+ if (typeof value === 'string') return [value];
699
+ if (!Array.isArray(value)) return [];
700
+ return value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim());
701
+ }
702
+
703
+ function uniqueStrings(items) {
704
+ return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean)));
705
+ }
706
+
652
707
  // ─── kdna select ───────────────────────────────────────────────────────
653
708
 
654
709
  function cmdSelect(args = []) {
@@ -674,9 +729,8 @@ function cmdSelect(args = []) {
674
729
  const scores = [];
675
730
 
676
731
  for (const e of installed) {
677
- const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
732
+ const { manifest = {}, core = {} } = readContainer(e.asset_path);
678
733
  if (manifest.yanked === true) continue;
679
- const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
680
734
 
681
735
  // Check does_not_apply_when hard exclusion
682
736
  let excluded = false;
@@ -774,14 +828,15 @@ function cmdPostvalidate(args = []) {
774
828
  console.error(`Invalid name "${input}".`);
775
829
  process.exit(2);
776
830
  }
777
- const dir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
778
- if (!fs.existsSync(dir)) {
831
+ const installed = getInstalled(parsed.full);
832
+ if (!installed) {
779
833
  console.error(`${parsed.full} is not installed.`);
780
834
  process.exit(2);
781
835
  }
782
836
 
783
- const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
784
- const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
837
+ const container = readContainer(installed.asset_path);
838
+ const core = container.core || {};
839
+ const pat = container.patterns || {};
785
840
 
786
841
  // Read agent output
787
842
  let agentOutput = '';
@@ -1017,7 +1072,7 @@ function cmdRoute(taskText, args = []) {
1017
1072
  const candidates = [];
1018
1073
 
1019
1074
  for (const e of installed) {
1020
- const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
1075
+ const { manifest = {}, core = {} } = readContainer(e.asset_path);
1021
1076
  if (manifest.yanked === true) {
1022
1077
  result.rejected_domains.push({
1023
1078
  domain: manifest.name || e.full,
@@ -1027,8 +1082,6 @@ function cmdRoute(taskText, args = []) {
1027
1082
  continue;
1028
1083
  }
1029
1084
 
1030
- const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
1031
-
1032
1085
  // Negative match: does_not_apply_when
1033
1086
  let disqualified = null;
1034
1087
  for (const a of core.axioms || []) {
@@ -1181,19 +1234,16 @@ function cmdRoute(taskText, args = []) {
1181
1234
  }
1182
1235
  }
1183
1236
 
1184
- function checkTrust(domainName, options = {}) {
1237
+ function checkTrust(domainName) {
1185
1238
  const failures = [];
1186
1239
  const warnings = [];
1187
- const installed = listInstalled();
1188
- const entry = installed.find(e => `${e.scope}/${e.ident}` === domainName || e.full === domainName);
1240
+ const entry = getInstalled(domainName);
1189
1241
  if (!entry) {
1190
- failures.push('domain not found in installed directory');
1242
+ failures.push('domain asset not found in package index');
1191
1243
  return { passed: false, failures, warnings };
1192
1244
  }
1193
1245
 
1194
- const manifest = readJson(path.join(entry.dir, 'kdna.json')) || {};
1195
- const core = readJson(path.join(entry.dir, 'KDNA_Core.json')) || {};
1196
- const evolution = readJson(path.join(entry.dir, 'KDNA_Evolution.json')) || {};
1246
+ const { manifest = {}, core = {}, evolution = {} } = readContainer(entry.asset_path);
1197
1247
 
1198
1248
  // 1. Yank check
1199
1249
  if (manifest.yanked === true) {
@@ -1236,31 +1286,17 @@ function checkTrust(domainName, options = {}) {
1236
1286
 
1237
1287
  // 6. License validity (commercial domains)
1238
1288
  if (manifest.access === 'licensed' || manifest.access === 'runtime') {
1239
- const licenseStorePath = path.join(process.env.HOME || '.', '.kdna', 'licenses.json');
1240
- let licenseOk = false;
1241
- try {
1242
- if (require('fs').existsSync(licenseStorePath)) {
1243
- const store = JSON.parse(require('fs').readFileSync(licenseStorePath, 'utf8'));
1244
- const keys = store.keys || {};
1245
- for (const [hash, entry] of Object.entries(keys)) {
1246
- if (entry.active && entry.domain_id === domainName) {
1247
- if (!entry.expires_at || new Date(entry.expires_at) > new Date()) {
1248
- licenseOk = true;
1249
- break;
1250
- }
1251
- }
1252
- }
1253
- if (!licenseOk) {
1254
- warnings.push('commercial domain has no active license — install with: kdna install ' + domainName + ' --license <key>');
1255
- }
1256
- } else {
1257
- warnings.push('no license store found — commercial domain may require a license');
1258
- }
1259
- } catch { /* license check unavailable */ }
1289
+ const licenseCheck = licenseDecryptOptionsForManifest({ ...manifest, name: domainName });
1290
+ if (!licenseCheck.ok) {
1291
+ warnings.push(
1292
+ 'commercial domain has no active entitlement — run: kdna license activate ' +
1293
+ domainName +
1294
+ ' --key <license-key> --server <url>'
1295
+ );
1296
+ }
1260
1297
  }
1261
1298
 
1262
1299
  // 7. Human Lock check (judgment-class cards)
1263
- const judgmentCardTypes = ['axiom', 'boundary', 'risk'];
1264
1300
  const axioms = core.axioms || [];
1265
1301
  const hasJudgmentCards = axioms.length > 0;
1266
1302
  if (hasJudgmentCards) {