agentikit 0.0.13 → 0.0.14

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 (145) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +180 -110
  3. package/dist/cli.js +671 -0
  4. package/dist/common.js +192 -0
  5. package/dist/{src/config-cli.js → config-cli.js} +14 -6
  6. package/dist/{src/config.js → config.js} +92 -24
  7. package/dist/{src/db.js → db.js} +109 -35
  8. package/dist/{src/embedder.js → embedder.js} +57 -2
  9. package/dist/file-context.js +158 -0
  10. package/dist/{src/handlers → handlers}/command-handler.js +2 -0
  11. package/dist/{src/handlers → handlers}/index.js +0 -6
  12. package/dist/{src/indexer.js → indexer.js} +34 -10
  13. package/dist/init.js +43 -0
  14. package/dist/lockfile.js +55 -0
  15. package/dist/matchers.js +157 -0
  16. package/dist/{src/metadata.js → metadata.js} +12 -1
  17. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  18. package/dist/paths.js +82 -0
  19. package/dist/{src/registry-install.js → registry-install.js} +145 -17
  20. package/dist/{src/registry-resolve.js → registry-resolve.js} +178 -18
  21. package/dist/{src/registry-search.js → registry-search.js} +8 -16
  22. package/dist/renderers.js +276 -0
  23. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +5 -5
  24. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  25. package/dist/self-update.js +220 -0
  26. package/dist/{src/stash-add.js → stash-add.js} +11 -2
  27. package/dist/stash-clone.js +115 -0
  28. package/dist/{src/stash-registry.js → stash-registry.js} +15 -41
  29. package/dist/{src/stash-search.js → stash-search.js} +67 -55
  30. package/dist/{src/stash-show.js → stash-show.js} +30 -3
  31. package/dist/{src/stash-source.js → stash-source.js} +56 -9
  32. package/dist/submit.js +552 -0
  33. package/dist/{src/walker.js → walker.js} +38 -0
  34. package/package.json +7 -16
  35. package/dist/index.d.ts +0 -28
  36. package/dist/index.js +0 -15
  37. package/dist/src/asset-spec.d.ts +0 -16
  38. package/dist/src/asset-type-handler.d.ts +0 -27
  39. package/dist/src/cli.d.ts +0 -2
  40. package/dist/src/cli.js +0 -399
  41. package/dist/src/common.d.ts +0 -13
  42. package/dist/src/common.js +0 -60
  43. package/dist/src/config-cli.d.ts +0 -9
  44. package/dist/src/config.d.ts +0 -50
  45. package/dist/src/db.d.ts +0 -46
  46. package/dist/src/embedder.d.ts +0 -10
  47. package/dist/src/frontmatter.d.ts +0 -30
  48. package/dist/src/github.d.ts +0 -4
  49. package/dist/src/handlers/agent-handler.d.ts +0 -2
  50. package/dist/src/handlers/command-handler.d.ts +0 -2
  51. package/dist/src/handlers/index.d.ts +0 -6
  52. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  53. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  54. package/dist/src/handlers/script-handler.d.ts +0 -2
  55. package/dist/src/handlers/skill-handler.d.ts +0 -2
  56. package/dist/src/handlers/tool-handler.d.ts +0 -2
  57. package/dist/src/indexer.d.ts +0 -22
  58. package/dist/src/init.d.ts +0 -19
  59. package/dist/src/init.js +0 -99
  60. package/dist/src/llm.d.ts +0 -15
  61. package/dist/src/markdown.d.ts +0 -18
  62. package/dist/src/metadata.d.ts +0 -41
  63. package/dist/src/origin-resolve.d.ts +0 -19
  64. package/dist/src/registry-install.d.ts +0 -11
  65. package/dist/src/registry-resolve.d.ts +0 -3
  66. package/dist/src/registry-search.d.ts +0 -27
  67. package/dist/src/registry-types.d.ts +0 -62
  68. package/dist/src/ripgrep-install.d.ts +0 -12
  69. package/dist/src/ripgrep-resolve.d.ts +0 -13
  70. package/dist/src/ripgrep.d.ts +0 -3
  71. package/dist/src/stash-add.d.ts +0 -4
  72. package/dist/src/stash-clone.d.ts +0 -22
  73. package/dist/src/stash-clone.js +0 -83
  74. package/dist/src/stash-ref.d.ts +0 -31
  75. package/dist/src/stash-registry.d.ts +0 -18
  76. package/dist/src/stash-resolve.d.ts +0 -2
  77. package/dist/src/stash-search.d.ts +0 -8
  78. package/dist/src/stash-show.d.ts +0 -5
  79. package/dist/src/stash-source.d.ts +0 -24
  80. package/dist/src/stash-types.d.ts +0 -227
  81. package/dist/src/stash.d.ts +0 -16
  82. package/dist/src/stash.js +0 -9
  83. package/dist/src/tool-runner.d.ts +0 -35
  84. package/dist/src/walker.d.ts +0 -19
  85. package/src/asset-spec.ts +0 -85
  86. package/src/asset-type-handler.ts +0 -77
  87. package/src/cli.ts +0 -427
  88. package/src/common.ts +0 -76
  89. package/src/config-cli.ts +0 -499
  90. package/src/config.ts +0 -305
  91. package/src/db.ts +0 -411
  92. package/src/embedder.ts +0 -128
  93. package/src/frontmatter.ts +0 -95
  94. package/src/github.ts +0 -21
  95. package/src/handlers/agent-handler.ts +0 -32
  96. package/src/handlers/command-handler.ts +0 -29
  97. package/src/handlers/index.ts +0 -25
  98. package/src/handlers/knowledge-handler.ts +0 -62
  99. package/src/handlers/markdown-helpers.ts +0 -19
  100. package/src/handlers/script-handler.ts +0 -92
  101. package/src/handlers/skill-handler.ts +0 -37
  102. package/src/handlers/tool-handler.ts +0 -71
  103. package/src/indexer.ts +0 -392
  104. package/src/init.ts +0 -114
  105. package/src/llm.ts +0 -125
  106. package/src/markdown.ts +0 -106
  107. package/src/metadata.ts +0 -333
  108. package/src/origin-resolve.ts +0 -67
  109. package/src/registry-install.ts +0 -361
  110. package/src/registry-resolve.ts +0 -341
  111. package/src/registry-search.ts +0 -335
  112. package/src/registry-types.ts +0 -72
  113. package/src/ripgrep-install.ts +0 -200
  114. package/src/ripgrep-resolve.ts +0 -72
  115. package/src/ripgrep.ts +0 -3
  116. package/src/stash-add.ts +0 -63
  117. package/src/stash-clone.ts +0 -127
  118. package/src/stash-ref.ts +0 -99
  119. package/src/stash-registry.ts +0 -259
  120. package/src/stash-resolve.ts +0 -50
  121. package/src/stash-search.ts +0 -613
  122. package/src/stash-show.ts +0 -55
  123. package/src/stash-source.ts +0 -103
  124. package/src/stash-types.ts +0 -231
  125. package/src/stash.ts +0 -39
  126. package/src/tool-runner.ts +0 -142
  127. package/src/walker.ts +0 -53
  128. /package/dist/{src/asset-spec.js → asset-spec.js} +0 -0
  129. /package/dist/{src/asset-type-handler.js → asset-type-handler.js} +0 -0
  130. /package/dist/{src/frontmatter.js → frontmatter.js} +0 -0
  131. /package/dist/{src/github.js → github.js} +0 -0
  132. /package/dist/{src/handlers → handlers}/agent-handler.js +0 -0
  133. /package/dist/{src/handlers → handlers}/knowledge-handler.js +0 -0
  134. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  135. /package/dist/{src/handlers → handlers}/script-handler.js +0 -0
  136. /package/dist/{src/handlers → handlers}/skill-handler.js +0 -0
  137. /package/dist/{src/handlers → handlers}/tool-handler.js +0 -0
  138. /package/dist/{src/llm.js → llm.js} +0 -0
  139. /package/dist/{src/markdown.js → markdown.js} +0 -0
  140. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  141. /package/dist/{src/ripgrep.js → ripgrep.js} +0 -0
  142. /package/dist/{src/stash-ref.js → stash-ref.js} +0 -0
  143. /package/dist/{src/stash-resolve.js → stash-resolve.js} +0 -0
  144. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
  145. /package/dist/{src/tool-runner.js → tool-runner.js} +0 -0
