agentikit 0.0.13 → 0.0.15

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 (156) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +187 -110
  3. package/dist/{src/asset-spec.js → asset-spec.js} +11 -2
  4. package/dist/{src/asset-type-handler.js → asset-type-handler.js} +4 -3
  5. package/dist/cli.js +709 -0
  6. package/dist/common.js +192 -0
  7. package/dist/{src/config-cli.js → config-cli.js} +36 -30
  8. package/dist/{src/config.js → config.js} +95 -25
  9. package/dist/{src/db.js → db.js} +123 -51
  10. package/dist/{src/embedder.js → embedder.js} +57 -2
  11. package/dist/errors.js +28 -0
  12. package/dist/file-context.js +188 -0
  13. package/dist/{src/frontmatter.js → frontmatter.js} +1 -1
  14. package/dist/{src/github.js → github.js} +1 -3
  15. package/dist/handlers/agent-handler.js +19 -0
  16. package/dist/handlers/command-handler.js +20 -0
  17. package/dist/handlers/handler-bridge.js +51 -0
  18. package/dist/handlers/index.js +19 -0
  19. package/dist/handlers/knowledge-handler.js +32 -0
  20. package/dist/handlers/script-handler.js +42 -0
  21. package/dist/{src/handlers → handlers}/skill-handler.js +5 -6
  22. package/dist/{src/handlers → handlers}/tool-handler.js +8 -24
  23. package/dist/{src/indexer.js → indexer.js} +50 -26
  24. package/dist/init.js +43 -0
  25. package/dist/{src/llm.js → llm.js} +6 -11
  26. package/dist/lockfile.js +60 -0
  27. package/dist/matchers.js +163 -0
  28. package/dist/{src/metadata.js → metadata.js} +36 -16
  29. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  30. package/dist/paths.js +83 -0
  31. package/dist/{src/registry-install.js → registry-install.js} +151 -19
  32. package/dist/{src/registry-resolve.js → registry-resolve.js} +190 -26
  33. package/dist/{src/registry-search.js → registry-search.js} +13 -21
  34. package/dist/renderers.js +286 -0
  35. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +8 -27
  36. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  37. package/dist/ripgrep.js +2 -0
  38. package/dist/self-update.js +226 -0
  39. package/dist/{src/stash-add.js → stash-add.js} +14 -4
  40. package/dist/stash-clone.js +115 -0
  41. package/dist/{src/stash-ref.js → stash-ref.js} +10 -9
  42. package/dist/{src/stash-registry.js → stash-registry.js} +21 -46
  43. package/dist/{src/stash-resolve.js → stash-resolve.js} +10 -9
  44. package/dist/{src/stash-search.js → stash-search.js} +89 -74
  45. package/dist/stash-show.js +74 -0
  46. package/dist/stash-source.js +127 -0
  47. package/dist/submit.js +557 -0
  48. package/dist/{src/tool-runner.js → tool-runner.js} +1 -5
  49. package/dist/{src/walker.js → walker.js} +38 -0
  50. package/dist/warn.js +20 -0
  51. package/package.json +13 -18
  52. package/dist/index.d.ts +0 -28
  53. package/dist/index.js +0 -15
  54. package/dist/src/asset-spec.d.ts +0 -16
  55. package/dist/src/asset-type-handler.d.ts +0 -27
  56. package/dist/src/cli.d.ts +0 -2
  57. package/dist/src/cli.js +0 -399
  58. package/dist/src/common.d.ts +0 -13
  59. package/dist/src/common.js +0 -60
  60. package/dist/src/config-cli.d.ts +0 -9
  61. package/dist/src/config.d.ts +0 -50
  62. package/dist/src/db.d.ts +0 -46
  63. package/dist/src/embedder.d.ts +0 -10
  64. package/dist/src/frontmatter.d.ts +0 -30
  65. package/dist/src/github.d.ts +0 -4
  66. package/dist/src/handlers/agent-handler.d.ts +0 -2
  67. package/dist/src/handlers/agent-handler.js +0 -26
  68. package/dist/src/handlers/command-handler.d.ts +0 -2
  69. package/dist/src/handlers/command-handler.js +0 -23
  70. package/dist/src/handlers/index.d.ts +0 -6
  71. package/dist/src/handlers/index.js +0 -23
  72. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  73. package/dist/src/handlers/knowledge-handler.js +0 -56
  74. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  75. package/dist/src/handlers/script-handler.d.ts +0 -2
  76. package/dist/src/handlers/script-handler.js +0 -78
  77. package/dist/src/handlers/skill-handler.d.ts +0 -2
  78. package/dist/src/handlers/tool-handler.d.ts +0 -2
  79. package/dist/src/indexer.d.ts +0 -22
  80. package/dist/src/init.d.ts +0 -19
  81. package/dist/src/init.js +0 -99
  82. package/dist/src/llm.d.ts +0 -15
  83. package/dist/src/markdown.d.ts +0 -18
  84. package/dist/src/metadata.d.ts +0 -41
  85. package/dist/src/origin-resolve.d.ts +0 -19
  86. package/dist/src/registry-install.d.ts +0 -11
  87. package/dist/src/registry-resolve.d.ts +0 -3
  88. package/dist/src/registry-search.d.ts +0 -27
  89. package/dist/src/registry-types.d.ts +0 -62
  90. package/dist/src/ripgrep-install.d.ts +0 -12
  91. package/dist/src/ripgrep-resolve.d.ts +0 -13
  92. package/dist/src/ripgrep.d.ts +0 -3
  93. package/dist/src/ripgrep.js +0 -2
  94. package/dist/src/stash-add.d.ts +0 -4
  95. package/dist/src/stash-clone.d.ts +0 -22
  96. package/dist/src/stash-clone.js +0 -83
  97. package/dist/src/stash-ref.d.ts +0 -31
  98. package/dist/src/stash-registry.d.ts +0 -18
  99. package/dist/src/stash-resolve.d.ts +0 -2
  100. package/dist/src/stash-search.d.ts +0 -8
  101. package/dist/src/stash-show.d.ts +0 -5
  102. package/dist/src/stash-show.js +0 -46
  103. package/dist/src/stash-source.d.ts +0 -24
  104. package/dist/src/stash-source.js +0 -81
  105. package/dist/src/stash-types.d.ts +0 -227
  106. package/dist/src/stash.d.ts +0 -16
  107. package/dist/src/stash.js +0 -9
  108. package/dist/src/tool-runner.d.ts +0 -35
  109. package/dist/src/walker.d.ts +0 -19
  110. package/src/asset-spec.ts +0 -85
  111. package/src/asset-type-handler.ts +0 -77
  112. package/src/cli.ts +0 -427
  113. package/src/common.ts +0 -76
  114. package/src/config-cli.ts +0 -499
  115. package/src/config.ts +0 -305
  116. package/src/db.ts +0 -411
  117. package/src/embedder.ts +0 -128
  118. package/src/frontmatter.ts +0 -95
  119. package/src/github.ts +0 -21
  120. package/src/handlers/agent-handler.ts +0 -32
  121. package/src/handlers/command-handler.ts +0 -29
  122. package/src/handlers/index.ts +0 -25
  123. package/src/handlers/knowledge-handler.ts +0 -62
  124. package/src/handlers/markdown-helpers.ts +0 -19
  125. package/src/handlers/script-handler.ts +0 -92
  126. package/src/handlers/skill-handler.ts +0 -37
  127. package/src/handlers/tool-handler.ts +0 -71
  128. package/src/indexer.ts +0 -392
  129. package/src/init.ts +0 -114
  130. package/src/llm.ts +0 -125
  131. package/src/markdown.ts +0 -106
  132. package/src/metadata.ts +0 -333
  133. package/src/origin-resolve.ts +0 -67
  134. package/src/registry-install.ts +0 -361
  135. package/src/registry-resolve.ts +0 -341
  136. package/src/registry-search.ts +0 -335
  137. package/src/registry-types.ts +0 -72
  138. package/src/ripgrep-install.ts +0 -200
  139. package/src/ripgrep-resolve.ts +0 -72
  140. package/src/ripgrep.ts +0 -3
  141. package/src/stash-add.ts +0 -63
  142. package/src/stash-clone.ts +0 -127
  143. package/src/stash-ref.ts +0 -99
  144. package/src/stash-registry.ts +0 -259
  145. package/src/stash-resolve.ts +0 -50
  146. package/src/stash-search.ts +0 -613
  147. package/src/stash-show.ts +0 -55
  148. package/src/stash-source.ts +0 -103
  149. package/src/stash-types.ts +0 -231
  150. package/src/stash.ts +0 -39
  151. package/src/tool-runner.ts +0 -142
  152. package/src/walker.ts +0 -53
  153. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  154. /package/dist/{src/markdown.js → markdown.js} +0 -0
  155. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  156. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
