agentikit 0.0.9 → 0.0.13

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.
Files changed (108) hide show
  1. package/README.md +139 -208
  2. package/dist/index.d.ts +8 -2
  3. package/dist/index.js +4 -1
  4. package/dist/src/asset-spec.d.ts +2 -0
  5. package/dist/src/asset-spec.js +22 -3
  6. package/dist/src/asset-type-handler.d.ts +27 -0
  7. package/dist/src/asset-type-handler.js +33 -0
  8. package/dist/src/cli.js +201 -75
  9. package/dist/src/common.d.ts +6 -1
  10. package/dist/src/common.js +18 -4
  11. package/dist/src/config-cli.d.ts +9 -0
  12. package/dist/src/config-cli.js +473 -0
  13. package/dist/src/config.d.ts +19 -6
  14. package/dist/src/config.js +139 -29
  15. package/dist/src/db.d.ts +46 -0
  16. package/dist/src/db.js +299 -0
  17. package/dist/src/embedder.js +12 -7
  18. package/dist/src/github.d.ts +4 -0
  19. package/dist/src/github.js +19 -0
  20. package/dist/src/handlers/agent-handler.d.ts +2 -0
  21. package/dist/src/handlers/agent-handler.js +26 -0
  22. package/dist/src/handlers/command-handler.d.ts +2 -0
  23. package/dist/src/handlers/command-handler.js +23 -0
  24. package/dist/src/handlers/index.d.ts +6 -0
  25. package/dist/src/handlers/index.js +23 -0
  26. package/dist/src/handlers/knowledge-handler.d.ts +2 -0
  27. package/dist/src/handlers/knowledge-handler.js +56 -0
  28. package/dist/src/handlers/markdown-helpers.d.ts +7 -0
  29. package/dist/src/handlers/markdown-helpers.js +15 -0
  30. package/dist/src/handlers/script-handler.d.ts +2 -0
  31. package/dist/src/handlers/script-handler.js +78 -0
  32. package/dist/src/handlers/skill-handler.d.ts +2 -0
  33. package/dist/src/handlers/skill-handler.js +30 -0
  34. package/dist/src/handlers/tool-handler.d.ts +2 -0
  35. package/dist/src/handlers/tool-handler.js +58 -0
  36. package/dist/src/indexer.d.ts +1 -23
  37. package/dist/src/indexer.js +162 -155
  38. package/dist/src/init.d.ts +2 -2
  39. package/dist/src/init.js +21 -9
  40. package/dist/src/llm.js +4 -3
  41. package/dist/src/metadata.d.ts +0 -1
  42. package/dist/src/metadata.js +6 -64
  43. package/dist/src/origin-resolve.d.ts +19 -0
  44. package/dist/src/origin-resolve.js +53 -0
  45. package/dist/src/registry-install.d.ts +2 -2
  46. package/dist/src/registry-install.js +142 -35
  47. package/dist/src/registry-resolve.js +90 -22
  48. package/dist/src/registry-search.d.ts +22 -0
  49. package/dist/src/registry-search.js +231 -97
  50. package/dist/src/registry-types.d.ts +9 -2
  51. package/dist/src/stash-add.js +4 -4
  52. package/dist/src/stash-clone.d.ts +22 -0
  53. package/dist/src/stash-clone.js +83 -0
  54. package/dist/src/stash-ref.d.ts +27 -3
  55. package/dist/src/stash-ref.js +63 -24
  56. package/dist/src/stash-registry.js +12 -12
  57. package/dist/src/stash-resolve.js +3 -0
  58. package/dist/src/stash-search.js +168 -164
  59. package/dist/src/stash-show.d.ts +1 -1
  60. package/dist/src/stash-show.js +28 -96
  61. package/dist/src/stash-source.d.ts +24 -0
  62. package/dist/src/stash-source.js +81 -0
  63. package/dist/src/stash-types.d.ts +14 -4
  64. package/dist/src/stash.d.ts +6 -0
  65. package/dist/src/stash.js +3 -0
  66. package/dist/src/tool-runner.d.ts +1 -1
  67. package/dist/src/tool-runner.js +18 -5
  68. package/package.json +7 -2
  69. package/src/asset-spec.ts +20 -4
  70. package/src/asset-type-handler.ts +77 -0
  71. package/src/cli.ts +213 -82
  72. package/src/common.ts +23 -5
  73. package/src/config-cli.ts +499 -0
  74. package/src/config.ts +160 -38
  75. package/src/db.ts +411 -0
  76. package/src/embedder.ts +22 -11
  77. package/src/github.ts +21 -0
  78. package/src/handlers/agent-handler.ts +32 -0
  79. package/src/handlers/command-handler.ts +29 -0
  80. package/src/handlers/index.ts +25 -0
  81. package/src/handlers/knowledge-handler.ts +62 -0
  82. package/src/handlers/markdown-helpers.ts +19 -0
  83. package/src/handlers/script-handler.ts +92 -0
  84. package/src/handlers/skill-handler.ts +37 -0
  85. package/src/handlers/tool-handler.ts +71 -0
  86. package/src/indexer.ts +208 -187
  87. package/src/init.ts +17 -9
  88. package/src/llm.ts +4 -3
  89. package/src/metadata.ts +5 -65
  90. package/src/origin-resolve.ts +67 -0
  91. package/src/registry-install.ts +158 -42
  92. package/src/registry-resolve.ts +92 -23
  93. package/src/registry-search.ts +288 -98
  94. package/src/registry-types.ts +10 -2
  95. package/src/stash-add.ts +14 -17
  96. package/src/stash-clone.ts +127 -0
  97. package/src/stash-ref.ts +84 -26
  98. package/src/stash-registry.ts +12 -12
  99. package/src/stash-resolve.ts +3 -0
  100. package/src/stash-search.ts +202 -184
  101. package/src/stash-show.ts +33 -90
  102. package/src/stash-source.ts +103 -0
  103. package/src/stash-types.ts +14 -4
  104. package/src/stash.ts +8 -0
  105. package/src/tool-runner.ts +18 -5
  106. package/dist/src/similarity.d.ts +0 -34
  107. package/dist/src/similarity.js +0 -211
  108. package/src/similarity.ts +0 -271
