@aikdna/kdna-cli 0.9.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +73 -0
- package/package.json +58 -0
- package/skills/kdna-loader/SKILL.md +257 -0
- package/src/agent.js +434 -0
- package/src/cli.js +260 -0
- package/src/cluster.js +235 -0
- package/src/cmds/_common.js +100 -0
- package/src/cmds/cluster.js +235 -0
- package/src/cmds/domain.js +638 -0
- package/src/cmds/identity.js +31 -0
- package/src/cmds/legacy.js +83 -0
- package/src/cmds/quality.js +87 -0
- package/src/cmds/registry.js +114 -0
- package/src/cmds/setup.js +8 -0
- package/src/compare.js +324 -0
- package/src/diff.js +288 -0
- package/src/identity.js +211 -0
- package/src/init.js +168 -0
- package/src/install.js +849 -0
- package/src/loader.js +70 -0
- package/src/publish.js +600 -0
- package/src/registry.js +258 -0
- package/src/search.js +73 -0
- package/src/setup.js +197 -0
- package/src/verify.js +423 -0
- package/src/version.js +112 -0
- package/templates/cluster/KDNA_Cluster.json +25 -0
- package/templates/cluster/README.md +32 -0
- package/templates/minimal-domain/KDNA_Core.json +54 -0
- package/templates/minimal-domain/KDNA_Patterns.json +37 -0
- package/templates/minimal-domain/kdna.json +31 -0
- package/templates/minimal-domain/tests/before-after.json +16 -0
- package/templates/standard-domain/KDNA_Core.json +76 -0
- package/templates/standard-domain/KDNA_Patterns.json +44 -0
- package/templates/standard-domain/README.md +74 -0
- package/templates/standard-domain/USAGE.md +59 -0
- package/templates/standard-domain/evals/1_excluded_case.json +16 -0
- package/templates/standard-domain/evals/3_boundary_cases.json +38 -0
- package/templates/standard-domain/evals/3_core_cases.json +35 -0
- package/templates/standard-domain/evals/3_failure_cases.json +35 -0
- package/templates/standard-domain/evals/scoring.json +60 -0
- package/templates/standard-domain/kdna.json +28 -0
- package/validators/kdna-lint.js +53 -0
- package/validators/kdna-validate.js +92 -0
package/src/registry.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RegistryResolver — KDNA v0.7 registry client.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* 1. Resolve names: bare → @aikdna/bare, validate @scope/name format
|
|
6
|
+
* 2. Route lookups to the right registry (official vs private)
|
|
7
|
+
* 3. Cache registry metadata locally
|
|
8
|
+
* 4. Surface scope trust info to install/publish
|
|
9
|
+
*
|
|
10
|
+
* Schema v2.0 — see kdna-registry/SCHEMA.md
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execFileSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
18
|
+
const REGISTRY_DIR = path.join(USER_KDNA_DIR, 'registry');
|
|
19
|
+
const CONFIG_FILE = path.join(USER_KDNA_DIR, 'config.json');
|
|
20
|
+
|
|
21
|
+
const DEFAULT_OFFICIAL_SCOPE = '@aikdna';
|
|
22
|
+
const CANONICAL_REGISTRY_URL =
|
|
23
|
+
process.env.KDNA_REGISTRY_URL ||
|
|
24
|
+
'https://raw.githubusercontent.com/knowledge-dna/kdna-registry/main/domains.json';
|
|
25
|
+
|
|
26
|
+
const NAME_RE = /^@([a-z][a-z0-9-]*)\/([a-z][a-z0-9_]*)$/;
|
|
27
|
+
const BARE_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
28
|
+
|
|
29
|
+
function readJson(file) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeJson(file, data) {
|
|
38
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
39
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Name parsing ───────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse a name string into { scope, ident, full }.
|
|
46
|
+
* - "@aikdna/writing" → { scope: "@aikdna", ident: "writing", full: "@aikdna/writing" }
|
|
47
|
+
* - "writing" → expanded to default official scope → @aikdna/writing
|
|
48
|
+
* Returns null if invalid.
|
|
49
|
+
*/
|
|
50
|
+
function parseName(input) {
|
|
51
|
+
if (typeof input !== 'string') return null;
|
|
52
|
+
const trimmed = input.trim();
|
|
53
|
+
|
|
54
|
+
const scoped = trimmed.match(NAME_RE);
|
|
55
|
+
if (scoped) {
|
|
56
|
+
return {
|
|
57
|
+
scope: `@${scoped[1]}`,
|
|
58
|
+
ident: scoped[2],
|
|
59
|
+
full: trimmed,
|
|
60
|
+
wasShort: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (BARE_NAME_RE.test(trimmed)) {
|
|
65
|
+
const scope = DEFAULT_OFFICIAL_SCOPE;
|
|
66
|
+
return {
|
|
67
|
+
scope,
|
|
68
|
+
ident: trimmed,
|
|
69
|
+
full: `${scope}/${trimmed}`,
|
|
70
|
+
wasShort: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Config (multi-registry routing) ────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function loadConfig() {
|
|
80
|
+
const cfg = readJson(CONFIG_FILE) || {};
|
|
81
|
+
return {
|
|
82
|
+
default_scope: cfg.default_scope || DEFAULT_OFFICIAL_SCOPE,
|
|
83
|
+
registries: cfg.registries || {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Registry source ────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
class RegistrySource {
|
|
90
|
+
constructor(url, cacheFile) {
|
|
91
|
+
this.url = url;
|
|
92
|
+
this.cacheFile = cacheFile;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fetch() {
|
|
96
|
+
const raw = execFileSync('curl', ['-fsSL', this.url], { encoding: 'utf8', timeout: 30000 });
|
|
97
|
+
const data = JSON.parse(raw);
|
|
98
|
+
writeJson(this.cacheFile, data);
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
load({ allowNetwork = false, refresh = false } = {}) {
|
|
103
|
+
if (!refresh) {
|
|
104
|
+
const cached = readJson(this.cacheFile);
|
|
105
|
+
if (cached) return cached;
|
|
106
|
+
}
|
|
107
|
+
if (allowNetwork) {
|
|
108
|
+
try {
|
|
109
|
+
return this.fetch();
|
|
110
|
+
} catch {
|
|
111
|
+
const cached = readJson(this.cacheFile);
|
|
112
|
+
if (cached) return cached;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Resolver ───────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
class RegistryResolver {
|
|
122
|
+
constructor({ allowNetwork = true, refresh = false } = {}) {
|
|
123
|
+
this.allowNetwork = allowNetwork;
|
|
124
|
+
this.refresh = refresh;
|
|
125
|
+
this.config = loadConfig();
|
|
126
|
+
this._sources = new Map();
|
|
127
|
+
this._registries = new Map();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_sourceForScope(scopeName) {
|
|
131
|
+
if (this._sources.has(scopeName)) return this._sources.get(scopeName);
|
|
132
|
+
|
|
133
|
+
const cfgEntry = this.config.registries[scopeName];
|
|
134
|
+
let url, cacheName;
|
|
135
|
+
|
|
136
|
+
if (cfgEntry) {
|
|
137
|
+
url = typeof cfgEntry === 'string' ? cfgEntry : cfgEntry.url;
|
|
138
|
+
cacheName = `${scopeName.replace('@', '')}.json`;
|
|
139
|
+
} else {
|
|
140
|
+
// Default: all unknown scopes route to canonical official registry
|
|
141
|
+
url = CANONICAL_REGISTRY_URL;
|
|
142
|
+
cacheName = 'domains.json';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cacheFile = path.join(REGISTRY_DIR, cacheName);
|
|
146
|
+
const source = new RegistrySource(url, cacheFile);
|
|
147
|
+
this._sources.set(scopeName, source);
|
|
148
|
+
return source;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_loadRegistryForScope(scopeName) {
|
|
152
|
+
if (this._registries.has(scopeName)) return this._registries.get(scopeName);
|
|
153
|
+
const source = this._sourceForScope(scopeName);
|
|
154
|
+
const data = source.load({ allowNetwork: this.allowNetwork, refresh: this.refresh });
|
|
155
|
+
this._registries.set(scopeName, data);
|
|
156
|
+
return data;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get a scope descriptor from its registry.
|
|
161
|
+
* Returns { type, trust_pubkey, registry_url, verified } or null.
|
|
162
|
+
*/
|
|
163
|
+
getScope(scopeName) {
|
|
164
|
+
const reg = this._loadRegistryForScope(scopeName);
|
|
165
|
+
if (!reg || !reg.scopes) return null;
|
|
166
|
+
return reg.scopes[scopeName] || null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolve a name (bare or @scope/name) into:
|
|
171
|
+
* { parsed, scope, entry, registry }
|
|
172
|
+
* Throws on any failure with a clear message.
|
|
173
|
+
*/
|
|
174
|
+
resolve(input) {
|
|
175
|
+
const parsed = parseName(input);
|
|
176
|
+
if (!parsed) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Invalid name "${input}". Use @scope/name (e.g. @aikdna/writing) or a bare name for the official scope.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const registry = this._loadRegistryForScope(parsed.scope);
|
|
183
|
+
if (!registry) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Cannot load registry for scope ${parsed.scope}. Network unavailable and no cache.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (registry.schema_version && registry.schema_version !== '2.0') {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Registry schema_version ${registry.schema_version} not supported. This CLI requires 2.0.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const scope = registry.scopes?.[parsed.scope];
|
|
196
|
+
if (!scope) {
|
|
197
|
+
throw new Error(`Scope ${parsed.scope} not registered in registry.`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const entry = (registry.domains || []).find((d) => d.name === parsed.full);
|
|
201
|
+
if (!entry) {
|
|
202
|
+
const sameScope = (registry.domains || [])
|
|
203
|
+
.filter((d) => d.name.startsWith(parsed.scope + '/'))
|
|
204
|
+
.map((d) => d.name);
|
|
205
|
+
const hint = sameScope.length
|
|
206
|
+
? `\nKnown ${parsed.scope}/ domains: ${sameScope.join(', ')}`
|
|
207
|
+
: '';
|
|
208
|
+
throw new Error(`Domain ${parsed.full} not found in registry.${hint}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (entry.yanked) {
|
|
212
|
+
const reason = entry.yanked_reason ? `\nReason: ${entry.yanked_reason}` : '';
|
|
213
|
+
const when = entry.yanked_at ? ` (yanked ${entry.yanked_at.slice(0, 10)})` : '';
|
|
214
|
+
const replace = entry.replaced_by ? `\nTry: kdna install ${entry.replaced_by}` : '';
|
|
215
|
+
throw new Error(`${entry.name}@${entry.version} has been yanked${when}.${reason}${replace}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { parsed, scope, entry, registry };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* List all domains across registries already loaded (does not trigger network for other scopes).
|
|
223
|
+
* For now this is just the official registry's domains.
|
|
224
|
+
*/
|
|
225
|
+
listAllDomains() {
|
|
226
|
+
const reg = this._loadRegistryForScope(DEFAULT_OFFICIAL_SCOPE);
|
|
227
|
+
return reg?.domains || [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Backwards-compatible helpers (used by remaining v0.6 code paths) ──
|
|
232
|
+
|
|
233
|
+
function loadRegistry(options = {}) {
|
|
234
|
+
const resolver = new RegistryResolver({
|
|
235
|
+
allowNetwork: options.allowNetwork ?? false,
|
|
236
|
+
refresh: options.refresh ?? false,
|
|
237
|
+
});
|
|
238
|
+
return resolver.listAllDomains();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function fetchRegistry() {
|
|
242
|
+
const source = new RegistrySource(
|
|
243
|
+
CANONICAL_REGISTRY_URL,
|
|
244
|
+
path.join(REGISTRY_DIR, 'domains.json'),
|
|
245
|
+
);
|
|
246
|
+
const data = source.fetch();
|
|
247
|
+
return data.domains || [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
RegistryResolver,
|
|
252
|
+
parseName,
|
|
253
|
+
loadRegistry,
|
|
254
|
+
fetchRegistry,
|
|
255
|
+
CANONICAL_REGISTRY_URL,
|
|
256
|
+
REGISTRY_CACHE: path.join(REGISTRY_DIR, 'domains.json'),
|
|
257
|
+
DEFAULT_OFFICIAL_SCOPE,
|
|
258
|
+
};
|
package/src/search.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kdna search <keyword> — Search registry by keyword.
|
|
3
|
+
*
|
|
4
|
+
* Matches against: name, description, core_insight, keywords[],
|
|
5
|
+
* domain_field, judgment_patterns. Case-insensitive substring search.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { RegistryResolver } = require('./registry');
|
|
9
|
+
|
|
10
|
+
function matchScore(d, q) {
|
|
11
|
+
const ql = q.toLowerCase();
|
|
12
|
+
let score = 0;
|
|
13
|
+
|
|
14
|
+
// Higher weight for stronger signals
|
|
15
|
+
if ((d.name || '').toLowerCase().includes(ql)) score += 10;
|
|
16
|
+
if ((d.id || '').toLowerCase().includes(ql)) score += 8;
|
|
17
|
+
if ((d.keywords || []).some((k) => (k || '').toLowerCase().includes(ql))) score += 6;
|
|
18
|
+
if ((d.core_insight || '').toLowerCase().includes(ql)) score += 4;
|
|
19
|
+
if ((d.description || '').toLowerCase().includes(ql)) score += 3;
|
|
20
|
+
if ((d.domain_field || []).some((f) => (f || '').toLowerCase().includes(ql))) score += 2;
|
|
21
|
+
if ((d.judgment_patterns || []).some((p) => (p || '').toLowerCase().includes(ql))) score += 2;
|
|
22
|
+
|
|
23
|
+
return score;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cmdSearch(query) {
|
|
27
|
+
if (!query) {
|
|
28
|
+
console.error('Usage: kdna search <keyword>');
|
|
29
|
+
console.error(' kdna search "content strategy"');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resolver = new RegistryResolver({ allowNetwork: true });
|
|
34
|
+
const domains = resolver.listAllDomains() || [];
|
|
35
|
+
|
|
36
|
+
if (!domains.length) {
|
|
37
|
+
console.log('No registry entries found. Run: kdna registry refresh');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const matches = domains
|
|
42
|
+
.map((d) => ({ d, score: matchScore(d, query) }))
|
|
43
|
+
.filter((m) => m.score > 0)
|
|
44
|
+
.sort((a, b) => b.score - a.score);
|
|
45
|
+
|
|
46
|
+
if (!matches.length) {
|
|
47
|
+
console.log(`No domains match "${query}".`);
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log('Try:');
|
|
50
|
+
console.log(' kdna list --available # show everything');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`Found ${matches.length} matching domain(s) for "${query}":`);
|
|
55
|
+
console.log('');
|
|
56
|
+
|
|
57
|
+
for (const { d, score } of matches) {
|
|
58
|
+
const yanked = d.yanked ? ' [yanked]' : '';
|
|
59
|
+
const dep = d.deprecated ? ' [deprecated]' : '';
|
|
60
|
+
console.log(
|
|
61
|
+
` ${(d.name || d.id || '?').padEnd(36)} v${d.version || '?'} ${(d.type || 'domain').padEnd(8)} score:${score}${yanked}${dep}`,
|
|
62
|
+
);
|
|
63
|
+
if (d.description) console.log(` ${d.description}`);
|
|
64
|
+
if (d.core_insight) console.log(` » ${d.core_insight}`);
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(
|
|
69
|
+
`To install: kdna install <name> # e.g. kdna install ${matches[0].d.name || matches[0].d.id}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { cmdSearch };
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* kdna setup — One-command KDNA installation.
|
|
4
|
+
*
|
|
5
|
+
* Detects the user's AI agent, installs the kdna-loader skill (the only
|
|
6
|
+
* KDNA skill), creates the data directory, and initializes the local
|
|
7
|
+
* registry cache. Zero domains are installed by default — domains are
|
|
8
|
+
* a separate `kdna install <name>` action.
|
|
9
|
+
*
|
|
10
|
+
* The kdna-loader skill teaches the agent how to discover and use KDNA
|
|
11
|
+
* domains via the kdna CLI's available/match/load commands. Domains
|
|
12
|
+
* themselves are not skills.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
19
|
+
const DOMAINS_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
20
|
+
const CLUSTERS_DIR = path.join(USER_KDNA_DIR, 'clusters');
|
|
21
|
+
const SKILLS_REPO = 'https://raw.githubusercontent.com/knowledge-dna/kdna-skills/main';
|
|
22
|
+
|
|
23
|
+
const AGENTS = [
|
|
24
|
+
{
|
|
25
|
+
name: 'OpenCode',
|
|
26
|
+
dir: path.join(process.env.HOME || '', '.agents'),
|
|
27
|
+
skillsDir: 'skills',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Codex',
|
|
31
|
+
dir: path.join(process.env.HOME || '', '.codex'),
|
|
32
|
+
skillsDir: 'skills',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Claude Code',
|
|
36
|
+
dir: path.join(process.env.HOME || '', '.claude'),
|
|
37
|
+
skillsDir: 'skills',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Cursor',
|
|
41
|
+
dir: path.join(process.env.HOME || '', '.cursor'),
|
|
42
|
+
skillsDir: 'skills',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Gemini Antigravity',
|
|
46
|
+
dir: path.join(process.env.HOME || '', '.gemini', 'antigravity'),
|
|
47
|
+
skillsDir: 'skills',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function log(msg) {
|
|
52
|
+
console.log(`\x1b[32m✓\x1b[0m ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
function warn(msg) {
|
|
55
|
+
console.log(`\x1b[33m⚠\x1b[0m ${msg}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensureDir(dir) {
|
|
59
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detectAgents() {
|
|
63
|
+
return AGENTS.filter((a) => fs.existsSync(a.dir));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// v2.1 marker — written into SKILL.md so we can detect outdated copies
|
|
67
|
+
const V2_1_MARKER = 'kdna available';
|
|
68
|
+
|
|
69
|
+
async function downloadSkill(agent) {
|
|
70
|
+
const skillDir = path.join(agent.dir, agent.skillsDir, 'kdna-loader');
|
|
71
|
+
ensureDir(skillDir);
|
|
72
|
+
const dest = path.join(skillDir, 'SKILL.md');
|
|
73
|
+
|
|
74
|
+
// 1. Try remote (source of truth)
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(`${SKILLS_REPO}/kdna-loader/SKILL.md`);
|
|
77
|
+
if (res.ok) {
|
|
78
|
+
const content = await res.text();
|
|
79
|
+
if (content.includes(V2_1_MARKER)) {
|
|
80
|
+
fs.writeFileSync(dest, content);
|
|
81
|
+
return { ok: true, source: 'remote' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
/* network failure — try fallback */
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Fall back to bundled copy (works offline)
|
|
89
|
+
const local = path.join(__dirname, '..', 'skills', 'kdna-loader', 'SKILL.md');
|
|
90
|
+
if (fs.existsSync(local)) {
|
|
91
|
+
fs.copyFileSync(local, dest);
|
|
92
|
+
return { ok: true, source: 'bundled fallback' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { ok: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cleanLegacySkills(agent) {
|
|
99
|
+
// Pre-v0.9 we also installed kdna-create. Remove any stale copy.
|
|
100
|
+
const legacy = path.join(agent.dir, agent.skillsDir, 'kdna-create');
|
|
101
|
+
if (fs.existsSync(legacy)) {
|
|
102
|
+
try {
|
|
103
|
+
fs.rmSync(legacy, { recursive: true, force: true });
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function cmdSetup() {
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log('KDNA Setup');
|
|
115
|
+
console.log('═'.repeat(40));
|
|
116
|
+
console.log('');
|
|
117
|
+
|
|
118
|
+
// 1. CLI version
|
|
119
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
120
|
+
log(`KDNA CLI v${pkg.version}`);
|
|
121
|
+
|
|
122
|
+
// 2. KDNA data root
|
|
123
|
+
ensureDir(DOMAINS_DIR);
|
|
124
|
+
ensureDir(CLUSTERS_DIR);
|
|
125
|
+
log(`Data root: ${USER_KDNA_DIR}/`);
|
|
126
|
+
|
|
127
|
+
// 2b. Clean legacy (un-scoped) domain directories from pre-v0.7
|
|
128
|
+
if (fs.existsSync(DOMAINS_DIR)) {
|
|
129
|
+
const legacy = fs.readdirSync(DOMAINS_DIR).filter((e) => {
|
|
130
|
+
if (e.startsWith('@') || e.startsWith('.')) return false;
|
|
131
|
+
try {
|
|
132
|
+
return fs.statSync(path.join(DOMAINS_DIR, e)).isDirectory();
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (legacy.length) {
|
|
138
|
+
console.log('');
|
|
139
|
+
warn(
|
|
140
|
+
`Removing ${legacy.length} legacy (un-scoped) domain director${legacy.length > 1 ? 'ies' : 'y'}:`,
|
|
141
|
+
);
|
|
142
|
+
for (const d of legacy) {
|
|
143
|
+
const dPath = path.join(DOMAINS_DIR, d);
|
|
144
|
+
try {
|
|
145
|
+
fs.rmSync(dPath, { recursive: true, force: true });
|
|
146
|
+
log(` removed ~/.kdna/domains/${d}/`);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
warn(` could not remove ~/.kdna/domains/${d}/ — ${e.message}`);
|
|
149
|
+
console.log(` To remove manually: rm -rf ~/.kdna/domains/${d}/`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
console.log(' Re-install with: kdna install @aikdna/<name>');
|
|
153
|
+
console.log('');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Detect agents
|
|
158
|
+
const detected = detectAgents();
|
|
159
|
+
|
|
160
|
+
if (!detected.length) {
|
|
161
|
+
warn('No supported AI agents detected.');
|
|
162
|
+
console.log(' Supported: OpenCode (~/.agents), Codex (~/.codex),');
|
|
163
|
+
console.log(' Claude Code (~/.claude), Cursor (~/.cursor),');
|
|
164
|
+
console.log(' Gemini Antigravity (~/.gemini/antigravity)');
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(' When you install one, re-run: kdna setup');
|
|
167
|
+
console.log('');
|
|
168
|
+
} else {
|
|
169
|
+
log(`Detected agents: ${detected.map((a) => a.name).join(', ')}`);
|
|
170
|
+
|
|
171
|
+
for (const agent of detected) {
|
|
172
|
+
const result = await downloadSkill(agent);
|
|
173
|
+
if (result.ok) {
|
|
174
|
+
log(`kdna-loader → ${agent.name} (${result.source})`);
|
|
175
|
+
} else {
|
|
176
|
+
warn(`Failed to install kdna-loader for ${agent.name}`);
|
|
177
|
+
}
|
|
178
|
+
if (cleanLegacySkills(agent)) {
|
|
179
|
+
log(`removed legacy kdna-create from ${agent.name}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log('Setup complete. KDNA is ready.');
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log('Next steps:');
|
|
188
|
+
console.log(' 1. Install a domain: kdna install @aikdna/writing');
|
|
189
|
+
console.log(' 2. Verify it: kdna verify @aikdna/writing');
|
|
190
|
+
console.log(' 3. Browse the registry: kdna list --available');
|
|
191
|
+
console.log(' 4. In your agent, ask any judgment-related question.');
|
|
192
|
+
console.log(' The kdna-loader skill will discover installed domains');
|
|
193
|
+
console.log(' and apply them silently when relevant.');
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { cmdSetup, detectAgents };
|