@@ -1,23 +1,56 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import { fetchWithTimeout, isWithin, TYPE_DIRS } from "./common";
5
+ import { TYPE_DIRS } from "./asset-spec";
6
+ import { fetchWithRetry, isWithin } from "./common";
5
7
  import { loadConfig, saveConfig } from "./config";
8
+ import { getRegistryCacheDir as _getRegistryCacheDir } from "./paths";
6
9
  import { parseRegistryRef, resolveRegistryArtifact } from "./registry-resolve";
7
10
  const REGISTRY_STASH_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
8
11
  export async function installRegistryRef(ref, options) {
9
12
  const parsed = parseRegistryRef(ref);
13
+ if (parsed.source === "local") {
14
+ return installLocalRegistryRef(parsed, options);
15
+ }
10
16
  if (parsed.source === "git") {
11
17
  return installGitRegistryRef(parsed, options);
12
18
  }
13
19
  const resolved = await resolveRegistryArtifact(parsed);
14
20
  const installedAt = (options?.now ?? new Date()).toISOString();
15
21
  const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
16
- const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id);
22
+ const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id, resolved.resolvedVersion ?? resolved.resolvedRevision);
17
23
  const archivePath = path.join(cacheDir, "artifact.tar.gz");
18
24
  const extractedDir = path.join(cacheDir, "extracted");
