@aikdna/kdna-cli 0.26.1 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,19 +51,19 @@ Successful validation returns:
51
51
 
52
52
  ## Core Commands
53
53
 
54
- | Command | Purpose |
55
- |---|---|
56
- | `kdna demo minimal <dir>` | Create a minimal v1 source directory |
57
- | `kdna inspect <path>` | Inspect a v1 source dir or `.kdna` container |
58
- | `kdna validate <path>` | Validate format, schema, payload, checksums, and load contract |
59
- | `kdna plan-load <path> --json` | Return the Core LoadPlan before runtime load |
60
- | `kdna plan-load <path> --json --has-password` | Diagnose password-authorized load state |
61
- | `kdna plan-load <path> --json --entitlement-status active` | Diagnose receipt/entitlement load state |
62
- | `kdna pack <source-dir> <output.kdna>` | Pack a v1 source directory |
63
- | `kdna unpack <input.kdna> <output-dir>` | Unpack a v1 container |
64
- | `kdna load <path> --profile=<index|compact|scenario|full> --as=<json|prompt>` | Render judgment context for agents or tools |
65
- | `kdna setup` | Install the `kdna-loader` skill for supported agents |
66
- | `kdna doctor --agents` | Check agent loader installation |
54
+ | Command | Purpose |
55
+ | ---------------------------------------------------------- | -------------------------------------------------------------- | -------- | ---------------- | -------- | ------------------------------------------- |
56
+ | `kdna demo minimal <dir>` | Create a minimal v1 source directory |
57
+ | `kdna inspect <path>` | Inspect a v1 source dir or `.kdna` container |
58
+ | `kdna validate <path>` | Validate format, schema, payload, checksums, and load contract |
59
+ | `kdna plan-load <path> --json` | Return the Core LoadPlan before runtime load |
60
+ | `kdna plan-load <path> --json --has-password` | Diagnose password-authorized load state |
61
+ | `kdna plan-load <path> --json --entitlement-status active` | Diagnose receipt/entitlement load state |
62
+ | `kdna pack <source-dir> <output.kdna>` | Pack a v1 source directory |
63
+ | `kdna unpack <input.kdna> <output-dir>` | Unpack a v1 container |
64
+ | `kdna load <path> --profile=<index | compact | scenario | full> --as=<json | prompt>` | Render judgment context for agents or tools |
65
+ | `kdna setup` | Install the `kdna-loader` skill for supported agents |
66
+ | `kdna doctor --agents` | Check agent loader installation |
67
67
 
68
68
  ## Producer Path
69
69
 
@@ -3,4 +3,4 @@
3
3
  "manifest_digest": "sha256:fae062dbca0eb8878eaf28eabeed732a6827443056eb66fb27f3961307630af7",
4
4
  "payload_digest": "sha256:593109dd2aaf3fcc7ab9f352d5cd4476596ab9db5850175824e0619796088caf",
5
5
  "asset_digest": "sha256:placeholder"
6
- }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-cli",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
4
4
  "description": "KDNA CLI — runtime control plane for verifying, installing, loading, comparing, publishing, and auditing existing .kdna assets.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -31,7 +31,8 @@
31
31
  "test:v1": "node --test tests/v1-global-cli.test.js tests/v1-demo-minimal.test.js",
32
32
  "test:smoke": "npm run test:core-smoke",
33
33
  "test:legacy": "node --test tests/v07-commands.test.js tests/v012-commands.test.js tests/asset-store.test.js",
34
- "test:demo": "node --test tests/v1-demo-minimal.test.js"
34
+ "test:demo": "node --test tests/v1-demo-minimal.test.js",
35
+ "prepublishOnly": "npm test"
35
36
  },
36
37
  "keywords": [
37
38
  "kdna",
@@ -55,7 +56,7 @@
55
56
  "node": ">=18"
56
57
  },
57
58
  "dependencies": {
58
- "@aikdna/kdna-core": "^0.12.0"
59
+ "@aikdna/kdna-core": "^0.12.1"
59
60
  },