package/dist/paths.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Centralized path resolution for all agentikit directories.
3
+ *
4
+ * Provides platform-aware paths for config, cache, and stash directories,
5
+ * following XDG Base Directory conventions on Unix and standard locations
6
+ * on Windows.
7
+ */
8
+ import path from "node:path";
9
+ const IS_WINDOWS = process.platform === "win32";
10
+ // ── Config directory ─────────────────────────────────────────────────────────
11
+ export function getConfigDir(env = process.env, platform = process.platform) {
12
+ if (platform === "win32") {
13
+ const appData = env.APPDATA?.trim();
14
+ if (appData)
15
+ return path.join(appData, "agentikit");
16
+ const userProfile = env.USERPROFILE?.trim();
17
+ if (!userProfile) {
18
+ throw new Error("Unable to determine config directory. Set APPDATA or USERPROFILE.");
19
+ }
20
+ return path.join(userProfile, "AppData", "Roaming", "agentikit");
21
+ }
22
+ const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
23
+ if (xdgConfigHome)
24
+ return path.join(xdgConfigHome, "agentikit");
25
+ const home = env.HOME?.trim();
26
+ if (!home) {
27
+ throw new Error("Unable to determine config directory. Set XDG_CONFIG_HOME or HOME.");
28
+ }
29
+ return path.join(home, ".config", "agentikit");
30
+ }
31
+ export function getConfigPath() {
32
+ return path.join(getConfigDir(), "config.json");
33
+ }
34
+ // ── Cache directory ──────────────────────────────────────────────────────────
35
+ export function getCacheDir() {
36
+ if (IS_WINDOWS) {
37
+ const localAppData = process.env.LOCALAPPDATA?.trim();
38
+ if (localAppData)
39
+ return path.join(localAppData, "agentikit");
40
+ const userProfile = process.env.USERPROFILE?.trim();
41
+ if (userProfile)
42
+ return path.join(userProfile, "AppData", "Local", "agentikit");
43
+ const appData = process.env.APPDATA?.trim();
44
+ if (!appData) {
45
+ throw new Error("Unable to determine cache directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.");
46
+ }
47
+ return path.join(appData, "..", "Local", "agentikit");
48
+ }
49
+ const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
50
+ if (xdgCacheHome)
51
+ return path.join(xdgCacheHome, "agentikit");
52
+ const home = process.env.HOME?.trim();
53
+ if (!home)
54
+ return path.join("/tmp", "agentikit-cache");
55
+ return path.join(home, ".cache", "agentikit");
56
+ }
57
+ export function getDbPath() {
58
+ return path.join(getCacheDir(), "index.db");
59
+ }
60
+ export function getRegistryCacheDir() {
61
+ return path.join(getCacheDir(), "registry");
62
+ }
63
+ export function getRegistryIndexCacheDir() {
64
+ return path.join(getCacheDir(), "registry-index");
65
+ }
66
+ export function getBinDir() {
67
+ return path.join(getCacheDir(), "bin");
68
+ }
69
+ // ── Default stash directory ──────────────────────────────────────────────────
70
+ export function getDefaultStashDir() {
71
+ if (IS_WINDOWS) {
72
+ const userProfile = process.env.USERPROFILE?.trim();
73
+ if (userProfile)
74
+ return path.join(userProfile, "Documents", "agentikit");
75
+ return path.join("C:\\", "agentikit");
76
+ }
77
+ const home = process.env.HOME?.trim();
78
+ if (!home) {
79
+ throw new Error("Unable to determine default stash directory. Set HOME.");
80
+ }
81
+ return path.join(home, "agentikit");
82
+ }
@@ -1,23 +1,55 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { spawnSync } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import { fetchWithTimeout, isWithin, TYPE_DIRS } from "./common";
5
+ import { fetchWithRetry, isWithin, TYPE_DIRS } from "./common";
5
6
  import { loadConfig, saveConfig } from "./config";
