agentikit 0.0.9 → 0.0.12
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 +129 -214
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/src/asset-spec.d.ts +2 -0
- package/dist/src/asset-spec.js +22 -3
- package/dist/src/asset-type-handler.d.ts +27 -0
- package/dist/src/asset-type-handler.js +33 -0
- package/dist/src/cli.js +201 -75
- package/dist/src/common.d.ts +6 -1
- package/dist/src/common.js +18 -4
- package/dist/src/config-cli.d.ts +9 -0
- package/dist/src/config-cli.js +473 -0
- package/dist/src/config.d.ts +19 -6
- package/dist/src/config.js +139 -29
- package/dist/src/db.d.ts +46 -0
- package/dist/src/db.js +299 -0
- package/dist/src/embedder.js +12 -7
- package/dist/src/github.d.ts +4 -0
- package/dist/src/github.js +19 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +26 -0
- package/dist/src/handlers/command-handler.d.ts +2 -0
- package/dist/src/handlers/command-handler.js +23 -0
- package/dist/src/handlers/index.d.ts +6 -0
- package/dist/src/handlers/index.js +23 -0
- package/dist/src/handlers/knowledge-handler.d.ts +2 -0
- package/dist/src/handlers/knowledge-handler.js +56 -0
- package/dist/src/handlers/markdown-helpers.d.ts +7 -0
- package/dist/src/handlers/markdown-helpers.js +15 -0
- package/dist/src/handlers/script-handler.d.ts +2 -0
- package/dist/src/handlers/script-handler.js +78 -0
- package/dist/src/handlers/skill-handler.d.ts +2 -0
- package/dist/src/handlers/skill-handler.js +30 -0
- package/dist/src/handlers/tool-handler.d.ts +2 -0
- package/dist/src/handlers/tool-handler.js +58 -0
- package/dist/src/indexer.d.ts +1 -23
- package/dist/src/indexer.js +162 -155
- package/dist/src/init.d.ts +2 -2
- package/dist/src/init.js +21 -9
- package/dist/src/llm.js +4 -3
- package/dist/src/metadata.d.ts +0 -1
- package/dist/src/metadata.js +6 -64
- package/dist/src/origin-resolve.d.ts +19 -0
- package/dist/src/origin-resolve.js +53 -0
- package/dist/src/registry-install.d.ts +2 -2
- package/dist/src/registry-install.js +142 -35
- package/dist/src/registry-resolve.js +90 -22
- package/dist/src/registry-search.d.ts +22 -0
- package/dist/src/registry-search.js +231 -97
- package/dist/src/registry-types.d.ts +9 -2
- package/dist/src/stash-add.js +4 -4
- package/dist/src/stash-clone.d.ts +22 -0
- package/dist/src/stash-clone.js +83 -0
- package/dist/src/stash-ref.d.ts +27 -3
- package/dist/src/stash-ref.js +63 -24
- package/dist/src/stash-registry.js +12 -12
- package/dist/src/stash-resolve.js +3 -0
- package/dist/src/stash-search.js +168 -164
- package/dist/src/stash-show.d.ts +1 -1
- package/dist/src/stash-show.js +28 -96
- package/dist/src/stash-source.d.ts +24 -0
- package/dist/src/stash-source.js +81 -0
- package/dist/src/stash-types.d.ts +14 -4
- package/dist/src/stash.d.ts +6 -0
- package/dist/src/stash.js +3 -0
- package/dist/src/tool-runner.d.ts +1 -1
- package/dist/src/tool-runner.js +18 -5
- package/package.json +7 -2
- package/src/asset-spec.ts +20 -4
- package/src/asset-type-handler.ts +77 -0
- package/src/cli.ts +213 -82
- package/src/common.ts +23 -5
- package/src/config-cli.ts +499 -0
- package/src/config.ts +160 -38
- package/src/db.ts +411 -0
- package/src/embedder.ts +22 -11
- package/src/github.ts +21 -0
- package/src/handlers/agent-handler.ts +32 -0
- package/src/handlers/command-handler.ts +29 -0
- package/src/handlers/index.ts +25 -0
- package/src/handlers/knowledge-handler.ts +62 -0
- package/src/handlers/markdown-helpers.ts +19 -0
- package/src/handlers/script-handler.ts +92 -0
- package/src/handlers/skill-handler.ts +37 -0
- package/src/handlers/tool-handler.ts +71 -0
- package/src/indexer.ts +208 -187
- package/src/init.ts +17 -9
- package/src/llm.ts +4 -3
- package/src/metadata.ts +5 -65
- package/src/origin-resolve.ts +67 -0
- package/src/registry-install.ts +158 -42
- package/src/registry-resolve.ts +92 -23
- package/src/registry-search.ts +288 -98
- package/src/registry-types.ts +10 -2
- package/src/stash-add.ts +14 -17
- package/src/stash-clone.ts +127 -0
- package/src/stash-ref.ts +84 -26
- package/src/stash-registry.ts +12 -12
- package/src/stash-resolve.ts +3 -0
- package/src/stash-search.ts +202 -184
- package/src/stash-show.ts +33 -90
- package/src/stash-source.ts +103 -0
- package/src/stash-types.ts +14 -4
- package/src/stash.ts +8 -0
- package/src/tool-runner.ts +18 -5
- package/dist/src/similarity.d.ts +0 -34
- package/src/similarity.ts +0 -271
|
@@ -1,128 +1,262 @@
|
|
|
1
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fetchWithTimeout } from "./common";
|
|
4
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
5
|
+
/** Default registry index URL. Override via config or AKM_REGISTRY_URL env var. */
|
|
6
|
+
const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/itlackey/agentikit-registry/main/index.json";
|
|
7
|
+
/** Cache TTL in milliseconds (1 hour). */
|
|
8
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
9
|
+
/** Maximum age before cache is considered stale but still usable as fallback (7 days). */
|
|
10
|
+
const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
2
12
|
export async function searchRegistry(query, options) {
|
|
3
13
|
const trimmed = query.trim();
|
|
4
14
|
if (!trimmed) {
|
|
5
15
|
return { query: "", hits: [], warnings: [] };
|
|
6
16
|
}
|
|
7
17
|
const limit = clampLimit(options?.limit);
|
|
8
|
-
const
|
|
9
|
-
searchNpm(trimmed, limit),
|
|
10
|
-
searchGithub(trimmed, limit),
|
|
11
|
-
]);
|
|
12
|
-
const hits = [];
|
|
18
|
+
const urls = resolveRegistryUrls(options?.registryUrls);
|
|
13
19
|
const warnings = [];
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
// Load index from all configured registries, merge kits
|
|
21
|
+
const allKits = [];
|
|
22
|
+
for (const url of urls) {
|
|
23
|
+
try {
|
|
24
|
+
const index = await loadIndex(url);
|
|
25
|
+
if (index) {
|
|
26
|
+
allKits.push(...index.kits);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
warnings.push(`Registry ${url}: ${toErrorMessage(err)}`);
|
|
31
|
+
}
|
|
16
32
|
}
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
// Score and rank
|
|
34
|
+
const hits = scoreKits(allKits, trimmed, limit);
|
|
35
|
+
return { query: trimmed, hits, warnings };
|
|
36
|
+
}
|
|
37
|
+
// ── Index loading with cache ────────────────────────────────────────────────
|
|
38
|
+
async function loadIndex(url) {
|
|
39
|
+
const cachePath = indexCachePath(url);
|
|
40
|
+
const cached = readCachedIndex(cachePath);
|
|
41
|
+
// Fresh cache: return immediately
|
|
42
|
+
if (cached && !isCacheExpired(cached.mtime)) {
|
|
43
|
+
return cached.index;
|
|
44
|
+
}
|
|
45
|
+
// Try to fetch fresh index
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetchWithTimeout(url, undefined, 10_000);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`HTTP ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
const data = (await response.json());
|
|
52
|
+
const index = parseRegistryIndex(data);
|
|
53
|
+
if (index) {
|
|
54
|
+
writeCachedIndex(cachePath, index);
|
|
55
|
+
return index;
|
|
56
|
+
}
|
|
57
|
+
throw new Error("Invalid registry index format");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Fetch failed — use stale cache if available
|
|
61
|
+
if (cached && !isCacheStale(cached.mtime)) {
|
|
62
|
+
return cached.index;
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readCachedIndex(cachePath) {
|
|
68
|
+
try {
|
|
69
|
+
const stat = fs.statSync(cachePath);
|
|
70
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
71
|
+
const index = parseRegistryIndex(raw);
|
|
72
|
+
if (!index)
|
|
73
|
+
return null;
|
|
74
|
+
return { index, mtime: stat.mtimeMs };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
19
78
|
}
|
|
20
|
-
|
|
21
|
-
|
|
79
|
+
}
|
|
80
|
+
function writeCachedIndex(cachePath, index) {
|
|
81
|
+
try {
|
|
82
|
+
const dir = path.dirname(cachePath);
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
const tmpPath = cachePath + `.tmp.${process.pid}`;
|
|
85
|
+
fs.writeFileSync(tmpPath, JSON.stringify(index), "utf8");
|
|
86
|
+
fs.renameSync(tmpPath, cachePath);
|
|
22
87
|
}
|
|
23
|
-
|
|
24
|
-
|
|
88
|
+
catch {
|
|
89
|
+
// Best-effort caching — don't fail the search if we can't write
|
|
25
90
|
}
|
|
26
|
-
|
|
91
|
+
}
|
|
92
|
+
function indexCachePath(url) {
|
|
93
|
+
const cacheRoot = resolveCacheDir();
|
|
94
|
+
// Deterministic filename from URL
|
|
95
|
+
const slug = url
|
|
96
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
97
|
+
.replace(/^-+|-+$/g, "")
|
|
98
|
+
.slice(0, 120);
|
|
99
|
+
return path.join(cacheRoot, "registry-index", `${slug}.json`);
|
|
100
|
+
}
|
|
101
|
+
function resolveCacheDir() {
|
|
102
|
+
const xdgCache = process.env.XDG_CACHE_HOME?.trim();
|
|
103
|
+
if (xdgCache)
|
|
104
|
+
return path.join(path.resolve(xdgCache), "agentikit");
|
|
105
|
+
const home = process.env.HOME?.trim();
|
|
106
|
+
if (!home)
|
|
107
|
+
return path.join("/tmp", "agentikit-cache");
|
|
108
|
+
return path.join(path.resolve(home), ".cache", "agentikit");
|
|
109
|
+
}
|
|
110
|
+
function isCacheExpired(mtimeMs) {
|
|
111
|
+
return Date.now() - mtimeMs > CACHE_TTL_MS;
|
|
112
|
+
}
|
|
113
|
+
function isCacheStale(mtimeMs) {
|
|
114
|
+
return Date.now() - mtimeMs > CACHE_STALE_MS;
|
|
115
|
+
}
|
|
116
|
+
// ── Index parsing ───────────────────────────────────────────────────────────
|
|
117
|
+
function parseRegistryIndex(data) {
|
|
118
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
119
|
+
return null;
|
|
120
|
+
const obj = data;
|
|
121
|
+
if (typeof obj.version !== "number" || obj.version !== 1)
|
|
122
|
+
return null;
|
|
123
|
+
if (typeof obj.updatedAt !== "string")
|
|
124
|
+
return null;
|
|
125
|
+
if (!Array.isArray(obj.kits))
|
|
126
|
+
return null;
|
|
127
|
+
const kits = obj.kits.flatMap((raw) => {
|
|
128
|
+
const kit = parseKitEntry(raw);
|
|
129
|
+
return kit ? [kit] : [];
|
|
130
|
+
});
|
|
131
|
+
return { version: 1, updatedAt: obj.updatedAt, kits };
|
|
132
|
+
}
|
|
133
|
+
function parseKitEntry(raw) {
|
|
134
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
135
|
+
return null;
|
|
136
|
+
const obj = raw;
|
|
137
|
+
const id = asString(obj.id);
|
|
138
|
+
const name = asString(obj.name);
|
|
139
|
+
const ref = asString(obj.ref);
|
|
140
|
+
const source = asSource(obj.source);
|
|
141
|
+
if (!id || !name || !ref || !source)
|
|
142
|
+
return null;
|
|
27
143
|
return {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
144
|
+
id,
|
|
145
|
+
name,
|
|
146
|
+
ref,
|
|
147
|
+
source,
|
|
148
|
+
description: asString(obj.description),
|
|
149
|
+
homepage: asString(obj.homepage),
|
|
150
|
+
tags: asStringArray(obj.tags),
|
|
151
|
+
assetTypes: asStringArray(obj.assetTypes),
|
|
152
|
+
author: asString(obj.author),
|
|
153
|
+
license: asString(obj.license),
|
|
154
|
+
latestVersion: asString(obj.latestVersion),
|
|
155
|
+
curated: obj.curated === true ? true : undefined,
|
|
31
156
|
};
|
|
32
157
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
158
|
+
// ── Scoring ─────────────────────────────────────────────────────────────────
|
|
159
|
+
function scoreKits(kits, query, limit) {
|
|
160
|
+
const tokens = query
|
|
161
|
+
.toLowerCase()
|
|
162
|
+
.split(/\s+/)
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
const scored = [];
|
|
165
|
+
for (const kit of kits) {
|
|
166
|
+
const score = scoreKit(kit, tokens);
|
|
167
|
+
if (score > 0) {
|
|
168
|
+
scored.push({ kit, score });
|
|
169
|
+
}
|
|
38
170
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return objects.flatMap((raw) => {
|
|
42
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
43
|
-
return [];
|
|
44
|
-
const obj = raw;
|
|
45
|
-
const pkg = asRecord(obj.package);
|
|
46
|
-
const name = asString(pkg.name);
|
|
47
|
-
if (!name)
|
|
48
|
-
return [];
|
|
49
|
-
const version = asString(pkg.version);
|
|
50
|
-
const metadata = {};
|
|
51
|
-
if (version)
|
|
52
|
-
metadata.version = version;
|
|
53
|
-
const date = asString(pkg.date);
|
|
54
|
-
if (date)
|
|
55
|
-
metadata.updatedAt = date;
|
|
56
|
-
return [{
|
|
57
|
-
source: "npm",
|
|
58
|
-
id: `npm:${name}`,
|
|
59
|
-
title: name,
|
|
60
|
-
description: asString(pkg.description),
|
|
61
|
-
ref: name,
|
|
62
|
-
homepage: asString(asRecord(pkg.links).homepage),
|
|
63
|
-
score: asNumber(obj.score),
|
|
64
|
-
metadata,
|
|
65
|
-
}];
|
|
66
|
-
});
|
|
171
|
+
scored.sort((a, b) => b.score - a.score);
|
|
172
|
+
return scored.slice(0, limit).map(({ kit, score }) => toSearchHit(kit, score));
|
|
67
173
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
174
|
+
function scoreKit(kit, tokens) {
|
|
175
|
+
let score = 0;
|
|
176
|
+
const nameLower = kit.name.toLowerCase();
|
|
177
|
+
const descLower = (kit.description ?? "").toLowerCase();
|
|
178
|
+
const tagsLower = (kit.tags ?? []).map((t) => t.toLowerCase());
|
|
179
|
+
for (const token of tokens) {
|
|
180
|
+
// Exact name match is strongest signal
|
|
181
|
+
if (nameLower === token) {
|
|
182
|
+
score += 1.0;
|
|
183
|
+
}
|
|
184
|
+
else if (nameLower.includes(token)) {
|
|
185
|
+
score += 0.6;
|
|
186
|
+
}
|
|
187
|
+
// Tag matches are high-signal (curated keywords)
|
|
188
|
+
if (tagsLower.some((tag) => tag === token)) {
|
|
189
|
+
score += 0.5;
|
|
190
|
+
}
|
|
191
|
+
else if (tagsLower.some((tag) => tag.includes(token))) {
|
|
192
|
+
score += 0.25;
|
|
193
|
+
}
|
|
194
|
+
// Description substring
|
|
195
|
+
if (descLower.includes(token)) {
|
|
196
|
+
score += 0.2;
|
|
197
|
+
}
|
|
198
|
+
// Author match
|
|
199
|
+
if (kit.author?.toLowerCase().includes(token)) {
|
|
200
|
+
score += 0.15;
|
|
201
|
+
}
|
|
74
202
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return items.flatMap((raw) => {
|
|
78
|
-
const repo = asRecord(raw);
|
|
79
|
-
const fullName = asString(repo.full_name);
|
|
80
|
-
if (!fullName)
|
|
81
|
-
return [];
|
|
82
|
-
const metadata = {};
|
|
83
|
-
const stars = asNumber(repo.stargazers_count);
|
|
84
|
-
if (stars > 0)
|
|
85
|
-
metadata.stars = String(stars);
|
|
86
|
-
const language = asString(repo.language);
|
|
87
|
-
if (language)
|
|
88
|
-
metadata.language = language;
|
|
89
|
-
return [{
|
|
90
|
-
source: "github",
|
|
91
|
-
id: `github:${fullName}`,
|
|
92
|
-
title: fullName,
|
|
93
|
-
description: asString(repo.description),
|
|
94
|
-
ref: fullName,
|
|
95
|
-
homepage: asString(repo.html_url),
|
|
96
|
-
score: stars,
|
|
97
|
-
metadata,
|
|
98
|
-
}];
|
|
99
|
-
});
|
|
203
|
+
// Normalize by token count so multi-word queries don't inflate scores
|
|
204
|
+
return tokens.length > 0 ? score / tokens.length : 0;
|
|
100
205
|
}
|
|
101
|
-
function
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
206
|
+
function toSearchHit(kit, score) {
|
|
207
|
+
const metadata = {};
|
|
208
|
+
if (kit.latestVersion)
|
|
209
|
+
metadata.version = kit.latestVersion;
|
|
210
|
+
if (kit.author)
|
|
211
|
+
metadata.author = kit.author;
|
|
212
|
+
if (kit.license)
|
|
213
|
+
metadata.license = kit.license;
|
|
214
|
+
if (kit.assetTypes?.length)
|
|
215
|
+
metadata.assetTypes = kit.assetTypes.join(", ");
|
|
216
|
+
return {
|
|
217
|
+
source: kit.source,
|
|
218
|
+
id: kit.id,
|
|
219
|
+
title: kit.name,
|
|
220
|
+
description: kit.description,
|
|
221
|
+
ref: kit.ref,
|
|
222
|
+
homepage: kit.homepage,
|
|
223
|
+
score: Math.round(score * 1000) / 1000,
|
|
224
|
+
metadata,
|
|
225
|
+
curated: kit.curated,
|
|
106
226
|
};
|
|
107
|
-
if (token)
|
|
108
|
-
headers.Authorization = `Bearer ${token}`;
|
|
109
|
-
return headers;
|
|
110
227
|
}
|
|
228
|
+
// ── Registry URL resolution ─────────────────────────────────────────────────
|
|
229
|
+
function resolveRegistryUrls(override) {
|
|
230
|
+
if (override) {
|
|
231
|
+
const urls = Array.isArray(override) ? override : [override];
|
|
232
|
+
return urls.filter(Boolean);
|
|
233
|
+
}
|
|
234
|
+
// Allow env var override (comma-separated)
|
|
235
|
+
const envUrls = process.env.AKM_REGISTRY_URL?.trim();
|
|
236
|
+
if (envUrls) {
|
|
237
|
+
return envUrls.split(",").map((u) => u.trim()).filter(Boolean);
|
|
238
|
+
}
|
|
239
|
+
return [DEFAULT_REGISTRY_URL];
|
|
240
|
+
}
|
|
241
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
111
242
|
function clampLimit(limit) {
|
|
112
243
|
if (!limit || !Number.isFinite(limit))
|
|
113
244
|
return 20;
|
|
114
245
|
return Math.min(100, Math.max(1, Math.trunc(limit)));
|
|
115
246
|
}
|
|
116
|
-
function asRecord(value) {
|
|
117
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
118
|
-
? value
|
|
119
|
-
: {};
|
|
120
|
-
}
|
|
121
247
|
function asString(value) {
|
|
122
248
|
return typeof value === "string" && value ? value : undefined;
|
|
123
249
|
}
|
|
124
|
-
function
|
|
125
|
-
return
|
|
250
|
+
function asSource(value) {
|
|
251
|
+
return value === "npm" || value === "github" || value === "git"
|
|
252
|
+
? value
|
|
253
|
+
: undefined;
|
|
254
|
+
}
|
|
255
|
+
function asStringArray(value) {
|
|
256
|
+
if (!Array.isArray(value))
|
|
257
|
+
return undefined;
|
|
258
|
+
const filtered = value.filter((v) => typeof v === "string");
|
|
259
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
126
260
|
}
|
|
127
261
|
function toErrorMessage(error) {
|
|
128
262
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RegistrySource = "npm" | "github";
|
|
1
|
+
export type RegistrySource = "npm" | "github" | "git";
|
|
2
2
|
export interface RegistryRefBase {
|
|
3
3
|
source: RegistrySource;
|
|
4
4
|
ref: string;
|
|
@@ -15,7 +15,12 @@ export interface ParsedGithubRef extends RegistryRefBase {
|
|
|
15
15
|
repo: string;
|
|
16
16
|
requestedRef?: string;
|
|
17
17
|
}
|
|
18
|
-
export
|
|
18
|
+
export interface ParsedGitRef extends RegistryRefBase {
|
|
19
|
+
source: "git";
|
|
20
|
+
repoRoot: string;
|
|
21
|
+
sourcePath: string;
|
|
22
|
+
}
|
|
23
|
+
export type ParsedRegistryRef = ParsedNpmRef | ParsedGithubRef | ParsedGitRef;
|
|
19
24
|
export interface ResolvedRegistryArtifact {
|
|
20
25
|
id: string;
|
|
21
26
|
source: RegistrySource;
|
|
@@ -47,6 +52,8 @@ export interface RegistrySearchHit {
|
|
|
47
52
|
homepage?: string;
|
|
48
53
|
score?: number;
|
|
49
54
|
metadata?: Record<string, string>;
|
|
55
|
+
/** Whether this entry was manually reviewed and approved */
|
|
56
|
+
curated?: boolean;
|
|
50
57
|
}
|
|
51
58
|
export interface RegistrySearchResponse {
|
|
52
59
|
query: string;
|
package/dist/src/stash-add.js
CHANGED
|
@@ -6,10 +6,10 @@ import { upsertInstalledRegistryEntry, installRegistryRef } from "./registry-ins
|
|
|
6
6
|
export async function agentikitAdd(input) {
|
|
7
7
|
const ref = input.ref.trim();
|
|
8
8
|
if (!ref)
|
|
9
|
-
throw new Error("
|
|
9
|
+
throw new Error("Install ref or local git directory is required.");
|
|
10
10
|
const stashDir = resolveStashDir();
|
|
11
11
|
const installed = await installRegistryRef(ref);
|
|
12
|
-
const replaced = loadConfig(
|
|
12
|
+
const replaced = loadConfig().registry?.installed.find((entry) => entry.id === installed.id);
|
|
13
13
|
const config = upsertInstalledRegistryEntry({
|
|
14
14
|
id: installed.id,
|
|
15
15
|
source: installed.source,
|
|
@@ -20,7 +20,7 @@ export async function agentikitAdd(input) {
|
|
|
20
20
|
stashRoot: installed.stashRoot,
|
|
21
21
|
cacheDir: installed.cacheDir,
|
|
22
22
|
installedAt: installed.installedAt,
|
|
23
|
-
}
|
|
23
|
+
});
|
|
24
24
|
if (replaced && replaced.cacheDir !== installed.cacheDir) {
|
|
25
25
|
try {
|
|
26
26
|
fs.rmSync(replaced.cacheDir, { recursive: true, force: true });
|
|
@@ -46,7 +46,7 @@ export async function agentikitAdd(input) {
|
|
|
46
46
|
installedAt: installed.installedAt,
|
|
47
47
|
},
|
|
48
48
|
config: {
|
|
49
|
-
|
|
49
|
+
mountedStashDirs: config.mountedStashDirs,
|
|
50
50
|
installedRegistryCount: config.registry?.installed.length ?? 0,
|
|
51
51
|
},
|
|
52
52
|
index: {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type StashSourceKind } from "./stash-source";
|
|
2
|
+
export interface CloneOptions {
|
|
3
|
+
/** Source ref (e.g., npm:@scope/pkg//tool:deploy.sh) */
|
|
4
|
+
sourceRef: string;
|
|
5
|
+
/** Optional new name for the cloned asset */
|
|
6
|
+
newName?: string;
|
|
7
|
+
/** If true, overwrite existing asset in working stash */
|
|
8
|
+
force?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface CloneResponse {
|
|
11
|
+
source: {
|
|
12
|
+
path: string;
|
|
13
|
+
sourceKind: StashSourceKind;
|
|
14
|
+
registryId?: string;
|
|
15
|
+
};
|
|
16
|
+
destination: {
|
|
17
|
+
path: string;
|
|
18
|
+
ref: string;
|
|
19
|
+
};
|
|
20
|
+
overwritten: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function agentikitClone(options: CloneOptions): Promise<CloneResponse>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { TYPE_DIRS } from "./asset-spec";
|
|
4
|
+
import { parseAssetRef, makeAssetRef } from "./stash-ref";
|
|
5
|
+
import { resolveSourcesForOrigin } from "./origin-resolve";
|
|
6
|
+
import { resolveAssetPath } from "./stash-resolve";
|
|
7
|
+
import { resolveStashSources, findSourceForPath } from "./stash-source";
|
|
8
|
+
export async function agentikitClone(options) {
|
|
9
|
+
const parsed = parseAssetRef(options.sourceRef);
|
|
10
|
+
const allSources = resolveStashSources();
|
|
11
|
+
const workingSource = allSources.find((s) => s.kind === "working");
|
|
12
|
+
if (!workingSource) {
|
|
13
|
+
throw new Error("No working stash configured. Run `akm init` first.");
|
|
14
|
+
}
|
|
15
|
+
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
|
|
16
|
+
let sourcePath;
|
|
17
|
+
let lastError;
|
|
18
|
+
for (const source of searchSources) {
|
|
19
|
+
try {
|
|
20
|
+
sourcePath = resolveAssetPath(source.path, parsed.type, parsed.name);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!sourcePath) {
|
|
28
|
+
throw lastError ?? new Error(`Source asset not found for ref: ${options.sourceRef}`);
|
|
29
|
+
}
|
|
30
|
+
const sourceSource = findSourceForPath(sourcePath, allSources);
|
|
31
|
+
const sourceKind = sourceSource?.kind ?? "working";
|
|
32
|
+
const destName = options.newName ?? parsed.name;
|
|
33
|
+
const typeDir = TYPE_DIRS[parsed.type];
|
|
34
|
+
const workingDir = workingSource.path;
|
|
35
|
+
// Guard against self-clone
|
|
36
|
+
if (parsed.type === "skill") {
|
|
37
|
+
const sourceSkillDir = path.resolve(path.dirname(sourcePath));
|
|
38
|
+
const destSkillDir = path.resolve(path.join(workingDir, typeDir, destName));
|
|
39
|
+
if (sourceSkillDir === destSkillDir) {
|
|
40
|
+
throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const resolvedSource = path.resolve(sourcePath);
|
|
45
|
+
const resolvedDest = path.resolve(path.join(workingDir, typeDir, destName));
|
|
46
|
+
if (resolvedSource === resolvedDest) {
|
|
47
|
+
throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let destPath;
|
|
51
|
+
if (parsed.type === "skill") {
|
|
52
|
+
const sourceSkillDir = path.dirname(sourcePath);
|
|
53
|
+
const destSkillDir = path.join(workingDir, typeDir, destName);
|
|
54
|
+
const overwritten = fs.existsSync(destSkillDir);
|
|
55
|
+
if (overwritten && !options.force) {
|
|
56
|
+
throw new Error(`Asset already exists in working stash: ${destSkillDir}. Use --force to overwrite.`);
|
|
57
|
+
}
|
|
58
|
+
if (overwritten) {
|
|
59
|
+
fs.rmSync(destSkillDir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
fs.cpSync(sourceSkillDir, destSkillDir, { recursive: true });
|
|
62
|
+
destPath = path.join(destSkillDir, "SKILL.md");
|
|
63
|
+
const ref = makeAssetRef(parsed.type, destName, "local");
|
|
64
|
+
return {
|
|
65
|
+
source: { path: sourcePath, sourceKind, registryId: sourceSource?.registryId },
|
|
66
|
+
destination: { path: destPath, ref },
|
|
67
|
+
overwritten,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
destPath = path.join(workingDir, typeDir, destName);
|
|
71
|
+
const overwritten = fs.existsSync(destPath);
|
|
72
|
+
if (overwritten && !options.force) {
|
|
73
|
+
throw new Error(`Asset already exists in working stash: ${destPath}. Use --force to overwrite.`);
|
|
74
|
+
}
|
|
75
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
76
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
77
|
+
const ref = makeAssetRef(parsed.type, destName, "local");
|
|
78
|
+
return {
|
|
79
|
+
source: { path: sourcePath, sourceKind, registryId: sourceSource?.registryId },
|
|
80
|
+
destination: { path: destPath, ref },
|
|
81
|
+
overwritten,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/src/stash-ref.d.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { type AgentikitAssetType } from "./common";
|
|
2
|
-
export interface
|
|
2
|
+
export interface AssetRef {
|
|
3
3
|
type: AgentikitAssetType;
|
|
4
4
|
name: string;
|
|
5
|
+
/**
|
|
6
|
+
* Where to find this asset.
|
|
7
|
+
* - undefined: search all sources (working → mounted → installed)
|
|
8
|
+
* - "local": working stash only
|
|
9
|
+
* - registry ref: e.g. "npm:@scope/pkg", "owner/repo", "github:owner/repo#v1"
|
|
10
|
+
* - filesystem path: e.g. "/mnt/shared-stash"
|
|
11
|
+
*/
|
|
12
|
+
origin?: string;
|
|
5
13
|
}
|
|
6
|
-
|
|
7
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Build a ref string from components.
|
|
16
|
+
*
|
|
17
|
+
* Examples:
|
|
18
|
+
* makeAssetRef("tool", "deploy.sh")
|
|
19
|
+
* → "tool:deploy.sh"
|
|
20
|
+
* makeAssetRef("tool", "deploy.sh", "npm:@scope/pkg")
|
|
21
|
+
* → "npm:@scope/pkg//tool:deploy.sh"
|
|
22
|
+
* makeAssetRef("skill", "code-review", "local")
|
|
23
|
+
* → "local//skill:code-review"
|
|
24
|
+
* makeAssetRef("tool", "db/migrate/run.sh", "owner/repo")
|
|
25
|
+
* → "owner/repo//tool:db/migrate/run.sh"
|
|
26
|
+
*/
|
|
27
|
+
export declare function makeAssetRef(type: AgentikitAssetType, name: string, origin?: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Parse a ref string in the format `[origin//]type:name`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseAssetRef(ref: string): AssetRef;
|
package/dist/src/stash-ref.js
CHANGED
|
@@ -1,33 +1,72 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { isAssetType } from "./common";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
// ── Construction ────────────────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Build a ref string from components.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* makeAssetRef("tool", "deploy.sh")
|
|
9
|
+
* → "tool:deploy.sh"
|
|
10
|
+
* makeAssetRef("tool", "deploy.sh", "npm:@scope/pkg")
|
|
11
|
+
* → "npm:@scope/pkg//tool:deploy.sh"
|
|
12
|
+
* makeAssetRef("skill", "code-review", "local")
|
|
13
|
+
* → "local//skill:code-review"
|
|
14
|
+
* makeAssetRef("tool", "db/migrate/run.sh", "owner/repo")
|
|
15
|
+
* → "owner/repo//tool:db/migrate/run.sh"
|
|
16
|
+
*/
|
|
17
|
+
export function makeAssetRef(type, name, origin) {
|
|
18
|
+
validateName(name);
|
|
19
|
+
const normalized = normalizeName(name);
|
|
20
|
+
const asset = `${type}:${normalized}`;
|
|
21
|
+
if (!origin)
|
|
22
|
+
return asset;
|
|
23
|
+
return `${origin}//${asset}`;
|
|
24
|
+
}
|
|
25
|
+
// ── Parsing ─────────────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Parse a ref string in the format `[origin//]type:name`.
|
|
28
|
+
*/
|
|
29
|
+
export function parseAssetRef(ref) {
|
|
30
|
+
const trimmed = ref.trim();
|
|
31
|
+
if (!trimmed)
|
|
32
|
+
throw new Error("Empty ref.");
|
|
33
|
+
let origin;
|
|
34
|
+
let body = trimmed;
|
|
35
|
+
const boundary = trimmed.indexOf("//");
|
|
36
|
+
if (boundary >= 0) {
|
|
37
|
+
origin = trimmed.slice(0, boundary);
|
|
38
|
+
body = trimmed.slice(boundary + 2);
|
|
39
|
+
if (!origin)
|
|
40
|
+
throw new Error("Empty origin in ref.");
|
|
12
41
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
42
|
+
const colon = body.indexOf(":");
|
|
43
|
+
if (colon <= 0) {
|
|
44
|
+
throw new Error(`Invalid ref "${trimmed}". Expected [origin//]type:name`);
|
|
16
45
|
}
|
|
17
|
-
|
|
18
|
-
|
|
46
|
+
const rawType = body.slice(0, colon);
|
|
47
|
+
const rawName = body.slice(colon + 1);
|
|
48
|
+
if (!isAssetType(rawType)) {
|
|
49
|
+
throw new Error(`Invalid asset type: "${rawType}".`);
|
|
19
50
|
}
|
|
51
|
+
validateName(rawName);
|
|
52
|
+
const name = normalizeName(rawName);
|
|
53
|
+
return { type: rawType, name, origin: origin || undefined };
|
|
54
|
+
}
|
|
55
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
56
|
+
function validateName(name) {
|
|
57
|
+
if (!name)
|
|
58
|
+
throw new Error("Empty asset name.");
|
|
59
|
+
if (name.includes("\0"))
|
|
60
|
+
throw new Error("Null byte in asset name.");
|
|
61
|
+
if (/^[A-Za-z]:/.test(name))
|
|
62
|
+
throw new Error("Windows drive path in asset name.");
|
|
20
63
|
const normalized = path.posix.normalize(name.replace(/\\/g, "/"));
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|| normalized === ".."
|
|
26
|
-
|| normalized.startsWith("../")) {
|
|
27
|
-
throw new Error("Invalid open ref name.");
|
|
64
|
+
if (path.posix.isAbsolute(normalized))
|
|
65
|
+
throw new Error("Absolute path in asset name.");
|
|
66
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
67
|
+
throw new Error("Path traversal in asset name.");
|
|
28
68
|
}
|
|
29
|
-
return { type: rawType, name: normalized };
|
|
30
69
|
}
|
|
31
|
-
|
|
32
|
-
return
|
|
70
|
+
function normalizeName(name) {
|
|
71
|
+
return path.posix.normalize(name.replace(/\\/g, "/"));
|
|
33
72
|
}
|