@aikdna/kdna-studio-cli 0.2.0 → 0.3.1

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.
@@ -12,13 +12,21 @@ const {
12
12
  compile: compileApi,
13
13
  quality,
14
14
  creator: creatorApi,
15
+ distillation: distillationApi,
15
16
  } = require('@aikdna/kdna-studio-core');
16
17
 
18
+ const llm = require('../src/llm');
19
+ const ai = require('../src/ai');
20
+
17
21
  const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
18
22
 
19
23
  function usage() {
20
24
  console.log(`kdna-studio — Studio-compatible KDNA authoring CLI
21
25
 
26
+ LLM (AI-powered authoring):
27
+ kdna-studio llm config [--provider <name>] [--model <name>] [--key <api-key>] [--url <base-url>]
28
+ kdna-studio llm show
29
+
22
30
  Identity:
23
31
  kdna-studio identity init [--name <display-name>]
24
32
  kdna-studio identity show
@@ -28,15 +36,39 @@ Create (three entry paths):
28
36
  kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
29
37
  kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
30
38
 
39
+ Migrate (dev source → trusted .kdna in one command):
40
+ kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign]
41
+
31
42
  Authoring:
32
- kdna-studio import <project> <source-file>
43
+ kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
44
+ kdna-studio filter <project> # check evidence for sensitive content
45
+ kdna-studio source classify <project> # classify evidence against declared target
33
46
  kdna-studio card list <project>
34
47
  kdna-studio card add <project> <type> --field key=value [--field key=value]
35
- kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]
48
+ kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign] [--passphrase <pass>]
36
49
  kdna-studio lock <project>
37
50
  kdna-studio compile <project> --out <dir>
38
51
  kdna-studio export <project> --out <file.kdna> [--sign]
52
+
53
+ AI Authoring (requires LLM config: kdna-studio llm config):
54
+ kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
55
+ kdna-studio distill <project> --candidates <file.json> # load pre-generated AI candidates
56
+ kdna-studio interview <project> [--stage <name>] # 4-stage guided AI interview
57
+ kdna-studio feynman <project> <card-id> # AI Feynman evaluation (5-dimension)
58
+ kdna-studio test <project> --input "<text>" [--preset baseline|edge|contradiction]
59
+
60
+ Distillation:
61
+ kdna-studio target declare <project> # declare distillation target interactively
62
+ kdna-studio target declare <project> --category <cat> --scope <scope> --granularity <gran> --task <task> [--include <area,area>] [--exclude <area,area>] [--load-condition <condition>]
63
+ kdna-studio target show <project> # show current distillation target
64
+ kdna-studio candidate list <project> # list candidates with scope and status
65
+ kdna-studio candidate accept <project> <candidate-id>
66
+ kdna-studio candidate reject <project> <candidate-id>
67
+ kdna-studio candidate override <project> <candidate-id> # override scope gate
68
+ kdna-studio candidate promote <project> # promote accepted+scope_fit → cards
39
69
  kdna-studio report <project>
70
+ kdna-studio install <@scope/name|file.kdna> [--trusted]
71
+ kdna-studio update <@scope/name>
40
72
 
41
73
  Project may be a directory containing studio.project.json or a project JSON file.`);
42
74
  }
@@ -157,7 +189,7 @@ function cmdCreate(args) {
157
189
  }
158
190
  } else if (fromFolder) {
159
191
  sourceMode = 'source_folder';
160
- importFromFolder(fromFolder, abs, name);
192
+ importFromFolder(fromFolder, abs, name, creatorIdentity);
161
193
  return;
162
194
  }
163
195
 