@@ -1,143 +1,333 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { fetchWithTimeout } from "./common"
1
4
  import type { RegistrySearchHit, RegistrySearchResponse } from "./registry-types"
2
5
 
3
- const GITHUB_API_BASE = "https://api.github.com"
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
+ }
4
41
 
5
42
  export interface RegistrySearchOptions {
6
43
  limit?: number
44
+ /** Override registry URL(s). Accepts a single URL or an array. */
45
+ registryUrls?: string | string[]
7
46
  }
8
47
 
9
- export async function searchRegistry(query: string, options?: RegistrySearchOptions): Promise<RegistrySearchResponse> {
48
+ // ── Public API ──────────────────────────────────────────────────────────────
49
+
50
+ export async function searchRegistry(
51
+ query: string,
52
+ options?: RegistrySearchOptions,
53
+ ): Promise<RegistrySearchResponse> {
10
54
  const trimmed = query.trim()
11
55
  if (!trimmed) {
12
56
  return { query: "", hits: [], warnings: [] }
13
57
  }
14
58
 
15
59
  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[] = []
60
+ const urls = resolveRegistryUrls(options?.registryUrls)
22
61
  const warnings: string[] = []
23
62
 
24
- if (npmResult.status === "fulfilled") {
25
- hits.push(...npmResult.value)
26
- } else {
27
- warnings.push(`npm search failed: ${toErrorMessage(npmResult.reason)}`)
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
+ }
28
74
  }
29
75
 
30
- if (githubResult.status === "fulfilled") {
31
- hits.push(...githubResult.value)
32
- } else {
33
- warnings.push(`GitHub search failed: ${toErrorMessage(githubResult.reason)}`)
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
34
91
  }
35
92
 
36
- hits.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
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
+ }
37
114
 
38
- return {
39
- query: trimmed,
40
- hits: hits.slice(0, limit * 2),
41
- warnings,
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
42
126
  }
43
127
  }
44
128
 
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}`)
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
50
138
  }
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
- }]
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] : []
77
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
+ }
78
209
  }
79
210
 
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}`)
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
+ }
86
230
  }
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
- })
231
+
232
+ scored.sort((a, b) => b.score - a.score)
233
+
234
+ return scored.slice(0, limit).map(({ kit, score }) => toSearchHit(kit, score))
112
235
  }
113
236
 
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",
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
+ }
119
267
  }
120
- if (token) headers.Authorization = `Bearer ${token}`
121
- return headers
268
+
269
+ // Normalize by token count so multi-word queries don't inflate scores
270
+ return tokens.length > 0 ? score / tokens.length : 0
122
271
  }
