@aikdna/kdna-cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -101
- package/SECURITY.md +1 -1
- package/package.json +6 -4
- package/skills/kdna-loader/SKILL.md +23 -22
- package/src/agent.js +290 -159
- package/src/cli.js +117 -67
- package/src/cmds/_common.js +40 -18
- package/src/cmds/badge.js +14 -9
- package/src/cmds/changelog.js +32 -12
- package/src/cmds/cluster.js +80 -85
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +114 -427
- package/src/cmds/explain.js +119 -0
- package/src/cmds/governance.js +111 -42
- package/src/cmds/legacy.js +8 -9
- package/src/cmds/license.js +491 -26
- package/src/cmds/quality.js +10 -3
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +99 -47
- package/src/cmds/test.js +9 -6
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +41 -22
- package/src/diff.js +38 -24
- package/src/identity.js +9 -7
- package/src/init.js +2 -2
- package/src/install.js +147 -459
- package/src/loader.js +10 -10
- package/src/package-store.js +232 -0
- package/src/paths.js +44 -0
- package/src/publish.js +150 -51
- package/src/registry.js +81 -9
- package/src/setup.js +19 -20
- package/src/verify.js +293 -140
- package/src/version.js +15 -7
- package/templates/minimal-domain/kdna.json +7 -7
- package/templates/standard-domain/README.md +10 -10
- package/templates/standard-domain/kdna.json +7 -3
- package/validators/kdna-lint.js +45 -11
- package/src/cmds/encrypt.js +0 -199
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { error, EXIT } = require('./_common');
|
|
2
|
+
const { parseName } = require('../registry');
|
|
3
|
+
const { getInstalled, readContainer } = require('../package-store');
|
|
4
|
+
|
|
5
|
+
function cmdExplain(args) {
|
|
6
|
+
const target = args.filter((a) => !a.startsWith('--'))[1];
|
|
7
|
+
if (!target) {
|
|
8
|
+
error(
|
|
9
|
+
'Usage: kdna explain <domain> [--locale zh-CN]\n\n' +
|
|
10
|
+
' Produces a natural language explanation of what a domain covers,\n' +
|
|
11
|
+
' its key axioms, applicable scenarios, and intended model types.',
|
|
12
|
+
EXIT.INPUT_ERROR,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parsed = parseName(target);
|
|
17
|
+
if (!parsed) {
|
|
18
|
+
error(`Invalid domain name: ${target}`, EXIT.INPUT_ERROR);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const installed = getInstalled(parsed.full);
|
|
22
|
+
if (!installed) {
|
|
23
|
+
error(`${parsed.full} is not installed.\nRun: kdna install ${target}`, EXIT.INPUT_ERROR);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { core, patterns, scenarios } = readContainer(installed.asset_path);
|
|
27
|
+
|
|
28
|
+
if (!core) {
|
|
29
|
+
error(`Failed to load KDNA_Core.json from ${installed.asset_path}`, EXIT.VALIDATION_FAILED);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const m = core.meta || {};
|
|
33
|
+
const purpose = m.purpose || '(not specified)';
|
|
34
|
+
const domain = m.domain || parsed.ident;
|
|
35
|
+
const version = m.version || 'unknown';
|
|
36
|
+
const axioms = core.axioms || [];
|
|
37
|
+
const bannedTerms = patterns?.terminology?.banned_terms || [];
|
|
38
|
+
const selfChecks = patterns?.self_checks || patterns?.self_check || [];
|
|
39
|
+
const standardTerms = patterns?.terminology?.standard_terms || [];
|
|
40
|
+
const misunderstandings = patterns?.misunderstandings || [];
|
|
41
|
+
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(`╔══════════════════════════════════════════════════════════════╗`);
|
|
44
|
+
console.log(`║ KDNA Domain: ${domain.padEnd(46)}║`);
|
|
45
|
+
console.log(`║ Package: ${parsed.full.padEnd(46)}║`);
|
|
46
|
+
console.log(`║ Version: ${version.padEnd(46)}║`);
|
|
47
|
+
console.log(`╚══════════════════════════════════════════════════════════════╝`);
|
|
48
|
+
console.log('');
|
|
49
|
+
|
|
50
|
+
console.log('── Purpose ──');
|
|
51
|
+
console.log(` ${purpose}`);
|
|
52
|
+
console.log('');
|
|
53
|
+
|
|
54
|
+
console.log(`── Axioms (${axioms.length} core principles) ──`);
|
|
55
|
+
for (const ax of axioms) {
|
|
56
|
+
console.log(` • ${ax.one_sentence || ax.id}`);
|
|
57
|
+
const applies = (ax.applies_when || []).join('; ');
|
|
58
|
+
if (applies) console.log(` Applies when: ${applies.slice(0, 120)}`);
|
|
59
|
+
const notApply = (ax.does_not_apply_when || []).join('; ');
|
|
60
|
+
if (notApply) console.log(` NOT when: ${notApply.slice(0, 120)}`);
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (standardTerms.length) {
|
|
65
|
+
console.log(`── Standard Terms (${standardTerms.length}) ──`);
|
|
66
|
+
for (const t of standardTerms.slice(0, 8)) {
|
|
67
|
+
const def = t.definition ? `: ${t.definition.slice(0, 80)}` : '';
|
|
68
|
+
console.log(` • ${t.term}${def}`);
|
|
69
|
+
}
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (misunderstandings.length) {
|
|
74
|
+
console.log(`── Common Misunderstandings (${misunderstandings.length}) ──`);
|
|
75
|
+
for (const mm of misunderstandings.slice(0, 5)) {
|
|
76
|
+
console.log(` ✗ "${mm.mistake || mm.id}"`);
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (bannedTerms.length) {
|
|
82
|
+
console.log(`── Banned Terms (${bannedTerms.length} — do not use) ──`);
|
|
83
|
+
for (const b of bannedTerms.slice(0, 6)) {
|
|
84
|
+
const why = b.why ? ` → ${b.why.slice(0, 60)}` : '';
|
|
85
|
+
console.log(` ✗ "${b.term}"${why}`);
|
|
86
|
+
}
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (selfChecks.length) {
|
|
91
|
+
console.log(`── Self-Checks (${selfChecks.length} — verify before responding) ──`);
|
|
92
|
+
for (const sc of selfChecks.slice(0, 8)) {
|
|
93
|
+
console.log(` ✓ ${sc.question || sc.id || sc}`);
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (scenarios && scenarios.scenarios && scenarios.scenarios.length) {
|
|
99
|
+
console.log(`── Scenarios (${scenarios.scenarios.length} — strategy shifts) ──`);
|
|
100
|
+
for (const s of scenarios.scenarios.slice(0, 6)) {
|
|
101
|
+
const desc = s.description ? s.description.slice(0, 80) : '';
|
|
102
|
+
console.log(` ▶ ${s.signal || s.id}: ${desc}`);
|
|
103
|
+
}
|
|
104
|
+
console.log('');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log('── Model Compatibility ──');
|
|
108
|
+
console.log(' Works with any LLM/agent that loads context before reasoning.');
|
|
109
|
+
console.log(' Tested: Claude, GPT, Gemini, Qwen, MiniMax');
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
console.log('── Quick Start ──');
|
|
113
|
+
console.log(` kdna verify ${target} --judgment`);
|
|
114
|
+
console.log(` kdna compare ${target} --input "<your task>"`);
|
|
115
|
+
console.log(` kdna trace --domain ${target.slice(0, 40)}`);
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { cmdExplain };
|
package/src/cmds/governance.js
CHANGED
|
@@ -36,7 +36,8 @@ function cmdProposalCreate(args = []) {
|
|
|
36
36
|
if (!fs.existsSync(absTest)) error(`Test run file not found: ${absTest}`, EXIT.INPUT_ERROR);
|
|
37
37
|
|
|
38
38
|
const runData = readJson(absTest);
|
|
39
|
-
if (!runData || !runData.test_id)
|
|
39
|
+
if (!runData || !runData.test_id)
|
|
40
|
+
error(`Not a valid test run file: ${absTest}`, EXIT.INPUT_ERROR);
|
|
40
41
|
|
|
41
42
|
const absDomain = path.resolve(domainPath);
|
|
42
43
|
|
|
@@ -63,7 +64,10 @@ function cmdProposalCreate(args = []) {
|
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
// Auto-detect suggested changes from test result gaps
|
|
66
|
-
if (
|
|
67
|
+
if (
|
|
68
|
+
runData.expected?.classification &&
|
|
69
|
+
runData.expected.classification !== runData.results?.classification
|
|
70
|
+
) {
|
|
67
71
|
proposal.suggested_changes.push({
|
|
68
72
|
what: 'axiom',
|
|
69
73
|
field: 'applies_when',
|
|
@@ -80,7 +84,17 @@ function cmdProposalCreate(args = []) {
|
|
|
80
84
|
writeJson(outFile, proposal);
|
|
81
85
|
|
|
82
86
|
if (jsonMode) {
|
|
83
|
-
console.log(
|
|
87
|
+
console.log(
|
|
88
|
+
JSON.stringify(
|
|
89
|
+
{
|
|
90
|
+
proposal_id: proposal.proposal_id,
|
|
91
|
+
saved: outFile,
|
|
92
|
+
suggested_changes: proposal.suggested_changes.length,
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
84
98
|
return;
|
|
85
99
|
}
|
|
86
100
|
|
|
@@ -98,7 +112,8 @@ function cmdProposalCreate(args = []) {
|
|
|
98
112
|
|
|
99
113
|
function cmdProposalValidate(args = []) {
|
|
100
114
|
const jsonMode = args.includes('--json');
|
|
101
|
-
const target =
|
|
115
|
+
const target =
|
|
116
|
+
args.filter((a) => !a.startsWith('--'))[2] || args.filter((a) => !a.startsWith('--'))[1];
|
|
102
117
|
if (!target) error('Usage: kdna proposal validate <proposal.json>', EXIT.INPUT_ERROR);
|
|
103
118
|
|
|
104
119
|
const abs = path.resolve(target);
|
|
@@ -113,7 +128,8 @@ function cmdProposalValidate(args = []) {
|
|
|
113
128
|
if (!proposal.source) issues.push('missing source');
|
|
114
129
|
if (!proposal.domain) issues.push('missing domain');
|
|
115
130
|
if (!proposal.trigger?.test_id) issues.push('missing trigger.test_id');
|
|
116
|
-
if (!proposal.reasoning || proposal.reasoning.length < 10)
|
|
131
|
+
if (!proposal.reasoning || proposal.reasoning.length < 10)
|
|
132
|
+
issues.push('reasoning too short (min 10 chars)');
|
|
117
133
|
if (!proposal.suggested_changes || proposal.suggested_changes.length === 0) {
|
|
118
134
|
issues.push('no suggested changes');
|
|
119
135
|
} else {
|
|
@@ -124,11 +140,17 @@ function cmdProposalValidate(args = []) {
|
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
if (jsonMode) {
|
|
127
|
-
console.log(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
console.log(
|
|
144
|
+
JSON.stringify(
|
|
145
|
+
{
|
|
146
|
+
proposal_id: proposal.proposal_id,
|
|
147
|
+
valid: issues.length === 0,
|
|
148
|
+
issues,
|
|
149
|
+
},
|
|
150
|
+
null,
|
|
151
|
+
2,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
132
154
|
process.exit(issues.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
|
|
133
155
|
}
|
|
134
156
|
|
|
@@ -184,12 +206,18 @@ function cmdReview(args = []) {
|
|
|
184
206
|
writeJson(abs, proposal);
|
|
185
207
|
|
|
186
208
|
if (jsonMode) {
|
|
187
|
-
console.log(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
console.log(
|
|
210
|
+
JSON.stringify(
|
|
211
|
+
{
|
|
212
|
+
proposal_id: proposal.proposal_id,
|
|
213
|
+
decision: sub,
|
|
214
|
+
by,
|
|
215
|
+
reason,
|
|
216
|
+
},
|
|
217
|
+
null,
|
|
218
|
+
2,
|
|
219
|
+
),
|
|
220
|
+
);
|
|
193
221
|
process.exit(sub === 'reject' ? EXIT.POLICY_VIOLATION : EXIT.OK);
|
|
194
222
|
}
|
|
195
223
|
|
|
@@ -217,7 +245,10 @@ function cmdLockCard(args = []) {
|
|
|
217
245
|
// Lock card in the current studio project (finds studio.project.json)
|
|
218
246
|
const projectPath = path.resolve('studio.project.json');
|
|
219
247
|
if (!fs.existsSync(projectPath)) {
|
|
220
|
-
error(
|
|
248
|
+
error(
|
|
249
|
+
'No studio.project.json found in current directory. Run: kdna studio scaffold',
|
|
250
|
+
EXIT.INPUT_ERROR,
|
|
251
|
+
);
|
|
221
252
|
}
|
|
222
253
|
|
|
223
254
|
const project = readJson(projectPath);
|
|
@@ -240,11 +271,17 @@ function cmdLockCard(args = []) {
|
|
|
240
271
|
found = true;
|
|
241
272
|
|
|
242
273
|
if (jsonMode) {
|
|
243
|
-
console.log(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
274
|
+
console.log(
|
|
275
|
+
JSON.stringify(
|
|
276
|
+
{
|
|
277
|
+
card: `${type}.${id}`,
|
|
278
|
+
locked: true,
|
|
279
|
+
lock: card.human_lock,
|
|
280
|
+
},
|
|
281
|
+
null,
|
|
282
|
+
2,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
248
285
|
} else {
|
|
249
286
|
console.log(`✓ Locked: ${type}.${id}`);
|
|
250
287
|
console.log(` By: ${by}`);
|
|
@@ -254,7 +291,10 @@ function cmdLockCard(args = []) {
|
|
|
254
291
|
}
|
|
255
292
|
|
|
256
293
|
if (!found) {
|
|
257
|
-
error(
|
|
294
|
+
error(
|
|
295
|
+
`Card not found: ${cardId}. Check the card ID and that a studio project exists.`,
|
|
296
|
+
EXIT.INPUT_ERROR,
|
|
297
|
+
);
|
|
258
298
|
}
|
|
259
299
|
}
|
|
260
300
|
|
|
@@ -320,17 +360,23 @@ function cmdEvolutionReport(domainPath, jsonMode) {
|
|
|
320
360
|
const pending = evolution.pending || [];
|
|
321
361
|
|
|
322
362
|
if (jsonMode) {
|
|
323
|
-
console.log(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
363
|
+
console.log(
|
|
364
|
+
JSON.stringify(
|
|
365
|
+
{
|
|
366
|
+
domain: path.basename(abs),
|
|
367
|
+
total_stages: stages.length,
|
|
368
|
+
pending_changes: pending.length,
|
|
369
|
+
stages: stages.map((s) => ({
|
|
370
|
+
stage: s.stage,
|
|
371
|
+
version: s.version,
|
|
372
|
+
date: s.date,
|
|
373
|
+
changes: s.changes?.length || 0,
|
|
374
|
+
})),
|
|
375
|
+
},
|
|
376
|
+
null,
|
|
377
|
+
2,
|
|
378
|
+
),
|
|
379
|
+
);
|
|
334
380
|
return;
|
|
335
381
|
}
|
|
336
382
|
|
|
@@ -362,7 +408,10 @@ function cmdRegression(args = []) {
|
|
|
362
408
|
const evalsDir = evalsIdx >= 0 ? args[evalsIdx + 1] : null;
|
|
363
409
|
|
|
364
410
|
if (!oldPath || !newPath) {
|
|
365
|
-
error(
|
|
411
|
+
error(
|
|
412
|
+
'Usage: kdna regression <old-domain> <new-domain> --evals <dir> [--json]',
|
|
413
|
+
EXIT.INPUT_ERROR,
|
|
414
|
+
);
|
|
366
415
|
}
|
|
367
416
|
|
|
368
417
|
const absOld = path.resolve(oldPath);
|
|
@@ -421,8 +470,18 @@ function cmdRegression(args = []) {
|
|
|
421
470
|
|
|
422
471
|
const result = {
|
|
423
472
|
domain: path.basename(absOld),
|
|
424
|
-
old: {
|
|
425
|
-
|
|
473
|
+
old: {
|
|
474
|
+
axioms: oldAxiomCount,
|
|
475
|
+
misunderstandings: oldMisCount,
|
|
476
|
+
self_checks: oldSelfCheckCount,
|
|
477
|
+
governance_coverage: oldGov,
|
|
478
|
+
},
|
|
479
|
+
new: {
|
|
480
|
+
axioms: newAxiomCount,
|
|
481
|
+
misunderstandings: newMisCount,
|
|
482
|
+
self_checks: newSelfCheckCount,
|
|
483
|
+
governance_coverage: newGov,
|
|
484
|
+
},
|
|
426
485
|
delta: {
|
|
427
486
|
axioms: newAxiomCount - oldAxiomCount,
|
|
428
487
|
misunderstandings: newMisCount - oldMisCount,
|
|
@@ -442,12 +501,22 @@ function cmdRegression(args = []) {
|
|
|
442
501
|
|
|
443
502
|
console.log(`Regression check: ${path.basename(absOld)} → ${path.basename(absNew)}`);
|
|
444
503
|
console.log('');
|
|
445
|
-
console.log(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
console.log(
|
|
504
|
+
console.log(
|
|
505
|
+
` Axioms: ${oldAxiomCount} → ${newAxiomCount} (${result.delta.axioms >= 0 ? '+' : ''}${result.delta.axioms})`,
|
|
506
|
+
);
|
|
507
|
+
console.log(
|
|
508
|
+
` Misunderstandings: ${oldMisCount} → ${newMisCount} (${result.delta.misunderstandings >= 0 ? '+' : ''}${result.delta.misunderstandings})`,
|
|
509
|
+
);
|
|
510
|
+
console.log(
|
|
511
|
+
` Self-checks: ${oldSelfCheckCount} → ${newSelfCheckCount} (${result.delta.self_checks >= 0 ? '+' : ''}${result.delta.self_checks})`,
|
|
512
|
+
);
|
|
513
|
+
console.log(
|
|
514
|
+
` Governance coverage: ${oldGov}% → ${newGov}% (${result.delta.governance_coverage >= 0 ? '+' : ''}${result.delta.governance_coverage}%)`,
|
|
515
|
+
);
|
|
449
516
|
if (totalEvals) {
|
|
450
|
-
console.log(
|
|
517
|
+
console.log(
|
|
518
|
+
` Evals: ${passedEvals} passed, ${failedEvals} failed out of ${totalEvals}`,
|
|
519
|
+
);
|
|
451
520
|
}
|
|
452
521
|
console.log('');
|
|
453
522
|
const mark = result.safe ? '✓' : '✗';
|
package/src/cmds/legacy.js
CHANGED
|
@@ -2,10 +2,9 @@ const { error } = require('./_common');
|
|
|
2
2
|
|
|
3
3
|
function cmdPreview() {
|
|
4
4
|
// Removed in v0.9 — no real user scenario for browser preview.
|
|
5
|
-
// To inspect a .kdna file, use: kdna inspect <
|
|
5
|
+
// To inspect a .kdna file, use: kdna inspect <file.kdna>
|
|
6
6
|
error(
|
|
7
|
-
'kdna preview was removed in v0.9.\n' +
|
|
8
|
-
'Use: kdna inspect <path> to view a .kdna file or domain directory.',
|
|
7
|
+
'kdna preview was removed in v0.9.\n' + 'Use: kdna inspect <file.kdna> to view a .kdna asset.',
|
|
9
8
|
);
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -18,8 +17,8 @@ function cmdProject() {
|
|
|
18
17
|
'kdna project was removed in v0.9. The .kdna/config.json file is no\n' +
|
|
19
18
|
'longer read by the kdna-loader skill — it would have forced KDNA\n' +
|
|
20
19
|
'loading on tasks where the user did not ask for it.\n\n' +
|
|
21
|
-
'The agent now discovers KDNA on demand
|
|
22
|
-
'
|
|
20
|
+
'The agent now discovers KDNA on demand through kdna available/load,\n' +
|
|
21
|
+
'which read installed .kdna assets from the package index.\n\n' +
|
|
23
22
|
'If you have stale .kdna/config.json files in your projects, you\n' +
|
|
24
23
|
'can delete them — nothing reads them anymore.',
|
|
25
24
|
);
|
|
@@ -32,7 +31,7 @@ function cmdEval() {
|
|
|
32
31
|
error(
|
|
33
32
|
'kdna eval was removed in v0.9.\n' +
|
|
34
33
|
'To compare with/without KDNA reasoning, use:\n' +
|
|
35
|
-
' kdna compare <name> --input "<task>"\n' +
|
|
34
|
+
' kdna compare <name|file.kdna> --input "<task>"\n' +
|
|
36
35
|
'To inspect a domain, use:\n' +
|
|
37
36
|
' kdna info <name>',
|
|
38
37
|
);
|
|
@@ -53,17 +52,17 @@ function cmdSelect() {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
function cmdExport() {
|
|
56
|
-
// Removed in v0.9 — was an alias for
|
|
55
|
+
// Removed in v0.9 — was an alias for the old top-level pack command.
|
|
57
56
|
error(
|
|
58
57
|
'kdna export was removed in v0.9 (it was an alias for pack).\n' +
|
|
59
|
-
'Use: kdna pack <
|
|
58
|
+
'Use: kdna dev pack <source-dir> [--output <dir>]',
|
|
60
59
|
);
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
function cmdDemo() {
|
|
64
63
|
// Removed in v0.9 — internal demo, not a user feature. To see
|
|
65
64
|
// before/after on a real input, use:
|
|
66
|
-
// kdna compare <name> --input "<task>" (requires LLM API key)
|
|
65
|
+
// kdna compare <name|file.kdna> --input "<task>" (requires LLM API key)
|
|
67
66
|
error(
|
|
68
67
|
'kdna demo was removed in v0.9.\n' +
|
|
69
68
|
'To see KDNA before/after on a real input, use:\n' +
|