6
7
  import { parseRegistryRef, resolveRegistryArtifact } from "./registry-resolve";
8
+ import { getRegistryCacheDir as _getRegistryCacheDir } from "./paths";
7
9
  const REGISTRY_STASH_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
8
10
  export async function installRegistryRef(ref, options) {
9
11
  const parsed = parseRegistryRef(ref);
12
+ if (parsed.source === "local") {
13
+ return installLocalRegistryRef(parsed, options);
14
+ }
10
15
  if (parsed.source === "git") {
11
16
  return installGitRegistryRef(parsed, options);
12
17
  }
13
18
  const resolved = await resolveRegistryArtifact(parsed);
14
19
  const installedAt = (options?.now ?? new Date()).toISOString();
15
20
  const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
16
- const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id);
21
+ const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id, resolved.resolvedVersion ?? resolved.resolvedRevision);
17
22
  const archivePath = path.join(cacheDir, "artifact.tar.gz");
18
23
  const extractedDir = path.join(cacheDir, "extracted");
24
+ // Check for cache hit: if extracted dir already exists and has a valid stash root, reuse it
25
+ if (isDirectory(extractedDir)) {
26
+ try {
27
+ const cachedStashRoot = detectStashRoot(extractedDir);
28
+ if (cachedStashRoot) {
29
+ const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
30
+ return {
31
+ id: resolved.id,
32
+ source: resolved.source,
33
+ ref: resolved.ref,
34
+ artifactUrl: resolved.artifactUrl,
35
+ resolvedVersion: resolved.resolvedVersion,
36
+ resolvedRevision: resolved.resolvedRevision,
37
+ installedAt,
38
+ cacheDir,
39
+ extractedDir,
40
+ stashRoot: cachedStashRoot,
41
+ integrity,
42
+ };
43
+ }
44
+ }
45
+ catch {
46
+ // Cache invalid, re-download
47
+ }
48
+ }
19
49
  fs.mkdirSync(cacheDir, { recursive: true });