123
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
+
124
312
  function clampLimit(limit: number | undefined): number {
125
313
  if (!limit || !Number.isFinite(limit)) return 20
126
314
  return Math.min(100, Math.max(1, Math.trunc(limit)))
127
315
  }
128
316
 
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
317
  function asString(value: unknown): string | undefined {
136
318
  return typeof value === "string" && value ? value : undefined
137
319
  }
138
320
 
139
- function asNumber(value: unknown): number {
140
- return typeof value === "number" && Number.isFinite(value) ? value : 0
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
141
331
  }
142
332
 
143
333
  function toErrorMessage(error: unknown): string {
@@ -1,4 +1,4 @@
1
- export type RegistrySource = "npm" | "github"
1
+ export type RegistrySource = "npm" | "github" | "git"
2
2
 
3
3
  export interface RegistryRefBase {
4
4
  source: RegistrySource
@@ -19,7 +19,13 @@ export interface ParsedGithubRef extends RegistryRefBase {
19
19
  requestedRef?: string
20
20
  }
21
21
 
22
- export type ParsedRegistryRef = ParsedNpmRef | ParsedGithubRef
22
+ export interface ParsedGitRef extends RegistryRefBase {
23
+ source: "git"
24
+ repoRoot: string
25
+ sourcePath: string
26
+ }
27
+
28
+ export type ParsedRegistryRef = ParsedNpmRef | ParsedGithubRef | ParsedGitRef
23
29
 
24
30
  export interface ResolvedRegistryArtifact {
25
31
  id: string
@@ -55,6 +61,8 @@ export interface RegistrySearchHit {
55
61
  homepage?: string
56
62
  score?: number
57
63
  metadata?: Record<string, string>
64
+ /** Whether this entry was manually reviewed and approved */
65
+ curated?: boolean
58
66
  }
59
67
 
60
68
  export interface RegistrySearchResponse {
package/src/stash-add.ts CHANGED
@@ -7,25 +7,22 @@ import type { AddResponse } from "./stash-types"
7
7
 
8
8
  export async function agentikitAdd(input: { ref: string }): Promise<AddResponse> {
9
9
  const ref = input.ref.trim()
10
- if (!ref) throw new Error("Registry ref is required.")
10
+ if (!ref) throw new Error("Install ref or local git directory is required.")
11
11
 
12
12
  const stashDir = resolveStashDir()
13
13
  const installed = await installRegistryRef(ref)
14
- const replaced = loadConfig(stashDir).registry?.installed.find((entry) => entry.id === installed.id)
15
- const config = upsertInstalledRegistryEntry(
16
- {
17
- id: installed.id,
18
- source: installed.source,
19
- ref: installed.ref,
20
- artifactUrl: installed.artifactUrl,
21
- resolvedVersion: installed.resolvedVersion,
22
- resolvedRevision: installed.resolvedRevision,
23
- stashRoot: installed.stashRoot,
24
- cacheDir: installed.cacheDir,
25
- installedAt: installed.installedAt,
26
- },
27
- stashDir,
28
- )
14
+ const replaced = loadConfig().registry?.installed.find((entry) => entry.id === installed.id)
15
+ const config = upsertInstalledRegistryEntry({
16
+ id: installed.id,
17
+ source: installed.source,
18
+ ref: installed.ref,
19
+ artifactUrl: installed.artifactUrl,
20
+ resolvedVersion: installed.resolvedVersion,
21
+ resolvedRevision: installed.resolvedRevision,
22
+ stashRoot: installed.stashRoot,
23
+ cacheDir: installed.cacheDir,
24
+ installedAt: installed.installedAt,
25
+ })
29
26
 
30
27
  if (replaced && replaced.cacheDir !== installed.cacheDir) {
31
28
  try {
@@ -53,7 +50,7 @@ export async function agentikitAdd(input: { ref: string }): Promise<AddResponse>
53
50
  installedAt: installed.installedAt,
54
51
  },
55
52
  config: {
56
- additionalStashDirs: config.additionalStashDirs,
53
+ mountedStashDirs: config.mountedStashDirs,
57
54
  installedRegistryCount: config.registry?.installed.length ?? 0,
58
55
  },
59
56
  index: {
@@ -0,0 +1,127 @@
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, type StashSource, type StashSourceKind } from "./stash-source"
8
+
9
+ export interface CloneOptions {
10
+ /** Source ref (e.g., npm:@scope/pkg//tool:deploy.sh) */
11
+ sourceRef: string
12
+ /** Optional new name for the cloned asset */
13
+ newName?: string
14
+ /** If true, overwrite existing asset in working stash */
15
+ force?: boolean
16
+ }
17
+
18
+ export interface CloneResponse {
19
+ source: {
20
+ path: string
21
+ sourceKind: StashSourceKind
22
+ registryId?: string
23
+ }
24
+ destination: {
25
+ path: string
26
+ ref: string
27
+ }
28
+ overwritten: boolean
29
+ }
30
+
31
+ export async function agentikitClone(options: CloneOptions): Promise<CloneResponse> {
32
+ const parsed = parseAssetRef(options.sourceRef)
33
+ const allSources = resolveStashSources()
34
+ const workingSource = allSources.find((s) => s.kind === "working")
35
+ if (!workingSource) {
36
+ throw new Error("No working stash configured. Run `akm init` first.")
37
+ }
38
+
39
+ const searchSources = resolveSourcesForOrigin(parsed.origin, allSources)
40
+
41
+ let sourcePath: string | undefined
42
+ let lastError: Error | undefined
43
+ for (const source of searchSources) {
44
+ try {
45
+ sourcePath = resolveAssetPath(source.path, parsed.type, parsed.name)
46
+ break
47
+ } catch (err) {
48
+ lastError = err instanceof Error ? err : new Error(String(err))
49
+ }
50
+ }
51
+ if (!sourcePath) {
52
+ throw lastError ?? new Error(`Source asset not found for ref: ${options.sourceRef}`)
53
+ }
54
+
55
+ const sourceSource = findSourceForPath(sourcePath, allSources)
56
+ const sourceKind = sourceSource?.kind ?? "working"
57
+
58
+ const destName = options.newName ?? parsed.name
59
+ const typeDir = TYPE_DIRS[parsed.type]
60
+ const workingDir = workingSource.path
61
+
62
+ // Guard against self-clone
63
+ if (parsed.type === "skill") {
64
+ const sourceSkillDir = path.resolve(path.dirname(sourcePath))
65
+ const destSkillDir = path.resolve(path.join(workingDir, typeDir, destName))
66
+ if (sourceSkillDir === destSkillDir) {
67
+ throw new Error(
68
+ `Source and destination are the same path. Use --name to provide a new name for the clone.`,
69
+ )
70
+ }
71
+ } else {
72
+ const resolvedSource = path.resolve(sourcePath)
73
+ const resolvedDest = path.resolve(path.join(workingDir, typeDir, destName))
74
+ if (resolvedSource === resolvedDest) {
75
+ throw new Error(
76
+ `Source and destination are the same path. Use --name to provide a new name for the clone.`,
77
+ )
78
+ }
79
+ }
80
+
81
+ let destPath: string
82
+ if (parsed.type === "skill") {
83
+ const sourceSkillDir = path.dirname(sourcePath)
84
+ const destSkillDir = path.join(workingDir, typeDir, destName)
85
+ const overwritten = fs.existsSync(destSkillDir)
86
+
87
+ if (overwritten && !options.force) {
88
+ throw new Error(
89
+ `Asset already exists in working stash: ${destSkillDir}. Use --force to overwrite.`,
90
+ )
91
+ }
92
+
93
+ if (overwritten) {
94
+ fs.rmSync(destSkillDir, { recursive: true, force: true })
95
+ }
96
+ fs.cpSync(sourceSkillDir, destSkillDir, { recursive: true })
97
+
98
+ destPath = path.join(destSkillDir, "SKILL.md")
99
+ const ref = makeAssetRef(parsed.type, destName, "local")
100
+
101
+ return {
102
+ source: { path: sourcePath, sourceKind, registryId: sourceSource?.registryId },
103
+ destination: { path: destPath, ref },
104
+ overwritten,
105
+ }
106
+ }
107
+
108
+ destPath = path.join(workingDir, typeDir, destName)
109
+ const overwritten = fs.existsSync(destPath)
110
+
111
+ if (overwritten && !options.force) {
112
+ throw new Error(
113
+ `Asset already exists in working stash: ${destPath}. Use --force to overwrite.`,
114
+ )
115
+ }
116
+
117
+ fs.mkdirSync(path.dirname(destPath), { recursive: true })
118
+ fs.copyFileSync(sourcePath, destPath)
119
+
120
+ const ref = makeAssetRef(parsed.type, destName, "local")
121
+
122
+ return {
123
+ source: { path: sourcePath, sourceKind, registryId: sourceSource?.registryId },
124
+ destination: { path: destPath, ref },
125
+ overwritten,
126
+ }
127
+ }