@@ -253,13 +285,14 @@ function importFromKdna(kdnaPath) {
253
285
  * Reads KDNA_*.json files and converts entries to draft judgment cards.
254
286
  * Outputs a schema audit report.
255
287
  */
256
- function importFromFolder(sourceDir, projectDir, projectName) {
288
+ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
257
289
  const absSource = path.resolve(sourceDir);
258
290
  if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
259
291
  if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
260
292
 
261
293
  const audit = { filesFound: [], cardsImported: 0, missingFields: [], schemaWarnings: [] };
262
294
  const importedCards = [];
295
+ const manifestData = {};
263
296
 
264
297
  function loadJson(filename) {
265
298
  const p = path.join(absSource, filename);
@@ -269,6 +302,17 @@ function importFromFolder(sourceDir, projectDir, projectName) {
269
302
  catch { audit.schemaWarnings.push(`${filename}: invalid JSON`); return null; }
270
303
  }
271
304
 
305
+ // Load manifest for version/languages/description
306
+ const manifest = loadJson('kdna.json');
307
+ if (manifest) {
308
+ if (manifest.version) manifestData.version = manifest.version;
309
+ if (manifest.judgment_version) manifestData.judgment_version = manifest.judgment_version;
310
+ if (manifest.languages) manifestData.languages = manifest.languages;
311
+ if (manifest.default_language) manifestData.default_language = manifest.default_language;
312
+ if (manifest.description) manifestData.description = manifest.description;
313
+ if (manifest.name) manifestData.name = manifest.name;
314
+ }
315
+
272
316
  const core = loadJson('KDNA_Core.json');
273
317
  const patterns = loadJson('KDNA_Patterns.json');
274
318
  const scenarios = loadJson('KDNA_Scenarios.json');
@@ -301,9 +345,38 @@ function importFromFolder(sourceDir, projectDir, projectName) {
301
345
  for (const r of (core.risks || core.risk_model || [])) {
302
346
  importedCards.push(cardApi.createCard('risk', r.fields || r));
303
347
  }
348
+ // NEW: import stances
349
+ for (const s of (core.stances || [])) {
350
+ const text = typeof s === 'string' ? s : s.stance || '';
351
+ importedCards.push(cardApi.createCard('stance', {
352
+ statement: text,
353
+ applies_when: s.applies_when || [],
354
+ does_not_apply_when: s.does_not_apply_when || [],
355
+ }));
356
+ }
357
+ // NEW: import frameworks
358
+ for (const fw of (core.frameworks || [])) {
359
+ importedCards.push(cardApi.createCard('framework', {
360
+ name: fw.name || '', when_to_use: fw.when_to_use || '',
361
+ steps: fw.steps || [],
362
+ }));
363
+ }
304
364
  }
305
365
 
306
366
  if (patterns) {
367
+ // NEW: import terminology
368
+ if (patterns.terminology) {
369
+ for (const t of (patterns.terminology.standard_terms || [])) {
370
+ importedCards.push(cardApi.createCard('term', {
371
+ term: t.term || '', definition: t.definition || '',
372
+ }));
373
+ }
374
+ for (const bt of (patterns.terminology.banned_terms || [])) {
375
+ importedCards.push(cardApi.createCard('banned_term', {
376
+ term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
377
+ }));
378
+ }
379
+ }
307
380
  for (const ms of (patterns.misunderstandings || [])) {
308
381
  importedCards.push(cardApi.createCard('misunderstanding', {
309
382
  wrong: ms.wrong || '', correct: ms.correct || '',
@@ -316,14 +389,70 @@ function importFromFolder(sourceDir, projectDir, projectName) {
316
389
  }
317
390
  }
318
391
 
392
+ // NEW: import scenarios
393
+ if (scenarios && scenarios.scenes) {
394
+ for (const scene of scenarios.scenes) {
395
+ importedCards.push(cardApi.createCard('scenario', {
396
+ name: scene.name || scene.id || '',
397
+ trigger: scene.trigger || '', action: scene.action || '',
398
+ }));
399
+ }
400
+ }
401
+
402
+ // NEW: import cases
403
+ if (cases && cases.cases) {
404
+ for (const c of cases.cases) {
405
+ importedCards.push(cardApi.createCard('case', {
406
+ title: c.title || c.id || '',
407
+ scenario: c.scenario || '', expected: c.expected || '',
408
+ }));
409
+ }
410
+ }
411
+
412
+ // NEW: import reasoning chains
413
+ if (reasoning && reasoning.reasoning_chains) {
414
+ for (const rc of reasoning.reasoning_chains) {
415
+ importedCards.push(cardApi.createCard('reasoning', {
416
+ name: rc.name || rc.id || '', conclusion: rc.conclusion || '',
417
+ so_what: rc.so_what || '',
418
+ }));
419
+ }
420
+ }
421
+
422
+ // NEW: import evolution stages
423
+ if (evolution && evolution.stages) {
424
+ for (const stage of evolution.stages) {
425
+ importedCards.push(cardApi.createCard('evolution_stage', {
426
+ name: stage.name || stage.id || '',
427
+ level: stage.level || '', description: stage.description || '',
428
+ }));
429
+ }
430
+ }
431
+
319
432
  audit.cardsImported = importedCards.length;
320
433
 
321
434
  const project = projectApi.createProject(projectName, 'domain', {
322
435
  sourceMode: 'source_folder',
323
436
  sourcePath: absSource,
324
- creatorIdentity: null,
325
- lineage: { type: 'migrated', parent_name: null, parent_asset_uid: null, parent_version: null, parent_asset_digest: null },
437
+ creatorIdentity: creatorIdentity || null,
438
+ lineage: { type: 'migrated', parent_name: manifestData.name || null, parent_asset_uid: null, parent_version: manifestData.version || null, parent_asset_digest: null },
326
439
  });
440
+ // Preserve manifest metadata
441
+ if (manifestData.version) {
442
+ if (!project.release) project.release = {};
443
+ project.release.version = manifestData.version;
444
+ }
445
+ if (manifestData.judgment_version) {
446
+ if (!project.release) project.release = {};
447
+ project.release.judgment_version = manifestData.judgment_version;
448
+ }
449
+ if (manifestData.description) {
450
+ if (!project.release) project.release = {};
451
+ project.release.description = manifestData.description;
452
+ }
453
+ if (manifestData.languages) project.languages = manifestData.languages;
454
+ if (manifestData.default_language) project.default_language = manifestData.default_language;
455
+
327
456
  project.cards = importedCards;
328
457
  if (project.stages?.judgment_cards) {
329
458
  project.stages.judgment_cards.total = importedCards.length;
@@ -333,6 +462,7 @@ function importFromFolder(sourceDir, projectDir, projectName) {
333
462
  fs.mkdirSync(projectDir, { recursive: true });
334
463
  writeProject(path.join(projectDir, 'studio.project.json'), project);
335
464
  console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
465
+ return { project, projectPath: path.join(projectDir, 'studio.project.json'), audit };
336
466
  }
337
467
 
338
468
  /**
@@ -380,17 +510,85 @@ function readZipEntries(buf) {
380
510
  return entries;
381
511
  }
382
512
 
513
+ const aiFilter = require('../src/ai/filter');
514
+
383
515
  function cmdImport(args) {
384
516
  const [projectInput, source] = args;
385
- if (!projectInput || !source) fail('Usage: kdna-studio import <project> <source-file>');
517
+ if (!projectInput || !source) fail('Usage: kdna-studio import <project> <source-file-or-dir>');
386
518
  const { projectPath, project } = readProject(projectInput);
519
+
387
520
  const sourcePath = path.resolve(source);
388
- if (!fs.existsSync(sourcePath)) fail(`Source file not found: ${sourcePath}`);
389
- const content = fs.readFileSync(sourcePath, 'utf8');
390
- const evidence = evidenceApi.createEvidenceEntry('text', path.basename(sourcePath), content, sourcePath);
391
- evidenceApi.addEvidence(project, evidence);
521
+ if (!fs.existsSync(sourcePath)) fail(`Source not found: ${sourcePath}`);
522
+
523
+ const TEXT_EXTS = new Set(['.txt', '.md', '.json', '.jsonl', '.yaml', '.yml', '.csv', '.log', '.srt', '.vtt', '.html', '.xml', '.rst']);
524
+ const BINARY_EXTS = new Set(['.pdf', '.docx', '.rtf']);
525
+ let imported = 0;
526
+ let skipped = 0;
527
+
528
+ function importFile(filePath) {
529
+ const ext = path.extname(filePath).toLowerCase();
530
+ const name = path.basename(filePath);
531
+
532
+ if (BINARY_EXTS.has(ext)) {
533
+ console.warn(`Skipping binary format (not yet supported in CLI): ${name}`);
534
+ skipped++;
535
+ return;
536
+ }
537
+
538
+ let content;
539
+ try { content = fs.readFileSync(filePath, 'utf8'); } catch { console.warn(`Cannot read: ${name}`); skipped++; return; }
540
+
541
+ if (content.length > 120000) {
542
+ console.warn(`File too large (${content.length} chars, max 120000): ${name}`);
543
+ skipped++;
544
+ return;
545
+ }
546
+
547
+ const evidence = evidenceApi.createEvidenceEntry('text', name, content, filePath);
548
+ evidenceApi.addEvidence(project, evidence);
549
+ imported++;
550
+ }
551
+
552
+ if (fs.statSync(sourcePath).isDirectory()) {
553
+ const files = fs.readdirSync(sourcePath).map(f => path.join(sourcePath, f)).filter(f => fs.statSync(f).isFile());
554
+ for (const f of files) {
555
+ if (imported >= 300) { console.warn('Reached 300 file limit.'); break; }
556
+ importFile(f);
557
+ }
558
+ } else {
559
+ importFile(sourcePath);
560
+ }
561
+
392
562
  writeProject(projectPath, project);
393
- console.log(`Imported evidence: ${evidence.id}`);
563
+
564
+ // Check for sensitive content
565
+ const evidenceList = project.evidence_materials || [];
566
+ const flagged = aiFilter.checkEvidence(evidenceList);
567
+ if (flagged.length > 0) {
568
+ console.warn(`\nSensitive content detected in ${flagged.length} file(s):`);
569
+ for (const f of flagged) console.warn(` ${f.filename}: ${f.flagged.join(', ')}`);
570
+ }
571
+
572
+ console.log(`Imported: ${imported} file(s)${skipped > 0 ? `, ${skipped} skipped` : ''}`);
573
+ }
574
+
575
+ function cmdFilter(args) {
576
+ const projectInput = args[0];
577
+ if (!projectInput) fail('Usage: kdna-studio filter <project>');
578
+ const { project } = readProject(projectInput);
579
+ const evidenceList = project.evidence_materials || [];
580
+ if (evidenceList.length === 0) { console.log('No evidence to filter.'); return; }
581
+
582
+ const flagged = aiFilter.checkEvidence(evidenceList);
583
+ if (flagged.length === 0) {
584
+ console.log(`All ${evidenceList.length} evidence file(s) passed sensitive content filter.`);
585
+ } else {
586
+ console.log(`Sensitive content detected in ${flagged.length}/${evidenceList.length} file(s):`);
587
+ for (const f of flagged) {
588
+ console.log(` ${f.filename}: ${f.flagged.join(', ')}`);
589
+ for (const [domain, match] of Object.entries(f.matches)) console.log(` ${domain}: "${match}"`);
590
+ }
591
+ }
394
592
  }
395
593
 
396
594
  function cmdCard(args) {
@@ -420,46 +618,66 @@ function cmdCard(args) {
420
618
  if (sub === 'approve') {
421
619
  const projectInput = args[1];
422
620
  const cardId = args[2];
423
- if (!projectInput || !cardId) {
621
+ const approveAll = args.includes('--all');
622
+ if (!projectInput || (!cardId && !approveAll)) {
424
623
  fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]');
425
624
  }
426
625
  const by = option(args, '--by');
427
626
  const statement = option(args, '--statement');
428
627
  if (!by || !statement) fail('card approve requires --by and --statement');
429
628
  const { projectPath, project } = readProject(projectInput);
430
- const idx = (project.cards || []).findIndex((c) => c.id === cardId);
431
- if (idx < 0) fail(`Card not found: ${cardId}`);
432
- let card = project.cards[idx];
433
- if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
434
629
 
435
- const lockPayload = {
436
- by,
437
- statement,
438
- checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
630
+ const lockOneCard = (card) => {
631
+ if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
632
+ const lockPayload = {
633
+ by,
634
+ statement,
635
+ checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
636
+ };
637
+ if (args.includes('--sign')) {
638
+ const identity = creatorApi.loadIdentity();
639
+ if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
640
+ lockPayload.creator_id = identity.creator_id;
641
+ const passphrase = option(args, '--passphrase');
642
+ try {
643
+ if (identity.encrypted && !passphrase) {
644
+ fail('Private key is encrypted — provide --passphrase to sign.', EXIT.TRUST_FAILED);
645
+ }
646
+ lockPayload.signature = creatorApi.signHumanLock(
647
+ card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
648
+ );
649
+ } catch (e) {
650
+ fail(`Failed to sign Human Lock: ${e.message}`, EXIT.TRUST_FAILED);
651
+ }
652
+ }
653
+ return [card, cardApi.lockCard(card, lockPayload)];
439
654
  };
440
655
 
441
- // Sign Human Lock with creator identity if --sign is passed
442
- if (args.includes('--sign')) {
443
- const identity = creatorApi.loadIdentity();
444
- if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
445
- lockPayload.creator_id = identity.creator_id;
446
- try {
447
- lockPayload.signature = creatorApi.signHumanLock(
448
- cardId, statement, cardApi.cardJudgmentFingerprint(card),
449
- );
450
- } catch (e) {
451
- fail(`Failed to sign Human Lock: ${e.message}`, EXIT.TRUST_FAILED);
656
+ if (cardId) {
657
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
658
+ if (idx < 0) fail(`Card not found: ${cardId}`);
659
+ const [, locked] = lockOneCard(project.cards[idx]);
660
+ project.cards[idx] = locked;
661
+ const signed = args.includes('--sign') ? ' (signed)' : '';
662
+ console.log(`Approved and Human Locked: ${cardId}${signed}`);
663
+ } else {
664
+ let approved = 0;
665
+ for (let i = 0; i < (project.cards || []).length; i++) {
666
+ const card = project.cards[i];
667
+ if (card.locked) continue;
668
+ const [, locked] = lockOneCard(card);
669
+ project.cards[i] = locked;
670
+ approved++;
452
671
  }
672
+ if (approved === 0) fail('No unlocked cards to approve.');
673
+ const signed = args.includes('--sign') ? ' (signed)' : '';
674
+ console.log(`Approved and Human Locked: ${approved} cards${signed}`);
453
675
  }
454
-
455
- project.cards[idx] = cardApi.lockCard(card, lockPayload);
456
676
  if (project.stages?.judgment_cards) {
457
677
  project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
458
678
  project.stages.judgment_cards.total = project.cards.length;
459
679
  }
460
680
  writeProject(projectPath, project);
461
- const signed = lockPayload.signature ? ' (signed)' : '';
462
- console.log(`Approved and Human Locked: ${cardId}${signed}`);
463
681
  return;
464
682
  }
465
683
  fail('Usage: kdna-studio card <list|add|approve> ...');
@@ -476,6 +694,83 @@ function cmdLock(args) {
476
694
  console.log(`Human Lock Gate passed: ${gate.lockedJudgmentCards} locked judgment cards`);
477
695
  }
478
696
 
697
+ function cmdMigrate(args) {
698
+ const sourceDir = args[0];
699
+ const out = option(args, '--out');
700
+ const by = option(args, '--by');
701
+ const statement = option(args, '--statement');
702
+ const name = option(args, '--name') || path.basename(path.resolve(sourceDir || '.'));
703
+ if (!sourceDir || !out || !by || !statement) {
704
+ fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pass>]');
705
+ }
706
+
707
+ // Step 1: create temp project and import everything from dev source
708
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
709
+ let creatorIdentity = null;
710
+ try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
711
+ const { project } = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
712
+
713
+ // Step 2: approve and Human Lock all cards
714
+ let locked = 0;
715
+ for (let i = 0; i < (project.cards || []).length; i++) {
716
+ let card = project.cards[i];
717
+ if (card.locked) { locked++; continue; }
718
+ if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
719
+ const lockPayload = {
720
+ by,
721
+ statement,
722
+ checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
723
+ };
724
+ if (args.includes('--sign')) {
725
+ const identity = creatorApi.loadIdentity();
726
+ if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
727
+ lockPayload.creator_id = identity.creator_id;
728
+ const passphrase = option(args, '--passphrase');
729
+ try {
730
+ lockPayload.signature = creatorApi.signHumanLock(
731
+ card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
732
+ );
733
+ } catch (e) {
734
+ fail(`Failed to sign Human Lock for ${card.id}: ${e.message}`, EXIT.TRUST_FAILED);
735
+ }
736
+ }
737
+ project.cards[i] = cardApi.lockCard(card, lockPayload);
738
+ locked++;
739
+ }
740
+ if (project.stages?.judgment_cards) {
741
+ project.stages.judgment_cards.locked = locked;
742
+ project.stages.judgment_cards.total = project.cards.length;
743
+ }
744
+ writeProject(path.join(tmpDir, 'studio.project.json'), project);
745
+ console.log(`Approved and Human Locked: ${locked} cards`);
746
+
747
+ // Step 3: verify Human Lock gate
748
+ const gate = projectApi.checkHumanLockGate(project);
749
+ if (gate.blocked) {
750
+ const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
751
+ fail(`Human Lock Gate blocked: ${reasons}`, EXIT.HUMAN_LOCK_REQUIRED);
752
+ }
753
+
754
+ // Step 4: compile and export as .kdna
755
+ const { result } = compileProject(tmpDir);
756
+ const files = { ...result.files };
757
+ files['README.md'] = compileApi.generateReadme(project);
758
+ files.LICENSE = project.license?.type || 'UNSPECIFIED';
759
+ files.mimetype = 'application/vnd.aikdna.kdna+zip';
760
+
761
+ if (args.includes('--sign')) applySignature(files, option(args, '--passphrase'));
762
+ const absOut = path.resolve(out);
763
+ buildZip(files, absOut);
764
+ console.log(`Exported: ${absOut}`);
765
+ console.log(` Name: ${name}`);
766
+ console.log(` Cards: ${project.cards.length} (${locked} locked)`);
767
+ console.log(` Files: ${Object.keys(files).length}`);
768
+ console.log(` Build ID: ${result.identity.build_id}`);
769
+
770
+ // Cleanup temp
771
+ try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
772
+ }
773
+
479
774
  function compileProject(projectInput) {
480
775
  const { project } = readProject(projectInput);
481
776
  const gate = projectApi.checkHumanLockGate(project);
@@ -623,7 +918,7 @@ function publicKeyFingerprint(publicKeyPem) {
623
918
  return `ed25519:${crypto.createHash('sha256').update(publicKeyPem).digest('hex')}`;
624
919
  }
625
920
 
626
- function applySignature(files) {
921
+ function applySignature(files, passphrase = null) {
627
922
  const paths = identityPaths();
628
923
  if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
629
924
  fail('Signing requires KDNA identity keys. Run: kdna identity init', EXIT.TRUST_FAILED);
@@ -636,7 +931,13 @@ function applySignature(files) {
636
931
  files['kdna.json'] = JSON.stringify(manifest, null, 2);
637
932
 
638
933
  const payload = canonicalPayload(files);
639
- const privateKeyPem = fs.readFileSync(paths.privateKey, 'utf8');
934
+ let privateKeyPem = fs.readFileSync(paths.privateKey, 'utf8');
935
+ if (creatorApi.isEncryptedKey(privateKeyPem)) {
936
+ if (!passphrase) {
937
+ fail('Private key is encrypted. Export with: kdna-studio export ... --sign --passphrase <your-passphrase>', EXIT.TRUST_FAILED);
938
+ }
939
+ privateKeyPem = creatorApi.decryptPrivateKey(privateKeyPem, passphrase);
940
+ }
640
941
  manifest.signature = `ed25519:${crypto.sign(null, Buffer.from(payload), privateKeyPem).toString('hex')}`;
641
942
  files['kdna.json'] = JSON.stringify(manifest, null, 2);
642
943
  }
@@ -673,7 +974,7 @@ function cmdExport(args) {
673
974
  provenance.content_fingerprint = finalDigest;
674
975
  files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
675
976
 
676
- if (args.includes('--sign')) applySignature(files);
977
+ if (args.includes('--sign')) applySignature(files, option(args, '--passphrase'));
677
978
 
678
979
  const entries = [['mimetype', files.mimetype]];
679
980
  for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
@@ -700,18 +1001,80 @@ function cmdExport(args) {
700
1001
  console.log(`Build ID: ${result.identity.build_id}`);
701
1002
  }
702
1003
 
1004
+ function cmdStudioInstall(args) {
1005
+ // Delegate to kdna CLI for runtime install operations
1006
+ const target = args[0];
1007
+ if (!target) fail('Usage: kdna-studio install <@scope/name|file.kdna>');
1008
+ try {
1009
+ const kdnaArgs = ['install', target, '--yes'];
1010
+ if (args.includes('--trusted')) kdnaArgs.push('--trusted');
1011
+ const result = require('child_process').spawnSync('kdna', kdnaArgs, {
1012
+ stdio: 'inherit', encoding: 'utf8',
1013
+ });
1014
+ if (result.status !== 0) process.exit(result.status);
1015
+ } catch {
1016
+ fail('kdna CLI not found. Install with: npm install -g @aikdna/kdna-cli');
1017
+ }
1018
+ }
1019
+
1020
+ function cmdStudioUpdate(args) {
1021
+ const target = args[0];
1022
+ if (!target) fail('Usage: kdna-studio update <@scope/name>');
1023
+ try {
1024
+ const result = require('child_process').spawnSync('kdna', ['update', target], {
1025
+ stdio: 'inherit', encoding: 'utf8',
1026
+ });
1027
+ if (result.status !== 0) process.exit(result.status);
1028
+ } catch {
1029
+ fail('kdna CLI not found. Install with: npm install -g @aikdna/kdna-cli');
1030
+ }
1031
+ }
1032
+
1033
+ function cmdLlm(args) {
1034
+ const sub = args[0];
1035
+ if (sub === 'show') {
1036
+ const cfg = llm.config();
1037
+ console.log(`Provider : ${cfg.provider || '(not set)'}`);
1038
+ console.log(`Model : ${cfg.model || '(not set)'}`);
1039
+ console.log(`Base URL : ${cfg.baseURL || '(not set)'}`);
1040
+ console.log(`API Key : ${cfg.apiKey ? '********' + cfg.apiKey.slice(-4) : '(not set)'}`);
1041
+ return;
1042
+ }
1043
+ if (sub === 'config') {
1044
+ const provider = option(args, '--provider') || option(args, '-p');
1045
+ const model = option(args, '--model') || option(args, '-m');
1046
+ const apiKey = option(args, '--key') || option(args, '-k');
1047
+ const baseURL = option(args, '--url') || option(args, '-u');
1048
+ const updates = {};
1049
+ if (provider) updates.provider = provider;
1050
+ if (model) updates.model = model;
1051
+ if (apiKey) updates.apiKey = apiKey;
1052
+ if (baseURL) updates.baseURL = baseURL;
1053
+ if (Object.keys(updates).length === 0) fail('Usage: kdna-studio llm config --provider <name> [--model <name>] [--key <api-key>] [--url <base-url>]');
1054
+ const cfg = llm.configure(updates);
1055
+ console.log(JSON.stringify({ provider: cfg.provider, model: cfg.model, baseURL: cfg.baseURL }, null, 2));
1056
+ return;
1057
+ }
1058
+ fail('Usage: kdna-studio llm <config|show>');
1059
+ }
1060
+
703
1061
  function cmdIdentity(args) {
704
1062
  const sub = args[0];
705
1063
  if (sub === 'init') {
706
1064
  const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
1065
+ const passphrase = option(args, '--passphrase');
707
1066
  try {
708
- const identity = creatorApi.initIdentity(name);
1067
+ const identity = creatorApi.initIdentity(name, null, passphrase);
709
1068
  console.log(`Creator identity initialized:`);
710
1069
  console.log(` creator_id: ${identity.creator_id}`);
711
1070
  console.log(` display_name: ${identity.display_name}`);
712
1071
  console.log(` identity_dir: ${identity.identity_dir}`);
713
1072
  console.log(` public_key saved: ${identity.public_key_path}`);
1073
+ console.log(` encrypted: ${identity.encrypted}`);
714
1074
  console.log(`\nBack up your private key at: ${creatorApi.privateKeyPath()}`);
1075
+ if (identity.encrypted) {
1076
+ console.log(` Private key is encrypted — keep your passphrase safe.`);
1077
+ }
715
1078
  } catch (err) {
716
1079
  fail(`Identity already exists. Use 'kdna-studio identity show' to view. ${err.message}`);
717
1080
  }
@@ -739,6 +1102,269 @@ function cmdReport(args) {
739
1102
  process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
740
1103
  }
741
1104
 
1105
+ // ─── Distillation Commands ──────────────────────────────────────────
1106
+
1107
+ function cmdTarget(args) {
1108
+ const sub = args[0];
1109
+ if (sub === 'declare') {
1110
+ const projectInput = args[1];
1111
+ if (!projectInput) fail('Usage: kdna-studio target declare <project> --category <cat> --scope <scope> --granularity <gran> --task <task>');
1112
+ const { projectPath, project } = readProject(projectInput);
1113
+ const category = option(args, '--category', 'expression_writing');
1114
+ const scope = option(args, '--scope', 'personal');
1115
+ const granularity = option(args, '--granularity', 'core_principles');
1116
+ const taskScope = option(args, '--task', 'general content creation');
1117
+ const include = option(args, '--include');
1118
+ const exclude = option(args, '--exclude');
1119
+ const loadCondition = option(args, '--load-condition');
1120
+ const domainName = project.name || path.basename(path.dirname(projectPath), '.json');
1121
+
1122
+ const target = distillationApi.createDistillationTarget({
1123
+ domainName,
1124
+ domainCategory: category,
1125
+ ownerScope: scope,
1126
+ granularity,
1127
+ taskScope,
1128
+ includeAreas: include ? include.split(',').map(s => s.trim()).filter(Boolean) : [],
1129
+ excludeAreas: exclude ? exclude.split(',').map(s => s.trim()).filter(Boolean) : [],
1130
+ loadCondition: loadCondition || '',
1131
+ });
1132
+
1133
+ const validation = distillationApi.validateDistillationTarget(target);
1134
+ if (!validation.valid) fail(`Invalid target: ${validation.errors.join('; ')}`);
1135
+
1136
+ project.distillation_target = target;
1137
+ writeProject(projectPath, project);
1138
+ console.log(JSON.stringify({ declared: true, target }, null, 2));
1139
+ return;
1140
+ }
1141
+ if (sub === 'show') {
1142
+ const { project } = readProject(args[1]);
1143
+ const target = project.distillation_target;
1144
+ if (!target) fail('No distillation target declared. Run: kdna-studio target declare <project>');
1145
+ console.log(JSON.stringify(target, null, 2));
1146
+ return;
1147
+ }
1148
+ fail('Usage: kdna-studio target <declare|show>');
1149
+ }
1150
+
1151
+ function cmdSourceClassify(args) {
1152
+ const projectInput = args[0];
1153
+ if (!projectInput) fail('Usage: kdna-studio source classify <project>');
1154
+ const { projectPath, project } = readProject(projectInput);
1155
+ const target = project.distillation_target;
1156
+ if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
1157
+
1158
+ const evidence = project.evidence || [];
1159
+ const results = evidence.map(e => {
1160
+ const text = (e.content || e.title || '').toLowerCase();
1161
+ const domainWords = (target.include_areas || []).concat([target.domain_category, target.task_scope]);
1162
+ const hitCount = domainWords.filter(w => text.includes(w.toLowerCase())).length;
1163
+ let relevance;
1164
+ if (hitCount >= 2) relevance = 'relevant';
1165
+ else if (hitCount === 1) relevance = 'weakly_relevant';
1166
+ else {
1167
+ const otherHits = Object.keys(distillationApi.DOMAIN_CATEGORIES)
1168
+ .filter(k => k !== target.domain_category)
1169
+ .filter(k => text.includes(k)).length;
1170
+ relevance = otherHits > 0 ? 'split_domain' : 'weakly_relevant';
1171
+ }
1172
+ return { id: e.id, title: e.title, relevance };
1173
+ });
1174
+
1175
+ const summary = {};
1176
+ for (const r of results) { summary[r.relevance] = (summary[r.relevance] || 0) + 1; }
1177
+
1178
+ project.distillation_evidence_relevance = results;
1179
+ writeProject(projectPath, project);
1180
+ console.log(JSON.stringify({ summary, results }, null, 2));
1181
+ }
1182
+
1183
+ async function cmdDistill(args) {
1184
+ const projectInput = args[0];
1185
+ const candidatesFile = option(args, '--candidates');
1186
+ const useAI = args.includes('--ai');
1187
+ const provider = option(args, '--provider');
1188
+ const model = option(args, '--model');
1189
+ const apiKey = option(args, '--key');
1190
+ if (!projectInput) fail('Usage: kdna-studio distill <project> [--ai] [--candidates <file.json>]');
1191
+ if (!candidatesFile && !useAI) fail('Either --ai or --candidates <file.json> required.');
1192
+
1193
+ const { projectPath, project } = readProject(projectInput);
1194
+ const target = project.distillation_target;
1195
+ if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
1196
+
1197
+ let rawCandidates;
1198
+ if (useAI) {
1199
+ const evidence = (project.evidence_materials || []).map(e => ({ filename: e.filename || e.name, content: e.raw_text || e.content || '' }));
1200
+ if (evidence.length === 0) console.warn('No evidence materials found. AI distillation works best with imported evidence. Run: kdna-studio import <project> <file>');
1201
+
1202
+ console.log('Running AI distillation...');
1203
+ rawCandidates = await ai.distill(llm.config(), evidence, target, { provider, model, apiKey });
1204
+ console.log(`AI extracted ${rawCandidates.length} candidates.`);
1205
+ } else {
1206
+ const rawPath = path.resolve(candidatesFile);
1207
+ if (!fs.existsSync(rawPath)) fail(`Candidates file not found: ${rawPath}`);
1208
+ rawCandidates = JSON.parse(fs.readFileSync(rawPath, 'utf8'));
1209
+ if (!Array.isArray(rawCandidates)) fail('Candidates file must contain a JSON array');
1210
+ }
1211
+
1212
+ const candidates = rawCandidates.map(c => {
1213
+ const base = {
1214
+ id: c.candidate_id || c.id || `cand_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1215
+ one_sentence: c.one_sentence || c.oneSentence || '',
1216
+ full_statement: c.full_statement || c.fullStatement || '',
1217
+ suggested_card_type: c.type || c.suggested_card_type || 'axiom',
1218
+ supporting_evidence_ids: (c.evidence_ids || c.evidenceIds || c.supporting_evidence_ids || []).map(String),
1219
+ confidence: c.confidence || 'medium',
1220
+ candidate_status: 'proposed',
1221
+ scope_fit: c.scope_fit ?? c.scopeFit ?? true,
1222
+ };
1223
+ return distillationApi.applyScopeGate(base, target);
1224
+ });
1225
+
1226
+ const summary = distillationApi.candidateStatusSummary(candidates);
1227
+ project.distillation_candidates = candidates;
1228
+ writeProject(projectPath, project);
1229
+ console.log(JSON.stringify({ summary, count: candidates.length }, null, 2));
1230
+ }
1231
+
1232
+ function cmdCandidate(args) {
1233
+ const sub = args[0];
1234
+ const projectInput = args[1];
1235
+ if (!sub || !projectInput) fail('Usage: kdna-studio candidate <list|accept|reject|override|promote>');
1236
+
1237
+ const { projectPath, project } = readProject(projectInput);
1238
+ const candidates = project.distillation_candidates || [];
1239
+
1240
+ if (sub === 'list') {
1241
+ for (const c of candidates) {
1242
+ const scopeMarker = c.scope_fit ? '' : ' [OUT_OF_SCOPE]';
1243
+ const split = c.suggested_split_domain ? ` → ${c.suggested_split_domain}` : '';
1244
+ console.log(`${c.id}\t${c.candidate_status}\t${c.suggested_card_type}\t${c.confidence}\t${c.one_sentence.substring(0,60)}${scopeMarker}${split}`);
1245
+ }
1246
+ console.log(JSON.stringify(distillationApi.candidateStatusSummary(candidates), null, 2));
1247
+ return;
1248
+ }
1249
+
1250
+ if (sub === 'accept' || sub === 'reject') {
1251
+ const candId = args[2];
1252
+ if (!candId) fail(`Usage: kdna-studio candidate ${sub} <project> <candidate-id>`);
1253
+ const idx = candidates.findIndex(c => c.id === candId);
1254
+ if (idx < 0) fail(`Candidate not found: ${candId}`);
1255
+ candidates[idx].candidate_status = sub === 'accept' ? 'accepted' : 'rejected';
1256
+ project.distillation_candidates = candidates;
1257
+ writeProject(projectPath, project);
1258
+ console.log(`${sub === 'accept' ? 'Accepted' : 'Rejected'}: ${candId}`);
1259
+ return;
1260
+ }
1261
+
1262
+ if (sub === 'override') {
1263
+ const candId = args[2];
1264
+ if (!candId) fail('Usage: kdna-studio candidate override <project> <candidate-id>');
1265
+ const idx = candidates.findIndex(c => c.id === candId);
1266
+ if (idx < 0) fail(`Candidate not found: ${candId}`);
1267
+ candidates[idx].scope_fit = true;
1268
+ candidates[idx].domain_relevance_score = 50;
1269
+ candidates[idx].relevance_evidence = 'User explicitly overrode scope gate via CLI';
1270
+ candidates[idx].suggested_split_domain = null;
1271
+ project.distillation_candidates = candidates;
1272
+ writeProject(projectPath, project);
1273
+ console.log(`Scope gate overridden: ${candId}`);
1274
+ return;
1275
+ }
1276
+
1277
+ if (sub === 'promote') {
1278
+ const accepted = candidates.filter(c => c.candidate_status === 'accepted' && c.scope_fit !== false);
1279
+ if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
1280
+
1281
+ for (const c of accepted) {
1282
+ const card = cardApi.createCard(c.suggested_card_type, {
1283
+ one_sentence: c.one_sentence,
1284
+ full_statement: c.full_statement,
1285
+ why: '',
1286
+ applies_when: '',
1287
+ does_not_apply_when: '',
1288
+ failure_risk: '',
1289
+ });
1290
+ card.evidence_refs = c.supporting_evidence_ids;
1291
+ project.cards = project.cards || [];
1292
+ project.cards.push(card);
1293
+ }
1294
+ if (project.stages?.judgment_cards) {
1295
+ project.stages.judgment_cards.total = project.cards.length;
1296
+ project.stages.judgment_cards.status = 'in_progress';
1297
+ }
1298
+ writeProject(projectPath, project);
1299
+ console.log(`Promoted ${accepted.length} candidates → ${accepted.length} Studio cards`);
1300
+ return;
1301
+ }
1302
+
1303
+ fail('Usage: kdna-studio candidate <list|accept|reject|override|promote>');
1304
+ }
1305
+
1306
+ async function cmdInterview(args) {
1307
+ const projectInput = args[0];
1308
+ const stage = option(args, '--stage');
1309
+ if (!projectInput) fail('Usage: kdna-studio interview <project> [--stage distill|clarify|correct|replay]');
1310
+ const { project } = readProject(projectInput);
1311
+
1312
+ if (stage) {
1313
+ const result = await ai.interview.runInterview(llm.config(), project, stage, '', {});
1314
+ console.log(`\n=== ${result.label} ===\n${result.content}`);
1315
+ } else {
1316
+ await ai.interview.runInterviewInteractive(llm.config(), project, {});
1317
+ }
1318
+ }
1319
+
1320
+ async function cmdFeynman(args) {
1321
+ const projectInput = args[0];
1322
+ const cardId = args[1];
1323
+ if (!projectInput || !cardId) fail('Usage: kdna-studio feynman <project> <card-id>');
1324
+ const { projectPath, project } = readProject(projectInput);
1325
+ const cardIdx = (project.cards || []).findIndex(c => c.id === cardId);
1326
+ if (cardIdx < 0) fail(`Card not found: ${cardId}`);
1327
+ const card = project.cards[cardIdx];
1328
+
1329
+ if (!card.feynman_text) fail(`Card ${cardId} has no Feynman restatement. Use card approve first with --statement to set it.`);
1330
+
1331
+ console.log(`Evaluating Feynman restatement for card: ${cardId}`);
1332
+ const result = await ai.feynman.evaluate(llm.config(), card, {});
1333
+ const score = result.score || 0;
1334
+ const passed = score >= 4;
1335
+
1336
+ console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}`);
1337
+ for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
1338
+ const r = (result.criteria || {})[criterion] ? '✓' : '✗';
1339
+ console.log(` ${r} ${criterion}`);
1340
+ }
1341
+ if (result.suggestions && result.suggestions.length > 0) {
1342
+ console.log('\nSuggestions:');
1343
+ result.suggestions.forEach(s => console.log(` - ${s}`));
1344
+ }
1345
+
1346
+ card.feynman_evaluation = { score, criteria: result.criteria, suggestions: result.suggestions, evaluated_at: new Date().toISOString() };
1347
+ project.cards[cardIdx] = card;
1348
+ writeProject(projectPath, project);
1349
+ }
1350
+
1351
+ async function cmdTest(args) {
1352
+ const projectInput = args[0];
1353
+ const input = option(args, '--input');
1354
+ const preset = option(args, '--preset', 'baseline');
1355
+ if (!projectInput || !input) fail('Usage: kdna-studio test <project> --input "<text>" [--preset baseline|edge|contradiction]');
1356
+ const { project } = readProject(projectInput);
1357
+ const domainName = project.name || 'Untitled Project';
1358
+ const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
1359
+
1360
+ console.log(`Testing: ${domainName} [${preset}]`);
1361
+ const prompt = preset === 'baseline' ? `Test the core judgment: ${input}`
1362
+ : preset === 'edge' ? `Test a boundary case: ${input}`
1363
+ : `Test for contradiction: ${input}`;
1364
+ const result = await ai.testlab.compare(llm.config(), domainName, prompt, domainPrompt, {});
1365
+ console.log(JSON.stringify(result, null, 2));
1366
+ }
1367
+
742
1368
  const args = process.argv.slice(2);
743
1369
  const cmd = args[0];
744
1370
  if (!cmd || cmd === '--help' || cmd === '-h') {
@@ -746,15 +1372,28 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
746
1372
  process.exit(EXIT.OK);
747
1373
  }
748
1374
 
1375
+ (async () => {
749
1376
  try {
750
1377
  if (cmd === 'create') cmdCreate(args.slice(1));
1378
+ else if (cmd === 'migrate') cmdMigrate(args.slice(1));
751
1379
  else if (cmd === 'import') cmdImport(args.slice(1));
1380
+ else if (cmd === 'filter') cmdFilter(args.slice(1));
1381
+ else if (cmd === 'target') cmdTarget(args.slice(1));
1382
+ else if (cmd === 'source') cmdSourceClassify(args.slice(1));
1383
+ else if (cmd === 'distill') await cmdDistill(args.slice(1));
1384
+ else if (cmd === 'candidate') cmdCandidate(args.slice(1));
1385
+ else if (cmd === 'interview') await cmdInterview(args.slice(1));
1386
+ else if (cmd === 'feynman') await cmdFeynman(args.slice(1));
1387
+ else if (cmd === 'test') await cmdTest(args.slice(1));
752
1388
  else if (cmd === 'card') cmdCard(args.slice(1));
753
1389
  else if (cmd === 'lock') cmdLock(args.slice(1));
754
1390
  else if (cmd === 'compile') cmdCompile(args.slice(1));
755
1391
  else if (cmd === 'export') cmdExport(args.slice(1));
1392
+ else if (cmd === 'llm') cmdLlm(args.slice(1));
756
1393
  else if (cmd === 'identity') cmdIdentity(args.slice(1));
757
1394
  else if (cmd === 'report') cmdReport(args.slice(1));
1395
+ else if (cmd === 'install') cmdStudioInstall(args.slice(1));
1396
+ else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
758
1397
  else {
759
1398
  usage();
760
1399
  fail(`Unknown command: ${cmd}`);
@@ -762,3 +1401,4 @@ try {
762
1401
  } catch (err) {
763
1402
  fail(err.message || String(err));
764
1403
  }
1404
+ })();