60
61
  "optionalDependencies": {
61
62
  "ajv": "^8.20.0",
@@ -29,11 +29,11 @@ incorrectly, assigns wrong priorities, applies wrong risk models, and
29
29
  offers wrong recommendations — all with the false confidence of having
30
30
  "loaded expert judgment."
31
31
 
32
- *No KDNA*: the agent uses model capability, tools, MCP, project files,
32
+ _No KDNA_: the agent uses model capability, tools, MCP, project files,
33
33
  and normal prompts. It may lack domain-specific judgment, but it is
34
34
  not polluted by incorrect judgment.
35
35
 
36
- *Wrong KDNA*: the agent applies a mismatched framework — e.g., diagnosing
36
+ _Wrong KDNA_: the agent applies a mismatched framework — e.g., diagnosing
37
37
  a website design task through a team management lens, or treating a
38
38
  price question as an editing issue. The output is worse than baseline.
39
39
 
@@ -152,15 +152,15 @@ what you'd produce if you loaded the domain? If yes, skip it.
152
152
  After evaluating against `applies_when`, `does_not_apply_when`, and
153
153
  `failure_risks`, classify into one of 7 states:
154
154
 
155
- | State | Condition | Action |
156
- |-------|-----------|--------|
157
- | **SKIP_NO_JUDGMENT_NEEDED** | Task is mechanical: format, translate, lookup, execute | Answer normally. Do not mention KDNA. |
158
- | **SKIP_NO_LOCAL_DOMAIN** | Task may need judgment, but no installed domain covers it | Answer normally. Only mention KDNA if user explicitly asks. |
159
- | **SKIP_WEAK_FIT** | A domain is weakly related but insufficiently matches | Answer normally. Trace notes "weak match, skipped." |
160
- | **REJECT_NEGATIVE_MATCH** | A domain's `does_not_apply_when` explicitly excludes this task | Block loading. Respect the author's boundary. |
161
- | **ASK_AMBIGUOUS_DOMAIN** | 2+ domains could apply but with different judgment frameworks | Ask user to choose. Do **not** silently blend. |
162
- | **LOAD_STRONG_FIT** | One local domain strongly matches and validates | Load it. |
163
- | **BLOCK_INTEGRITY_FAILED** | Domain matches but validation, checksum, parsing, or runtime loading fails | Block loading. Notify if appropriate. |
155
+ | State | Condition | Action |
156
+ | --------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------- |
157
+ | **SKIP_NO_JUDGMENT_NEEDED** | Task is mechanical: format, translate, lookup, execute | Answer normally. Do not mention KDNA. |
158
+ | **SKIP_NO_LOCAL_DOMAIN** | Task may need judgment, but no installed domain covers it | Answer normally. Only mention KDNA if user explicitly asks. |
159
+ | **SKIP_WEAK_FIT** | A domain is weakly related but insufficiently matches | Answer normally. Trace notes "weak match, skipped." |
160
+ | **REJECT_NEGATIVE_MATCH** | A domain's `does_not_apply_when` explicitly excludes this task | Block loading. Respect the author's boundary. |
161
+ | **ASK_AMBIGUOUS_DOMAIN** | 2+ domains could apply but with different judgment frameworks | Ask user to choose. Do **not** silently blend. |
162
+ | **LOAD_STRONG_FIT** | One local domain strongly matches and validates | Load it. |
163
+ | **BLOCK_INTEGRITY_FAILED** | Domain matches but validation, checksum, parsing, or runtime loading fails | Block loading. Notify if appropriate. |
164
164
 
165
165
  **Rule: Negative Match First.** Check `does_not_apply_when` before
166
166
  checking `applies_when`. A domain that says "not for visual design"
@@ -223,8 +223,8 @@ stages.
223
223
  You have now internalized the domain's judgment surface. From this
224
224
  point on:
225
225
 
226
- 1. **Adopt the axioms as your reasoning frame** — reason *from*
227
- them, not *around* them.
226
+ 1. **Adopt the axioms as your reasoning frame** — reason _from_
227
+ them, not _around_ them.
228
228
  2. **Honour the boundaries** — for each axiom you'd apply, confirm
229
229
  the task is in `applies_when` AND not in `does_not_apply_when`.
230
230
  3. **Pre-check failure_risk** — before producing output, ask:
@@ -259,14 +259,14 @@ KDNA does not override:
259
259
 
260
260
  ## Failure handling
261
261
 
262
- | Situation | What to do |
263
- |---|---|
264
- | `kdna` CLI not installed | Skip KDNA. Answer normally. Mention installation only if user asks about KDNA itself. |
265
- | No local v1 assets are found | No domains installed. Skip KDNA. |
266
- | `kdna plan-load <asset>` returns `can_load_now=false` | Do not load. Follow `required_action` and `issues[].code`. |
267
- | `kdna load <name>` exits non-zero | That domain is broken (validation, authorization, parse, or runtime loading failure). Try next candidate or skip KDNA. The error message tells you why. |
268
- | User explicitly asks for a domain that isn't installed | Tell them, suggest `kdna install <name>`. Do not fabricate the domain. |
269
- | Two domains' stances directly conflict on the task | Surface to user. Do not blend. |
262
+ | Situation | What to do |
263
+ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
264
+ | `kdna` CLI not installed | Skip KDNA. Answer normally. Mention installation only if user asks about KDNA itself. |
265
+ | No local v1 assets are found | No domains installed. Skip KDNA. |
266
+ | `kdna plan-load <asset>` returns `can_load_now=false` | Do not load. Follow `required_action` and `issues[].code`. |
267
+ | `kdna load <name>` exits non-zero | That domain is broken (validation, authorization, parse, or runtime loading failure). Try next candidate or skip KDNA. The error message tells you why. |
268
+ | User explicitly asks for a domain that isn't installed | Tell them, suggest `kdna install <name>`. Do not fabricate the domain. |
269
+ | Two domains' stances directly conflict on the task | Surface to user. Do not blend. |
270
270
 
271
271
  ---
272
272
 
package/src/agent.js CHANGED
@@ -306,9 +306,7 @@ function cmdMatch(taskText, args = []) {
306
306
  }
307
307
  }
