@aikdna/kdna-studio-cli 0.1.3 → 0.3.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 +43 -1
- package/bin/kdna-studio.js +1009 -33
- package/package.json +5 -2
- package/tests/cli.test.js +502 -0
package/bin/kdna-studio.js
CHANGED
|
@@ -11,23 +11,64 @@ const {
|
|
|
11
11
|
evidence: evidenceApi,
|
|
12
12
|
compile: compileApi,
|
|
13
13
|
quality,
|
|
14
|
+
creator: creatorApi,
|
|
15
|
+
distillation: distillationApi,
|
|
14
16
|
} = require('@aikdna/kdna-studio-core');
|
|
15
17
|
|
|
18
|
+
const llm = require('../src/llm');
|
|
19
|
+
const ai = require('../src/ai');
|
|
20
|
+
|
|
16
21
|
const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
|
|
17
22
|
|
|
18
23
|
function usage() {
|
|
19
24
|
console.log(`kdna-studio — Studio-compatible KDNA authoring CLI
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
kdna-studio
|
|
23
|
-
kdna-studio
|
|
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
|
+
|
|
30
|
+
Identity:
|
|
31
|
+
kdna-studio identity init [--name <display-name>]
|
|
32
|
+
kdna-studio identity show
|
|
33
|
+
|
|
34
|
+
Create (three entry paths):
|
|
35
|
+
kdna-studio create <project-dir> --name <@scope/name> # blank
|
|
36
|
+
kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
|
|
37
|
+
kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
|
|
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
|
+
|
|
42
|
+
Authoring:
|
|
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
|
|
24
46
|
kdna-studio card list <project>
|
|
25
47
|
kdna-studio card add <project> <type> --field key=value [--field key=value]
|
|
26
|
-
kdna-studio card approve <project> <card-id> --by <id> --statement <text>
|
|
48
|
+
kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign] [--passphrase <pass>]
|
|
27
49
|
kdna-studio lock <project>
|
|
28
50
|
kdna-studio compile <project> --out <dir>
|
|
29
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
|
|
30
69
|
kdna-studio report <project>
|
|
70
|
+
kdna-studio install <@scope/name|file.kdna> [--trusted]
|
|
71
|
+
kdna-studio update <@scope/name>
|
|
31
72
|
|
|
32
73
|
Project may be a directory containing studio.project.json or a project JSON file.`);
|
|
33
74
|
}
|
|
@@ -110,29 +151,444 @@ function parseFields(args) {
|
|
|
110
151
|
|
|
111
152
|
function cmdCreate(args) {
|
|
112
153
|
const dir = args[0];
|
|
113
|
-
if (!dir) fail('Usage: kdna-studio create <project-dir>
|
|
154
|
+
if (!dir) fail('Usage: kdna-studio create <project-dir> --name <name> [--from-kdna <file> | --from-folder <dir>]');
|
|
114
155
|
const abs = path.resolve(dir);
|
|
115
156
|
if (fs.existsSync(abs)) fail(`Directory already exists: ${abs}`);
|
|
157
|
+
|
|
116
158
|
const name = option(args, '--name', path.basename(abs));
|
|
159
|
+
const fromKdna = option(args, '--from-kdna');
|
|
160
|
+
const fromFolder = option(args, '--from-folder');
|
|
161
|
+
|
|
162
|
+
let sourceMode = 'blank';
|
|
163
|
+
let lineage = null;
|
|
164
|
+
let creatorIdentity = null;
|
|
165
|
+
|
|
166
|
+
// Load creator identity if available
|
|
167
|
+
try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* no identity yet */ }
|
|
168
|
+
|
|
169
|
+
if (fromKdna) {
|
|
170
|
+
sourceMode = 'kdna_asset';
|
|
171
|
+
const kdnaData = importFromKdna(fromKdna);
|
|
172
|
+
lineage = kdnaData.lineage;
|
|
173
|
+
if (kdnaData.cards) {
|
|
174
|
+
const project = projectApi.createProject(name, 'domain', {
|
|
175
|
+
author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
|
|
176
|
+
sourceMode,
|
|
177
|
+
creatorIdentity,
|
|
178
|
+
lineage,
|
|
179
|
+
});
|
|
180
|
+
project.cards = kdnaData.cards;
|
|
181
|
+
if (project.stages?.judgment_cards) {
|
|
182
|
+
project.stages.judgment_cards.total = kdnaData.cards.length;
|
|
183
|
+
project.stages.judgment_cards.status = 'in_progress';
|
|
184
|
+
}
|
|
185
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
186
|
+
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
187
|
+
console.log(`Created Studio project (source_mode: ${sourceMode}, imported: ${kdnaData.cards.length} cards): ${abs}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
} else if (fromFolder) {
|
|
191
|
+
sourceMode = 'source_folder';
|
|
192
|
+
importFromFolder(fromFolder, abs, name, creatorIdentity);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
117
196
|
const project = projectApi.createProject(name, 'domain', {
|
|
118
197
|
author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
|
|
198
|
+
sourceMode,
|
|
199
|
+
creatorIdentity,
|
|
200
|
+
lineage,
|
|
201
|
+
sourcePath: fromFolder ? path.resolve(fromFolder) : null,
|
|
119
202
|
});
|
|
203
|
+
|
|
204
|
+
if (fromKdna || fromFolder) {
|
|
205
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
206
|
+
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
207
|
+
if (fromFolder) {
|
|
208
|
+
// Cards were already added by importFromFolder
|
|
209
|
+
}
|
|
210
|
+
console.log(`Created Studio project (source_mode: ${sourceMode}): ${abs}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
120
214
|
fs.mkdirSync(abs, { recursive: true });
|
|
121
215
|
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
122
216
|
console.log(`Created Studio project: ${abs}`);
|
|
123
217
|
}
|
|
124
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Import cards from an existing .kdna asset into a new Studio project.
|
|
221
|
+
* Cards are imported as draft — they do NOT inherit trust from the source.
|
|
222
|
+
*/
|
|
223
|
+
function importFromKdna(kdnaPath) {
|
|
224
|
+
const absKdna = path.resolve(kdnaPath);
|
|
225
|
+
if (!fs.existsSync(absKdna)) fail(`KDNA asset not found: ${absKdna}`);
|
|
226
|
+
if (!absKdna.endsWith('.kdna')) fail('--from-kdna requires a .kdna file');
|
|
227
|
+
|
|
228
|
+
const zipBuf = fs.readFileSync(absKdna);
|
|
229
|
+
const entries = readZipEntries(zipBuf);
|
|
230
|
+
if (!entries.has('kdna.json')) fail('Not a valid .kdna asset: missing kdna.json');
|
|
231
|
+
|
|
232
|
+
const manifest = JSON.parse(entries.get('kdna.json').toString());
|
|
233
|
+
const lineage = {
|
|
234
|
+
type: 'fork',
|
|
235
|
+
parent_name: manifest.name || null,
|
|
236
|
+
parent_asset_uid: manifest.asset_uid || null,
|
|
237
|
+
parent_version: manifest.version || null,
|
|
238
|
+
parent_asset_digest: manifest.content_digest || null,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Extract cards from KDNA files
|
|
242
|
+
const importedCards = [];
|
|
243
|
+
if (entries.has('KDNA_Core.json')) {
|
|
244
|
+
const core = JSON.parse(entries.get('KDNA_Core.json').toString());
|
|
245
|
+
for (const ax of (core.axioms || [])) {
|
|
246
|
+
const fields = {};
|
|
247
|
+
for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
|
|
248
|
+
if (k in ax) fields[k] = ax[k];
|
|
249
|
+
}
|
|
250
|
+
importedCards.push(cardApi.createCard('axiom', fields));
|
|
251
|
+
}
|
|
252
|
+
for (const ont of (core.ontology || [])) {
|
|
253
|
+
importedCards.push(cardApi.createCard('ontology', {
|
|
254
|
+
one_sentence: ont.one_sentence || ont.essence || '',
|
|
255
|
+
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
256
|
+
trigger_signal: ont.trigger_signal || '',
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
for (const b of (core.boundaries || [])) {
|
|
260
|
+
importedCards.push(cardApi.createCard('boundary', {
|
|
261
|
+
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
262
|
+
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (entries.has('KDNA_Patterns.json')) {
|
|
267
|
+
const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
|
|
268
|
+
for (const ms of (pat.misunderstandings || [])) {
|
|
269
|
+
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
270
|
+
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
271
|
+
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
for (const sc of (pat.self_check || [])) {
|
|
275
|
+
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
276
|
+
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { lineage, cards: importedCards };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Import cards from a legacy source folder into a new Studio project.
|
|
285
|
+
* Reads KDNA_*.json files and converts entries to draft judgment cards.
|
|
286
|
+
* Outputs a schema audit report.
|
|
287
|
+
*/
|
|
288
|
+
function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
289
|
+
const absSource = path.resolve(sourceDir);
|
|
290
|
+
if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
|
|
291
|
+
if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
|
|
292
|
+
|
|
293
|
+
const audit = { filesFound: [], cardsImported: 0, missingFields: [], schemaWarnings: [] };
|
|
294
|
+
const importedCards = [];
|
|
295
|
+
const manifestData = {};
|
|
296
|
+
|
|
297
|
+
function loadJson(filename) {
|
|
298
|
+
const p = path.join(absSource, filename);
|
|
299
|
+
if (!fs.existsSync(p)) return null;
|
|
300
|
+
audit.filesFound.push(filename);
|
|
301
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
302
|
+
catch { audit.schemaWarnings.push(`${filename}: invalid JSON`); return null; }
|
|
303
|
+
}
|
|
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
|
+
|
|
316
|
+
const core = loadJson('KDNA_Core.json');
|
|
317
|
+
const patterns = loadJson('KDNA_Patterns.json');
|
|
318
|
+
const scenarios = loadJson('KDNA_Scenarios.json');
|
|
319
|
+
const cases = loadJson('KDNA_Cases.json');
|
|
320
|
+
const reasoning = loadJson('KDNA_Reasoning.json');
|
|
321
|
+
const evolution = loadJson('KDNA_Evolution.json');
|
|
322
|
+
|
|
323
|
+
if (core) {
|
|
324
|
+
for (const ax of (core.axioms || [])) {
|
|
325
|
+
const fields = {};
|
|
326
|
+
for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
|
|
327
|
+
if (key in ax) fields[key] = ax[key];
|
|
328
|
+
else audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
|
|
329
|
+
}
|
|
330
|
+
importedCards.push(cardApi.createCard('axiom', fields));
|
|
331
|
+
}
|
|
332
|
+
for (const ont of (core.ontology || [])) {
|
|
333
|
+
importedCards.push(cardApi.createCard('ontology', {
|
|
334
|
+
one_sentence: ont.one_sentence || ont.essence || '',
|
|
335
|
+
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
336
|
+
trigger_signal: ont.trigger_signal || '',
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
for (const b of (core.boundaries || [])) {
|
|
340
|
+
importedCards.push(cardApi.createCard('boundary', {
|
|
341
|
+
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
342
|
+
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
for (const r of (core.risks || core.risk_model || [])) {
|
|
346
|
+
importedCards.push(cardApi.createCard('risk', r.fields || r));
|
|
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
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
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
|
+
}
|
|
380
|
+
for (const ms of (patterns.misunderstandings || [])) {
|
|
381
|
+
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
382
|
+
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
383
|
+
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
for (const sc of (patterns.self_check || [])) {
|
|
387
|
+
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
388
|
+
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
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
|
+
|
|
432
|
+
audit.cardsImported = importedCards.length;
|
|
433
|
+
|
|
434
|
+
const project = projectApi.createProject(projectName, 'domain', {
|
|
435
|
+
sourceMode: 'source_folder',
|
|
436
|
+
sourcePath: absSource,
|
|
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 },
|
|
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
|
+
|
|
456
|
+
project.cards = importedCards;
|
|
457
|
+
if (project.stages?.judgment_cards) {
|
|
458
|
+
project.stages.judgment_cards.total = importedCards.length;
|
|
459
|
+
project.stages.judgment_cards.status = 'in_progress';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
463
|
+
writeProject(path.join(projectDir, 'studio.project.json'), project);
|
|
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 };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Minimal ZIP central directory parser — reads entries into a Map.
|
|
470
|
+
*/
|
|
471
|
+
function readZipEntries(buf) {
|
|
472
|
+
const entries = new Map();
|
|
473
|
+
// Find end-of-central-directory record
|
|
474
|
+
let eocdOffset = buf.length - 22;
|
|
475
|
+
while (eocdOffset >= 0) {
|
|
476
|
+
if (buf.readUInt32LE(eocdOffset) === 0x06054b50) break;
|
|
477
|
+
eocdOffset--;
|
|
478
|
+
}
|
|
479
|
+
if (eocdOffset < 0) throw new Error('Not a valid ZIP file');
|
|
480
|
+
|
|
481
|
+
const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
482
|
+
let offset = centralDirOffset;
|
|
483
|
+
|
|
484
|
+
while (offset < eocdOffset) {
|
|
485
|
+
const sig = buf.readUInt32LE(offset);
|
|
486
|
+
if (sig !== 0x02014b50) break;
|
|
487
|
+
const nameLen = buf.readUInt16LE(offset + 28);
|
|
488
|
+
const extraLen = buf.readUInt16LE(offset + 30);
|
|
489
|
+
const commentLen = buf.readUInt16LE(offset + 32);
|
|
490
|
+
const compMethod = buf.readUInt16LE(offset + 10);
|
|
491
|
+
const compSize = buf.readUInt32LE(offset + 20);
|
|
492
|
+
const uncompSize = buf.readUInt32LE(offset + 24);
|
|
493
|
+
const localOffset = buf.readUInt32LE(offset + 42);
|
|
494
|
+
const name = buf.toString('utf8', offset + 46, offset + 46 + nameLen);
|
|
495
|
+
|
|
496
|
+
// Read local file header to get data
|
|
497
|
+
const localNameLen = buf.readUInt16LE(localOffset + 26);
|
|
498
|
+
const localExtraLen = buf.readUInt16LE(localOffset + 28);
|
|
499
|
+
const dataStart = localOffset + 30 + localNameLen + localExtraLen;
|
|
500
|
+
|
|
501
|
+
if (compMethod === 0) {
|
|
502
|
+
entries.set(name, buf.subarray(dataStart, dataStart + uncompSize));
|
|
503
|
+
} else if (compMethod === 8) {
|
|
504
|
+
entries.set(name, zlib.inflateRawSync(buf.subarray(dataStart, dataStart + compSize)));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
offset += 46 + nameLen + extraLen + commentLen;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return entries;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const aiFilter = require('../src/ai/filter');
|
|
514
|
+
|
|
125
515
|
function cmdImport(args) {
|
|
126
516
|
const [projectInput, source] = args;
|
|
127
|
-
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>');
|
|
128
518
|
const { projectPath, project } = readProject(projectInput);
|
|
519
|
+
|
|
129
520
|
const sourcePath = path.resolve(source);
|
|
130
|
-
if (!fs.existsSync(sourcePath)) fail(`Source
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
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
|
+
|
|
134
562
|
writeProject(projectPath, project);
|
|
135
|
-
|
|
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
|
+
}
|
|
136
592
|
}
|
|
137
593
|
|
|
138
594
|
function cmdCard(args) {
|
|
@@ -162,28 +618,66 @@ function cmdCard(args) {
|
|
|
162
618
|
if (sub === 'approve') {
|
|
163
619
|
const projectInput = args[1];
|
|
164
620
|
const cardId = args[2];
|
|
165
|
-
|
|
166
|
-
|
|
621
|
+
const approveAll = args.includes('--all');
|
|
622
|
+
if (!projectInput || (!cardId && !approveAll)) {
|
|
623
|
+
fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]');
|
|
167
624
|
}
|
|
168
625
|
const by = option(args, '--by');
|
|
169
626
|
const statement = option(args, '--statement');
|
|
170
627
|
if (!by || !statement) fail('card approve requires --by and --statement');
|
|
171
628
|
const { projectPath, project } = readProject(projectInput);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
629
|
+
|
|
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)];
|
|
654
|
+
};
|
|
655
|
+
|
|
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++;
|
|
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}`);
|
|
675
|
+
}
|
|
181
676
|
if (project.stages?.judgment_cards) {
|
|
182
677
|
project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
|
|
183
678
|
project.stages.judgment_cards.total = project.cards.length;
|
|
184
679
|
}
|
|
185
680
|
writeProject(projectPath, project);
|
|
186
|
-
console.log(`Approved and Human Locked: ${cardId}`);
|
|
187
681
|
return;
|
|
188
682
|
}
|
|
189
683
|
fail('Usage: kdna-studio card <list|add|approve> ...');
|
|
@@ -200,6 +694,83 @@ function cmdLock(args) {
|
|
|
200
694
|
console.log(`Human Lock Gate passed: ${gate.lockedJudgmentCards} locked judgment cards`);
|
|
201
695
|
}
|
|
202
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
|
+
|
|
203
774
|
function compileProject(projectInput) {
|
|
204
775
|
const { project } = readProject(projectInput);
|
|
205
776
|
const gate = projectApi.checkHumanLockGate(project);
|
|
@@ -320,6 +891,7 @@ function manifestForSigning(manifest) {
|
|
|
320
891
|
delete copy.signature;
|
|
321
892
|
delete copy.asset_digest;
|
|
322
893
|
delete copy.container_sha256;
|
|
894
|
+
delete copy.content_digest;
|
|
323
895
|
return copy;
|
|
324
896
|
}
|
|
325
897
|
|
|
@@ -346,7 +918,7 @@ function publicKeyFingerprint(publicKeyPem) {
|
|
|
346
918
|
return `ed25519:${crypto.createHash('sha256').update(publicKeyPem).digest('hex')}`;
|
|
347
919
|
}
|
|
348
920
|
|
|
349
|
-
function applySignature(files) {
|
|
921
|
+
function applySignature(files, passphrase = null) {
|
|
350
922
|
const paths = identityPaths();
|
|
351
923
|
if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
|
|
352
924
|
fail('Signing requires KDNA identity keys. Run: kdna identity init', EXIT.TRUST_FAILED);
|
|
@@ -359,7 +931,13 @@ function applySignature(files) {
|
|
|
359
931
|
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
360
932
|
|
|
361
933
|
const payload = canonicalPayload(files);
|
|
362
|
-
|
|
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
|
+
}
|
|
363
941
|
manifest.signature = `ed25519:${crypto.sign(null, Buffer.from(payload), privateKeyPem).toString('hex')}`;
|
|
364
942
|
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
365
943
|
}
|
|
@@ -372,21 +950,48 @@ function cmdExport(args) {
|
|
|
372
950
|
const files = { ...result.files };
|
|
373
951
|
files['README.md'] = compileApi.generateReadme(project);
|
|
374
952
|
files.LICENSE = project.license?.type || 'UNSPECIFIED';
|
|
375
|
-
|
|
953
|
+
files.mimetype = 'application/vnd.aikdna.kdna+zip';
|
|
954
|
+
|
|
955
|
+
// Recompute content_digest after README.md / LICENSE / mimetype are added
|
|
956
|
+
// so that manifest, receipt, and provenance report all agree with the final ZIP contents.
|
|
957
|
+
const finalDigest = compileApi.computeContentDigest(files);
|
|
958
|
+
result.identity.content_digest = finalDigest;
|
|
376
959
|
|
|
377
|
-
|
|
378
|
-
|
|
960
|
+
// Update kdna.json manifest with final digest
|
|
961
|
+
const manifest = JSON.parse(files['kdna.json']);
|
|
962
|
+
manifest.content_digest = finalDigest;
|
|
963
|
+
if (manifest.authoring) manifest.authoring.content_digest = finalDigest;
|
|
964
|
+
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
965
|
+
|
|
966
|
+
// Update build-receipt.json
|
|
967
|
+
let receiptData = JSON.parse(files['build-receipt.json']);
|
|
968
|
+
receiptData.content_digest = finalDigest;
|
|
969
|
+
files['build-receipt.json'] = JSON.stringify(receiptData, null, 2);
|
|
970
|
+
|
|
971
|
+
// Update provenance report
|
|
972
|
+
const provenance = JSON.parse(files['reports/provenance-report.json']);
|
|
973
|
+
provenance.content_digest = finalDigest;
|
|
974
|
+
provenance.content_fingerprint = finalDigest;
|
|
975
|
+
files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
|
|
976
|
+
|
|
977
|
+
if (args.includes('--sign')) applySignature(files, option(args, '--passphrase'));
|
|
978
|
+
|
|
979
|
+
const entries = [['mimetype', files.mimetype]];
|
|
980
|
+
for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
|
|
981
|
+
entries.push([name, files[name]]);
|
|
982
|
+
}
|
|
379
983
|
const zip = buildZip(entries);
|
|
380
984
|
const outPath = path.resolve(out);
|
|
381
985
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
382
986
|
fs.writeFileSync(outPath, zip);
|
|
383
987
|
|
|
384
988
|
const assetDigest = `sha256:${crypto.createHash('sha256').update(zip).digest('hex')}`;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(
|
|
989
|
+
receiptData = JSON.parse(files['build-receipt.json']);
|
|
990
|
+
receiptData.asset_path = outPath;
|
|
991
|
+
receiptData.asset_digest = assetDigest;
|
|
992
|
+
receiptData.signature_status = args.includes('--sign') ? 'signed' : 'unsigned';
|
|
993
|
+
fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(receiptData, null, 2));
|
|
994
|
+
fs.writeFileSync(path.join(path.dirname(outPath), 'kdna.json'), files['kdna.json']);
|
|
390
995
|
fs.writeFileSync(path.join(path.dirname(outPath), 'provenance-report.json'), files['reports/provenance-report.json']);
|
|
391
996
|
fs.writeFileSync(path.join(path.dirname(outPath), 'quality-gate-report.json'), files['reports/quality-gate-report.json']);
|
|
392
997
|
fs.writeFileSync(path.join(path.dirname(outPath), 'human-lock-report.json'), files['reports/human-lock-report.json']);
|
|
@@ -396,6 +1001,99 @@ function cmdExport(args) {
|
|
|
396
1001
|
console.log(`Build ID: ${result.identity.build_id}`);
|
|
397
1002
|
}
|
|
398
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
|
+
|
|
1061
|
+
function cmdIdentity(args) {
|
|
1062
|
+
const sub = args[0];
|
|
1063
|
+
if (sub === 'init') {
|
|
1064
|
+
const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
|
|
1065
|
+
const passphrase = option(args, '--passphrase');
|
|
1066
|
+
try {
|
|
1067
|
+
const identity = creatorApi.initIdentity(name, null, passphrase);
|
|
1068
|
+
console.log(`Creator identity initialized:`);
|
|
1069
|
+
console.log(` creator_id: ${identity.creator_id}`);
|
|
1070
|
+
console.log(` display_name: ${identity.display_name}`);
|
|
1071
|
+
console.log(` identity_dir: ${identity.identity_dir}`);
|
|
1072
|
+
console.log(` public_key saved: ${identity.public_key_path}`);
|
|
1073
|
+
console.log(` encrypted: ${identity.encrypted}`);
|
|
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
|
+
}
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
fail(`Identity already exists. Use 'kdna-studio identity show' to view. ${err.message}`);
|
|
1080
|
+
}
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (sub === 'show') {
|
|
1084
|
+
const identity = creatorApi.loadIdentity();
|
|
1085
|
+
if (!identity) fail('No identity found. Run: kdna-studio identity init --name "Your Name"');
|
|
1086
|
+
console.log(JSON.stringify({
|
|
1087
|
+
creator_id: identity.creator_id,
|
|
1088
|
+
display_name: identity.display_name,
|
|
1089
|
+
verified: identity.verified || false,
|
|
1090
|
+
created_at: identity.created_at,
|
|
1091
|
+
}, null, 2));
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
fail('Usage: kdna-studio identity <init|show>');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
399
1097
|
function cmdReport(args) {
|
|
400
1098
|
const { project } = readProject(args[0]);
|
|
401
1099
|
const readiness = quality.computeReadiness(project);
|
|
@@ -404,6 +1102,269 @@ function cmdReport(args) {
|
|
|
404
1102
|
process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
405
1103
|
}
|
|
406
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
|
+
|
|
407
1368
|
const args = process.argv.slice(2);
|
|
408
1369
|
const cmd = args[0];
|
|
409
1370
|
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
@@ -411,14 +1372,28 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
411
1372
|
process.exit(EXIT.OK);
|
|
412
1373
|
}
|
|
413
1374
|
|
|
1375
|
+
(async () => {
|
|
414
1376
|
try {
|
|
415
1377
|
if (cmd === 'create') cmdCreate(args.slice(1));
|
|
1378
|
+
else if (cmd === 'migrate') cmdMigrate(args.slice(1));
|
|
416
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));
|
|
417
1388
|
else if (cmd === 'card') cmdCard(args.slice(1));
|
|
418
1389
|
else if (cmd === 'lock') cmdLock(args.slice(1));
|
|
419
1390
|
else if (cmd === 'compile') cmdCompile(args.slice(1));
|
|
420
1391
|
else if (cmd === 'export') cmdExport(args.slice(1));
|
|
1392
|
+
else if (cmd === 'llm') cmdLlm(args.slice(1));
|
|
1393
|
+
else if (cmd === 'identity') cmdIdentity(args.slice(1));
|
|
421
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));
|
|
422
1397
|
else {
|
|
423
1398
|
usage();
|
|
424
1399
|
fail(`Unknown command: ${cmd}`);
|
|
@@ -426,3 +1401,4 @@ try {
|
|
|
426
1401
|
} catch (err) {
|
|
427
1402
|
fail(err.message || String(err));
|
|
428
1403
|
}
|
|
1404
|
+
})();
|