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.
- package/LICENSE +385 -0
- package/README.md +187 -110
- package/dist/{src/asset-spec.js → asset-spec.js} +11 -2
- package/dist/{src/asset-type-handler.js → asset-type-handler.js} +4 -3
- package/dist/cli.js +709 -0
- package/dist/common.js +192 -0
- package/dist/{src/config-cli.js → config-cli.js} +36 -30
- package/dist/{src/config.js → config.js} +95 -25
- package/dist/{src/db.js → db.js} +123 -51
- package/dist/{src/embedder.js → embedder.js} +57 -2
- package/dist/errors.js +28 -0
- package/dist/file-context.js +188 -0
- package/dist/{src/frontmatter.js → frontmatter.js} +1 -1
- package/dist/{src/github.js → github.js} +1 -3
- package/dist/handlers/agent-handler.js +19 -0
- package/dist/handlers/command-handler.js +20 -0
- package/dist/handlers/handler-bridge.js +51 -0
- package/dist/handlers/index.js +19 -0
- package/dist/handlers/knowledge-handler.js +32 -0
- package/dist/handlers/script-handler.js +42 -0
- package/dist/{src/handlers → handlers}/skill-handler.js +5 -6
- package/dist/{src/handlers → handlers}/tool-handler.js +8 -24
- package/dist/{src/indexer.js → indexer.js} +50 -26
- package/dist/init.js +43 -0
- package/dist/{src/llm.js → llm.js} +6 -11
- package/dist/lockfile.js +60 -0
- package/dist/matchers.js +163 -0
- package/dist/{src/metadata.js → metadata.js} +36 -16
- package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
- package/dist/paths.js +83 -0
- package/dist/{src/registry-install.js → registry-install.js} +151 -19
- package/dist/{src/registry-resolve.js → registry-resolve.js} +190 -26
- package/dist/{src/registry-search.js → registry-search.js} +13 -21
- package/dist/renderers.js +286 -0
- package/dist/{src/ripgrep-install.js → ripgrep-install.js} +8 -27
- package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
- package/dist/ripgrep.js +2 -0
- package/dist/self-update.js +226 -0
- package/dist/{src/stash-add.js → stash-add.js} +14 -4
- package/dist/stash-clone.js +115 -0
- package/dist/{src/stash-ref.js → stash-ref.js} +10 -9
- package/dist/{src/stash-registry.js → stash-registry.js} +21 -46
- package/dist/{src/stash-resolve.js → stash-resolve.js} +10 -9
- package/dist/{src/stash-search.js → stash-search.js} +89 -74
- package/dist/stash-show.js +74 -0
- package/dist/stash-source.js +127 -0
- package/dist/submit.js +557 -0
- package/dist/{src/tool-runner.js → tool-runner.js} +1 -5
- package/dist/{src/walker.js → walker.js} +38 -0
- package/dist/warn.js +20 -0
- package/package.json +13 -18
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -15
- package/dist/src/asset-spec.d.ts +0 -16
- package/dist/src/asset-type-handler.d.ts +0 -27
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.js +0 -399
- package/dist/src/common.d.ts +0 -13
- package/dist/src/common.js +0 -60
- package/dist/src/config-cli.d.ts +0 -9
- package/dist/src/config.d.ts +0 -50
- package/dist/src/db.d.ts +0 -46
- package/dist/src/embedder.d.ts +0 -10
- package/dist/src/frontmatter.d.ts +0 -30
- package/dist/src/github.d.ts +0 -4
- package/dist/src/handlers/agent-handler.d.ts +0 -2
- package/dist/src/handlers/agent-handler.js +0 -26
- package/dist/src/handlers/command-handler.d.ts +0 -2
- package/dist/src/handlers/command-handler.js +0 -23
- package/dist/src/handlers/index.d.ts +0 -6
- package/dist/src/handlers/index.js +0 -23
- package/dist/src/handlers/knowledge-handler.d.ts +0 -2
- package/dist/src/handlers/knowledge-handler.js +0 -56
- package/dist/src/handlers/markdown-helpers.d.ts +0 -7
- package/dist/src/handlers/script-handler.d.ts +0 -2
- package/dist/src/handlers/script-handler.js +0 -78
- package/dist/src/handlers/skill-handler.d.ts +0 -2
- package/dist/src/handlers/tool-handler.d.ts +0 -2
- package/dist/src/indexer.d.ts +0 -22
- package/dist/src/init.d.ts +0 -19
- package/dist/src/init.js +0 -99
- package/dist/src/llm.d.ts +0 -15
- package/dist/src/markdown.d.ts +0 -18
- package/dist/src/metadata.d.ts +0 -41
- package/dist/src/origin-resolve.d.ts +0 -19
- package/dist/src/registry-install.d.ts +0 -11
- package/dist/src/registry-resolve.d.ts +0 -3
- package/dist/src/registry-search.d.ts +0 -27
- package/dist/src/registry-types.d.ts +0 -62
- package/dist/src/ripgrep-install.d.ts +0 -12
- package/dist/src/ripgrep-resolve.d.ts +0 -13
- package/dist/src/ripgrep.d.ts +0 -3
- package/dist/src/ripgrep.js +0 -2
- package/dist/src/stash-add.d.ts +0 -4
- package/dist/src/stash-clone.d.ts +0 -22
- package/dist/src/stash-clone.js +0 -83
- package/dist/src/stash-ref.d.ts +0 -31
- package/dist/src/stash-registry.d.ts +0 -18
- package/dist/src/stash-resolve.d.ts +0 -2
- package/dist/src/stash-search.d.ts +0 -8
- package/dist/src/stash-show.d.ts +0 -5
- package/dist/src/stash-show.js +0 -46
- package/dist/src/stash-source.d.ts +0 -24
- package/dist/src/stash-source.js +0 -81
- package/dist/src/stash-types.d.ts +0 -227
- package/dist/src/stash.d.ts +0 -16
- package/dist/src/stash.js +0 -9
- package/dist/src/tool-runner.d.ts +0 -35
- package/dist/src/walker.d.ts +0 -19
- package/src/asset-spec.ts +0 -85
- package/src/asset-type-handler.ts +0 -77
- package/src/cli.ts +0 -427
- package/src/common.ts +0 -76
- package/src/config-cli.ts +0 -499
- package/src/config.ts +0 -305
- package/src/db.ts +0 -411
- package/src/embedder.ts +0 -128
- package/src/frontmatter.ts +0 -95
- package/src/github.ts +0 -21
- package/src/handlers/agent-handler.ts +0 -32
- package/src/handlers/command-handler.ts +0 -29
- package/src/handlers/index.ts +0 -25
- package/src/handlers/knowledge-handler.ts +0 -62
- package/src/handlers/markdown-helpers.ts +0 -19
- package/src/handlers/script-handler.ts +0 -92
- package/src/handlers/skill-handler.ts +0 -37
- package/src/handlers/tool-handler.ts +0 -71
- package/src/indexer.ts +0 -392
- package/src/init.ts +0 -114
- package/src/llm.ts +0 -125
- package/src/markdown.ts +0 -106
- package/src/metadata.ts +0 -333
- package/src/origin-resolve.ts +0 -67
- package/src/registry-install.ts +0 -361
- package/src/registry-resolve.ts +0 -341
- package/src/registry-search.ts +0 -335
- package/src/registry-types.ts +0 -72
- package/src/ripgrep-install.ts +0 -200
- package/src/ripgrep-resolve.ts +0 -72
- package/src/ripgrep.ts +0 -3
- package/src/stash-add.ts +0 -63
- package/src/stash-clone.ts +0 -127
- package/src/stash-ref.ts +0 -99
- package/src/stash-registry.ts +0 -259
- package/src/stash-resolve.ts +0 -50
- package/src/stash-search.ts +0 -613
- package/src/stash-show.ts +0 -55
- package/src/stash-source.ts +0 -103
- package/src/stash-types.ts +0 -231
- package/src/stash.ts +0 -39
- package/src/tool-runner.ts +0 -142
- package/src/walker.ts +0 -53
- /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
- /package/dist/{src/markdown.js → markdown.js} +0 -0
- /package/dist/{src/registry-types.js → registry-types.js} +0 -0
- /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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
123
|
-
|
|
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
|
|
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
|
|
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 === ".." ||
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
|
24
|
+
return parseRemoteUrl(ref);
|
|
19
25
|
}
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
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
|
|
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
|
|
81
|
-
|
|
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
|
|
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: "
|
|
142
|
+
source: "local",
|
|
120
143
|
ref: rawRef,
|
|
121
|
-
id: `
|
|
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
|
|
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(
|
|
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(
|
|
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) ||
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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))
|