25
+ // Check for cache hit: if extracted dir already exists and has a valid stash root, reuse it
26
+ if (isDirectory(extractedDir)) {
27
+ try {
28
+ const cachedStashRoot = detectStashRoot(extractedDir);
29
+ if (cachedStashRoot) {
30
+ const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
31
+ return {
32
+ id: resolved.id,
33
+ source: resolved.source,
34
+ ref: resolved.ref,
35
+ artifactUrl: resolved.artifactUrl,
36
+ resolvedVersion: resolved.resolvedVersion,
37
+ resolvedRevision: resolved.resolvedRevision,
38
+ installedAt,
39
+ cacheDir,
40
+ extractedDir,
41
+ stashRoot: cachedStashRoot,
42
+ integrity,
43
+ };
44
+ }
45
+ }
46
+ catch {
47
+ // Cache invalid, re-download
48
+ }
49
+ }
19
50
  fs.mkdirSync(cacheDir, { recursive: true });
20
51
  await downloadArchive(resolved.artifactUrl, archivePath);
52
+ verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
53
+ const integrity = await computeFileHash(archivePath);
21
54
  extractTarGzSecure(archivePath, extractedDir);
22
55
  const provisionalKitRoot = detectStashRoot(extractedDir);
23
56
  const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
@@ -33,9 +66,10 @@ export async function installRegistryRef(ref, options) {
33
66
  cacheDir,
34
67
  extractedDir,
35
68
  stashRoot,
69
+ integrity,
36
70
  };
37
71
  }
