agentikit 0.0.8 → 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 +135 -117
- package/dist/index.d.ts +13 -3
- package/dist/index.js +7 -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 +335 -100
- 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 +25 -6
- package/dist/src/config.js +188 -28
- 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 +1 -1
- package/dist/src/metadata.js +22 -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 +11 -0
- package/dist/src/registry-install.js +315 -0
- package/dist/src/registry-resolve.d.ts +3 -0
- package/dist/src/registry-resolve.js +299 -0
- package/dist/src/registry-search.d.ts +27 -0
- package/dist/src/registry-search.js +263 -0
- package/dist/src/registry-types.d.ts +62 -0
- package/dist/src/registry-types.js +1 -0
- package/dist/src/stash-add.d.ts +4 -0
- package/dist/src/stash-add.js +59 -0
- 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.d.ts +18 -0
- package/dist/src/stash-registry.js +221 -0
- package/dist/src/stash-resolve.js +3 -0
- package/dist/src/stash-search.d.ts +3 -1
- package/dist/src/stash-search.js +357 -138
- package/dist/src/stash-show.d.ts +1 -1
- package/dist/src/stash-show.js +28 -89
- package/dist/src/stash-source.d.ts +24 -0
- package/dist/src/stash-source.js +81 -0
- package/dist/src/stash-types.d.ts +175 -1
- package/dist/src/stash.d.ts +9 -1
- package/dist/src/stash.js +5 -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 +354 -103
- package/src/common.ts +23 -5
- package/src/config-cli.ts +499 -0
- package/src/config.ts +218 -37
- 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 +21 -65
- package/src/origin-resolve.ts +67 -0
- package/src/registry-install.ts +361 -0
- package/src/registry-resolve.ts +341 -0
- package/src/registry-search.ts +335 -0
- package/src/registry-types.ts +72 -0
- package/src/stash-add.ts +63 -0
- package/src/stash-clone.ts +127 -0
- package/src/stash-ref.ts +84 -26
- package/src/stash-registry.ts +259 -0
- package/src/stash-resolve.ts +3 -0
- package/src/stash-search.ts +425 -155
- package/src/stash-show.ts +33 -82
- package/src/stash-source.ts +103 -0
- package/src/stash-types.ts +186 -1
- package/src/stash.ts +23 -0
- package/src/tool-runner.ts +18 -5
- package/dist/src/similarity.d.ts +0 -34
- package/src/similarity.ts +0 -271
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { pathToFileURL } from "node:url"
|
|
5
|
+
import { fetchWithTimeout } from "./common"
|
|
6
|
+
import type { ParsedGitRef, ParsedGithubRef, ParsedNpmRef, ParsedRegistryRef, ResolvedRegistryArtifact } from "./registry-types"
|
|
7
|
+
import { GITHUB_API_BASE, githubHeaders, asRecord, asString } from "./github"
|
|
8
|
+
|
|
9
|
+
export function parseRegistryRef(rawRef: string): ParsedRegistryRef {
|
|
10
|
+
const ref = rawRef.trim()
|
|
11
|
+
if (!ref) throw new Error("Registry ref is required.")
|
|
12
|
+
|
|
13
|
+
if (ref.startsWith("npm:")) {
|
|
14
|
+
return parseNpmRef(ref.slice(4), ref)
|
|
15
|
+
}
|
|
16
|
+
if (ref.startsWith("github:")) {
|
|
17
|
+
return parseGithubShorthand(ref.slice(7), ref)
|
|
18
|
+
}
|
|
19
|
+
if (ref.startsWith("http://") || ref.startsWith("https://")) {
|
|
20
|
+
return parseGithubUrl(ref)
|
|
21
|
+
}
|
|
22
|
+
const localGitRef = tryParseLocalGitRef(ref, isPathLikeRef(ref))
|
|
23
|
+
if (localGitRef) {
|
|
24
|
+
return localGitRef
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (ref.startsWith("@") || !looksLikeGithubOwnerRepo(ref)) {
|
|
28
|
+
return parseNpmRef(ref, ref)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return parseGithubShorthand(ref, ref)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function resolveRegistryArtifact(parsed: ParsedRegistryRef): Promise<ResolvedRegistryArtifact> {
|
|
35
|
+
if (parsed.source === "npm") {
|
|
36
|
+
return resolveNpmArtifact(parsed)
|
|
37
|
+
}
|
|
38
|
+
if (parsed.source === "git") {
|
|
39
|
+
return resolveGitArtifact(parsed)
|
|
40
|
+
}
|
|
41
|
+
return resolveGithubArtifact(parsed)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseNpmRef(input: string, originalRef: string): ParsedNpmRef {
|
|
45
|
+
const trimmed = input.trim()
|
|
46
|
+
if (!trimmed) throw new Error("Invalid npm ref.")
|
|
47
|
+
|
|
48
|
+
const parsed = splitNpmNameAndVersion(trimmed)
|
|
49
|
+
validateNpmPackageName(parsed.packageName)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
source: "npm",
|
|
53
|
+
ref: originalRef,
|
|
54
|
+
id: `npm:${parsed.packageName}`,
|
|
55
|
+
packageName: parsed.packageName,
|
|
56
|
+
requestedVersionOrTag: parsed.requestedVersionOrTag,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseGithubShorthand(input: string, originalRef: string): ParsedGithubRef {
|
|
61
|
+
const [repoPart, requestedRef] = splitRefSuffix(input.trim())
|
|
62
|
+
const segments = repoPart.split("/").filter(Boolean)
|
|
63
|
+
if (segments.length !== 2) {
|
|
64
|
+
throw new Error("Invalid GitHub ref. Expected owner/repo or owner/repo#ref.")
|
|
65
|
+
}
|
|
66
|
+
const owner = segments[0]
|
|
67
|
+
const repo = segments[1].replace(/\.git$/i, "")
|
|
68
|
+
if (!owner || !repo) {
|
|
69
|
+
throw new Error("Invalid GitHub ref. Expected owner/repo.")
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
source: "github",
|
|
73
|
+
ref: originalRef,
|
|
74
|
+
id: `github:${owner}/${repo}`,
|
|
75
|
+
owner,
|
|
76
|
+
repo,
|
|
77
|
+
requestedRef,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseGithubUrl(rawUrl: string): ParsedGithubRef {
|
|
82
|
+
let url: URL
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(rawUrl)
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error("Invalid registry URL.")
|
|
87
|
+
}
|
|
88
|
+
if (url.hostname !== "github.com") {
|
|
89
|
+
throw new Error("Only GitHub URLs are currently supported for URL refs.")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const segments = url.pathname.split("/").filter(Boolean)
|
|
93
|
+
if (segments.length < 2) {
|
|
94
|
+
throw new Error("Invalid GitHub URL. Expected https://github.com/owner/repo.")
|
|
95
|
+
}
|
|
96
|
+
const owner = segments[0]
|
|
97
|
+
const repo = segments[1].replace(/\.git$/i, "")
|
|
98
|
+
const requestedRef = url.hash ? decodeURIComponent(url.hash.slice(1)) : undefined
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
source: "github",
|
|
102
|
+
ref: rawUrl,
|
|
103
|
+
id: `github:${owner}/${repo}`,
|
|
104
|
+
owner,
|
|
105
|
+
repo,
|
|
106
|
+
requestedRef,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function tryParseLocalGitRef(rawRef: string, explicitPath: boolean): ParsedGitRef | undefined {
|
|
111
|
+
if (!explicitPath) {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resolvedPath = path.resolve(rawRef)
|
|
116
|
+
let stat: fs.Stats
|
|
117
|
+
try {
|
|
118
|
+
stat = fs.statSync(resolvedPath)
|
|
119
|
+
} catch {
|
|
120
|
+
throw new Error(`Local path not found: ${resolvedPath}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!stat.isDirectory()) {
|
|
124
|
+
throw new Error("Local add path must be a directory, but the provided path is not one.")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const repoRoot = findGitRepoRoot(resolvedPath)
|
|
128
|
+
if (!repoRoot) {
|
|
129
|
+
throw new Error("Local add path must be inside a git repository.")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
source: "git",
|
|
134
|
+
ref: rawRef,
|
|
135
|
+
id: `git:${encodeURIComponent(resolvedPath)}`,
|
|
136
|
+
repoRoot,
|
|
137
|
+
sourcePath: resolvedPath,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isPathLikeRef(ref: string): boolean {
|
|
142
|
+
if (path.isAbsolute(ref)) return true
|
|
143
|
+
if (ref.startsWith("./") || ref.startsWith("../") || ref.startsWith(".\\") || ref.startsWith("..\\")) {
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
return ref.includes("/") || ref.includes("\\")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function resolveNpmArtifact(parsed: ParsedNpmRef): Promise<ResolvedRegistryArtifact> {
|
|
150
|
+
const encodedName = encodeURIComponent(parsed.packageName)
|
|
151
|
+
const metadata = await fetchJson<Record<string, unknown>>(`https://registry.npmjs.org/${encodedName}`)
|
|
152
|
+
|
|
153
|
+
const versions = asRecord(metadata.versions)
|
|
154
|
+
const distTags = asRecord(metadata["dist-tags"])
|
|
155
|
+
|
|
156
|
+
const requested = parsed.requestedVersionOrTag
|
|
157
|
+
let resolvedVersion: string | undefined
|
|
158
|
+
if (!requested) {
|
|
159
|
+
resolvedVersion = asString(distTags.latest)
|
|
160
|
+
} else if (requested in versions) {
|
|
161
|
+
resolvedVersion = requested
|
|
162
|
+
} else {
|
|
163
|
+
resolvedVersion = asString(distTags[requested])
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!resolvedVersion || !(resolvedVersion in versions)) {
|
|
167
|
+
throw new Error(`Unable to resolve npm ref \"${parsed.ref}\".`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const versionMeta = asRecord(versions[resolvedVersion])
|
|
171
|
+
const dist = asRecord(versionMeta.dist)
|
|
172
|
+
const tarballUrl = asString(dist.tarball)
|
|
173
|
+
if (!tarballUrl) {
|
|
174
|
+
throw new Error(`npm package ${parsed.packageName}@${resolvedVersion} does not expose a tarball URL.`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const resolvedRevision = asString(dist.shasum) ?? asString(dist.integrity)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
id: parsed.id,
|
|
181
|
+
source: parsed.source,
|
|
182
|
+
ref: parsed.ref,
|
|
183
|
+
artifactUrl: tarballUrl,
|
|
184
|
+
resolvedVersion,
|
|
185
|
+
resolvedRevision,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function resolveGithubArtifact(parsed: ParsedGithubRef): Promise<ResolvedRegistryArtifact> {
|
|
190
|
+
const headers = githubHeaders()
|
|
191
|
+
|
|
192
|
+
if (parsed.requestedRef) {
|
|
193
|
+
const commit = await tryFetchJson<Record<string, unknown>>(
|
|
194
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(parsed.requestedRef)}`,
|
|
195
|
+
headers,
|
|
196
|
+
)
|
|
197
|
+
const resolvedRevision = asString(commit?.sha) ?? parsed.requestedRef
|
|
198
|
+
return {
|
|
199
|
+
id: parsed.id,
|
|
200
|
+
source: parsed.source,
|
|
201
|
+
ref: parsed.ref,
|
|
202
|
+
artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(parsed.requestedRef)}`,
|
|
203
|
+
resolvedRevision,
|
|
204
|
+
resolvedVersion: parsed.requestedRef,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const latestRelease = await tryFetchJson<Record<string, unknown>>(
|
|
209
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/releases/latest`,
|
|
210
|
+
headers,
|
|
211
|
+
)
|
|
212
|
+
if (latestRelease) {
|
|
213
|
+
const tarballUrl = asString(latestRelease.tarball_url)
|
|
214
|
+
if (tarballUrl) {
|
|
215
|
+
return {
|
|
216
|
+
id: parsed.id,
|
|
217
|
+
source: parsed.source,
|
|
218
|
+
ref: parsed.ref,
|
|
219
|
+
artifactUrl: tarballUrl,
|
|
220
|
+
resolvedVersion: asString(latestRelease.tag_name),
|
|
221
|
+
resolvedRevision: asString(latestRelease.target_commitish),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const repoMeta = await fetchJson<Record<string, unknown>>(
|
|
227
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}`,
|
|
228
|
+
headers,
|
|
229
|
+
)
|
|
230
|
+
const defaultBranch = asString(repoMeta.default_branch)
|
|
231
|
+
if (!defaultBranch) {
|
|
232
|
+
throw new Error(`Unable to resolve default branch for ${parsed.owner}/${parsed.repo}.`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const commit = await tryFetchJson<Record<string, unknown>>(
|
|
236
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(defaultBranch)}`,
|
|
237
|
+
headers,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
id: parsed.id,
|
|
242
|
+
source: parsed.source,
|
|
243
|
+
ref: parsed.ref,
|
|
244
|
+
artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(defaultBranch)}`,
|
|
245
|
+
resolvedVersion: defaultBranch,
|
|
246
|
+
resolvedRevision: asString(commit?.sha) ?? defaultBranch,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function resolveGitArtifact(parsed: ParsedGitRef): Promise<ResolvedRegistryArtifact> {
|
|
251
|
+
return {
|
|
252
|
+
id: parsed.id,
|
|
253
|
+
source: parsed.source,
|
|
254
|
+
ref: parsed.ref,
|
|
255
|
+
artifactUrl: pathToFileURL(parsed.sourcePath).toString(),
|
|
256
|
+
resolvedRevision: readGitValue(parsed.repoRoot, "rev-parse", "HEAD"),
|
|
257
|
+
resolvedVersion: readGitValue(parsed.repoRoot, "rev-parse", "--abbrev-ref", "HEAD"),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function splitNpmNameAndVersion(input: string): { packageName: string; requestedVersionOrTag?: string } {
|
|
262
|
+
if (input.startsWith("@")) {
|
|
263
|
+
const secondAt = input.indexOf("@", 1)
|
|
264
|
+
if (secondAt > 0) {
|
|
265
|
+
return {
|
|
266
|
+
packageName: input.slice(0, secondAt),
|
|
267
|
+
requestedVersionOrTag: input.slice(secondAt + 1) || undefined,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { packageName: input }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const at = input.lastIndexOf("@")
|
|
274
|
+
if (at > 0) {
|
|
275
|
+
return {
|
|
276
|
+
packageName: input.slice(0, at),
|
|
277
|
+
requestedVersionOrTag: input.slice(at + 1) || undefined,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { packageName: input }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function validateNpmPackageName(name: string): void {
|
|
284
|
+
if (!name) throw new Error('Invalid npm package name: name is required.')
|
|
285
|
+
if (name.length > 214) throw new Error(`Invalid npm package name: "${name}" exceeds 214 characters.`)
|
|
286
|
+
if (name !== name.toLowerCase() && !name.startsWith('@')) {
|
|
287
|
+
throw new Error(`Invalid npm package name: "${name}" must be lowercase.`)
|
|
288
|
+
}
|
|
289
|
+
if (name.startsWith('.') || name.startsWith('_')) {
|
|
290
|
+
throw new Error(`Invalid npm package name: "${name}" cannot start with . or _.`)
|
|
291
|
+
}
|
|
292
|
+
if (/[~'!()*]/.test(name) || name.includes(' ') || encodeURIComponent(name).replace(/%40/g, '@').replace(/%2[Ff]/g, '/') !== name) {
|
|
293
|
+
throw new Error(`Invalid npm package name: "${name}" contains invalid characters.`)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function looksLikeGithubOwnerRepo(ref: string): boolean {
|
|
298
|
+
const [repoPart] = splitRefSuffix(ref)
|
|
299
|
+
const parts = repoPart.split("/").filter(Boolean)
|
|
300
|
+
return parts.length === 2
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function splitRefSuffix(value: string): [string, string | undefined] {
|
|
304
|
+
const hash = value.indexOf("#")
|
|
305
|
+
if (hash < 0) return [value, undefined]
|
|
306
|
+
return [value.slice(0, hash), value.slice(hash + 1) || undefined]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function findGitRepoRoot(startDir: string): string | undefined {
|
|
310
|
+
let current = path.resolve(startDir)
|
|
311
|
+
while (true) {
|
|
312
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
313
|
+
return current
|
|
314
|
+
}
|
|
315
|
+
const parent = path.dirname(current)
|
|
316
|
+
if (parent === current) return undefined
|
|
317
|
+
current = parent
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readGitValue(repoRoot: string, ...args: string[]): string | undefined {
|
|
322
|
+
const result = spawnSync("git", ["-C", repoRoot, ...args], { encoding: "utf8" })
|
|
323
|
+
if (result.status !== 0) return undefined
|
|
324
|
+
const value = result.stdout.trim()
|
|
325
|
+
return value || undefined
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
|
|
329
|
+
const response = await fetchWithTimeout(url, { headers })
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
throw new Error(`Request failed (${response.status}) for ${url}`)
|
|
332
|
+
}
|
|
333
|
+
return await response.json() as T
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function tryFetchJson<T>(url: string, headers?: HeadersInit): Promise<T | null> {
|
|
337
|
+
const response = await fetchWithTimeout(url, { headers })
|
|
338
|
+
if (!response.ok) return null
|
|
339
|
+
return await response.json() as T
|
|
340
|
+
}
|
|
341
|
+
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { fetchWithTimeout } from "./common"
|
|
4
|
+
import type { RegistrySearchHit, RegistrySearchResponse } from "./registry-types"
|
|
5
|
+
|
|
6
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/** Default registry index URL. Override via config or AKM_REGISTRY_URL env var. */
|
|
9
|
+
const DEFAULT_REGISTRY_URL =
|
|
10
|
+
"https://raw.githubusercontent.com/itlackey/agentikit-registry/main/index.json"
|
|
11
|
+
|
|
12
|
+
/** Cache TTL in milliseconds (1 hour). */
|
|
13
|
+
const CACHE_TTL_MS = 60 * 60 * 1000
|
|
14
|
+
|
|
15
|
+
/** Maximum age before cache is considered stale but still usable as fallback (7 days). */
|
|
16
|
+
const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface RegistryIndex {
|
|
21
|
+
version: number
|
|
22
|
+
updatedAt: string
|
|
23
|
+
kits: RegistryKitEntry[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RegistryKitEntry {
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
description?: string
|
|
30
|
+
ref: string
|
|
31
|
+
source: "npm" | "github" | "git"
|
|
32
|
+
homepage?: string
|
|
33
|
+
tags?: string[]
|
|
34
|
+
assetTypes?: string[]
|
|
35
|
+
author?: string
|
|
36
|
+
license?: string
|
|
37
|
+
latestVersion?: string
|
|
38
|
+
/** Whether this entry was manually reviewed and approved */
|
|
39
|
+
curated?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RegistrySearchOptions {
|
|
43
|
+
limit?: number
|
|
44
|
+
/** Override registry URL(s). Accepts a single URL or an array. */
|
|
45
|
+
registryUrls?: string | string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export async function searchRegistry(
|
|
51
|
+
query: string,
|
|
52
|
+
options?: RegistrySearchOptions,
|
|
53
|
+
): Promise<RegistrySearchResponse> {
|
|
54
|
+
const trimmed = query.trim()
|
|
55
|
+
if (!trimmed) {
|
|
56
|
+
return { query: "", hits: [], warnings: [] }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const limit = clampLimit(options?.limit)
|
|
60
|
+
const urls = resolveRegistryUrls(options?.registryUrls)
|
|
61
|
+
const warnings: string[] = []
|
|
62
|
+
|
|
63
|
+
// Load index from all configured registries, merge kits
|
|
64
|
+
const allKits: RegistryKitEntry[] = []
|
|
65
|
+
for (const url of urls) {
|
|
66
|
+
try {
|
|
67
|
+
const index = await loadIndex(url)
|
|
68
|
+
if (index) {
|
|
69
|
+
allKits.push(...index.kits)
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
warnings.push(`Registry ${url}: ${toErrorMessage(err)}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Score and rank
|
|
77
|
+
const hits = scoreKits(allKits, trimmed, limit)
|
|
78
|
+
|
|
79
|
+
return { query: trimmed, hits, warnings }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Index loading with cache ────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async function loadIndex(url: string): Promise<RegistryIndex | null> {
|
|
85
|
+
const cachePath = indexCachePath(url)
|
|
86
|
+
const cached = readCachedIndex(cachePath)
|
|
87
|
+
|
|
88
|
+
// Fresh cache: return immediately
|
|
89
|
+
if (cached && !isCacheExpired(cached.mtime)) {
|
|
90
|
+
return cached.index
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Try to fetch fresh index
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetchWithTimeout(url, undefined, 10_000)
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`HTTP ${response.status}`)
|
|
98
|
+
}
|
|
99
|
+
const data = (await response.json()) as unknown
|
|
100
|
+
const index = parseRegistryIndex(data)
|
|
101
|
+
if (index) {
|
|
102
|
+
writeCachedIndex(cachePath, index)
|
|
103
|
+
return index
|
|
104
|
+
}
|
|
105
|
+
throw new Error("Invalid registry index format")
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Fetch failed — use stale cache if available
|
|
108
|
+
if (cached && !isCacheStale(cached.mtime)) {
|
|
109
|
+
return cached.index
|
|
110
|
+
}
|
|
111
|
+
throw err
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readCachedIndex(
|
|
116
|
+
cachePath: string,
|
|
117
|
+
): { index: RegistryIndex; mtime: number } | null {
|
|
118
|
+
try {
|
|
119
|
+
const stat = fs.statSync(cachePath)
|
|
120
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"))
|
|
121
|
+
const index = parseRegistryIndex(raw)
|
|
122
|
+
if (!index) return null
|
|
123
|
+
return { index, mtime: stat.mtimeMs }
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeCachedIndex(cachePath: string, index: RegistryIndex): void {
|
|
130
|
+
try {
|
|
131
|
+
const dir = path.dirname(cachePath)
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
133
|
+
const tmpPath = cachePath + `.tmp.${process.pid}`
|
|
134
|
+
fs.writeFileSync(tmpPath, JSON.stringify(index), "utf8")
|
|
135
|
+
fs.renameSync(tmpPath, cachePath)
|
|
136
|
+
} catch {
|
|
137
|
+
// Best-effort caching — don't fail the search if we can't write
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function indexCachePath(url: string): string {
|
|
142
|
+
const cacheRoot = resolveCacheDir()
|
|
143
|
+
// Deterministic filename from URL
|
|
144
|
+
const slug = url
|
|
145
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
146
|
+
.replace(/^-+|-+$/g, "")
|
|
147
|
+
.slice(0, 120)
|
|
148
|
+
return path.join(cacheRoot, "registry-index", `${slug}.json`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveCacheDir(): string {
|
|
152
|
+
const xdgCache = process.env.XDG_CACHE_HOME?.trim()
|
|
153
|
+
if (xdgCache) return path.join(path.resolve(xdgCache), "agentikit")
|
|
154
|
+
const home = process.env.HOME?.trim()
|
|
155
|
+
if (!home) return path.join("/tmp", "agentikit-cache")
|
|
156
|
+
return path.join(path.resolve(home), ".cache", "agentikit")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isCacheExpired(mtimeMs: number): boolean {
|
|
160
|
+
return Date.now() - mtimeMs > CACHE_TTL_MS
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isCacheStale(mtimeMs: number): boolean {
|
|
164
|
+
return Date.now() - mtimeMs > CACHE_STALE_MS
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Index parsing ───────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function parseRegistryIndex(data: unknown): RegistryIndex | null {
|
|
170
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return null
|
|
171
|
+
const obj = data as Record<string, unknown>
|
|
172
|
+
|
|
173
|
+
if (typeof obj.version !== "number" || obj.version !== 1) return null
|
|
174
|
+
if (typeof obj.updatedAt !== "string") return null
|
|
175
|
+
if (!Array.isArray(obj.kits)) return null
|
|
176
|
+
|
|
177
|
+
const kits = obj.kits.flatMap((raw): RegistryKitEntry[] => {
|
|
178
|
+
const kit = parseKitEntry(raw)
|
|
179
|
+
return kit ? [kit] : []
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return { version: 1, updatedAt: obj.updatedAt, kits }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseKitEntry(raw: unknown): RegistryKitEntry | null {
|
|
186
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null
|
|
187
|
+
const obj = raw as Record<string, unknown>
|
|
188
|
+
|
|
189
|
+
const id = asString(obj.id)
|
|
190
|
+
const name = asString(obj.name)
|
|
191
|
+
const ref = asString(obj.ref)
|
|
192
|
+
const source = asSource(obj.source)
|
|
193
|
+
if (!id || !name || !ref || !source) return null
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
id,
|
|
197
|
+
name,
|
|
198
|
+
ref,
|
|
199
|
+
source,
|
|
200
|
+
description: asString(obj.description),
|
|
201
|
+
homepage: asString(obj.homepage),
|
|
202
|
+
tags: asStringArray(obj.tags),
|
|
203
|
+
assetTypes: asStringArray(obj.assetTypes),
|
|
204
|
+
author: asString(obj.author),
|
|
205
|
+
license: asString(obj.license),
|
|
206
|
+
latestVersion: asString(obj.latestVersion),
|
|
207
|
+
curated: obj.curated === true ? true : undefined,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Scoring ─────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
function scoreKits(
|
|
214
|
+
kits: RegistryKitEntry[],
|
|
215
|
+
query: string,
|
|
216
|
+
limit: number,
|
|
217
|
+
): RegistrySearchHit[] {
|
|
218
|
+
const tokens = query
|
|
219
|
+
.toLowerCase()
|
|
220
|
+
.split(/\s+/)
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
|
|
223
|
+
const scored: Array<{ kit: RegistryKitEntry; score: number }> = []
|
|
224
|
+
|
|
225
|
+
for (const kit of kits) {
|
|
226
|
+
const score = scoreKit(kit, tokens)
|
|
227
|
+
if (score > 0) {
|
|
228
|
+
scored.push({ kit, score })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
scored.sort((a, b) => b.score - a.score)
|
|
233
|
+
|
|
234
|
+
return scored.slice(0, limit).map(({ kit, score }) => toSearchHit(kit, score))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function scoreKit(kit: RegistryKitEntry, tokens: string[]): number {
|
|
238
|
+
let score = 0
|
|
239
|
+
const nameLower = kit.name.toLowerCase()
|
|
240
|
+
const descLower = (kit.description ?? "").toLowerCase()
|
|
241
|
+
const tagsLower = (kit.tags ?? []).map((t) => t.toLowerCase())
|
|
242
|
+
|
|
243
|
+
for (const token of tokens) {
|
|
244
|
+
// Exact name match is strongest signal
|
|
245
|
+
if (nameLower === token) {
|
|
246
|
+
score += 1.0
|
|
247
|
+
} else if (nameLower.includes(token)) {
|
|
248
|
+
score += 0.6
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Tag matches are high-signal (curated keywords)
|
|
252
|
+
if (tagsLower.some((tag) => tag === token)) {
|
|
253
|
+
score += 0.5
|
|
254
|
+
} else if (tagsLower.some((tag) => tag.includes(token))) {
|
|
255
|
+
score += 0.25
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Description substring
|
|
259
|
+
if (descLower.includes(token)) {
|
|
260
|
+
score += 0.2
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Author match
|
|
264
|
+
if (kit.author?.toLowerCase().includes(token)) {
|
|
265
|
+
score += 0.15
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Normalize by token count so multi-word queries don't inflate scores
|
|
270
|
+
return tokens.length > 0 ? score / tokens.length : 0
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function toSearchHit(kit: RegistryKitEntry, score: number): RegistrySearchHit {
|
|
274
|
+
const metadata: Record<string, string> = {}
|
|
275
|
+
if (kit.latestVersion) metadata.version = kit.latestVersion
|
|
276
|
+
if (kit.author) metadata.author = kit.author
|
|
277
|
+
if (kit.license) metadata.license = kit.license
|
|
278
|
+
if (kit.assetTypes?.length) metadata.assetTypes = kit.assetTypes.join(", ")
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
source: kit.source,
|
|
282
|
+
id: kit.id,
|
|
283
|
+
title: kit.name,
|
|
284
|
+
description: kit.description,
|
|
285
|
+
ref: kit.ref,
|
|
286
|
+
homepage: kit.homepage,
|
|
287
|
+
score: Math.round(score * 1000) / 1000,
|
|
288
|
+
metadata,
|
|
289
|
+
curated: kit.curated,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Registry URL resolution ─────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
function resolveRegistryUrls(override?: string | string[]): string[] {
|
|
296
|
+
if (override) {
|
|
297
|
+
const urls = Array.isArray(override) ? override : [override]
|
|
298
|
+
return urls.filter(Boolean)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Allow env var override (comma-separated)
|
|
302
|
+
const envUrls = process.env.AKM_REGISTRY_URL?.trim()
|
|
303
|
+
if (envUrls) {
|
|
304
|
+
return envUrls.split(",").map((u) => u.trim()).filter(Boolean)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return [DEFAULT_REGISTRY_URL]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
function clampLimit(limit: number | undefined): number {
|
|
313
|
+
if (!limit || !Number.isFinite(limit)) return 20
|
|
314
|
+
return Math.min(100, Math.max(1, Math.trunc(limit)))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function asString(value: unknown): string | undefined {
|
|
318
|
+
return typeof value === "string" && value ? value : undefined
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function asSource(value: unknown): "npm" | "github" | "git" | undefined {
|
|
322
|
+
return value === "npm" || value === "github" || value === "git"
|
|
323
|
+
? value
|
|
324
|
+
: undefined
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function asStringArray(value: unknown): string[] | undefined {
|
|
328
|
+
if (!Array.isArray(value)) return undefined
|
|
329
|
+
const filtered = value.filter((v): v is string => typeof v === "string")
|
|
330
|
+
return filtered.length > 0 ? filtered : undefined
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function toErrorMessage(error: unknown): string {
|
|
334
|
+
return error instanceof Error ? error.message : String(error)
|
|
335
|
+
}
|