agentikit 0.0.7 → 0.0.9
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 +215 -76
- package/dist/index.d.ts +17 -3
- package/dist/index.js +10 -2
- package/dist/src/asset-spec.d.ts +14 -0
- package/dist/src/asset-spec.js +46 -0
- package/dist/src/cli.js +268 -57
- package/dist/src/common.d.ts +8 -0
- package/dist/src/common.js +46 -0
- package/dist/src/config.d.ts +37 -0
- package/dist/src/config.js +124 -0
- package/dist/src/embedder.d.ts +10 -0
- package/dist/src/embedder.js +87 -0
- package/dist/src/frontmatter.d.ts +30 -0
- package/dist/src/frontmatter.js +86 -0
- package/dist/src/indexer.d.ts +20 -2
- package/dist/src/indexer.js +212 -80
- package/dist/src/init.d.ts +19 -0
- package/dist/src/init.js +87 -0
- package/dist/src/llm.d.ts +15 -0
- package/dist/src/llm.js +91 -0
- package/dist/src/markdown.d.ts +18 -0
- package/dist/src/markdown.js +77 -0
- package/dist/src/metadata.d.ts +11 -2
- package/dist/src/metadata.js +161 -29
- package/dist/src/registry-install.d.ts +11 -0
- package/dist/src/registry-install.js +208 -0
- package/dist/src/registry-resolve.d.ts +3 -0
- package/dist/src/registry-resolve.js +231 -0
- package/dist/src/registry-search.d.ts +5 -0
- package/dist/src/registry-search.js +129 -0
- package/dist/src/registry-types.d.ts +55 -0
- package/dist/src/registry-types.js +1 -0
- package/dist/src/ripgrep-install.d.ts +12 -0
- package/dist/src/ripgrep-install.js +169 -0
- package/dist/src/ripgrep-resolve.d.ts +13 -0
- package/dist/src/ripgrep-resolve.js +68 -0
- package/dist/src/ripgrep.d.ts +3 -36
- package/dist/src/ripgrep.js +2 -262
- package/dist/src/similarity.d.ts +1 -2
- package/dist/src/similarity.js +11 -0
- package/dist/src/stash-add.d.ts +4 -0
- package/dist/src/stash-add.js +59 -0
- package/dist/src/stash-ref.d.ts +7 -0
- package/dist/src/stash-ref.js +33 -0
- package/dist/src/stash-registry.d.ts +18 -0
- package/dist/src/stash-registry.js +221 -0
- package/dist/src/stash-resolve.d.ts +2 -0
- package/dist/src/stash-resolve.js +45 -0
- package/dist/src/stash-search.d.ts +8 -0
- package/dist/src/stash-search.js +484 -0
- package/dist/src/stash-show.d.ts +5 -0
- package/dist/src/stash-show.js +114 -0
- package/dist/src/stash-types.d.ts +217 -0
- package/dist/src/stash-types.js +1 -0
- package/dist/src/stash.d.ts +10 -63
- package/dist/src/stash.js +6 -633
- package/dist/src/tool-runner.d.ts +35 -0
- package/dist/src/tool-runner.js +100 -0
- package/dist/src/walker.d.ts +19 -0
- package/dist/src/walker.js +47 -0
- package/package.json +8 -14
- package/src/asset-spec.ts +69 -0
- package/src/cli.ts +282 -46
- package/src/common.ts +58 -0
- package/src/config.ts +183 -0
- package/src/embedder.ts +117 -0
- package/src/frontmatter.ts +95 -0
- package/src/indexer.ts +244 -84
- package/src/init.ts +106 -0
- package/src/llm.ts +124 -0
- package/src/markdown.ts +106 -0
- package/src/metadata.ts +171 -27
- package/src/registry-install.ts +245 -0
- package/src/registry-resolve.ts +272 -0
- package/src/registry-search.ts +145 -0
- package/src/registry-types.ts +64 -0
- package/src/ripgrep-install.ts +200 -0
- package/src/ripgrep-resolve.ts +72 -0
- package/src/ripgrep.ts +3 -315
- package/src/similarity.ts +13 -1
- package/src/stash-add.ts +66 -0
- package/src/stash-ref.ts +41 -0
- package/src/stash-registry.ts +259 -0
- package/src/stash-resolve.ts +47 -0
- package/src/stash-search.ts +595 -0
- package/src/stash-show.ts +112 -0
- package/src/stash-types.ts +221 -0
- package/src/stash.ts +31 -760
- package/src/tool-runner.ts +129 -0
- package/src/walker.ts +53 -0
- package/.claude-plugin/plugin.json +0 -21
- package/commands/open.md +0 -11
- package/commands/run.md +0 -11
- package/commands/search.md +0 -11
- package/dist/src/plugin.d.ts +0 -2
- package/dist/src/plugin.js +0 -55
- package/skills/stash/SKILL.md +0 -73
- package/src/plugin.ts +0 -56
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { ParsedGithubRef, ParsedNpmRef, ParsedRegistryRef, ResolvedRegistryArtifact } from "./registry-types"
|
|
2
|
+
|
|
3
|
+
const GITHUB_API_BASE = "https://api.github.com"
|
|
4
|
+
|
|
5
|
+
export function parseRegistryRef(rawRef: string): ParsedRegistryRef {
|
|
6
|
+
const ref = rawRef.trim()
|
|
7
|
+
if (!ref) throw new Error("Registry ref is required.")
|
|
8
|
+
|
|
9
|
+
if (ref.startsWith("npm:")) {
|
|
10
|
+
return parseNpmRef(ref.slice(4), ref)
|
|
11
|
+
}
|
|
12
|
+
if (ref.startsWith("github:")) {
|
|
13
|
+
return parseGithubShorthand(ref.slice(7), ref)
|
|
14
|
+
}
|
|
15
|
+
if (ref.startsWith("http://") || ref.startsWith("https://")) {
|
|
16
|
+
return parseGithubUrl(ref)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (ref.startsWith("@") || !looksLikeGithubOwnerRepo(ref)) {
|
|
20
|
+
return parseNpmRef(ref, ref)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return parseGithubShorthand(ref, ref)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function resolveRegistryArtifact(parsed: ParsedRegistryRef): Promise<ResolvedRegistryArtifact> {
|
|
27
|
+
if (parsed.source === "npm") {
|
|
28
|
+
return resolveNpmArtifact(parsed)
|
|
29
|
+
}
|
|
30
|
+
return resolveGithubArtifact(parsed)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseNpmRef(input: string, originalRef: string): ParsedNpmRef {
|
|
34
|
+
const trimmed = input.trim()
|
|
35
|
+
if (!trimmed) throw new Error("Invalid npm ref.")
|
|
36
|
+
|
|
37
|
+
const parsed = splitNpmNameAndVersion(trimmed)
|
|
38
|
+
validateNpmPackageName(parsed.packageName)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
source: "npm",
|
|
42
|
+
ref: originalRef,
|
|
43
|
+
id: `npm:${parsed.packageName}`,
|
|
44
|
+
packageName: parsed.packageName,
|
|
45
|
+
requestedVersionOrTag: parsed.requestedVersionOrTag,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseGithubShorthand(input: string, originalRef: string): ParsedGithubRef {
|
|
50
|
+
const [repoPart, requestedRef] = splitRefSuffix(input.trim())
|
|
51
|
+
const segments = repoPart.split("/").filter(Boolean)
|
|
52
|
+
if (segments.length !== 2) {
|
|
53
|
+
throw new Error("Invalid GitHub ref. Expected owner/repo or owner/repo#ref.")
|
|
54
|
+
}
|
|
55
|
+
const owner = segments[0]
|
|
56
|
+
const repo = segments[1].replace(/\.git$/i, "")
|
|
57
|
+
if (!owner || !repo) {
|
|
58
|
+
throw new Error("Invalid GitHub ref. Expected owner/repo.")
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
source: "github",
|
|
62
|
+
ref: originalRef,
|
|
63
|
+
id: `github:${owner}/${repo}`,
|
|
64
|
+
owner,
|
|
65
|
+
repo,
|
|
66
|
+
requestedRef,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseGithubUrl(rawUrl: string): ParsedGithubRef {
|
|
71
|
+
let url: URL
|
|
72
|
+
try {
|
|
73
|
+
url = new URL(rawUrl)
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("Invalid registry URL.")
|
|
76
|
+
}
|
|
77
|
+
if (url.hostname !== "github.com") {
|
|
78
|
+
throw new Error("Only GitHub URLs are currently supported for URL refs.")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const segments = url.pathname.split("/").filter(Boolean)
|
|
82
|
+
if (segments.length < 2) {
|
|
83
|
+
throw new Error("Invalid GitHub URL. Expected https://github.com/owner/repo.")
|
|
84
|
+
}
|
|
85
|
+
const owner = segments[0]
|
|
86
|
+
const repo = segments[1].replace(/\.git$/i, "")
|
|
87
|
+
const requestedRef = url.hash ? decodeURIComponent(url.hash.slice(1)) : undefined
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
source: "github",
|
|
91
|
+
ref: rawUrl,
|
|
92
|
+
id: `github:${owner}/${repo}`,
|
|
93
|
+
owner,
|
|
94
|
+
repo,
|
|
95
|
+
requestedRef,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function resolveNpmArtifact(parsed: ParsedNpmRef): Promise<ResolvedRegistryArtifact> {
|
|
100
|
+
const encodedName = encodeURIComponent(parsed.packageName)
|
|
101
|
+
const metadata = await fetchJson<Record<string, unknown>>(`https://registry.npmjs.org/${encodedName}`)
|
|
102
|
+
|
|
103
|
+
const versions = asRecord(metadata.versions)
|
|
104
|
+
const distTags = asRecord(metadata["dist-tags"])
|
|
105
|
+
|
|
106
|
+
const requested = parsed.requestedVersionOrTag
|
|
107
|
+
let resolvedVersion: string | undefined
|
|
108
|
+
if (!requested) {
|
|
109
|
+
resolvedVersion = asString(distTags.latest)
|
|
110
|
+
} else if (requested in versions) {
|
|
111
|
+
resolvedVersion = requested
|
|
112
|
+
} else {
|
|
113
|
+
resolvedVersion = asString(distTags[requested])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!resolvedVersion || !(resolvedVersion in versions)) {
|
|
117
|
+
throw new Error(`Unable to resolve npm ref \"${parsed.ref}\".`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const versionMeta = asRecord(versions[resolvedVersion])
|
|
121
|
+
const dist = asRecord(versionMeta.dist)
|
|
122
|
+
const tarballUrl = asString(dist.tarball)
|
|
123
|
+
if (!tarballUrl) {
|
|
124
|
+
throw new Error(`npm package ${parsed.packageName}@${resolvedVersion} does not expose a tarball URL.`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const resolvedRevision = asString(dist.shasum) ?? asString(dist.integrity)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id: parsed.id,
|
|
131
|
+
source: parsed.source,
|
|
132
|
+
ref: parsed.ref,
|
|
133
|
+
artifactUrl: tarballUrl,
|
|
134
|
+
resolvedVersion,
|
|
135
|
+
resolvedRevision,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function resolveGithubArtifact(parsed: ParsedGithubRef): Promise<ResolvedRegistryArtifact> {
|
|
140
|
+
const headers = githubHeaders()
|
|
141
|
+
|
|
142
|
+
if (parsed.requestedRef) {
|
|
143
|
+
const commit = await tryFetchJson<Record<string, unknown>>(
|
|
144
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(parsed.requestedRef)}`,
|
|
145
|
+
headers,
|
|
146
|
+
)
|
|
147
|
+
const resolvedRevision = asString(commit?.sha) ?? parsed.requestedRef
|
|
148
|
+
return {
|
|
149
|
+
id: parsed.id,
|
|
150
|
+
source: parsed.source,
|
|
151
|
+
ref: parsed.ref,
|
|
152
|
+
artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(parsed.requestedRef)}`,
|
|
153
|
+
resolvedRevision,
|
|
154
|
+
resolvedVersion: parsed.requestedRef,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const latestRelease = await tryFetchJson<Record<string, unknown>>(
|
|
159
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/releases/latest`,
|
|
160
|
+
headers,
|
|
161
|
+
)
|
|
162
|
+
if (latestRelease) {
|
|
163
|
+
const tarballUrl = asString(latestRelease.tarball_url)
|
|
164
|
+
if (tarballUrl) {
|
|
165
|
+
return {
|
|
166
|
+
id: parsed.id,
|
|
167
|
+
source: parsed.source,
|
|
168
|
+
ref: parsed.ref,
|
|
169
|
+
artifactUrl: tarballUrl,
|
|
170
|
+
resolvedVersion: asString(latestRelease.tag_name),
|
|
171
|
+
resolvedRevision: asString(latestRelease.target_commitish),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const repoMeta = await fetchJson<Record<string, unknown>>(
|
|
177
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}`,
|
|
178
|
+
headers,
|
|
179
|
+
)
|
|
180
|
+
const defaultBranch = asString(repoMeta.default_branch)
|
|
181
|
+
if (!defaultBranch) {
|
|
182
|
+
throw new Error(`Unable to resolve default branch for ${parsed.owner}/${parsed.repo}.`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const commit = await tryFetchJson<Record<string, unknown>>(
|
|
186
|
+
`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(defaultBranch)}`,
|
|
187
|
+
headers,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
id: parsed.id,
|
|
192
|
+
source: parsed.source,
|
|
193
|
+
ref: parsed.ref,
|
|
194
|
+
artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(defaultBranch)}`,
|
|
195
|
+
resolvedVersion: defaultBranch,
|
|
196
|
+
resolvedRevision: asString(commit?.sha) ?? defaultBranch,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function splitNpmNameAndVersion(input: string): { packageName: string; requestedVersionOrTag?: string } {
|
|
201
|
+
if (input.startsWith("@")) {
|
|
202
|
+
const secondAt = input.indexOf("@", 1)
|
|
203
|
+
if (secondAt > 0) {
|
|
204
|
+
return {
|
|
205
|
+
packageName: input.slice(0, secondAt),
|
|
206
|
+
requestedVersionOrTag: input.slice(secondAt + 1) || undefined,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { packageName: input }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const at = input.lastIndexOf("@")
|
|
213
|
+
if (at > 0) {
|
|
214
|
+
return {
|
|
215
|
+
packageName: input.slice(0, at),
|
|
216
|
+
requestedVersionOrTag: input.slice(at + 1) || undefined,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { packageName: input }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateNpmPackageName(name: string): void {
|
|
223
|
+
if (!name || name.includes(" ")) {
|
|
224
|
+
throw new Error(`Invalid npm package name: \"${name}\".`)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function looksLikeGithubOwnerRepo(ref: string): boolean {
|
|
229
|
+
const [repoPart] = splitRefSuffix(ref)
|
|
230
|
+
const parts = repoPart.split("/").filter(Boolean)
|
|
231
|
+
return parts.length === 2
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function splitRefSuffix(value: string): [string, string | undefined] {
|
|
235
|
+
const hash = value.indexOf("#")
|
|
236
|
+
if (hash < 0) return [value, undefined]
|
|
237
|
+
return [value.slice(0, hash), value.slice(hash + 1) || undefined]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function githubHeaders(): HeadersInit {
|
|
241
|
+
const token = process.env.GITHUB_TOKEN?.trim()
|
|
242
|
+
const headers: Record<string, string> = {
|
|
243
|
+
Accept: "application/vnd.github+json",
|
|
244
|
+
"User-Agent": "agentikit-registry",
|
|
245
|
+
}
|
|
246
|
+
if (token) headers.Authorization = `Bearer ${token}`
|
|
247
|
+
return headers
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
|
|
251
|
+
const response = await fetch(url, { headers })
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new Error(`Request failed (${response.status}) for ${url}`)
|
|
254
|
+
}
|
|
255
|
+
return await response.json() as T
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function tryFetchJson<T>(url: string, headers?: HeadersInit): Promise<T | null> {
|
|
259
|
+
const response = await fetch(url, { headers })
|
|
260
|
+
if (!response.ok) return null
|
|
261
|
+
return await response.json() as T
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
265
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
266
|
+
? value as Record<string, unknown>
|
|
267
|
+
: {}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function asString(value: unknown): string | undefined {
|
|
271
|
+
return typeof value === "string" && value ? value : undefined
|
|
272
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { RegistrySearchHit, RegistrySearchResponse } from "./registry-types"
|
|
2
|
+
|
|
3
|
+
const GITHUB_API_BASE = "https://api.github.com"
|
|
4
|
+
|
|
5
|
+
export interface RegistrySearchOptions {
|
|
6
|
+
limit?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function searchRegistry(query: string, options?: RegistrySearchOptions): Promise<RegistrySearchResponse> {
|
|
10
|
+
const trimmed = query.trim()
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
return { query: "", hits: [], warnings: [] }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const limit = clampLimit(options?.limit)
|
|
16
|
+
const [npmResult, githubResult] = await Promise.allSettled([
|
|
17
|
+
searchNpm(trimmed, limit),
|
|
18
|
+
searchGithub(trimmed, limit),
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
const hits: RegistrySearchHit[] = []
|
|
22
|
+
const warnings: string[] = []
|
|
23
|
+
|
|
24
|
+
if (npmResult.status === "fulfilled") {
|
|
25
|
+
hits.push(...npmResult.value)
|
|
26
|
+
} else {
|
|
27
|
+
warnings.push(`npm search failed: ${toErrorMessage(npmResult.reason)}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (githubResult.status === "fulfilled") {
|
|
31
|
+
hits.push(...githubResult.value)
|
|
32
|
+
} else {
|
|
33
|
+
warnings.push(`GitHub search failed: ${toErrorMessage(githubResult.reason)}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
hits.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
query: trimmed,
|
|
40
|
+
hits: hits.slice(0, limit * 2),
|
|
41
|
+
warnings,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function searchNpm(query: string, limit: number): Promise<RegistrySearchHit[]> {
|
|
46
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`
|
|
47
|
+
const response = await fetch(url)
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`HTTP ${response.status}`)
|
|
50
|
+
}
|
|
51
|
+
const data = await response.json() as Record<string, unknown>
|
|
52
|
+
const objects = Array.isArray(data.objects) ? data.objects : []
|
|
53
|
+
|
|
54
|
+
return objects.flatMap((raw): RegistrySearchHit[] => {
|
|
55
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return []
|
|
56
|
+
const obj = raw as Record<string, unknown>
|
|
57
|
+
const pkg = asRecord(obj.package)
|
|
58
|
+
const name = asString(pkg.name)
|
|
59
|
+
if (!name) return []
|
|
60
|
+
|
|
61
|
+
const version = asString(pkg.version)
|
|
62
|
+
const metadata: Record<string, string> = {}
|
|
63
|
+
if (version) metadata.version = version
|
|
64
|
+
const date = asString(pkg.date)
|
|
65
|
+
if (date) metadata.updatedAt = date
|
|
66
|
+
|
|
67
|
+
return [{
|
|
68
|
+
source: "npm",
|
|
69
|
+
id: `npm:${name}`,
|
|
70
|
+
title: name,
|
|
71
|
+
description: asString(pkg.description),
|
|
72
|
+
ref: name,
|
|
73
|
+
homepage: asString(asRecord(pkg.links).homepage),
|
|
74
|
+
score: asNumber(obj.score),
|
|
75
|
+
metadata,
|
|
76
|
+
}]
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function searchGithub(query: string, limit: number): Promise<RegistrySearchHit[]> {
|
|
81
|
+
const q = encodeURIComponent(`${query} in:name,description,readme`)
|
|
82
|
+
const url = `${GITHUB_API_BASE}/search/repositories?q=${q}&sort=stars&order=desc&per_page=${limit}`
|
|
83
|
+
const response = await fetch(url, { headers: githubHeaders() })
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`HTTP ${response.status}`)
|
|
86
|
+
}
|
|
87
|
+
const data = await response.json() as Record<string, unknown>
|
|
88
|
+
const items = Array.isArray(data.items) ? data.items : []
|
|
89
|
+
|
|
90
|
+
return items.flatMap((raw): RegistrySearchHit[] => {
|
|
91
|
+
const repo = asRecord(raw)
|
|
92
|
+
const fullName = asString(repo.full_name)
|
|
93
|
+
if (!fullName) return []
|
|
94
|
+
|
|
95
|
+
const metadata: Record<string, string> = {}
|
|
96
|
+
const stars = asNumber(repo.stargazers_count)
|
|
97
|
+
if (stars > 0) metadata.stars = String(stars)
|
|
98
|
+
const language = asString(repo.language)
|
|
99
|
+
if (language) metadata.language = language
|
|
100
|
+
|
|
101
|
+
return [{
|
|
102
|
+
source: "github",
|
|
103
|
+
id: `github:${fullName}`,
|
|
104
|
+
title: fullName,
|
|
105
|
+
description: asString(repo.description),
|
|
106
|
+
ref: fullName,
|
|
107
|
+
homepage: asString(repo.html_url),
|
|
108
|
+
score: stars,
|
|
109
|
+
metadata,
|
|
110
|
+
}]
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function githubHeaders(): HeadersInit {
|
|
115
|
+
const token = process.env.GITHUB_TOKEN?.trim()
|
|
116
|
+
const headers: Record<string, string> = {
|
|
117
|
+
Accept: "application/vnd.github+json",
|
|
118
|
+
"User-Agent": "agentikit-registry",
|
|
119
|
+
}
|
|
120
|
+
if (token) headers.Authorization = `Bearer ${token}`
|
|
121
|
+
return headers
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function clampLimit(limit: number | undefined): number {
|
|
125
|
+
if (!limit || !Number.isFinite(limit)) return 20
|
|
126
|
+
return Math.min(100, Math.max(1, Math.trunc(limit)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
130
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
131
|
+
? value as Record<string, unknown>
|
|
132
|
+
: {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function asString(value: unknown): string | undefined {
|
|
136
|
+
return typeof value === "string" && value ? value : undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function asNumber(value: unknown): number {
|
|
140
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toErrorMessage(error: unknown): string {
|
|
144
|
+
return error instanceof Error ? error.message : String(error)
|
|
145
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type RegistrySource = "npm" | "github"
|
|
2
|
+
|
|
3
|
+
export interface RegistryRefBase {
|
|
4
|
+
source: RegistrySource
|
|
5
|
+
ref: string
|
|
6
|
+
id: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ParsedNpmRef extends RegistryRefBase {
|
|
10
|
+
source: "npm"
|
|
11
|
+
packageName: string
|
|
12
|
+
requestedVersionOrTag?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ParsedGithubRef extends RegistryRefBase {
|
|
16
|
+
source: "github"
|
|
17
|
+
owner: string
|
|
18
|
+
repo: string
|
|
19
|
+
requestedRef?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ParsedRegistryRef = ParsedNpmRef | ParsedGithubRef
|
|
23
|
+
|
|
24
|
+
export interface ResolvedRegistryArtifact {
|
|
25
|
+
id: string
|
|
26
|
+
source: RegistrySource
|
|
27
|
+
ref: string
|
|
28
|
+
artifactUrl: string
|
|
29
|
+
resolvedVersion?: string
|
|
30
|
+
resolvedRevision?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RegistryInstalledEntry {
|
|
34
|
+
id: string
|
|
35
|
+
source: RegistrySource
|
|
36
|
+
ref: string
|
|
37
|
+
resolvedVersion?: string
|
|
38
|
+
resolvedRevision?: string
|
|
39
|
+
artifactUrl: string
|
|
40
|
+
stashRoot: string
|
|
41
|
+
cacheDir: string
|
|
42
|
+
installedAt: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RegistryInstallResult extends RegistryInstalledEntry {
|
|
46
|
+
extractedDir: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RegistrySearchHit {
|
|
50
|
+
source: RegistrySource
|
|
51
|
+
id: string
|
|
52
|
+
title: string
|
|
53
|
+
description?: string
|
|
54
|
+
ref: string
|
|
55
|
+
homepage?: string
|
|
56
|
+
score?: number
|
|
57
|
+
metadata?: Record<string, string>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RegistrySearchResponse {
|
|
61
|
+
query: string
|
|
62
|
+
hits: RegistrySearchHit[]
|
|
63
|
+
warnings: string[]
|
|
64
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { IS_WINDOWS } from "./common"
|
|
5
|
+
import { RG_BINARY, resolveRg } from "./ripgrep-resolve"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Platform and architecture detection for ripgrep binary downloads.
|
|
9
|
+
*/
|
|
10
|
+
function getRgPlatformTarget(): { platform: string; arch: string; ext: string } | null {
|
|
11
|
+
const platform = process.platform
|
|
12
|
+
const arch = process.arch
|
|
13
|
+
|
|
14
|
+
if (platform === "linux" && arch === "x64") {
|
|
15
|
+
return { platform: "x86_64-unknown-linux-musl", arch: "x64", ext: ".tar.gz" }
|
|
16
|
+
}
|
|
17
|
+
if (platform === "linux" && arch === "arm64") {
|
|
18
|
+
return { platform: "aarch64-unknown-linux-gnu", arch: "arm64", ext: ".tar.gz" }
|
|
19
|
+
}
|
|
20
|
+
if (platform === "darwin" && arch === "x64") {
|
|
21
|
+
return { platform: "x86_64-apple-darwin", arch: "x64", ext: ".tar.gz" }
|
|
22
|
+
}
|
|
23
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
24
|
+
return { platform: "aarch64-apple-darwin", arch: "arm64", ext: ".tar.gz" }
|
|
25
|
+
}
|
|
26
|
+
if (platform === "win32" && arch === "x64") {
|
|
27
|
+
return { platform: "x86_64-pc-windows-msvc", arch: "x64", ext: ".zip" }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const RG_VERSION = "14.1.1"
|
|
34
|
+
|
|
35
|
+
export interface EnsureRgResult {
|
|
36
|
+
rgPath: string
|
|
37
|
+
installed: boolean
|
|
38
|
+
version: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ensure ripgrep is available. If not found on PATH or in stash/bin,
|
|
43
|
+
* download and install it to stash/bin.
|
|
44
|
+
*
|
|
45
|
+
* Returns the path to the ripgrep binary and whether it was newly installed.
|
|
46
|
+
*/
|
|
47
|
+
export function ensureRg(stashDir: string): EnsureRgResult {
|
|
48
|
+
// Already available?
|
|
49
|
+
const existing = resolveRg(stashDir)
|
|
50
|
+
if (existing) {
|
|
51
|
+
return { rgPath: existing, installed: false, version: getRgVersion(existing) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Determine platform
|
|
55
|
+
const target = getRgPlatformTarget()
|
|
56
|
+
if (!target) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Unsupported platform for ripgrep auto-install: ${process.platform}/${process.arch}. ` +
|
|
59
|
+
`Install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const binDir = path.join(stashDir, "bin")
|
|
64
|
+
if (!fs.existsSync(binDir)) {
|
|
65
|
+
fs.mkdirSync(binDir, { recursive: true })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const archiveName = `ripgrep-${RG_VERSION}-${target.platform}`
|
|
69
|
+
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${archiveName}${target.ext}`
|
|
70
|
+
const destBinary = path.join(binDir, RG_BINARY)
|
|
71
|
+
|
|
72
|
+
if (target.ext === ".tar.gz") {
|
|
73
|
+
downloadAndExtractTarGz(url, archiveName, destBinary)
|
|
74
|
+
} else {
|
|
75
|
+
downloadAndExtractZip(url, archiveName, destBinary)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Make executable
|
|
79
|
+
if (!IS_WINDOWS) {
|
|
80
|
+
fs.chmodSync(destBinary, 0o755)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { rgPath: destBinary, installed: true, version: RG_VERSION }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function downloadAndExtractTarGz(url: string, archiveName: string, destBinary: string): void {
|
|
87
|
+
const destDir = path.dirname(destBinary)
|
|
88
|
+
const tmpTarGz = path.join(destDir, "rg-download.tar.gz")
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Download archive to a temporary file without using a shell
|
|
92
|
+
const curlResult = spawnSync(
|
|
93
|
+
"curl",
|
|
94
|
+
["-fsSL", "-o", tmpTarGz, url],
|
|
95
|
+
{
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
timeout: 60_000,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (curlResult.status !== 0) {
|
|
102
|
+
const err = curlResult.stderr?.trim() || curlResult.error?.message || "unknown error"
|
|
103
|
+
throw new Error(`Failed to download ripgrep from ${url}: ${err}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Extract the specific binary from the archive into destDir
|
|
107
|
+
const tarResult = spawnSync(
|
|
108
|
+
"tar",
|
|
109
|
+
[
|
|
110
|
+
"xzf",
|
|
111
|
+
tmpTarGz,
|
|
112
|
+
"--strip-components=1",
|
|
113
|
+
"-C",
|
|
114
|
+
destDir,
|
|
115
|
+
`${archiveName}/rg`,
|
|
116
|
+
],
|
|
117
|
+
{
|
|
118
|
+
encoding: "utf8",
|
|
119
|
+
timeout: 60_000,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if (tarResult.status !== 0) {
|
|
124
|
+
const err = tarResult.stderr?.trim() || tarResult.error?.message || "unknown error"
|
|
125
|
+
throw new Error(`Failed to extract ripgrep from ${url}: ${err}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(destBinary)) {
|
|
129
|
+
throw new Error(`ripgrep binary not found at ${destBinary} after extraction`)
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
// Best-effort cleanup of temporary archive
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(tmpTarGz)) {
|
|
135
|
+
fs.unlinkSync(tmpTarGz)
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// ignore cleanup errors
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function downloadAndExtractZip(url: string, archiveName: string, destBinary: string): void {
|
|
144
|
+
const destDir = path.dirname(destBinary)
|
|
145
|
+
const tmpZip = path.join(destDir, "rg-download.zip")
|
|
146
|
+
const expandedDir = path.join(destDir, archiveName)
|
|
147
|
+
try {
|
|
148
|
+
// Download
|
|
149
|
+
const dlResult = spawnSync("curl", ["-fsSL", "-o", tmpZip, url], {
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
timeout: 60_000,
|
|
152
|
+
})
|
|
153
|
+
if (dlResult.status !== 0) {
|
|
154
|
+
throw new Error(dlResult.stderr?.trim() || "download failed")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Extract the zip archive using separate spawnSync calls with argument arrays
|
|
158
|
+
// to avoid shell injection via path interpolation in PowerShell -Command strings
|
|
159
|
+
const expandResult = spawnSync("powershell", [
|
|
160
|
+
"-Command",
|
|
161
|
+
"Expand-Archive",
|
|
162
|
+
"-Path", tmpZip,
|
|
163
|
+
"-DestinationPath", destDir,
|
|
164
|
+
"-Force",
|
|
165
|
+
], {
|
|
166
|
+
encoding: "utf8",
|
|
167
|
+
timeout: 60_000,
|
|
168
|
+
})
|
|
169
|
+
if (expandResult.status !== 0) {
|
|
170
|
+
throw new Error(expandResult.stderr?.trim() || "extraction failed")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const srcRgExe = path.join(destDir, archiveName, "rg.exe")
|
|
174
|
+
const moveResult = spawnSync("powershell", [
|
|
175
|
+
"-Command",
|
|
176
|
+
"Move-Item",
|
|
177
|
+
"-Force",
|
|
178
|
+
"-Path", srcRgExe,
|
|
179
|
+
"-Destination", destBinary,
|
|
180
|
+
], {
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
timeout: 60_000,
|
|
183
|
+
})
|
|
184
|
+
if (moveResult.status !== 0) {
|
|
185
|
+
throw new Error(moveResult.stderr?.trim() || "move failed")
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
if (fs.existsSync(tmpZip)) fs.unlinkSync(tmpZip)
|
|
189
|
+
if (fs.existsSync(expandedDir)) fs.rmSync(expandedDir, { recursive: true, force: true })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getRgVersion(rgPath: string): string {
|
|
194
|
+
const result = spawnSync(rgPath, ["--version"], { encoding: "utf8", timeout: 5_000 })
|
|
195
|
+
if (result.status === 0 && result.stdout) {
|
|
196
|
+
const match = result.stdout.match(/ripgrep\s+([\d.]+)/)
|
|
197
|
+
return match ? match[1] : "unknown"
|
|
198
|
+
}
|
|
199
|
+
return "unknown"
|
|
200
|
+
}
|