20
50
  await downloadArchive(resolved.artifactUrl, archivePath);
51
+ verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
52
+ const integrity = await computeFileHash(archivePath);
21
53
  extractTarGzSecure(archivePath, extractedDir);
22
54
  const provisionalKitRoot = detectStashRoot(extractedDir);
23
55
  const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
@@ -33,9 +65,10 @@ export async function installRegistryRef(ref, options) {
33
65
  cacheDir,
34
66
  extractedDir,
35
67
  stashRoot,
68
+ integrity,
36
69
  };
37
70
  }
38
- async function installGitRegistryRef(parsed, options) {
71
+ async function installLocalRegistryRef(parsed, options) {
39
72
  const resolved = await resolveRegistryArtifact(parsed);
40
73
  const installedAt = (options?.now ?? new Date()).toISOString();
41
74
  const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
@@ -44,7 +77,8 @@ async function installGitRegistryRef(parsed, options) {
44
77
  fs.mkdirSync(cacheDir, { recursive: true });
45
78
  fs.rmSync(extractedDir, { recursive: true, force: true });
46
79
  fs.mkdirSync(extractedDir, { recursive: true });
47
- const includeConfig = findNearestAgentikitIncludeConfig(parsed.sourcePath, parsed.repoRoot);
80
+ const searchRoot = parsed.repoRoot ?? parsed.sourcePath;
81
+ const includeConfig = findNearestAgentikitIncludeConfig(parsed.sourcePath, searchRoot);
48
82
  if (includeConfig) {
49
83
  copyIncludedPaths(includeConfig.baseDir, includeConfig.include, extractedDir);
50
84
  }
@@ -65,6 +99,70 @@ async function installGitRegistryRef(parsed, options) {
65
99
  stashRoot,
66
100
  };
67
101
  }
102
+ async function installGitRegistryRef(parsed, options) {
103
+ const resolved = await resolveRegistryArtifact(parsed);
104
+ const installedAt = (options?.now ?? new Date()).toISOString();
105
+ const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
106
+ const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, resolved.resolvedRevision);
107
+ const cloneDir = path.join(cacheDir, "clone");
108
+ const extractedDir = path.join(cacheDir, "extracted");
109
+ // Check for cache hit
110
+ if (isDirectory(extractedDir)) {
111
+ try {
112
+ const provisionalKitRoot = detectStashRoot(extractedDir);
113
+ const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
114
+ const stashRoot = detectStashRoot(installRoot);
115
+ if (stashRoot) {
116
+ return {
117
+ id: resolved.id,
118
+ source: resolved.source,
119
+ ref: resolved.ref,
120
+ artifactUrl: resolved.artifactUrl,
121
+ resolvedVersion: resolved.resolvedVersion,
122
+ resolvedRevision: resolved.resolvedRevision,
123
+ installedAt,
124
+ cacheDir,
125
+ extractedDir,
126
+ stashRoot,
127
+ };
128
+ }
129
+ }
130
+ catch {
131
+ // Cache invalid, re-clone
132
+ }
133
+ }
134
+ fs.mkdirSync(cacheDir, { recursive: true });
135
+ const cloneArgs = ["clone", "--depth", "1"];
136
+ if (parsed.requestedRef) {
137
+ cloneArgs.push("--branch", parsed.requestedRef);
138
+ }
139
+ cloneArgs.push(parsed.url, cloneDir);
140
+ const cloneResult = spawnSync("git", cloneArgs, { encoding: "utf8", timeout: 120_000 });
141
+ if (cloneResult.status !== 0) {
142
+ const err = cloneResult.stderr?.trim() || cloneResult.error?.message || "unknown error";
143
+ throw new Error(`Failed to clone ${parsed.url}: ${err}`);
144
+ }
145
+ // Copy contents to extracted dir without .git
146
+ fs.mkdirSync(extractedDir, { recursive: true });
147
+ copyDirectoryContents(cloneDir, extractedDir);
148
+ // Clean up the clone dir
149
+ fs.rmSync(cloneDir, { recursive: true, force: true });
150
+ const provisionalKitRoot = detectStashRoot(extractedDir);
151
+ const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
152
+ const stashRoot = detectStashRoot(installRoot);
153
+ return {
154
+ id: resolved.id,
155
+ source: resolved.source,
156
+ ref: resolved.ref,
157
+ artifactUrl: resolved.artifactUrl,
158
+ resolvedVersion: resolved.resolvedVersion,
159
+ resolvedRevision: resolved.resolvedRevision,
160
+ installedAt,
161
+ cacheDir,
162
+ extractedDir,
163
+ stashRoot,
164
+ };
165
+ }
68
166
  export function upsertInstalledRegistryEntry(entry) {
69
167
  const current = loadConfig();
70
168
  const currentInstalled = current.registry?.installed ?? [];
@@ -89,15 +187,7 @@ export function removeInstalledRegistryEntry(id) {
89
187
  return nextConfig;
90
188
  }
91
189
  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");
190
+ return _getRegistryCacheDir();
101
191
  }
102
192
  export function detectStashRoot(extractedDir) {
103
193
  const root = path.resolve(extractedDir);
@@ -117,10 +207,12 @@ export function detectStashRoot(extractedDir) {
117
207
  return shallowest;
118
208
  return root;
119
209
  }
120
- function buildInstallCacheDir(cacheRootDir, source, id) {
210
+ function buildInstallCacheDir(cacheRootDir, source, id, version) {
121
211
  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);
212
+ const versionSlug = source === "local"
213
+ ? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
214
+ : (version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
215
+ return path.join(cacheRootDir, slug || source, versionSlug);
124
216
  }
125
217
  function applyAgentikitIncludeConfig(sourceRoot, cacheDir, searchRoot = sourceRoot) {
126
218
  const includeConfig = findNearestAgentikitIncludeConfig(sourceRoot, searchRoot);
@@ -133,7 +225,7 @@ function applyAgentikitIncludeConfig(sourceRoot, cacheDir, searchRoot = sourceRo
133
225
  return selectedDir;
134
226
  }
135
227
  async function downloadArchive(url, destination) {
136
- const response = await fetchWithTimeout(url, undefined, 120_000);
228
+ const response = await fetchWithRetry(url, undefined, { timeout: 120_000 });
137
229
  if (!response.ok) {
138
230
  throw new Error(`Failed to download archive (${response.status}) from ${url}`);
139
231
  }
@@ -149,6 +241,37 @@ async function downloadArchive(url, destination) {
149
241
  fs.writeFileSync(destination, Buffer.from(arrayBuffer));
150
242
  }
151
243
  }
244
+ export function verifyArchiveIntegrity(archivePath, expected, source) {
245
+ if (!expected)
246
+ return;
247
+ // For GitHub and git sources, resolvedRevision is a commit SHA, not a content hash.
248
+ // Content integrity cannot be verified from a commit hash, so skip verification.
249
+ if (source === "github" || source === "git")
250
+ return;
251
+ const fileBuffer = fs.readFileSync(archivePath);
252
+ // SRI hash format: sha256-<base64> or sha512-<base64>
253
+ if (expected.startsWith("sha256-") || expected.startsWith("sha512-")) {
254
+ const dashIndex = expected.indexOf("-");
255
+ const algorithm = expected.slice(0, dashIndex);
256
+ const expectedBase64 = expected.slice(dashIndex + 1);
257
+ const actualBase64 = createHash(algorithm).update(fileBuffer).digest("base64");
258
+ if (actualBase64 !== expectedBase64) {
259
+ fs.unlinkSync(archivePath);
260
+ throw new Error(`Integrity check failed for ${archivePath}: expected ${algorithm} digest ${expectedBase64}, got ${actualBase64}`);
261
+ }
262
+ return;
263
+ }
264
+ // Hex shasum (SHA-1 from npm)
265
+ if (/^[0-9a-f]{40}$/i.test(expected)) {
266
+ const actualHex = createHash("sha1").update(fileBuffer).digest("hex");
267
+ if (actualHex.toLowerCase() !== expected.toLowerCase()) {
268
+ fs.unlinkSync(archivePath);
269
+ throw new Error(`Integrity check failed for ${archivePath}: expected sha1 ${expected}, got ${actualHex}`);
270
+ }
271
+ return;
272
+ }
273
+ // Unrecognized format — skip verification
274
+ }
152
275
  function extractTarGzSecure(archivePath, destinationDir) {
153
276
  const listResult = spawnSync("tar", ["tzf", archivePath], { encoding: "utf8" });
154
277
  if (listResult.status !== 0) {
@@ -313,3 +436,8 @@ function normalizeInstalledEntry(entry) {
313
436
  cacheDir: path.resolve(entry.cacheDir),
314
437
  };
315
438
  }
439
+ async function computeFileHash(filePath) {
440
+ const data = fs.readFileSync(filePath);
441
+ const hash = createHash("sha256").update(data).digest("hex");
442
+ return `sha256:${hash}`;
443
+ }
@@ -2,7 +2,7 @@ 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";
5
+ import { fetchWithRetry } from "./common";
6
6
  import { GITHUB_API_BASE, githubHeaders, asRecord, asString } from "./github";
7
7
  export function parseRegistryRef(rawRef) {
8
8
  const ref = rawRef.trim();
@@ -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,7 +168,13 @@ 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
180
  throw new Error(`Unable to resolve npm ref \"${parsed.ref}\".`);
@@ -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) {
@@ -265,6 +311,30 @@ function splitRefSuffix(value) {
265
311
  return [value, undefined];
266
312
  return [value.slice(0, hash), value.slice(hash + 1) || undefined];
267
313
  }
314
+ /**
315
+ * Strip the `git+` transport prefix from a ref, returning the inner URL.
316
+ * Handles `git+https://...`, `git+ssh://...`, `git+http://...`, etc.
317
+ */
318
+ function stripGitTransport(ref) {
319
+ return ref.slice(4); // strip "git+"
320
+ }
321
+ /**
322
+ * Convert a `file:` URI to a local filesystem path.
323
+ * Supports `file:./relative`, `file:../relative`, and `file:///absolute`.
324
+ */
325
+ function fileUriToPath(ref) {
326
+ const after = ref.slice(5); // strip "file:"
327
+ // file:///absolute/path or file:///C:/path
328
+ if (after.startsWith("///")) {
329
+ return after.slice(2); // keep one leading /
330
+ }
331
+ // file://hostname/path (rare, treat hostname/path as absolute)
332
+ if (after.startsWith("//")) {
333
+ return after.slice(1);
334
+ }
335
+ // file:./relative or file:../relative or file:/absolute
336
+ return after;
337
+ }
268
338
  function findGitRepoRoot(startDir) {
269
339
  let current = path.resolve(startDir);
270
340
  while (true) {
@@ -284,15 +354,105 @@ function readGitValue(repoRoot, ...args) {
284
354
  const value = result.stdout.trim();
285
355
  return value || undefined;
286
356
  }
357
+ function parseSemver(version) {
358
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
359
+ if (!match)
360
+ return undefined;
361
+ return {
362
+ major: parseInt(match[1], 10),
363
+ minor: parseInt(match[2], 10),
364
+ patch: parseInt(match[3], 10),
365
+ prerelease: match[4],
366
+ };
367
+ }
368
+ function isExactSemver(version) {
369
+ return /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.+-]+)?$/.test(version);
370
+ }
371
+ function isSemverRange(input) {
372
+ return /^[~^>=<*]/.test(input) || /^\d+\.(\d+|\*)/.test(input);
373
+ }
374
+ function compareSemver(a, b) {
375
+ if (a.major !== b.major)
376
+ return a.major - b.major;
377
+ if (a.minor !== b.minor)
378
+ return a.minor - b.minor;
379
+ if (a.patch !== b.patch)
380
+ return a.patch - b.patch;
381
+ // Versions with prerelease are lower than release
382
+ if (a.prerelease && !b.prerelease)
383
+ return -1;
384
+ if (!a.prerelease && b.prerelease)
385
+ return 1;
386
+ return 0;
387
+ }
388
+ function semverGte(a, b) {
389
+ return compareSemver(a, b) >= 0;
390
+ }
391
+ function satisfiesRange(version, range) {
392
+ // Skip pre-release versions unless range specifically mentions one
393
+ if (version.prerelease && !range.includes("-"))
394
+ return false;
395
+ // ^1.2.3 — compatible with version: same major, >= minor.patch
396
+ const caretMatch = range.match(/^\^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
397
+ if (caretMatch) {
398
+ const rMajor = parseInt(caretMatch[1], 10);
399
+ const rMinor = parseInt(caretMatch[2], 10);
400
+ const rPatch = parseInt(caretMatch[3], 10);
401
+ if (version.major !== rMajor)
402
+ return false;
403
+ // ^0.x has special behavior: ^0.2.3 means >=0.2.3 <0.3.0
404
+ if (rMajor === 0) {
405
+ if (version.minor !== rMinor)
406
+ return false;
407
+ return version.patch >= rPatch;
408
+ }
409
+ return semverGte(version, { major: rMajor, minor: rMinor, patch: rPatch });
410
+ }
411
+ // ~1.2.3 — same major.minor, patch >= specified
412
+ const tildeMatch = range.match(/^~(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
413
+ if (tildeMatch) {
414
+ const rMajor = parseInt(tildeMatch[1], 10);
415
+ const rMinor = parseInt(tildeMatch[2], 10);
416
+ const rPatch = parseInt(tildeMatch[3], 10);
417
+ return version.major === rMajor && version.minor === rMinor && version.patch >= rPatch;
418
+ }
419
+ // >=1.2.3
420
+ const gteMatch = range.match(/^>=(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
421
+ if (gteMatch) {
422
+ const rMajor = parseInt(gteMatch[1], 10);
423
+ const rMinor = parseInt(gteMatch[2], 10);
424
+ const rPatch = parseInt(gteMatch[3], 10);
425
+ return semverGte(version, { major: rMajor, minor: rMinor, patch: rPatch });
426
+ }
427
+ // * or latest
428
+ if (range === "*" || range === "latest")
429
+ return true;
430
+ return false;
431
+ }
432
+ export function maxSatisfying(versions, range) {
433
+ const candidates = [];
434
+ for (const v of versions) {
435
+ const parsed = parseSemver(v);
436
+ if (!parsed)
437
+ continue;
438
+ if (satisfiesRange(parsed, range)) {
439
+ candidates.push({ version: v, parsed });
440
+ }
441
+ }
442
+ if (candidates.length === 0)
443
+ return undefined;
444
+ candidates.sort((a, b) => compareSemver(b.parsed, a.parsed));
445
+ return candidates[0].version;
446
+ }
287
447
  async function fetchJson(url, headers) {
288
- const response = await fetchWithTimeout(url, { headers });
448
+ const response = await fetchWithRetry(url, { headers });
289
449
  if (!response.ok) {
290
450
  throw new Error(`Request failed (${response.status}) for ${url}`);
291
451
  }
292
452
  return await response.json();
293
453
  }
294
454
  async function tryFetchJson(url, headers) {
295
- const response = await fetchWithTimeout(url, { headers });
455
+ const response = await fetchWithRetry(url, { headers });
296
456
  if (!response.ok)
297
457
  return null;
298
458
  return await response.json();
@@ -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;
@@ -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))