@aikdna/kdna-cli 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -23
- package/package.json +5 -4
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +145 -109
- package/src/cli.js +104 -60
- package/src/cmds/_common.js +32 -16
- package/src/cmds/badge.js +7 -7
- package/src/cmds/changelog.js +1 -1
- package/src/cmds/cluster.js +16 -48
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +77 -400
- package/src/cmds/explain.js +122 -0
- package/src/cmds/legacy.js +8 -8
- package/src/cmds/license.js +483 -26
- package/src/cmds/quality.js +2 -2
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +4 -5
- package/src/cmds/test.js +4 -4
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +28 -22
- package/src/diff.js +11 -13
- package/src/init.js +2 -2
- package/src/install.js +138 -460
- package/src/loader.js +10 -10
- package/src/package-store.js +229 -0
- package/src/paths.js +44 -0
- package/src/publish.js +73 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +233 -124
- package/templates/standard-domain/kdna.json +2 -1
- package/src/cmds/encrypt.js +0 -199
package/src/cmds/license.js
CHANGED
|
@@ -10,23 +10,340 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const crypto = require('crypto');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const https = require('https');
|
|
13
15
|
const { EXIT, error } = require('./_common');
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
verifyLicense,
|
|
18
|
-
verifyLicenseSignature,
|
|
19
|
-
machineFingerprint,
|
|
20
|
-
} = require('./encrypt');
|
|
21
|
-
|
|
22
|
-
const IDENTITY_DIR = path.join(
|
|
23
|
-
process.env.HOME || process.env.USERPROFILE || '.',
|
|
24
|
-
'.kdna',
|
|
25
|
-
'identity',
|
|
26
|
-
);
|
|
16
|
+
const { recordTrace } = require('./trace');
|
|
17
|
+
const PATHS = require('../paths');
|
|
18
|
+
const IDENTITY_DIR = PATHS.identity;
|
|
27
19
|
const PRIVATE_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.key');
|
|
28
20
|
const PUBLIC_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.pub');
|
|
29
21
|
|
|
22
|
+
function machineFingerprint() {
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const parts = [os.hostname(), os.userInfo().uid.toString(), os.platform(), os.arch()];
|
|
25
|
+
try {
|
|
26
|
+
const { execSync } = require('child_process');
|
|
27
|
+
if (os.platform() === 'darwin') {
|
|
28
|
+
const uuid = execSync('ioreg -d2 -c IOPlatformExpertDevice | grep IOPlatformUUID', {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
timeout: 3000,
|
|
31
|
+
})
|
|
32
|
+
.match(/"[A-F0-9-]{36}"/)?.[0]
|
|
33
|
+
?.replace(/"/g, '');
|
|
34
|
+
if (uuid) parts.push(uuid);
|
|
35
|
+
}
|
|
36
|
+
if (os.platform() === 'linux') {
|
|
37
|
+
try {
|
|
38
|
+
const mid = fs.readFileSync('/etc/machine-id', 'utf8').trim();
|
|
39
|
+
if (mid) parts.push(mid);
|
|
40
|
+
} catch {
|
|
41
|
+
/* ignore */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
/* non-critical */
|
|
46
|
+
}
|
|
47
|
+
return crypto.createHash('sha256').update(parts.join('|')).digest('hex');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createLicense(domain, options = {}) {
|
|
51
|
+
return {
|
|
52
|
+
version: '1.0',
|
|
53
|
+
license_id: `lic_${crypto.randomBytes(8).toString('hex')}`,
|
|
54
|
+
license_key: options.licenseKey || `KDNA-LIC-${crypto.randomBytes(18).toString('base64url')}`,
|
|
55
|
+
domain,
|
|
56
|
+
issued_to: options.issuedTo || 'licensee@example.com',
|
|
57
|
+
issued_at: new Date().toISOString(),
|
|
58
|
+
expires_at: options.expiresAt || null,
|
|
59
|
+
max_agents: options.maxAgents || 1,
|
|
60
|
+
require_machine_binding: options.requireMachineBinding !== false,
|
|
61
|
+
require_online_check: !!options.requireOnlineCheck,
|
|
62
|
+
offline_grace_days: options.offlineGraceDays || 7,
|
|
63
|
+
allowed_agents: options.allowedAgents || ['claude_code', 'codex', 'opencode'],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function signLicense(license, privateKeyPem) {
|
|
68
|
+
const payload = { ...license };
|
|
69
|
+
delete payload.signature;
|
|
70
|
+
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
71
|
+
const sig = crypto.sign(null, Buffer.from(data), privateKeyPem);
|
|
72
|
+
return { ...license, signature: `ed25519:${sig.toString('hex')}` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function verifyLicenseSignature(license, publicKeyPem) {
|
|
76
|
+
const signature = license.signature?.replace('ed25519:', '') || '';
|
|
77
|
+
if (!signature) return false;
|
|
78
|
+
const payload = { ...license };
|
|
79
|
+
delete payload.signature;
|
|
80
|
+
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
81
|
+
try {
|
|
82
|
+
return crypto.verify(null, Buffer.from(data), publicKeyPem, Buffer.from(signature, 'hex'));
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function verifyLicense(license, _scopePubkey, fingerprint) {
|
|
89
|
+
const issues = [];
|
|
90
|
+
const now = new Date();
|
|
91
|
+
if (license.expires_at && new Date(license.expires_at) < new Date()) {
|
|
92
|
+
issues.push('License has expired');
|
|
93
|
+
}
|
|
94
|
+
if (license.revoked === true || license.status === 'revoked') {
|
|
95
|
+
issues.push('License has been revoked');
|
|
96
|
+
}
|
|
97
|
+
if (license.require_online_check) {
|
|
98
|
+
const offlineUntil = license.offline_valid_until
|
|
99
|
+
? new Date(license.offline_valid_until)
|
|
100
|
+
: null;
|
|
101
|
+
if (!offlineUntil || Number.isNaN(offlineUntil.getTime()) || offlineUntil < now) {
|
|
102
|
+
issues.push('License offline grace has expired');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (license.require_machine_binding) {
|
|
106
|
+
if (!license.machine_fingerprint) {
|
|
107
|
+
issues.push('License is not bound to this machine');
|
|
108
|
+
} else if (license.machine_fingerprint !== fingerprint) {
|
|
109
|
+
issues.push('Machine fingerprint mismatch');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
valid: issues.length === 0,
|
|
114
|
+
issues,
|
|
115
|
+
domain: license.domain,
|
|
116
|
+
license_id: license.license_id,
|
|
117
|
+
issued_to: license.issued_to,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function licenseServerType(server) {
|
|
122
|
+
if (!server) return null;
|
|
123
|
+
if (server.startsWith('file://')) return 'file';
|
|
124
|
+
if (server.startsWith('/')) return 'local-file';
|
|
125
|
+
try {
|
|
126
|
+
return new URL(server).protocol.replace(':', '');
|
|
127
|
+
} catch {
|
|
128
|
+
return 'unknown';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function recordLicenseTrace(action, license, extra = {}) {
|
|
133
|
+
const fingerprint = machineFingerprint();
|
|
134
|
+
const result = verifyLicense(license || {}, null, fingerprint);
|
|
135
|
+
recordTrace({
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
event: 'license',
|
|
138
|
+
action,
|
|
139
|
+
agent: 'kdna-cli',
|
|
140
|
+
domain: license?.domain || extra.domain || null,
|
|
141
|
+
license_id: license?.license_id || extra.license_id || null,
|
|
142
|
+
valid: result.valid,
|
|
143
|
+
issues: result.issues,
|
|
144
|
+
revoked: license?.revoked === true || license?.status === 'revoked',
|
|
145
|
+
require_online_check: !!license?.require_online_check,
|
|
146
|
+
offline_valid_until: license?.offline_valid_until || null,
|
|
147
|
+
server_type: licenseServerType(extra.server || license?.activation_server || license?.license_server_url),
|
|
148
|
+
synced: extra.synced,
|
|
149
|
+
sync_error: extra.sync_error,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function safeLicenseName(domain) {
|
|
154
|
+
return domain.replace(/^@/, '').replace('/', '-');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function licensePathForDomain(domain) {
|
|
158
|
+
return path.join(PATHS.licenses, `${safeLicenseName(domain)}.json`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readLicenseForDomain(domain) {
|
|
162
|
+
const file = licensePathForDomain(domain);
|
|
163
|
+
if (!fs.existsSync(file)) return null;
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function listInstalledLicenses() {
|
|
172
|
+
if (!fs.existsSync(PATHS.licenses)) return [];
|
|
173
|
+
return fs
|
|
174
|
+
.readdirSync(PATHS.licenses)
|
|
175
|
+
.filter((name) => name.endsWith('.json'))
|
|
176
|
+
.map((name) => {
|
|
177
|
+
const file = path.join(PATHS.licenses, name);
|
|
178
|
+
try {
|
|
179
|
+
return { file, license: JSON.parse(fs.readFileSync(file, 'utf8')) };
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function licenseKey(license) {
|
|
188
|
+
return license?.license_key || license?.activation_key || license?.key || null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function licenseDecryptOptionsForManifest(manifest) {
|
|
192
|
+
const domain = manifest?.name || manifest?.asset_id;
|
|
193
|
+
if (!domain) {
|
|
194
|
+
return { ok: false, error: 'licensed asset missing manifest name' };
|
|
195
|
+
}
|
|
196
|
+
const license = readLicenseForDomain(domain);
|
|
197
|
+
if (!license) {
|
|
198
|
+
return { ok: false, error: `no installed license for ${domain}` };
|
|
199
|
+
}
|
|
200
|
+
if (license.domain !== domain) {
|
|
201
|
+
return { ok: false, error: `installed license domain mismatch: ${license.domain}` };
|
|
202
|
+
}
|
|
203
|
+
const fingerprint = machineFingerprint();
|
|
204
|
+
const result = verifyLicense(license, null, fingerprint);
|
|
205
|
+
if (!result.valid) {
|
|
206
|
+
return { ok: false, error: result.issues.join('; ') };
|
|
207
|
+
}
|
|
208
|
+
const key = licenseKey(license);
|
|
209
|
+
if (!key) {
|
|
210
|
+
return { ok: false, error: `installed license for ${domain} has no license_key` };
|
|
211
|
+
}
|
|
212
|
+
const { createLicensedDecryptEntry } = require('@aikdna/kdna-core');
|
|
213
|
+
return {
|
|
214
|
+
ok: true,
|
|
215
|
+
license,
|
|
216
|
+
decryptEntry: createLicensedDecryptEntry({
|
|
217
|
+
licenseKey: key,
|
|
218
|
+
machineFingerprint: license.machine_fingerprint || fingerprint,
|
|
219
|
+
}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function argValue(args, flag) {
|
|
224
|
+
const idx = args.indexOf(flag);
|
|
225
|
+
return idx >= 0 ? args[idx + 1] : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function addOfflineLease(activation) {
|
|
229
|
+
const days = activation.offline_grace_days || 7;
|
|
230
|
+
const until = new Date();
|
|
231
|
+
until.setDate(until.getDate() + days);
|
|
232
|
+
return {
|
|
233
|
+
...activation,
|
|
234
|
+
last_checked_at: new Date().toISOString(),
|
|
235
|
+
offline_valid_until: activation.require_online_check
|
|
236
|
+
? until.toISOString()
|
|
237
|
+
: activation.offline_valid_until || null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeActivation(domain, key, payload, server = null) {
|
|
242
|
+
const source = payload.activation || payload.license || payload;
|
|
243
|
+
if (source.ok === false || payload.ok === false) {
|
|
244
|
+
throw new Error(source.error || payload.error || 'activation denied');
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(payload.activations)) {
|
|
247
|
+
const found = payload.activations.find(
|
|
248
|
+
(entry) => entry.domain === domain && licenseKey(entry) === key,
|
|
249
|
+
);
|
|
250
|
+
if (!found) throw new Error('activation not found for domain/key');
|
|
251
|
+
return normalizeActivation(domain, key, found, server);
|
|
252
|
+
}
|
|
253
|
+
if (source.domain && source.domain !== domain) {
|
|
254
|
+
throw new Error(`activation domain mismatch: ${source.domain}`);
|
|
255
|
+
}
|
|
256
|
+
if (licenseKey(source) && licenseKey(source) !== key) {
|
|
257
|
+
throw new Error('activation key mismatch');
|
|
258
|
+
}
|
|
259
|
+
const fingerprint = machineFingerprint();
|
|
260
|
+
return addOfflineLease({
|
|
261
|
+
version: source.version || '1.0',
|
|
262
|
+
license_id: source.license_id || `lic_${crypto.randomBytes(8).toString('hex')}`,
|
|
263
|
+
license_key: key,
|
|
264
|
+
domain,
|
|
265
|
+
issued_to: source.issued_to || source.email || null,
|
|
266
|
+
issued_at: source.issued_at || new Date().toISOString(),
|
|
267
|
+
expires_at: source.expires_at || null,
|
|
268
|
+
status: source.status || 'active',
|
|
269
|
+
revoked: source.revoked === true,
|
|
270
|
+
require_machine_binding: source.require_machine_binding !== false,
|
|
271
|
+
machine_fingerprint: source.machine_fingerprint || fingerprint,
|
|
272
|
+
require_online_check: source.require_online_check !== false,
|
|
273
|
+
offline_grace_days: source.offline_grace_days || 7,
|
|
274
|
+
allowed_agents: source.allowed_agents || ['claude_code', 'codex', 'opencode', 'cursor', 'gemini'],
|
|
275
|
+
activation_server: server || source.activation_server || source.license_server_url || null,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function readActivationFromFile(url, domain, key) {
|
|
280
|
+
const filePath = url.startsWith('file://') ? new URL(url).pathname : url;
|
|
281
|
+
const payload = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
282
|
+
return normalizeActivation(domain, key, payload, url);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function postJson(url, body) {
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
const u = new URL(url);
|
|
288
|
+
const client = u.protocol === 'http:' ? http : https;
|
|
289
|
+
const data = JSON.stringify(body);
|
|
290
|
+
const req = client.request(
|
|
291
|
+
{
|
|
292
|
+
method: 'POST',
|
|
293
|
+
hostname: u.hostname,
|
|
294
|
+
port: u.port || (u.protocol === 'http:' ? 80 : 443),
|
|
295
|
+
path: `${u.pathname}${u.search}`,
|
|
296
|
+
headers: {
|
|
297
|
+
'content-type': 'application/json',
|
|
298
|
+
'content-length': Buffer.byteLength(data),
|
|
299
|
+
},
|
|
300
|
+
timeout: 15000,
|
|
301
|
+
},
|
|
302
|
+
(res) => {
|
|
303
|
+
const chunks = [];
|
|
304
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
305
|
+
res.on('end', () => {
|
|
306
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
307
|
+
if (res.statusCode >= 400) {
|
|
308
|
+
reject(new Error(`activation server HTTP ${res.statusCode}: ${text.slice(0, 300)}`));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
resolve(JSON.parse(text));
|
|
313
|
+
} catch {
|
|
314
|
+
reject(new Error(`activation server returned invalid JSON`));
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
req.on('error', reject);
|
|
320
|
+
req.on('timeout', () => req.destroy(new Error('activation server timeout')));
|
|
321
|
+
req.write(data);
|
|
322
|
+
req.end();
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function requestActivation(domain, key, server) {
|
|
327
|
+
if (!server) throw new Error('--server <url> is required for activation');
|
|
328
|
+
if (server.startsWith('file://') || server.startsWith('/')) {
|
|
329
|
+
return readActivationFromFile(server, domain, key);
|
|
330
|
+
}
|
|
331
|
+
const payload = await postJson(server, {
|
|
332
|
+
domain,
|
|
333
|
+
license_key: key,
|
|
334
|
+
machine_fingerprint: machineFingerprint(),
|
|
335
|
+
client: 'kdna-cli',
|
|
336
|
+
});
|
|
337
|
+
return normalizeActivation(domain, key, payload, server);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function writeInstalledLicense(license) {
|
|
341
|
+
fs.mkdirSync(PATHS.licenses, { recursive: true });
|
|
342
|
+
const dest = licensePathForDomain(license.domain);
|
|
343
|
+
fs.writeFileSync(dest, JSON.stringify(license, null, 2) + '\n');
|
|
344
|
+
return dest;
|
|
345
|
+
}
|
|
346
|
+
|
|
30
347
|
function readIdentity() {
|
|
31
348
|
if (!fs.existsSync(PRIVATE_KEY_PATH) || !fs.existsSync(PUBLIC_KEY_PATH)) {
|
|
32
349
|
error('No identity found. Run: kdna identity init', EXIT.INPUT_ERROR);
|
|
@@ -90,7 +407,7 @@ function cmdLicenseGenerate(args) {
|
|
|
90
407
|
if (expiresAt) console.error(` Expires: ${expiresAt}`);
|
|
91
408
|
console.error(` Machine binding: ${requireBinding ? 'required' : 'disabled'}`);
|
|
92
409
|
console.error('');
|
|
93
|
-
console.error('Save this
|
|
410
|
+
console.error('Save this license activation outside the .kdna asset under ~/.kdna/licenses/.');
|
|
94
411
|
console.error('Share the license key with the licensee.');
|
|
95
412
|
}
|
|
96
413
|
|
|
@@ -171,7 +488,7 @@ function cmdLicenseBind(args) {
|
|
|
171
488
|
process.exit(EXIT.OK);
|
|
172
489
|
}
|
|
173
490
|
|
|
174
|
-
const { privateKey
|
|
491
|
+
const { privateKey } = readIdentity();
|
|
175
492
|
const fp = machineFingerprint();
|
|
176
493
|
|
|
177
494
|
// Remove old signature before re-signing
|
|
@@ -209,17 +526,8 @@ function cmdLicenseInstall(args) {
|
|
|
209
526
|
|
|
210
527
|
if (!license.domain) error('License missing domain field', EXIT.INPUT_ERROR);
|
|
211
528
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
'.kdna',
|
|
215
|
-
'licenses',
|
|
216
|
-
);
|
|
217
|
-
fs.mkdirSync(licenseDir, { recursive: true });
|
|
218
|
-
|
|
219
|
-
const safeName = license.domain.replace(/^@/, '').replace('/', '-');
|
|
220
|
-
const dest = path.join(licenseDir, `${safeName}.json`);
|
|
221
|
-
|
|
222
|
-
fs.writeFileSync(dest, JSON.stringify(license, null, 2) + '\n');
|
|
529
|
+
const dest = writeInstalledLicense(license);
|
|
530
|
+
recordLicenseTrace('install', license);
|
|
223
531
|
|
|
224
532
|
console.log(`License installed for ${license.domain}`);
|
|
225
533
|
console.log(` License ID: ${license.license_id || 'unknown'}`);
|
|
@@ -228,10 +536,159 @@ function cmdLicenseInstall(args) {
|
|
|
228
536
|
console.log(`Now install the domain: kdna install ${license.domain}`);
|
|
229
537
|
}
|
|
230
538
|
|
|
539
|
+
async function cmdLicenseActivate(args = []) {
|
|
540
|
+
const domain = args.find((arg) => !arg.startsWith('--'));
|
|
541
|
+
const key = argValue(args, '--key') || argValue(args, '--license-key');
|
|
542
|
+
const server = argValue(args, '--server');
|
|
543
|
+
const jsonMode = args.includes('--json');
|
|
544
|
+
if (!domain || !key) {
|
|
545
|
+
error('Usage: kdna license activate <domain> --key <license-key> --server <url>', EXIT.INPUT_ERROR);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let activation;
|
|
549
|
+
try {
|
|
550
|
+
activation = await requestActivation(domain, key, server);
|
|
551
|
+
} catch (e) {
|
|
552
|
+
error(`License activation failed: ${e.message}`, EXIT.TRUST_FAILED);
|
|
553
|
+
}
|
|
554
|
+
const dest = writeInstalledLicense(activation);
|
|
555
|
+
recordLicenseTrace('activate', activation, { server });
|
|
556
|
+
const record = licenseStatusRecord(activation, dest);
|
|
557
|
+
if (jsonMode) {
|
|
558
|
+
console.log(JSON.stringify(record, null, 2));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
console.log(`License activated for ${domain}`);
|
|
562
|
+
console.log(` License ID: ${activation.license_id}`);
|
|
563
|
+
console.log(` Status: ${record.valid ? 'valid' : 'invalid'}`);
|
|
564
|
+
if (activation.offline_valid_until) {
|
|
565
|
+
console.log(` Offline valid until: ${activation.offline_valid_until}`);
|
|
566
|
+
}
|
|
567
|
+
console.log(` Saved to: ${dest}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function syncOneLicense(entry, serverOverride = null) {
|
|
571
|
+
const license = entry.license;
|
|
572
|
+
const key = licenseKey(license);
|
|
573
|
+
const server = serverOverride || license.activation_server || license.license_server_url || null;
|
|
574
|
+
if (!license.domain || !key || !server) {
|
|
575
|
+
return {
|
|
576
|
+
...licenseStatusRecord(license, entry.file),
|
|
577
|
+
synced: false,
|
|
578
|
+
sync_error: 'missing domain, license key, or activation server',
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const activation = await requestActivation(license.domain, key, server);
|
|
583
|
+
const merged = {
|
|
584
|
+
...license,
|
|
585
|
+
...activation,
|
|
586
|
+
license_key: key,
|
|
587
|
+
activation_server: server,
|
|
588
|
+
};
|
|
589
|
+
fs.writeFileSync(entry.file, JSON.stringify(merged, null, 2) + '\n');
|
|
590
|
+
recordLicenseTrace('sync', merged, { server, synced: true });
|
|
591
|
+
return { ...licenseStatusRecord(merged, entry.file), synced: true };
|
|
592
|
+
} catch (e) {
|
|
593
|
+
recordLicenseTrace('sync', license, { server, synced: false, sync_error: e.message });
|
|
594
|
+
return {
|
|
595
|
+
...licenseStatusRecord(license, entry.file),
|
|
596
|
+
synced: false,
|
|
597
|
+
sync_error: e.message,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function cmdLicenseSync(args = []) {
|
|
603
|
+
const jsonMode = args.includes('--json');
|
|
604
|
+
const server = argValue(args, '--server');
|
|
605
|
+
const filtered = args.filter((arg) => !arg.startsWith('--') && arg !== server);
|
|
606
|
+
const domain = filtered[0] || null;
|
|
607
|
+
const entries = domain
|
|
608
|
+
? [{ file: licensePathForDomain(domain), license: readLicenseForDomain(domain) }].filter(
|
|
609
|
+
(entry) => entry.license,
|
|
610
|
+
)
|
|
611
|
+
: listInstalledLicenses();
|
|
612
|
+
const records = [];
|
|
613
|
+
for (const entry of entries) records.push(await syncOneLicense(entry, server));
|
|
614
|
+
if (jsonMode) {
|
|
615
|
+
console.log(JSON.stringify(domain ? records[0] || null : records, null, 2));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!records.length) {
|
|
619
|
+
console.log(domain ? `No installed license for ${domain}.` : 'No installed KDNA licenses.');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
for (const record of records) {
|
|
623
|
+
console.log(`${record.domain || '(unknown)'} ${record.license_id || '(no license_id)'}`);
|
|
624
|
+
console.log(` Sync: ${record.synced ? 'ok' : 'failed'}`);
|
|
625
|
+
console.log(` Status: ${record.valid ? 'valid' : 'invalid'}`);
|
|
626
|
+
if (record.sync_error) console.log(` Error: ${record.sync_error}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function licenseStatusRecord(license, file) {
|
|
631
|
+
const fingerprint = machineFingerprint();
|
|
632
|
+
const result = verifyLicense(license, null, fingerprint);
|
|
633
|
+
return {
|
|
634
|
+
domain: license.domain || null,
|
|
635
|
+
license_id: license.license_id || null,
|
|
636
|
+
issued_to: license.issued_to || null,
|
|
637
|
+
valid: result.valid,
|
|
638
|
+
issues: result.issues,
|
|
639
|
+
require_machine_binding: !!license.require_machine_binding,
|
|
640
|
+
machine_bound: !license.require_machine_binding
|
|
641
|
+
|| license.machine_fingerprint === fingerprint,
|
|
642
|
+
expires_at: license.expires_at || null,
|
|
643
|
+
revoked: license.revoked === true || license.status === 'revoked',
|
|
644
|
+
file,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function cmdLicenseStatus(args = []) {
|
|
649
|
+
const jsonMode = args.includes('--json');
|
|
650
|
+
const filtered = args.filter((a) => !a.startsWith('--'));
|
|
651
|
+
const domain = filtered[0] || null;
|
|
652
|
+
const entries = domain
|
|
653
|
+
? [{ file: licensePathForDomain(domain), license: readLicenseForDomain(domain) }].filter(
|
|
654
|
+
(entry) => entry.license,
|
|
655
|
+
)
|
|
656
|
+
: listInstalledLicenses();
|
|
657
|
+
const records = entries.map((entry) => licenseStatusRecord(entry.license, entry.file));
|
|
658
|
+
|
|
659
|
+
if (jsonMode) {
|
|
660
|
+
console.log(JSON.stringify(domain ? records[0] || null : records, null, 2));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!records.length) {
|
|
665
|
+
console.log(domain ? `No installed license for ${domain}.` : 'No installed KDNA licenses.');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
for (const record of records) {
|
|
669
|
+
console.log(`${record.domain || '(unknown)'} ${record.license_id || '(no license_id)'}`);
|
|
670
|
+
console.log(` Status: ${record.valid ? 'valid' : 'invalid'}`);
|
|
671
|
+
if (record.issued_to) console.log(` Issued to: ${record.issued_to}`);
|
|
672
|
+
if (record.expires_at) console.log(` Expires: ${record.expires_at}`);
|
|
673
|
+
console.log(` Machine bound: ${record.machine_bound ? 'yes' : 'no'}`);
|
|
674
|
+
if (record.issues.length) {
|
|
675
|
+
console.log(` Issues: ${record.issues.join('; ')}`);
|
|
676
|
+
}
|
|
677
|
+
console.log(` File: ${record.file}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
231
681
|
module.exports = {
|
|
232
682
|
cmdLicenseGenerate,
|
|
233
683
|
cmdLicenseVerify,
|
|
234
684
|
cmdLicenseBind,
|
|
235
685
|
cmdLicenseShow,
|
|
236
686
|
cmdLicenseInstall,
|
|
687
|
+
cmdLicenseStatus,
|
|
688
|
+
cmdLicenseActivate,
|
|
689
|
+
cmdLicenseSync,
|
|
690
|
+
licenseDecryptOptionsForManifest,
|
|
691
|
+
licensePathForDomain,
|
|
692
|
+
machineFingerprint,
|
|
693
|
+
verifyLicense,
|
|
237
694
|
};
|
package/src/cmds/quality.js
CHANGED
|
@@ -7,7 +7,7 @@ function cmdCompare(args) {
|
|
|
7
7
|
if (!target || !args.includes('--input')) {
|
|
8
8
|
error(
|
|
9
9
|
'Usage:\n' +
|
|
10
|
-
' kdna compare <name> --input "<text>" [--json]\n' +
|
|
10
|
+
' kdna compare <name|file.kdna> --input "<text>" [--json]\n' +
|
|
11
11
|
'\n' +
|
|
12
12
|
'Runs your input through the LLM twice (with/without KDNA loaded),\n' +
|
|
13
13
|
'then diffs the reasoning trajectory. Requires ANTHROPIC_API_KEY or\n' +
|
|
@@ -91,7 +91,7 @@ function cmdSelect(args) {
|
|
|
91
91
|
function cmdLoad(args) {
|
|
92
92
|
const { cmdLoad } = require('../agent');
|
|
93
93
|
const target = args.filter((a) => !a.startsWith('--'))[1];
|
|
94
|
-
if (!target) error('Usage: kdna load <name> [--as=prompt|json|raw] [--profile=index|compact|scenario|full]');
|
|
94
|
+
if (!target) error('Usage: kdna load <name|file.kdna> [--as=prompt|json|raw] [--profile=index|compact|scenario|full]');
|
|
95
95
|
cmdLoad(target, args);
|
|
96
96
|
}
|
|
97
97
|
|
package/src/cmds/registry.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
1
|
const { CANONICAL_REGISTRY_URL, REGISTRY_CACHE, fetchRegistry } = require('../registry');
|
|
4
|
-
const { error,
|
|
2
|
+
const { error, loadRegistry, EXIT } = require('./_common');
|
|
3
|
+
const { listInstalled, readContainer } = require('../package-store');
|
|
5
4
|
|
|
6
|
-
function cmdList(showAvailable, jsonMode = false,
|
|
5
|
+
function cmdList(showAvailable, jsonMode = false, _locale = null) {
|
|
7
6
|
if (showAvailable) {
|
|
8
7
|
const domains = loadRegistry({ allowNetwork: true });
|
|
9
8
|
if (!domains || !domains.length) {
|
|
@@ -33,9 +32,7 @@ function cmdList(showAvailable, jsonMode = false, locale = null) {
|
|
|
33
32
|
console.log('');
|
|
34
33
|
for (const d of domains) {
|
|
35
34
|
const name = d.name || d.id || '?';
|
|
36
|
-
const
|
|
37
|
-
const installedPath = scope ? path.join(INSTALL_DIR, scope, ident) : null;
|
|
38
|
-
const installed = installedPath && fs.existsSync(installedPath) ? '[installed]' : '';
|
|
35
|
+
const installed = listInstalled().some((entry) => entry.full === name) ? '[installed]' : '';
|
|
39
36
|
const yanked = d.yanked ? '[yanked] ' : '';
|
|
40
37
|
const dep = d.deprecated ? '[deprecated] ' : '';
|
|
41
38
|
console.log(
|
|
@@ -47,78 +44,29 @@ function cmdList(showAvailable, jsonMode = false, locale = null) {
|
|
|
47
44
|
return;
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
if (jsonMode) {
|
|
52
|
-
console.log(JSON.stringify([]));
|
|
53
|
-
process.exit(EXIT.OK);
|
|
54
|
-
}
|
|
55
|
-
console.log('No domains installed.');
|
|
56
|
-
console.log(`Installation directory: ${INSTALL_DIR}`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// v0.7 layout: ~/.kdna/domains/@scope/name/
|
|
61
|
-
const scopes = fs.readdirSync(INSTALL_DIR).filter((d) => {
|
|
62
|
-
if (!d.startsWith('@')) return false;
|
|
63
|
-
try {
|
|
64
|
-
return fs.statSync(path.join(INSTALL_DIR, d)).isDirectory();
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const installed = [];
|
|
71
|
-
for (const scope of scopes) {
|
|
72
|
-
const sd = path.join(INSTALL_DIR, scope);
|
|
73
|
-
for (const ident of fs.readdirSync(sd)) {
|
|
74
|
-
if (ident.startsWith('.')) continue;
|
|
75
|
-
const full = path.join(sd, ident);
|
|
76
|
-
try {
|
|
77
|
-
if (!fs.statSync(full).isDirectory()) continue;
|
|
78
|
-
} catch {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
installed.push({ scope, ident, full });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Detect and warn about legacy (un-scoped) installs
|
|
86
|
-
if (!jsonMode) {
|
|
87
|
-
const legacy = fs.readdirSync(INSTALL_DIR).filter((d) => {
|
|
88
|
-
if (d.startsWith('@') || d.startsWith('.')) return false;
|
|
89
|
-
try {
|
|
90
|
-
return fs.statSync(path.join(INSTALL_DIR, d)).isDirectory();
|
|
91
|
-
} catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
if (legacy.length) {
|
|
96
|
-
console.log('⚠ Legacy (un-scoped) directories detected — please remove + re-install:');
|
|
97
|
-
legacy.forEach((d) => console.log(` ~/.kdna/domains/${d}/`));
|
|
98
|
-
console.log('');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
47
|
+
const installed = listInstalled();
|
|
101
48
|
|
|
102
49
|
if (!installed.length) {
|
|
103
50
|
if (jsonMode) {
|
|
104
51
|
console.log(JSON.stringify([]));
|
|
105
52
|
process.exit(EXIT.OK);
|
|
106
53
|
}
|
|
107
|
-
console.log('No
|
|
54
|
+
console.log('No KDNA assets installed.');
|
|
108
55
|
console.log(`Run: kdna install <name> # e.g. kdna install writing`);
|
|
109
56
|
return;
|
|
110
57
|
}
|
|
111
58
|
|
|
112
59
|
// Build structured data for installed domains
|
|
113
|
-
const domains = installed.map((
|
|
114
|
-
const core =
|
|
115
|
-
const manifest = readJson(path.join(full, 'kdna.json'));
|
|
116
|
-
const cluster = readJson(path.join(full, 'cluster.json'));
|
|
60
|
+
const domains = installed.map((entry) => {
|
|
61
|
+
const { core = {}, manifest = {} } = readContainer(entry.asset_path);
|
|
117
62
|
return {
|
|
118
|
-
name:
|
|
119
|
-
version: manifest?.version ||
|
|
120
|
-
type:
|
|
63
|
+
name: entry.full,
|
|
64
|
+
version: manifest?.version || entry.version || core?.meta?.version || '?',
|
|
65
|
+
type: 'domain',
|
|
121
66
|
description: manifest?.description || core?.meta?.purpose || '',
|
|
67
|
+
asset: entry.asset_path,
|
|
68
|
+
asset_digest: entry.asset_digest || null,
|
|
69
|
+
content_digest: entry.content_digest || null,
|
|
122
70
|
};
|
|
123
71
|
});
|
|
124
72
|
|
|
@@ -135,7 +83,7 @@ function cmdList(showAvailable, jsonMode = false, locale = null) {
|
|
|
135
83
|
if (d.description) console.log(` ${d.description}`);
|
|
136
84
|
}
|
|
137
85
|
console.log('');
|
|
138
|
-
console.log(
|
|
86
|
+
console.log('Assets are stored under ~/.kdna/packages/.');
|
|
139
87
|
}
|
|
140
88
|
|
|
141
89
|
function cmdRegistry(subcommand) {
|