308
308
  console.log('');
309
- console.log(
310
- 'To load a domain: kdna load <name|file.kdna>',
311
- );
309
+ console.log('To load a domain: kdna load <name|file.kdna>');
312
310
  }
313
311
  }
314
312
 
package/src/cli.js CHANGED
@@ -89,6 +89,7 @@ Start here:
89
89
  Core v1:
90
90
  inspect <path> Inspect v1 source dir or .kdna container
91
91
  validate <path> Validate v1 source dir or .kdna container
92
+ validate <path> --runtime Validate and require LoadPlan readiness
92
93
  plan-load <path> Return a LoadPlan before runtime load
93
94
  Add --has-password or --entitlement-status for diagnostics
94
95
  pack <src> <out> Deterministic pack into .kdna container
@@ -97,6 +98,11 @@ Core v1:
97
98
  More:
98
99
  kdna help advanced Agent runtime, setup, loading, comparison
99
100
  kdna help legacy Pre-v1 compatibility commands
101
+ Advanced index: doctor, trace, history, license, verify, compare, diff, search
102
+ verify <name|file> Legacy asset verification
103
+ compare <name|file> --input "..." Legacy comparison report
104
+ diff <left> <right> Legacy asset diff
105
+ search <query> Search installed/available legacy entries
100
106
 
101
107
  Flags:
102
108
  --json Structured JSON output