38
- async function installGitRegistryRef(parsed, options) {
72
+ async function installLocalRegistryRef(parsed, options) {
39
73
  const resolved = await resolveRegistryArtifact(parsed);
40
74
  const installedAt = (options?.now ?? new Date()).toISOString();
41
75
  const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
@@ -44,7 +78,8 @@ async function installGitRegistryRef(parsed, options) {
44
78
  fs.mkdirSync(cacheDir, { recursive: true });
45
79
  fs.rmSync(extractedDir, { recursive: true, force: true });
46
80
  fs.mkdirSync(extractedDir, { recursive: true });
47
- const includeConfig = findNearestAgentikitIncludeConfig(parsed.sourcePath, parsed.repoRoot);
81
+ const searchRoot = parsed.repoRoot ?? parsed.sourcePath;
82
+ const includeConfig = findNearestAgentikitIncludeConfig(parsed.sourcePath, searchRoot);
48
83
  if (includeConfig) {
49
84
  copyIncludedPaths(includeConfig.baseDir, includeConfig.include, extractedDir);
50
85
  }
@@ -65,6 +100,70 @@ async function installGitRegistryRef(parsed, options) {
65
100
  stashRoot,
66
101
  };
67
102
  }
103
+ async function installGitRegistryRef(parsed, options) {
104
+ const resolved = await resolveRegistryArtifact(parsed);
105
+ const installedAt = (options?.now ?? new Date()).toISOString();
106
+ const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
107
+ const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, resolved.resolvedRevision);
108
+ const cloneDir = path.join(cacheDir, "clone");
109
+ const extractedDir = path.join(cacheDir, "extracted");
110
+ // Check for cache hit
111
+ if (isDirectory(extractedDir)) {
112
+ try {
113
+ const provisionalKitRoot = detectStashRoot(extractedDir);
114
+ const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
115
+ const stashRoot = detectStashRoot(installRoot);
116
+ if (stashRoot) {
117
+ return {
118
+ id: resolved.id,
119
+ source: resolved.source,
120
+ ref: resolved.ref,
121
+ artifactUrl: resolved.artifactUrl,
122
+ resolvedVersion: resolved.resolvedVersion,
123
+ resolvedRevision: resolved.resolvedRevision,
124
+ installedAt,
125
+ cacheDir,
126
+ extractedDir,
127
+ stashRoot,
128
+ };
129
+ }
130
+ }
131
+ catch {
132
+ // Cache invalid, re-clone
133
+ }
134
+ }
135
+ fs.mkdirSync(cacheDir, { recursive: true });
136
+ const cloneArgs = ["clone", "--depth", "1"];
137
+ if (parsed.requestedRef) {
138
+ cloneArgs.push("--branch", parsed.requestedRef);
139
+ }
140
+ cloneArgs.push(parsed.url, cloneDir);
141
+ const cloneResult = spawnSync("git", cloneArgs, { encoding: "utf8", timeout: 120_000 });
142
+ if (cloneResult.status !== 0) {
143
+ const err = cloneResult.stderr?.trim() || cloneResult.error?.message || "unknown error";
144
+ throw new Error(`Failed to clone ${parsed.url}: ${err}`);
145
+ }
146
+ // Copy contents to extracted dir without .git
147
+ fs.mkdirSync(extractedDir, { recursive: true });
148
+ copyDirectoryContents(cloneDir, extractedDir);
149
+ // Clean up the clone dir
150
+ fs.rmSync(cloneDir, { recursive: true, force: true });
151
+ const provisionalKitRoot = detectStashRoot(extractedDir);
152
+ const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
153
+ const stashRoot = detectStashRoot(installRoot);
154
+ return {
155
+ id: resolved.id,
156
+ source: resolved.source,
157
+ ref: resolved.ref,
158
+ artifactUrl: resolved.artifactUrl,
159
+ resolvedVersion: resolved.resolvedVersion,
160
+ resolvedRevision: resolved.resolvedRevision,
161
+ installedAt,
162
+ cacheDir,
163
+ extractedDir,
164
+ stashRoot,
165
+ };
166
+ }
68
167
  export function upsertInstalledRegistryEntry(entry) {
69
168
  const current = loadConfig();
70
169
  const currentInstalled = current.registry?.installed ?? [];
@@ -89,15 +188,7 @@ export function removeInstalledRegistryEntry(id) {
89
188
  return nextConfig;
90
189
  }
91
190
  export function getRegistryCacheRootDir() {
92
- const xdgCache = process.env.XDG_CACHE_HOME?.trim();
93
- if (xdgCache) {
94
- return path.join(path.resolve(xdgCache), "agentikit", "registry");
95
- }
96
- const home = process.env.HOME?.trim();
97
- if (!home) {
98
- throw new Error("Unable to determine cache directory. Set XDG_CACHE_HOME or HOME.");
99
- }
100
- return path.join(path.resolve(home), ".cache", "agentikit", "registry");
191
+ return _getRegistryCacheDir();
101
192
  }
102
193
  export function detectStashRoot(extractedDir) {
103
194
  const root = path.resolve(extractedDir);
@@ -117,10 +208,12 @@ export function detectStashRoot(extractedDir) {
117
208
  return shallowest;
118
209
  return root;
119
210
  }
120
- function buildInstallCacheDir(cacheRootDir, source, id) {
211
+ function buildInstallCacheDir(cacheRootDir, source, id, version) {
121
212
  const slug = `${source}-${id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "")}`;
122
- const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
123
- return path.join(cacheRootDir, slug || source, stamp);
213
+ const versionSlug = source === "local"
214
+ ? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
215
+ : (version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
216
+ return path.join(cacheRootDir, slug || source, versionSlug);
124
217
  }
125
218
  function applyAgentikitIncludeConfig(sourceRoot, cacheDir, searchRoot = sourceRoot) {
126
219
  const includeConfig = findNearestAgentikitIncludeConfig(sourceRoot, searchRoot);
@@ -133,13 +226,14 @@ function applyAgentikitIncludeConfig(sourceRoot, cacheDir, searchRoot = sourceRo
133
226
  return selectedDir;
134
227
  }
135
228
  async function downloadArchive(url, destination) {
136
- const response = await fetchWithTimeout(url, undefined, 120_000);
229
+ const response = await fetchWithRetry(url, undefined, { timeout: 120_000 });
137
230
  if (!response.ok) {
138
231
  throw new Error(`Failed to download archive (${response.status}) from ${url}`);
139
232
  }
140
233
  // Stream response to disk instead of buffering the entire archive in memory.
141
234
  // Uses Bun.write which handles Response streaming natively.
142
- const BunRuntime = globalThis.Bun;
235
+ const BunRuntime = globalThis
236
+ .Bun;
143
237
  if (BunRuntime?.write) {
144
238
  await BunRuntime.write(destination, response);
145
239
  }
@@ -149,6 +243,37 @@ async function downloadArchive(url, destination) {
149
243
  fs.writeFileSync(destination, Buffer.from(arrayBuffer));
150
244
  }
151
245
  }
246
+ export function verifyArchiveIntegrity(archivePath, expected, source) {
247
+ if (!expected)
248
+ return;
249
+ // For GitHub and git sources, resolvedRevision is a commit SHA, not a content hash.
250
+ // Content integrity cannot be verified from a commit hash, so skip verification.
251
+ if (source === "github" || source === "git")
252
+ return;
253
+ const fileBuffer = fs.readFileSync(archivePath);
254
+ // SRI hash format: sha256-<base64> or sha512-<base64>
255
+ if (expected.startsWith("sha256-") || expected.startsWith("sha512-")) {
256
+ const dashIndex = expected.indexOf("-");
257
+ const algorithm = expected.slice(0, dashIndex);
258
+ const expectedBase64 = expected.slice(dashIndex + 1);
259
+ const actualBase64 = createHash(algorithm).update(fileBuffer).digest("base64");
260
+ if (actualBase64 !== expectedBase64) {
261
+ fs.unlinkSync(archivePath);
262
+ throw new Error(`Integrity check failed for ${archivePath}: expected ${algorithm} digest ${expectedBase64}, got ${actualBase64}`);
263
+ }
264
+ return;
265
+ }
266
+ // Hex shasum (SHA-1 from npm)
267
+ if (/^[0-9a-f]{40}$/i.test(expected)) {
268
+ const actualHex = createHash("sha1").update(fileBuffer).digest("hex");
269
+ if (actualHex.toLowerCase() !== expected.toLowerCase()) {
270
+ fs.unlinkSync(archivePath);
271
+ throw new Error(`Integrity check failed for ${archivePath}: expected sha1 ${expected}, got ${actualHex}`);
272
+ }
273
+ return;
274
+ }
275
+ // Unrecognized format — skip verification
276
+ }
152
277
  function extractTarGzSecure(archivePath, destinationDir) {
153
278
  const listResult = spawnSync("tar", ["tzf", archivePath], { encoding: "utf8" });
154
279
  if (listResult.status !== 0) {
@@ -185,7 +310,9 @@ function validateTarEntries(listOutput) {
185
310
  if (!stripped)
186
311
  continue;
187
312
  const normalizedStripped = path.posix.normalize(stripped);
188
- if (normalizedStripped === ".." || normalizedStripped.startsWith("../") || path.posix.isAbsolute(normalizedStripped)) {
313
+ if (normalizedStripped === ".." ||
314
+ normalizedStripped.startsWith("../") ||
315
+ path.posix.isAbsolute(normalizedStripped)) {
189
316
  throw new Error(`Archive contains an unsafe entry after strip-components: ${entry}`);
190
317
  }
191
318
  }
@@ -313,3 +440,8 @@ function normalizeInstalledEntry(entry) {
313
440
  cacheDir: path.resolve(entry.cacheDir),
314
441
  };
315
442
  }
443
+ async function computeFileHash(filePath) {
444
+ const data = fs.readFileSync(filePath);
445
+ const hash = createHash("sha256").update(data).digest("hex");
446
+ return `sha256:${hash}`;
447
+ }
@@ -2,8 +2,8 @@ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { fetchWithTimeout } from "./common";
6
- import { GITHUB_API_BASE, githubHeaders, asRecord, asString } from "./github";
5
+ import { fetchWithRetry } from "./common";
6
+ import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "./github";
7
7
  export function parseRegistryRef(rawRef) {
8
8
  const ref = rawRef.trim();
9
9
  if (!ref)
@@ -14,12 +14,18 @@ export function parseRegistryRef(rawRef) {
14
14
  if (ref.startsWith("github:")) {
15
15
  return parseGithubShorthand(ref.slice(7), ref);
16
16
  }
17
+ if (ref.startsWith("git+")) {
18
+ return parseGitUrl(stripGitTransport(ref), ref);
19
+ }
20
+ if (ref.startsWith("file:")) {
21
+ return tryParseLocalRef(fileUriToPath(ref), true);
22
+ }
17
23
  if (ref.startsWith("http://") || ref.startsWith("https://")) {
18
- return parseGithubUrl(ref);
24
+ return parseRemoteUrl(ref);
19
25
  }
20
- const localGitRef = tryParseLocalGitRef(ref, isPathLikeRef(ref));
21
- if (localGitRef) {
22
- return localGitRef;
26
+ const localRef = tryParseLocalRef(ref, isPathLikeRef(ref));
27
+ if (localRef) {
28
+ return localRef;
23
29
  }
24
30
  if (ref.startsWith("@") || !looksLikeGithubOwnerRepo(ref)) {
25
31
  return parseNpmRef(ref, ref);
@@ -30,6 +36,9 @@ export async function resolveRegistryArtifact(parsed) {
30
36
  if (parsed.source === "npm") {
31
37
  return resolveNpmArtifact(parsed);
32
38
  }
39
+ if (parsed.source === "local") {
40
+ return resolveLocalArtifact(parsed);
41
+ }
33
42
  if (parsed.source === "git") {
34
43
  return resolveGitArtifact(parsed);
35
44
  }
@@ -69,7 +78,7 @@ function parseGithubShorthand(input, originalRef) {
69
78
  requestedRef,
70
79
  };
71
80
  }
72
- function parseGithubUrl(rawUrl) {
81
+ function parseRemoteUrl(rawUrl) {
73
82
  let url;
74
83
  try {
75
84
  url = new URL(rawUrl);
@@ -77,9 +86,12 @@ function parseGithubUrl(rawUrl) {
77
86
  catch {
78
87
  throw new Error("Invalid registry URL.");
79
88
  }
80
- if (url.hostname !== "github.com") {
81
- throw new Error("Only GitHub URLs are currently supported for URL refs.");
89
+ if (url.hostname === "github.com") {
90
+ return parseGithubUrl(url, rawUrl);
82
91
  }
92
+ return parseGitUrl(rawUrl, rawUrl);
93
+ }
94
+ function parseGithubUrl(url, rawUrl) {
83
95
  const segments = url.pathname.split("/").filter(Boolean);
84
96
  if (segments.length < 2) {
85
97
  throw new Error("Invalid GitHub URL. Expected https://github.com/owner/repo.");
@@ -96,7 +108,21 @@ function parseGithubUrl(rawUrl) {
96
108
  requestedRef,
97
109
  };
98
110
  }
99
- function tryParseLocalGitRef(rawRef, explicitPath) {
111
+ function parseGitUrl(input, originalRef) {
112
+ const [urlPart, requestedRef] = splitRefSuffix(input.trim());
113
+ if (!urlPart)
114
+ throw new Error("Invalid git ref. A URL is required.");
115
+ // Normalize the URL for the id (strip .git suffix, fragment)
116
+ const normalized = urlPart.replace(/\.git$/i, "");
117
+ return {
118
+ source: "git",
119
+ ref: originalRef,
120
+ id: `git:${normalized}`,
121
+ url: urlPart,
122
+ requestedRef,
123
+ };
124
+ }
125
+ function tryParseLocalRef(rawRef, explicitPath) {
100
126
  if (!explicitPath) {
101
127
  return undefined;
102
128
  }
@@ -112,13 +138,10 @@ function tryParseLocalGitRef(rawRef, explicitPath) {
112
138
  throw new Error("Local add path must be a directory, but the provided path is not one.");
113
139
  }
114
140
  const repoRoot = findGitRepoRoot(resolvedPath);
115
- if (!repoRoot) {
116
- throw new Error("Local add path must be inside a git repository.");
117
- }
118
141
  return {
119
- source: "git",
142
+ source: "local",
120
143
  ref: rawRef,
121
- id: `git:${encodeURIComponent(resolvedPath)}`,
144
+ id: `local:${encodeURIComponent(resolvedPath)}`,
122
145
  repoRoot,
123
146
  sourcePath: resolvedPath,
124
147
  };
@@ -145,10 +168,16 @@ async function resolveNpmArtifact(parsed) {
145
168
  resolvedVersion = requested;
146
169
  }
147
170
  else {
171
+ // Try dist-tag first
148
172
  resolvedVersion = asString(distTags[requested]);
173
+ // If not a dist-tag, try semver range resolution
174
+ if (!resolvedVersion && isSemverRange(requested)) {
175
+ const versionKeys = Object.keys(versions).filter(isExactSemver);
176
+ resolvedVersion = maxSatisfying(versionKeys, requested);
177
+ }
149
178
  }
150
179
  if (!resolvedVersion || !(resolvedVersion in versions)) {
151
- throw new Error(`Unable to resolve npm ref \"${parsed.ref}\".`);
180
+ throw new Error(`Unable to resolve npm ref "${parsed.ref}".`);
152
181
  }
153
182
  const versionMeta = asRecord(versions[resolvedVersion]);
154
183
  const dist = asRecord(versionMeta.dist);
@@ -210,13 +239,30 @@ async function resolveGithubArtifact(parsed) {
210
239
  };
211
240
  }
212
241
  async function resolveGitArtifact(parsed) {
242
+ const ref = parsed.requestedRef ?? "HEAD";
243
+ const result = spawnSync("git", ["ls-remote", parsed.url, ref], { encoding: "utf8", timeout: 30_000 });
244
+ let resolvedRevision;
245
+ if (result.status === 0) {
246
+ const firstLine = result.stdout.trim().split(/\r?\n/)[0];
247
+ resolvedRevision = firstLine?.split(/\s/)[0] || undefined;
248
+ }
249
+ return {
250
+ id: parsed.id,
251
+ source: parsed.source,
252
+ ref: parsed.ref,
253
+ artifactUrl: parsed.url,
254
+ resolvedVersion: parsed.requestedRef,
255
+ resolvedRevision,
256
+ };
257
+ }
258
+ async function resolveLocalArtifact(parsed) {
213
259
  return {
214
260
  id: parsed.id,
215
261
  source: parsed.source,
216
262
  ref: parsed.ref,
217
263
  artifactUrl: pathToFileURL(parsed.sourcePath).toString(),
218
- resolvedRevision: readGitValue(parsed.repoRoot, "rev-parse", "HEAD"),
219
- resolvedVersion: readGitValue(parsed.repoRoot, "rev-parse", "--abbrev-ref", "HEAD"),
264
+ resolvedRevision: parsed.repoRoot ? readGitValue(parsed.repoRoot, "rev-parse", "HEAD") : undefined,
265
+ resolvedVersion: parsed.repoRoot ? readGitValue(parsed.repoRoot, "rev-parse", "--abbrev-ref", "HEAD") : undefined,
220
266
  };
221
267
  }
222
268
  function splitNpmNameAndVersion(input) {
@@ -241,16 +287,20 @@ function splitNpmNameAndVersion(input) {
241
287
  }
242
288
  function validateNpmPackageName(name) {
243
289
  if (!name)
244
- throw new Error('Invalid npm package name: name is required.');
290
+ throw new Error("Invalid npm package name: name is required.");
245
291
  if (name.length > 214)
246
292
  throw new Error(`Invalid npm package name: "${name}" exceeds 214 characters.`);
247
- if (name !== name.toLowerCase() && !name.startsWith('@')) {
293
+ if (name !== name.toLowerCase() && !name.startsWith("@")) {
248
294
  throw new Error(`Invalid npm package name: "${name}" must be lowercase.`);
249
295
  }
250
- if (name.startsWith('.') || name.startsWith('_')) {
296
+ if (name.startsWith(".") || name.startsWith("_")) {
251
297
  throw new Error(`Invalid npm package name: "${name}" cannot start with . or _.`);
252
298
  }
253
- if (/[~'!()*]/.test(name) || name.includes(' ') || encodeURIComponent(name).replace(/%40/g, '@').replace(/%2[Ff]/g, '/') !== name) {
299
+ if (/[~'!()*]/.test(name) ||
300
+ name.includes(" ") ||
301
+ encodeURIComponent(name)
302
+ .replace(/%40/g, "@")
303
+ .replace(/%2[Ff]/g, "/") !== name) {
254
304
  throw new Error(`Invalid npm package name: "${name}" contains invalid characters.`);
255
305
  }
256
306
  }
@@ -265,6 +315,30 @@ function splitRefSuffix(value) {
265
315
  return [value, undefined];
266
316
  return [value.slice(0, hash), value.slice(hash + 1) || undefined];
267
317
  }
318
+ /**
319
+ * Strip the `git+` transport prefix from a ref, returning the inner URL.
320
+ * Handles `git+https://...`, `git+ssh://...`, `git+http://...`, etc.
321
+ */
322
+ function stripGitTransport(ref) {
323
+ return ref.slice(4); // strip "git+"
324
+ }
325
+ /**
326
+ * Convert a `file:` URI to a local filesystem path.
327
+ * Supports `file:./relative`, `file:../relative`, and `file:///absolute`.
328
+ */
329
+ function fileUriToPath(ref) {
330
+ const after = ref.slice(5); // strip "file:"
331
+ // file:///absolute/path or file:///C:/path
332
+ if (after.startsWith("///")) {
333
+ return after.slice(2); // keep one leading /
334
+ }
335
+ // file://hostname/path (rare, treat hostname/path as absolute)
336
+ if (after.startsWith("//")) {
337
+ return after.slice(1);
338
+ }
339
+ // file:./relative or file:../relative or file:/absolute
340
+ return after;
341
+ }
268
342
  function findGitRepoRoot(startDir) {
269
343
  let current = path.resolve(startDir);
270
344
  while (true) {
@@ -284,16 +358,106 @@ function readGitValue(repoRoot, ...args) {
284
358
  const value = result.stdout.trim();
285
359
  return value || undefined;
286
360
  }
361
+ function parseSemver(version) {
362
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
363
+ if (!match)
364
+ return undefined;
365
+ return {
366
+ major: parseInt(match[1], 10),
367
+ minor: parseInt(match[2], 10),
368
+ patch: parseInt(match[3], 10),
369
+ prerelease: match[4],
370
+ };
371
+ }
372
+ function isExactSemver(version) {
373
+ return /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.+-]+)?$/.test(version);
374
+ }
375
+ function isSemverRange(input) {
376
+ return /^[~^>=<*]/.test(input) || /^\d+\.(\d+|\*)/.test(input);
377
+ }
378
+ function compareSemver(a, b) {
379
+ if (a.major !== b.major)
380
+ return a.major - b.major;
381
+ if (a.minor !== b.minor)
382
+ return a.minor - b.minor;
383
+ if (a.patch !== b.patch)
384
+ return a.patch - b.patch;
385
+ // Versions with prerelease are lower than release
386
+ if (a.prerelease && !b.prerelease)
387
+ return -1;
388
+ if (!a.prerelease && b.prerelease)
389
+ return 1;
390
+ return 0;
391
+ }
392
+ function semverGte(a, b) {
393
+ return compareSemver(a, b) >= 0;
394
+ }
395
+ function satisfiesRange(version, range) {
396
+ // Skip pre-release versions unless range specifically mentions one
397
+ if (version.prerelease && !range.includes("-"))
398
+ return false;
399
+ // ^1.2.3 — compatible with version: same major, >= minor.patch
400
+ const caretMatch = range.match(/^\^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
401
+ if (caretMatch) {
402
+ const rMajor = parseInt(caretMatch[1], 10);
403
+ const rMinor = parseInt(caretMatch[2], 10);
404
+ const rPatch = parseInt(caretMatch[3], 10);
405
+ if (version.major !== rMajor)
406
+ return false;
407
+ // ^0.x has special behavior: ^0.2.3 means >=0.2.3 <0.3.0
408
+ if (rMajor === 0) {
409
+ if (version.minor !== rMinor)
410
+ return false;
411
+ return version.patch >= rPatch;
412
+ }
413
+ return semverGte(version, { major: rMajor, minor: rMinor, patch: rPatch });
414
+ }
415
+ // ~1.2.3 — same major.minor, patch >= specified
416
+ const tildeMatch = range.match(/^~(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
417
+ if (tildeMatch) {
418
+ const rMajor = parseInt(tildeMatch[1], 10);
419
+ const rMinor = parseInt(tildeMatch[2], 10);
420
+ const rPatch = parseInt(tildeMatch[3], 10);
421
+ return version.major === rMajor && version.minor === rMinor && version.patch >= rPatch;
422
+ }
423
+ // >=1.2.3
424
+ const gteMatch = range.match(/^>=(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
425
+ if (gteMatch) {
426
+ const rMajor = parseInt(gteMatch[1], 10);
427
+ const rMinor = parseInt(gteMatch[2], 10);
428
+ const rPatch = parseInt(gteMatch[3], 10);
429
+ return semverGte(version, { major: rMajor, minor: rMinor, patch: rPatch });
430
+ }
431
+ // * or latest
432
+ if (range === "*" || range === "latest")
433
+ return true;
434
+ return false;
435
+ }
436
+ export function maxSatisfying(versions, range) {
437
+ const candidates = [];
438
+ for (const v of versions) {
439
+ const parsed = parseSemver(v);
440
+ if (!parsed)
441
+ continue;
442
+ if (satisfiesRange(parsed, range)) {
443
+ candidates.push({ version: v, parsed });
444
+ }
445
+ }
446
+ if (candidates.length === 0)
447
+ return undefined;
448
+ candidates.sort((a, b) => compareSemver(b.parsed, a.parsed));
449
+ return candidates[0].version;
450
+ }
287
451
  async function fetchJson(url, headers) {
288
- const response = await fetchWithTimeout(url, { headers });
452
+ const response = await fetchWithRetry(url, { headers });
289
453
  if (!response.ok) {
290
454
  throw new Error(`Request failed (${response.status}) for ${url}`);
291
455
  }
292
- return await response.json();
456
+ return (await response.json());
293
457
  }
294
458
  async function tryFetchJson(url, headers) {
295
- const response = await fetchWithTimeout(url, { headers });
459
+ const response = await fetchWithRetry(url, { headers });
296
460
  if (!response.ok)
297
461
  return null;
298
- return await response.json();
462
+ return (await response.json());
299
463
  }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { fetchWithTimeout } from "./common";
3
+ import { fetchWithRetry } from "./common";
4
+ import { getRegistryIndexCacheDir } from "./paths";
4
5
  // ── Constants ───────────────────────────────────────────────────────────────
5
6
  /** Default registry index URL. Override via config or AKM_REGISTRY_URL env var. */
6
7
  const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/itlackey/agentikit-registry/main/index.json";
@@ -44,7 +45,7 @@ async function loadIndex(url) {
44
45
  }
45
46
  // Try to fetch fresh index
46
47
  try {
47
- const response = await fetchWithTimeout(url, undefined, 10_000);
48
+ const response = await fetchWithRetry(url, undefined, { timeout: 10_000 });
48
49
  if (!response.ok) {
49
50
  throw new Error(`HTTP ${response.status}`);
50
51
  }
@@ -90,22 +91,13 @@ function writeCachedIndex(cachePath, index) {
90
91
  }
91
92
  }
92
93
  function indexCachePath(url) {
93
- const cacheRoot = resolveCacheDir();
94
+ const indexDir = getRegistryIndexCacheDir();
94
95
  // Deterministic filename from URL
95
96
  const slug = url
96
97
  .replace(/[^a-zA-Z0-9]+/g, "-")
97
98
  .replace(/^-+|-+$/g, "")
98
99
  .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");
100
+ return path.join(indexDir, `${slug}.json`);
109
101
  }
110
102
  function isCacheExpired(mtimeMs) {
111
103
  return Date.now() - mtimeMs > CACHE_TTL_MS;
@@ -157,10 +149,7 @@ function parseKitEntry(raw) {
157
149
  }
158
150
  // ── Scoring ─────────────────────────────────────────────────────────────────
159
151
  function scoreKits(kits, query, limit) {
160
- const tokens = query
161
- .toLowerCase()
162
- .split(/\s+/)
163
- .filter(Boolean);
152
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
164
153
  const scored = [];
165
154
  for (const kit of kits) {
166
155
  const score = scoreKit(kit, tokens);
@@ -234,7 +223,10 @@ function resolveRegistryUrls(override) {
234
223
  // Allow env var override (comma-separated)
235
224
  const envUrls = process.env.AKM_REGISTRY_URL?.trim();
236
225
  if (envUrls) {
237
- return envUrls.split(",").map((u) => u.trim()).filter(Boolean);
226
+ return envUrls
227
+ .split(",")
228
+ .map((u) => u.trim())
229
+ .filter(Boolean);
238
230
  }
239
231
  return [DEFAULT_REGISTRY_URL];
240
232
  }
@@ -248,9 +240,9 @@ function asString(value) {
248
240
  return typeof value === "string" && value ? value : undefined;
249
241
  }
250
242
  function asSource(value) {
251
- return value === "npm" || value === "github" || value === "git"
252
- ? value
253
- : undefined;
243
+ if (value === "npm" || value === "github" || value === "git" || value === "local")
244
+ return value;
245
+ return undefined;
254
246
  }
255
247
  function asStringArray(value) {
256
248
  if (!Array.isArray(value))