@@ -111,6 +117,7 @@ function showHelpAdvanced() {
111
117
  Core v1:
112
118
  inspect <path> Inspect v1 source dir or .kdna container
113
119
  validate <path> Validate v1 source dir or .kdna container
120
+ validate <path> --runtime Validate and require LoadPlan readiness
114
121
  plan-load <path> [--has-password] [--entitlement-status <status>]
115
122
  Return a LoadPlan before runtime load
116
123
  pack <src> <out> Deterministic pack into .kdna container
@@ -261,12 +268,56 @@ switch (cmd) {
261
268
  case 'validate': {
262
269
  const v1Target = args.filter((a) => !a.startsWith('--'))[1];
263
270
  if (v1Target) {
264
- const { isV1SourceDir, detectContainerFormat, validate, pack, unpack, inspect } = require('@aikdna/kdna-core');
271
+ const {
272
+ isV1SourceDir,
273
+ detectContainerFormat,
274
+ validate,
275
+ pack,
276
+ unpack,
277
+ inspect,
278
+ } = require('@aikdna/kdna-core');
265
279
  const abs = require('node:path').resolve(v1Target);
266
280
  if (isV1SourceDir(abs) || detectContainerFormat(abs) === 'v1') {
281
+ const runtimeMode = args.includes('--runtime');
267
282
  const result = validate(v1Target);
283
+ if (runtimeMode) {
284
+ const entitlementStatusIndex = args.indexOf('--entitlement-status');
285
+ const entitlementStatus =
286
+ entitlementStatusIndex >= 0 ? args[entitlementStatusIndex + 1] : null;
287
+ const allowedEntitlementStatuses = new Set([
288
+ 'active',
289
+ 'expired',
290
+ 'revoked',
291
+ 'offline_grace',
292
+ ]);
293
+ if (entitlementStatusIndex >= 0 && !allowedEntitlementStatuses.has(entitlementStatus)) {
294
+ error(
295
+ 'Invalid --entitlement-status. Use active, expired, revoked, or offline_grace.',
296
+ EXIT.INPUT_ERROR,
297
+ );
298
+ }
299
+ const core = require('@aikdna/kdna-core');
300
+ if (typeof core.planLoad !== 'function') {
301
+ error(
302
+ 'kdna validate --runtime requires @aikdna/kdna-core with the LoadPlan v1 API. Update @aikdna/kdna-core before enabling runtime authorization diagnostics.',
303
+ EXIT.PROVIDER_ERROR,
304
+ );
305
+ }
306
+ result.runtime_load_plan = core.planLoad(v1Target, {
307
+ hasPassword: args.includes('--has-password'),
308
+ entitlement: entitlementStatus ? { status: entitlementStatus } : undefined,
309
+ });
310
+ }
268
311
  console.log(JSON.stringify(result, null, 2));
269
- process.exit(result.overall_valid ? 0 : 1);
312
+ if (!result.overall_valid) process.exit(1);
313
+ if (
314
+ runtimeMode &&
315
+ result.runtime_load_plan &&
316
+ result.runtime_load_plan.can_load_now !== true
317
+ ) {
318
+ process.exit(result.runtime_load_plan.state === 'invalid' ? 1 : 3);
319
+ }
320
+ process.exit(0);
270
321
  }
271
322
  }
272
323
  error(
@@ -277,7 +328,11 @@ switch (cmd) {
277
328
  }
278
329
  case 'plan-load': {
279
330
  const v1Target = args.filter((a) => !a.startsWith('--'))[1];
280
- if (!v1Target) error('Usage: kdna plan-load <path> [--json] [--has-password] [--entitlement-status <status>]', EXIT.INPUT_ERROR);
331
+ if (!v1Target)
332
+ error(
333
+ 'Usage: kdna plan-load <path> [--json] [--has-password] [--entitlement-status <status>]',
334
+ EXIT.INPUT_ERROR,
335
+ );
281
336
  const core = require('@aikdna/kdna-core');
282
337
  const abs = require('node:path').resolve(v1Target);
283
338
  if (!(core.isV1SourceDir(abs) || core.detectContainerFormat(abs) === 'v1')) {
@@ -293,19 +348,30 @@ switch (cmd) {
293
348
  const entitlementStatus = entitlementStatusIndex >= 0 ? args[entitlementStatusIndex + 1] : null;
294
349
  const allowedEntitlementStatuses = new Set(['active', 'expired', 'revoked', 'offline_grace']);
295
350
  if (entitlementStatusIndex >= 0 && !allowedEntitlementStatuses.has(entitlementStatus)) {
296
- error('Invalid --entitlement-status. Use active, expired, revoked, or offline_grace.', EXIT.INPUT_ERROR);
351
+ error(
352
+ 'Invalid --entitlement-status. Use active, expired, revoked, or offline_grace.',
353
+ EXIT.INPUT_ERROR,
354
+ );
297
355
  }
298
356
  const plan = core.planLoad(v1Target, {
299
357
  hasPassword: args.includes('--has-password'),
300
358
  entitlement: entitlementStatus ? { status: entitlementStatus } : undefined,
301
359
  });
302
360
  console.log(JSON.stringify(plan, null, 2));
303
- process.exit(plan.state === 'invalid' ? 1 : 0);
361
+ process.exit(plan.state === 'invalid' ? 1 : plan.can_load_now === true ? 0 : 3);
362
+ break;
304
363
  }
305
364
  case 'pack': {
306
365
  const v1Target = args.filter((a) => !a.startsWith('--'))[1];
307
366
  if (v1Target) {
308
- const { isV1SourceDir, detectContainerFormat, validate, pack, unpack, inspect } = require('@aikdna/kdna-core');
367
+ const {
368
+ isV1SourceDir,
369
+ detectContainerFormat,
370
+ validate,
371
+ pack,
372
+ unpack,
373
+ inspect,
374
+ } = require('@aikdna/kdna-core');
309
375
  const abs = require('node:path').resolve(v1Target);
310
376
  if (isV1SourceDir(abs)) {
311
377
  const out = args.filter((a) => !a.startsWith('--'))[2];
@@ -329,7 +395,14 @@ switch (cmd) {
329
395
  case 'unpack': {
330
396
  const v1Target = args.filter((a) => !a.startsWith('--'))[1];
331
397
  if (v1Target) {
332
- const { isV1SourceDir, detectContainerFormat, validate, pack, unpack, inspect } = require('@aikdna/kdna-core');
398
+ const {
399
+ isV1SourceDir,
400
+ detectContainerFormat,
401
+ validate,
402
+ pack,
403
+ unpack,
404
+ inspect,
405
+ } = require('@aikdna/kdna-core');
333
406
  const abs = require('node:path').resolve(v1Target);
334
407
  if (detectContainerFormat(abs) === 'v1') {
335
408
  const out = args.filter((a) => !a.startsWith('--'))[2];
@@ -408,7 +481,14 @@ switch (cmd) {
408
481
  case 'inspect': {
409
482
  const target = args.filter((a) => !a.startsWith('--'))[1];
410
483
  if (!target) error('Usage: kdna inspect <path> [--json] [--locale zh-CN]');
411
- const { isV1SourceDir, detectContainerFormat, validate, pack, unpack, inspect } = require('@aikdna/kdna-core');
484
+ const {
485
+ isV1SourceDir,
486
+ detectContainerFormat,
487
+ validate,
488
+ pack,
489
+ unpack,
490
+ inspect,
491
+ } = require('@aikdna/kdna-core');
412
492
  const abs = require('node:path').resolve(target);
413
493
  if (isV1SourceDir(abs) || detectContainerFormat(abs) === 'v1') {
414
494
  const out = inspect(target);
@@ -466,7 +546,8 @@ switch (cmd) {
466
546
  case 'load': {
467
547
  const target = args.filter((a) => !a.startsWith('--'))[1];
468
548
  if (target) {
469
- const { isV1SourceDir, detectContainerFormat, loadV1 } = require('@aikdna/kdna-core');
549
+ const core = require('@aikdna/kdna-core');
550
+ const { isV1SourceDir, detectContainerFormat } = core;
470
551
  const abs = require('node:path').resolve(target);
471
552
  if (isV1SourceDir(abs) || detectContainerFormat(abs) === 'v1') {
472
553
  const getFlag = (name) => {
@@ -478,7 +559,25 @@ switch (cmd) {
478
559
  const profile = getFlag('--profile') || 'compact';
479
560
  const as = getFlag('--as') || 'json';
480
561
  try {
481
- const r = loadV1(target, { profile, as });
562
+ const loadV1Authorized =
563
+ core.loadAuthorized ||
564
+ ((input, options) => {
565
+ if (typeof core.planLoad !== 'function') {
566
+ throw new Error('LoadPlan API is required before loading KDNA Core v1 assets');
567
+ }
568
+ const plan = core.planLoad(input, options);
569
+ if (plan.can_load_now !== true) {
570
+ const err = new Error(
571
+ `LoadPlan denied loading: state=${plan.state || 'invalid'} required_action=${plan.required_action || 'block'}`,
572
+ );
573
+ err.code =
574
+ (plan.issues && plan.issues[0] && plan.issues[0].code) ||
575
+ 'KDNA_LOAD_NOT_AUTHORIZED';
576
+ throw err;
577
+ }
578
+ return core.loadV1(input, options);
579
+ });
580
+ const r = loadV1Authorized(target, { profile, as });
482
581
  if (as === 'prompt') {
483
582
  process.stdout.write(r.text + '\n');
484
583
  } else {
@@ -38,7 +38,7 @@
38
38
  const fs = require('fs');
39
39
  const path = require('path');
40
40
 
41
- const AXIOM_THRESHOLD = 6; // SPEC §5.2 says "between 2 and 6 axioms"
41
+ const AXIOM_THRESHOLD = 6; // SPEC §5.2 says "between 2 and 6 axioms"
42
42
  const FRAMEWORK_THRESHOLD = 3; // RFC-0013 §4 companion rule
43
43
  const RATIONALE_MIN_LENGTH = 30;
44
44
 
@@ -103,7 +103,11 @@ function runAntiMonolithicCheck(dir, opts = {}) {
103
103
  hasRationale = typeof rationale === 'string' && rationale.trim().length >= RATIONALE_MIN_LENGTH;
104
104
  result.summary.has_decomposition_rationale = hasRationale;
105
105
 
106
- if (rationale && rationale.trim().length > 0 && rationale.trim().length < RATIONALE_MIN_LENGTH) {
106
+ if (
107
+ rationale &&
108
+ rationale.trim().length > 0 &&
109
+ rationale.trim().length < RATIONALE_MIN_LENGTH
110
+ ) {
107
111
  result.warnings.push(
108
112
  `module_manifest.json: decomposition_rationale is only ${rationale.trim().length} chars; ` +
109
113
  `minimum is ${RATIONALE_MIN_LENGTH} chars to count as a real sign-off.`,
package/src/cmds/demo.js CHANGED
@@ -26,9 +26,7 @@ function cmdDemo(args) {
26
26
  if (fs.existsSync(outDir)) {
27
27
  const existing = fs.readdirSync(outDir).filter((f) => f !== '.DS_Store');
28
28
  if (existing.length > 0 && !force) {
29
- console.error(
30
- `Target already exists and is not empty: ${outDir}`,
31
- );
29
+ console.error(`Target already exists and is not empty: ${outDir}`);
32
30
  console.error('Use --force to overwrite.');
33
31
  process.exit(2);
34
32
  }
@@ -238,9 +238,7 @@ function cmdValidateAntiMonolithic(dir, opts = {}) {
238
238
  const amResults = [];
239
239
  if (schemaResult.cluster && Array.isArray(schemaResult.domains)) {
240
240
  for (const d of schemaResult.domains) {
241
- amResults.push(
242
- runAntiMonolithicCheckOnCore(d.path || abs, { strict }),
243
- );
241
+ amResults.push(runAntiMonolithicCheckOnCore(d.path || abs, { strict }));
244
242
  }
245
243
  } else {
246
244
  amResults.push(runAntiMonolithicCheckOnCore(abs, { strict }));
@@ -68,7 +68,7 @@ function readContainerJson(kdnaPath, fileName, options = {}) {
68
68
 
69
69
  function readContainerDataMap(kdnaPath, options = {}) {
70
70
  const asset = assetReader.openSync(kdnaPath);
71
- const dataMap = assetReader.readDataMapSync(asset, undefined, options);
71
+ const dataMap = readDataMapCompatSync(asset, options);
72
72
  if (asset.entries.has('kdna.json')) {
73
73
  dataMap['kdna.json'] = assetReader.readJsonSync(asset, 'kdna.json', options);
74
74
  }
@@ -87,7 +87,7 @@ function listContainerEntries(kdnaPath) {
87
87
 
88
88
  function readContainer(kdnaPath, options = {}) {
89
89
  const asset = assetReader.openSync(kdnaPath);
90
- const dataMap = assetReader.readDataMapSync(asset, undefined, options);
90
+ const dataMap = readDataMapCompatSync(asset, options);
91
91
  return {
92
92
  manifest: dataMap['kdna.json'] || {},
93
93
  core: dataMap['KDNA_Core.json'] || {},
@@ -100,6 +100,31 @@ function readContainer(kdnaPath, options = {}) {
100
100
  };
101
101
  }
102
102
 
103
+ function readDataMapCompatSync(asset, options = {}) {
104
+ try {
105
+ return assetReader.readDataMapSync(asset, undefined, options);
106
+ } catch (e) {
107
+ if (!String(e?.message || '').includes('missing payload.kdnab')) {
108
+ throw e;
109
+ }
110
+ }
111
+
112
+ const dataMap = {};
113
+ for (const entry of [
114
+ 'KDNA_Core.json',
115
+ 'KDNA_Patterns.json',
116
+ 'KDNA_Scenarios.json',
117
+ 'KDNA_Cases.json',
118
+ 'KDNA_Reasoning.json',
119
+ 'KDNA_Evolution.json',
120
+ ]) {
121
+ if (asset.entries.has(entry)) {
122
+ dataMap[entry] = assetReader.readJsonSync(asset, entry, options);
123
+ }
124
+ }
125
+ return dataMap;
126
+ }
127
+
103
128
  function verifyAsset(kdnaPath, options = {}) {
104
129
  const asset = assetReader.openSync(kdnaPath);
105
130
  return assetReader.verifySync(asset, options);
package/src/publish.js CHANGED
@@ -689,7 +689,7 @@ function cmdPublish(assetPath, args = []) {
689
689
 
690
690
  console.log('');
691
691
  console.log('─'.repeat(60));
692
- console.log('Legacy registry patch (historical compatibility only):');
692
+ console.log('Legacy Registry patch (historical compatibility only):');
693
693
  console.log('─'.repeat(60));
694
694
  console.log(JSON.stringify(patch, null, 2));
695
695
  console.log('');
@@ -1,245 +0,0 @@
1
- /**
2
- * KDNA Protected Asset Commands (RFC-0009)
3
- *
4
- * Commands:
5
- * kdna protect <file.kdna> --out <file.kdna> [--entries <list>]
6
- * kdna unlock <file.kdna> [--profile compact|index|full]
7
- * kdna recover <file.kdna> --out <file.kdna> [--code-stdin]
8
- *
9
-
10
- const fs = require('fs');
11
- const path = require('path');
12
- const { EXIT, error, promptPassword } = require('./_common');
13
- const {
14
- createKdnaAssetReader,
15
- createPasswordDecryptEntry,
16
- createRecoveryDecryptEntry,
17
- encryptProtectedEntry,
18
- generateRecoveryCode,
19
- } = require('@aikdna/kdna-core');
20
-
21
- function parseEntriesFlag(flag) {
22
- if (!flag) return ['KDNA_Core.json'];
23
- return flag.split(',').map((s) => s.trim());
24
- }
25
-
26
- function cmdProtect(args) {
27
- const file = args[0];
28
- if (!file) error('Usage: kdna protect <file.kdna> --out <file.kdna> [--entries KDNA_Core.json,KDNA_Patterns.json]', EXIT.INPUT_ERROR);
29
-
30
- const outIdx = args.indexOf('--out');
31
- const outPath = outIdx >= 0 ? args[outIdx + 1] : null;
32
- if (!outPath) error('Missing --out', EXIT.INPUT_ERROR);
33
-
34
- const entriesIdx = args.indexOf('--entries');
35
- const entriesToEncrypt = parseEntriesFlag(entriesIdx >= 0 ? args[entriesIdx + 1] : null);
36
-
37
- if (!fs.existsSync(file)) error(`File not found: ${file}`, EXIT.INPUT_ERROR);
38
-
39
- const password = promptPassword('Password: ');
40
- if (!password) error('Password is required.', EXIT.INPUT_ERROR);
41
-
42
- const reader = createKdnaAssetReader();
43
- const asset = reader.openSync(file);
44
- const manifest = reader.readManifestSync(asset);
45
-
46
- if (manifest.access === 'protected') {
47
- error('Asset is already protected. Use recover to change password.', EXIT.INPUT_ERROR);
48
- }
49
-
50
- // Update manifest
51
- const newManifest = { ...manifest, access: 'protected', encryption: { profile: 'kdna-password-protected-v1', encrypted_entries: entriesToEncrypt } };
52
-
53
- // Build new ZIP with encrypted entries
54
- const allEntries = reader.listEntriesSync(asset);
55
- const zipEntries = {};
56
- const recoveryCode = generateRecoveryCode();
57
-
58
- for (const entryName of allEntries) {
59
- if (entryName === 'kdna.json') {
60
- zipEntries[entryName] = JSON.stringify(newManifest);
61
- } else if (entriesToEncrypt.includes(entryName)) {
62
- const plaintext = reader.readEntrySync(asset, entryName);
63
- const encrypted = encryptProtectedEntry(plaintext, {
64
- entryName,
65
- manifest: newManifest,
66
- password,
67
- recoveryCode,
68
- });
69
- zipEntries[entryName] = JSON.stringify(encrypted);
70
- } else {
71
- zipEntries[entryName] = reader.readEntrySync(asset, entryName);
72
- }
73
- }
74
-
75
- // Add mimetype if missing
76
- if (!zipEntries.mimetype) {
77
- zipEntries.mimetype = 'application/vnd.aikdna.kdna+zip';
78
- }
79
-
80
- // Write new asset using core's internal ZIP builder or a simple approach
81
- // For the CLI, we use the asset reader's internal helper if available, otherwise manual ZIP
82
- const zipBuffer = buildZip(zipEntries);
83
- fs.writeFileSync(outPath, zipBuffer);
84
-
85
- console.log(`Protected asset written to: ${outPath}`);
86
- console.log(`Encrypted entries: ${entriesToEncrypt.join(', ')}`);
87
- console.log('Recovery code: (displayed once — save it)');
88
- console.log(` ${recoveryCode}`);
89
- console.log(' Use `kdna recover` if you forget the password.');
90
- }
91
-
92
- function cmdUnlock(args) {
93
- const file = args[0];
94
- if (!file) error('Usage: kdna unlock <file.kdna> [--profile compact|index|full]', EXIT.INPUT_ERROR);
95
-
96
- const profileIdx = args.indexOf('--profile');
97
- const profile = profileIdx >= 0 ? args[profileIdx + 1] : 'compact';
98
-
99
- if (!fs.existsSync(file)) error(`File not found: ${file}`, EXIT.INPUT_ERROR);
100
-
101
- const password = promptPassword('Password: ');
102
- if (!password) error('Password is required.', EXIT.INPUT_ERROR);
103
-
104
- const reader = createKdnaAssetReader();
105
- const asset = reader.openSync(file);
106
- const manifest = reader.readManifestSync(asset);
107
-
108
- if (manifest.access !== 'protected') {
109
- error(`Asset access is "${manifest.access}", expected "protected"`, EXIT.INPUT_ERROR);
110
- }
111
-
112
- const decryptEntry = createPasswordDecryptEntry({ password });
113
-
114
- try {
115
- const loaded = reader.loadProfileSync(asset, profile, { decryptEntry });
116
- console.log(JSON.stringify(loaded, null, 2));
117
- } catch (e) {
118
- error(`Unlock failed: ${e.message}`, EXIT.TRUST_FAILED);
119
- }
120
- }
121
-
122
- function cmdRecover(args) {
123
- const file = args[0];
124
- if (!file) error('Usage: kdna recover <file.kdna> --out <file.kdna> [--code-stdin]', EXIT.INPUT_ERROR);
125
-
126
- const outIdx = args.indexOf('--out');
127
- const outPath = outIdx >= 0 ? args[outIdx + 1] : null;
128
- if (!outPath) error('Missing --out', EXIT.INPUT_ERROR);
129
-
130
- if (!fs.existsSync(file)) error(`File not found: ${file}`, EXIT.INPUT_ERROR);
131
-
132
- let recoveryCode;
133
- if (args.includes('--code-stdin')) {
134
- const stdinData = fs.readFileSync(0, 'utf8').trim();
135
- if (!stdinData) error('No recovery code provided on stdin.', EXIT.INPUT_ERROR);
136
- recoveryCode = stdinData;
137
- } else {
138
- recoveryCode = promptPassword('Recovery code: ');
139
- if (!recoveryCode) error('Recovery code is required.', EXIT.INPUT_ERROR);
140
- }
141
-
142
- const newPassword = promptPassword('New password: ');
143
- if (!newPassword) error('New password is required.', EXIT.INPUT_ERROR);
144
-
145
- const reader = createKdnaAssetReader();
146
- const asset = reader.openSync(file);
147
- const manifest = reader.readManifestSync(asset);
148
-
149
- if (manifest.access !== 'protected') {
150
- error(`Asset access is "${manifest.access}", expected "protected"`, EXIT.INPUT_ERROR);
151
- }
152
-
153
- const decryptEntry = createRecoveryDecryptEntry({ recoveryCode });
154
-
155
- // Decrypt all encrypted entries with recovery code, then re-encrypt with new password
156
- const entriesToEncrypt = manifest.encryption?.encrypted_entries || ['KDNA_Core.json'];
157
- const allEntries = reader.listEntriesSync(asset);
158
- const zipEntries = {};
159
- const newRecoveryCode = generateRecoveryCode();
160
-
161
- for (const entryName of allEntries) {
162
- if (entriesToEncrypt.includes(entryName)) {
163
- // Decrypt with recovery code
164
- const encryptedData = reader.readEntrySync(asset, entryName);
165
- const plaintext = decryptEntry({ entryName, ciphertext: encryptedData, manifest });
166
-
167
- // Re-encrypt with new password and new recovery code
168
- const encrypted = encryptProtectedEntry(plaintext, {
169
- entryName,
170
- manifest: { ...manifest, encryption: { ...manifest.encryption, encrypted_entries: entriesToEncrypt } },
171
- password: newPassword,
172
- recoveryCode: newRecoveryCode,
173
- });
174
- zipEntries[entryName] = JSON.stringify(encrypted);
175
- } else {
176
- zipEntries[entryName] = reader.readEntrySync(asset, entryName);
177
- }
178
- }
179
-
180
- if (!zipEntries.mimetype) {
181
- zipEntries.mimetype = 'application/vnd.aikdna.kdna+zip';
182
- }
183
-
184
- const zipBuffer = buildZip(zipEntries);
185
- fs.writeFileSync(outPath, zipBuffer);
186
-
187
- console.log(`Recovered asset written to: ${outPath}`);
188
- console.log('Password has been reset.');
189
- console.log('New recovery code: (displayed once — save it)');
190
- console.log(` ${newRecoveryCode}`);
191
- console.log(' The old recovery code is no longer valid.');
192
- }
193
-
194
- // Simple ZIP builder for CLI usage
195
- function u16(n) {
196
- const b = Buffer.alloc(2);
197
- b.writeUInt16LE(n);
198
- return b;
199
- }
200
-
201
- function u32(n) {
202
- const b = Buffer.alloc(4);
203
- b.writeUInt32LE(n);
204
- return b;
205
- }
206
-
207
- function buildZip(entries) {
208
- const localParts = [];
209
- const centralParts = [];
210
- let offset = 0;
211
-
212
- for (const [name, value] of Object.entries(entries)) {
213
- const nameBuf = Buffer.from(name);
214
- const data = Buffer.from(value);
215
- const local = Buffer.concat([
216
- u32(0x04034b50), u16(20), u16(0), u16(0), u16(0), u16(0),
217
- u32(0), u32(data.length), u32(data.length), u16(nameBuf.length), u16(0),
218
- nameBuf, data,
219
- ]);
220
- localParts.push(local);
221
-
222
- centralParts.push(
223
- Buffer.concat([
224
- u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0),
225
- u32(0), u32(data.length), u32(data.length), u16(nameBuf.length), u16(0),
226
- u16(0), u16(0), u16(0), u32(0), u32(offset), nameBuf,
227
- ]),
228
- );
229
- offset += local.length;
230
- }
231
-
232
- const central = Buffer.concat(centralParts);
233
- const local = Buffer.concat(localParts);
234
- const eocd = Buffer.concat([
235
- u32(0x06054b50), u16(0), u16(0), u16(centralParts.length), u16(centralParts.length),
236
- u32(central.length), u32(local.length), u16(0),
237
- ]);
238
- return Buffer.concat([local, central, eocd]);
239
- }
240
-
241
- module.exports = {
242
- cmdProtect,
243
- cmdUnlock,
244
- cmdRecover,
245
- };