agentikit 0.0.9 → 0.0.12

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 (107) hide show
  1. package/README.md +129 -214
  2. package/dist/index.d.ts +8 -2
  3. package/dist/index.js +4 -1
  4. package/dist/src/asset-spec.d.ts +2 -0
  5. package/dist/src/asset-spec.js +22 -3
  6. package/dist/src/asset-type-handler.d.ts +27 -0
  7. package/dist/src/asset-type-handler.js +33 -0
  8. package/dist/src/cli.js +201 -75
  9. package/dist/src/common.d.ts +6 -1
  10. package/dist/src/common.js +18 -4
  11. package/dist/src/config-cli.d.ts +9 -0
  12. package/dist/src/config-cli.js +473 -0
  13. package/dist/src/config.d.ts +19 -6
  14. package/dist/src/config.js +139 -29
  15. package/dist/src/db.d.ts +46 -0
  16. package/dist/src/db.js +299 -0
  17. package/dist/src/embedder.js +12 -7
  18. package/dist/src/github.d.ts +4 -0
  19. package/dist/src/github.js +19 -0
  20. package/dist/src/handlers/agent-handler.d.ts +2 -0
  21. package/dist/src/handlers/agent-handler.js +26 -0
  22. package/dist/src/handlers/command-handler.d.ts +2 -0
  23. package/dist/src/handlers/command-handler.js +23 -0
  24. package/dist/src/handlers/index.d.ts +6 -0
  25. package/dist/src/handlers/index.js +23 -0
  26. package/dist/src/handlers/knowledge-handler.d.ts +2 -0
  27. package/dist/src/handlers/knowledge-handler.js +56 -0
  28. package/dist/src/handlers/markdown-helpers.d.ts +7 -0
  29. package/dist/src/handlers/markdown-helpers.js +15 -0
  30. package/dist/src/handlers/script-handler.d.ts +2 -0
  31. package/dist/src/handlers/script-handler.js +78 -0
  32. package/dist/src/handlers/skill-handler.d.ts +2 -0
  33. package/dist/src/handlers/skill-handler.js +30 -0
  34. package/dist/src/handlers/tool-handler.d.ts +2 -0
  35. package/dist/src/handlers/tool-handler.js +58 -0
  36. package/dist/src/indexer.d.ts +1 -23
  37. package/dist/src/indexer.js +162 -155
  38. package/dist/src/init.d.ts +2 -2
  39. package/dist/src/init.js +21 -9
  40. package/dist/src/llm.js +4 -3
  41. package/dist/src/metadata.d.ts +0 -1
  42. package/dist/src/metadata.js +6 -64
  43. package/dist/src/origin-resolve.d.ts +19 -0
  44. package/dist/src/origin-resolve.js +53 -0
  45. package/dist/src/registry-install.d.ts +2 -2
  46. package/dist/src/registry-install.js +142 -35
  47. package/dist/src/registry-resolve.js +90 -22
  48. package/dist/src/registry-search.d.ts +22 -0
  49. package/dist/src/registry-search.js +231 -97
  50. package/dist/src/registry-types.d.ts +9 -2
  51. package/dist/src/stash-add.js +4 -4
  52. package/dist/src/stash-clone.d.ts +22 -0
  53. package/dist/src/stash-clone.js +83 -0
  54. package/dist/src/stash-ref.d.ts +27 -3
  55. package/dist/src/stash-ref.js +63 -24
  56. package/dist/src/stash-registry.js +12 -12
  57. package/dist/src/stash-resolve.js +3 -0
  58. package/dist/src/stash-search.js +168 -164
  59. package/dist/src/stash-show.d.ts +1 -1
  60. package/dist/src/stash-show.js +28 -96
  61. package/dist/src/stash-source.d.ts +24 -0
  62. package/dist/src/stash-source.js +81 -0
  63. package/dist/src/stash-types.d.ts +14 -4
  64. package/dist/src/stash.d.ts +6 -0
  65. package/dist/src/stash.js +3 -0
  66. package/dist/src/tool-runner.d.ts +1 -1
  67. package/dist/src/tool-runner.js +18 -5
  68. package/package.json +7 -2
  69. package/src/asset-spec.ts +20 -4
  70. package/src/asset-type-handler.ts +77 -0
  71. package/src/cli.ts +213 -82
  72. package/src/common.ts +23 -5
  73. package/src/config-cli.ts +499 -0
  74. package/src/config.ts +160 -38
  75. package/src/db.ts +411 -0
  76. package/src/embedder.ts +22 -11
  77. package/src/github.ts +21 -0
  78. package/src/handlers/agent-handler.ts +32 -0
  79. package/src/handlers/command-handler.ts +29 -0
  80. package/src/handlers/index.ts +25 -0
  81. package/src/handlers/knowledge-handler.ts +62 -0
  82. package/src/handlers/markdown-helpers.ts +19 -0
  83. package/src/handlers/script-handler.ts +92 -0
  84. package/src/handlers/skill-handler.ts +37 -0
  85. package/src/handlers/tool-handler.ts +71 -0
  86. package/src/indexer.ts +208 -187
  87. package/src/init.ts +17 -9
  88. package/src/llm.ts +4 -3
  89. package/src/metadata.ts +5 -65
  90. package/src/origin-resolve.ts +67 -0
  91. package/src/registry-install.ts +158 -42
  92. package/src/registry-resolve.ts +92 -23
  93. package/src/registry-search.ts +288 -98
  94. package/src/registry-types.ts +10 -2
  95. package/src/stash-add.ts +14 -17
  96. package/src/stash-clone.ts +127 -0
  97. package/src/stash-ref.ts +84 -26
  98. package/src/stash-registry.ts +12 -12
  99. package/src/stash-resolve.ts +3 -0
  100. package/src/stash-search.ts +202 -184
  101. package/src/stash-show.ts +33 -90
  102. package/src/stash-source.ts +103 -0
  103. package/src/stash-types.ts +14 -4
  104. package/src/stash.ts +8 -0
  105. package/src/tool-runner.ts +18 -5
  106. package/dist/src/similarity.d.ts +0 -34
  107. package/src/similarity.ts +0 -271
@@ -0,0 +1,67 @@
1
+ import path from "node:path"
2
+ import type { StashSource } from "./stash-source"
3
+ import { parseRegistryRef } from "./registry-resolve"
4
+
5
+ /**
6
+ * Given an origin string (from an AssetRef) and the full list of stash
7
+ * sources, return the subset of sources to search.
8
+ *
9
+ * Resolution order:
10
+ * 1. undefined → all sources (working → mounted → installed)
11
+ * 2. "local" → working stash only
12
+ * 3. exact match → installed source whose registryId matches verbatim
13
+ * 4. parsed match → parse origin as a registry ref, match by parsed ID
14
+ * 5. path match → mounted source whose path matches
15
+ * 6. empty → indicates a remote/uninstalled origin (caller decides)
16
+ */
17
+ export function resolveSourcesForOrigin(
18
+ origin: string | undefined,
19
+ allSources: StashSource[],
20
+ ): StashSource[] {
21
+ if (!origin) return allSources
22
+
23
+ if (origin === "local") {
24
+ return allSources.filter((s) => s.kind === "working")
25
+ }
26
+
27
+ // Exact registryId match (e.g. origin is "npm:@scope/pkg")
28
+ const byExactId = allSources.filter(
29
+ (s) => s.kind === "installed" && s.registryId === origin,
30
+ )
31
+ if (byExactId.length > 0) return byExactId
32
+
33
+ // Parse origin as a registry ref and match by parsed ID.
34
+ // Allows shorthand: "owner/repo" matches "github:owner/repo",
35
+ // "@scope/pkg" matches "npm:@scope/pkg".
36
+ try {
37
+ const parsed = parseRegistryRef(origin)
38
+ const byParsedId = allSources.filter(
39
+ (s) => s.kind === "installed" && s.registryId === parsed.id,
40
+ )
41
+ if (byParsedId.length > 0) return byParsedId
42
+ } catch {
43
+ // Not a valid registry ref — continue to path matching
44
+ }
45
+
46
+ // Mounted stash by resolved path
47
+ const resolvedOrigin = path.resolve(origin)
48
+ const byPath = allSources.filter(
49
+ (s) => s.kind === "mounted" && path.resolve(s.path) === resolvedOrigin,
50
+ )
51
+ if (byPath.length > 0) return byPath
52
+
53
+ // No match — origin may be remote/uninstalled
54
+ return []
55
+ }
56
+
57
+ /**
58
+ * Check whether an origin refers to something that could be fetched remotely
59
+ * (i.e. it looks like a registry ref but isn't installed locally).
60
+ */
61
+ export function isRemoteOrigin(
62
+ origin: string,
63
+ allSources: StashSource[],
64
+ ): boolean {
65
+ if (origin === "local") return false
66
+ return resolveSourcesForOrigin(origin, allSources).length === 0
67
+ }
@@ -1,10 +1,10 @@
1
1
  import { spawnSync } from "node:child_process"
2
2
  import fs from "node:fs"
3
3
  import path from "node:path"
4
- import { TYPE_DIRS } from "./common"
4
+ import { fetchWithTimeout, isWithin, TYPE_DIRS } from "./common"
5
5
  import { loadConfig, saveConfig, type AgentikitConfig } from "./config"
6
6
  import { parseRegistryRef, resolveRegistryArtifact } from "./registry-resolve"
7
- import type { RegistryInstallResult, RegistryInstalledEntry, RegistrySource } from "./registry-types"
7
+ import type { ParsedGitRef, RegistryInstallResult, RegistryInstalledEntry, RegistrySource } from "./registry-types"
8
8
 
9
9
  const REGISTRY_STASH_DIR_NAMES = new Set<string>(Object.values(TYPE_DIRS))
10
10
 
@@ -15,6 +15,9 @@ export interface InstallRegistryRefOptions {
15
15
 
16
16
  export async function installRegistryRef(ref: string, options?: InstallRegistryRefOptions): Promise<RegistryInstallResult> {
17
17
  const parsed = parseRegistryRef(ref)
18
+ if (parsed.source === "git") {
19
+ return installGitRegistryRef(parsed, options)
20
+ }
18
21
  const resolved = await resolveRegistryArtifact(parsed)
19
22
 
20
23
  const installedAt = (options?.now ?? new Date()).toISOString()
@@ -28,6 +31,42 @@ export async function installRegistryRef(ref: string, options?: InstallRegistryR
28
31
  await downloadArchive(resolved.artifactUrl, archivePath)
29
32
  extractTarGzSecure(archivePath, extractedDir)
30
33
 
34
+ const provisionalKitRoot = detectStashRoot(extractedDir)
35
+ const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot
36
+ const stashRoot = detectStashRoot(installRoot)
37
+
38
+ return {
39
+ id: resolved.id,
40
+ source: resolved.source,
41
+ ref: resolved.ref,
42
+ artifactUrl: resolved.artifactUrl,
43
+ resolvedVersion: resolved.resolvedVersion,
44
+ resolvedRevision: resolved.resolvedRevision,
45
+ installedAt,
46
+ cacheDir,
47
+ extractedDir,
48
+ stashRoot,
49
+ }
50
+ }
51
+
52
+ async function installGitRegistryRef(parsed: ParsedGitRef, options?: InstallRegistryRefOptions): Promise<RegistryInstallResult> {
53
+ const resolved = await resolveRegistryArtifact(parsed)
54
+ const installedAt = (options?.now ?? new Date()).toISOString()
55
+ const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir()
56
+ const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id)
57
+ const extractedDir = path.join(cacheDir, "extracted")
58
+
59
+ fs.mkdirSync(cacheDir, { recursive: true })
60
+ fs.rmSync(extractedDir, { recursive: true, force: true })
61
+ fs.mkdirSync(extractedDir, { recursive: true })
62
+
63
+ const includeConfig = findNearestAgentikitIncludeConfig(parsed.sourcePath, parsed.repoRoot)
64
+ if (includeConfig) {
65
+ copyIncludedPaths(includeConfig.baseDir, includeConfig.include, extractedDir)
66
+ } else {
67
+ copyDirectoryContents(parsed.sourcePath, extractedDir)
68
+ }
69
+
31
70
  const stashRoot = detectStashRoot(extractedDir)
32
71
 
33
72
  return {
@@ -44,49 +83,30 @@ export async function installRegistryRef(ref: string, options?: InstallRegistryR
44
83
  }
45
84
  }
46
85
 
47
- export function upsertInstalledRegistryEntry(entry: RegistryInstalledEntry, stashDir?: string): AgentikitConfig {
48
- const current = loadConfig(stashDir)
86
+ export function upsertInstalledRegistryEntry(entry: RegistryInstalledEntry): AgentikitConfig {
87
+ const current = loadConfig()
49
88
  const currentInstalled = current.registry?.installed ?? []
50
- const previousRegistryRoots = new Set(currentInstalled.map((item) => path.resolve(item.stashRoot)))
51
-
52
89
  const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id)
53
90
  const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)]
54
- const nextRegistryRoots = new Set(nextInstalled.map((item) => path.resolve(item.stashRoot)))
55
- const preservedAdditional = current.additionalStashDirs.filter(
56
- (dir) => !previousRegistryRoots.has(path.resolve(dir)),
57
- )
58
- const syncedAdditional = uniquePaths([...preservedAdditional, ...nextRegistryRoots])
59
91
 
60
92
  const nextConfig: AgentikitConfig = {
61
93
  ...current,
62
- additionalStashDirs: syncedAdditional,
63
- registry: {
64
- installed: nextInstalled,
65
- },
94
+ registry: { installed: nextInstalled },
66
95
  }
67
- saveConfig(nextConfig, stashDir)
96
+ saveConfig(nextConfig)
68
97
  return nextConfig
69
98
  }
70
99
 
71
- export function removeInstalledRegistryEntry(id: string, stashDir?: string): AgentikitConfig {
72
- const current = loadConfig(stashDir)
100
+ export function removeInstalledRegistryEntry(id: string): AgentikitConfig {
101
+ const current = loadConfig()
73
102
  const currentInstalled = current.registry?.installed ?? []
74
- const previousRegistryRoots = new Set(currentInstalled.map((item) => path.resolve(item.stashRoot)))
75
-
76
103
  const nextInstalled = currentInstalled.filter((item) => item.id !== id)
77
- const nextRegistryRoots = new Set(nextInstalled.map((item) => path.resolve(item.stashRoot)))
78
-
79
- const preservedAdditional = current.additionalStashDirs.filter(
80
- (dir) => !previousRegistryRoots.has(path.resolve(dir)),
81
- )
82
- const syncedAdditional = uniquePaths([...preservedAdditional, ...nextRegistryRoots])
83
104
 
84
105
  const nextConfig: AgentikitConfig = {
85
106
  ...current,
86
- additionalStashDirs: syncedAdditional,
87
107
  registry: nextInstalled.length > 0 ? { installed: nextInstalled } : undefined,
88
108
  }
89
- saveConfig(nextConfig, stashDir)
109
+ saveConfig(nextConfig)
90
110
  return nextConfig
91
111
  }
92
112
 
@@ -131,13 +151,37 @@ function buildInstallCacheDir(cacheRootDir: string, source: RegistrySource, id:
131
151
  return path.join(cacheRootDir, slug || source, stamp)
132
152
  }
133
153
 
154
+ function applyAgentikitIncludeConfig(
155
+ sourceRoot: string,
156
+ cacheDir: string,
157
+ searchRoot: string = sourceRoot,
158
+ ): string | undefined {
159
+ const includeConfig = findNearestAgentikitIncludeConfig(sourceRoot, searchRoot)
160
+ if (!includeConfig) return undefined
161
+
162
+ const selectedDir = path.join(cacheDir, "selected")
163
+ fs.rmSync(selectedDir, { recursive: true, force: true })
164
+ fs.mkdirSync(selectedDir, { recursive: true })
165
+ copyIncludedPaths(includeConfig.baseDir, includeConfig.include, selectedDir)
166
+ return selectedDir
167
+ }
168
+
134
169
  async function downloadArchive(url: string, destination: string): Promise<void> {
135
- const response = await fetch(url)
170
+ const response = await fetchWithTimeout(url, undefined, 120_000)
136
171
  if (!response.ok) {
137
172
  throw new Error(`Failed to download archive (${response.status}) from ${url}`)
138
173
  }
139
- const arrayBuffer = await response.arrayBuffer()
140
- fs.writeFileSync(destination, Buffer.from(arrayBuffer))
174
+ // Stream response to disk instead of buffering the entire archive in memory.
175
+ // Uses Bun.write which handles Response streaming natively.
176
+ const BunRuntime: { write(path: string, body: Response): Promise<number> } =
177
+ (globalThis as Record<string, unknown>).Bun as typeof BunRuntime
178
+ if (BunRuntime?.write) {
179
+ await BunRuntime.write(destination, response)
180
+ } else {
181
+ // Fallback for non-Bun environments (e.g., tests)
182
+ const arrayBuffer = await response.arrayBuffer()
183
+ fs.writeFileSync(destination, Buffer.from(arrayBuffer))
184
+ }
141
185
  }
142
186
 
143
187
  function extractTarGzSecure(archivePath: string, destinationDir: string): void {
@@ -195,6 +239,89 @@ function isDirectory(target: string): boolean {
195
239
  }
196
240
  }
197
241
 
242
+ function readAgentikitIncludeConfigAtDir(dirPath: string): { baseDir: string; include: string[] } | undefined {
243
+ const packageJsonPath = path.join(dirPath, "package.json")
244
+ if (!fs.existsSync(packageJsonPath)) return undefined
245
+
246
+ let pkg: unknown
247
+ try {
248
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
249
+ } catch {
250
+ return undefined
251
+ }
252
+ if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg)) return undefined
253
+
254
+ const agentikit = (pkg as Record<string, unknown>).agentikit
255
+ if (typeof agentikit !== "object" || agentikit === null || Array.isArray(agentikit)) return undefined
256
+
257
+ const include = (agentikit as Record<string, unknown>).include
258
+ if (!Array.isArray(include)) return undefined
259
+
260
+ const parsedInclude = include
261
+ .filter((value): value is string => typeof value === "string")
262
+ .map((value) => value.trim())
263
+ .filter(Boolean)
264
+
265
+ return parsedInclude.length > 0 ? { baseDir: dirPath, include: parsedInclude } : undefined
266
+ }
267
+
268
+ function findNearestAgentikitIncludeConfig(
269
+ startDir: string,
270
+ stopDir: string,
271
+ ): { baseDir: string; include: string[] } | undefined {
272
+ let current = path.resolve(startDir)
273
+ const boundary = path.resolve(stopDir)
274
+
275
+ while (isWithin(current, boundary)) {
276
+ const config = readAgentikitIncludeConfigAtDir(current)
277
+ if (config) return config
278
+ if (current === boundary) break
279
+ const parent = path.dirname(current)
280
+ if (parent === current) break
281
+ current = parent
282
+ }
283
+
284
+ return undefined
285
+ }
286
+
287
+ function copyIncludedPaths(baseDir: string, include: string[], destinationDir: string): void {
288
+ for (const entry of include) {
289
+ const resolvedSource = path.resolve(baseDir, entry)
290
+ if (!isWithin(resolvedSource, baseDir)) {
291
+ throw new Error(`Path in agentikit.include escapes the package root: ${entry}`)
292
+ }
293
+ if (!fs.existsSync(resolvedSource)) {
294
+ throw new Error(`Path in agentikit.include does not exist: ${entry}`)
295
+ }
296
+ if (path.basename(resolvedSource) === ".git") {
297
+ continue
298
+ }
299
+ const relativePath = path.relative(baseDir, resolvedSource)
300
+ if (!relativePath || relativePath === ".") {
301
+ copyDirectoryContents(baseDir, destinationDir)
302
+ continue
303
+ }
304
+ copyPath(resolvedSource, path.join(destinationDir, relativePath))
305
+ }
306
+ }
307
+
308
+ function copyDirectoryContents(sourceDir: string, destinationDir: string): void {
309
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
310
+ if (entry.name === ".git") continue
311
+ copyPath(path.join(sourceDir, entry.name), path.join(destinationDir, entry.name))
312
+ }
313
+ }
314
+
315
+ function copyPath(sourcePath: string, destinationPath: string): void {
316
+ const stat = fs.statSync(sourcePath)
317
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true })
318
+ if (stat.isDirectory()) {
319
+ fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true })
320
+ return
321
+ }
322
+ fs.copyFileSync(sourcePath, destinationPath)
323
+ }
324
+
198
325
  function hasStashDirs(dirPath: string): boolean {
199
326
  if (!isDirectory(dirPath)) return false
200
327
  const entries = fs.readdirSync(dirPath, { withFileTypes: true })
@@ -232,14 +359,3 @@ function normalizeInstalledEntry(entry: RegistryInstalledEntry): RegistryInstall
232
359
  }
233
360
  }
234
361
 
235
- function uniquePaths(paths: Iterable<string>): string[] {
236
- const seen = new Set<string>()
237
- const result: string[] = []
238
- for (const candidate of paths) {
239
- const normalized = path.resolve(candidate)
240
- if (seen.has(normalized)) continue
241
- seen.add(normalized)
242
- result.push(normalized)
243
- }
244
- return result
245
- }
@@ -1,6 +1,10 @@
1
- import type { ParsedGithubRef, ParsedNpmRef, ParsedRegistryRef, ResolvedRegistryArtifact } from "./registry-types"
2
-
3
- const GITHUB_API_BASE = "https://api.github.com"
1
+ import { spawnSync } from "node:child_process"
2
+ import fs from "node:fs"
3
+ import path from "node:path"
4
+ import { pathToFileURL } from "node:url"
5
+ import { fetchWithTimeout } from "./common"
6
+ import type { ParsedGitRef, ParsedGithubRef, ParsedNpmRef, ParsedRegistryRef, ResolvedRegistryArtifact } from "./registry-types"
7
+ import { GITHUB_API_BASE, githubHeaders, asRecord, asString } from "./github"
4
8
 
5
9
  export function parseRegistryRef(rawRef: string): ParsedRegistryRef {
6
10
  const ref = rawRef.trim()
@@ -15,6 +19,10 @@ export function parseRegistryRef(rawRef: string): ParsedRegistryRef {
15
19
  if (ref.startsWith("http://") || ref.startsWith("https://")) {
16
20
  return parseGithubUrl(ref)
17
21
  }
22
+ const localGitRef = tryParseLocalGitRef(ref, isPathLikeRef(ref))
23
+ if (localGitRef) {
24
+ return localGitRef
25
+ }
18
26
 
19
27
  if (ref.startsWith("@") || !looksLikeGithubOwnerRepo(ref)) {
20
28
  return parseNpmRef(ref, ref)
@@ -27,6 +35,9 @@ export async function resolveRegistryArtifact(parsed: ParsedRegistryRef): Promis
27
35
  if (parsed.source === "npm") {
28
36
  return resolveNpmArtifact(parsed)
29
37
  }
38
+ if (parsed.source === "git") {
39
+ return resolveGitArtifact(parsed)
40
+ }
30
41
  return resolveGithubArtifact(parsed)
31
42
  }
32
43
 
@@ -96,6 +107,45 @@ function parseGithubUrl(rawUrl: string): ParsedGithubRef {
96
107
  }
97
108
  }
98
109
 
110
+ function tryParseLocalGitRef(rawRef: string, explicitPath: boolean): ParsedGitRef | undefined {
111
+ if (!explicitPath) {
112
+ return undefined
113
+ }
114
+
115
+ const resolvedPath = path.resolve(rawRef)
116
+ let stat: fs.Stats
117
+ try {
118
+ stat = fs.statSync(resolvedPath)
119
+ } catch {
120
+ throw new Error(`Local path not found: ${resolvedPath}`)
121
+ }
122
+
123
+ if (!stat.isDirectory()) {
124
+ throw new Error("Local add path must be a directory, but the provided path is not one.")
125
+ }
126
+
127
+ const repoRoot = findGitRepoRoot(resolvedPath)
128
+ if (!repoRoot) {
129
+ throw new Error("Local add path must be inside a git repository.")
130
+ }
131
+
132
+ return {
133
+ source: "git",
134
+ ref: rawRef,
135
+ id: `git:${encodeURIComponent(resolvedPath)}`,
136
+ repoRoot,
137
+ sourcePath: resolvedPath,
138
+ }
139
+ }
140
+
141
+ function isPathLikeRef(ref: string): boolean {
142
+ if (path.isAbsolute(ref)) return true
143
+ if (ref.startsWith("./") || ref.startsWith("../") || ref.startsWith(".\\") || ref.startsWith("..\\")) {
144
+ return true
145
+ }
146
+ return ref.includes("/") || ref.includes("\\")
147
+ }
148
+
99
149
  async function resolveNpmArtifact(parsed: ParsedNpmRef): Promise<ResolvedRegistryArtifact> {
100
150
  const encodedName = encodeURIComponent(parsed.packageName)
101
151
  const metadata = await fetchJson<Record<string, unknown>>(`https://registry.npmjs.org/${encodedName}`)
@@ -197,6 +247,17 @@ async function resolveGithubArtifact(parsed: ParsedGithubRef): Promise<ResolvedR
197
247
  }
198
248
  }
199
249
 
250
+ async function resolveGitArtifact(parsed: ParsedGitRef): Promise<ResolvedRegistryArtifact> {
251
+ return {
252
+ id: parsed.id,
253
+ source: parsed.source,
254
+ ref: parsed.ref,
255
+ artifactUrl: pathToFileURL(parsed.sourcePath).toString(),
256
+ resolvedRevision: readGitValue(parsed.repoRoot, "rev-parse", "HEAD"),
257
+ resolvedVersion: readGitValue(parsed.repoRoot, "rev-parse", "--abbrev-ref", "HEAD"),
258
+ }
259
+ }
260
+
200
261
  function splitNpmNameAndVersion(input: string): { packageName: string; requestedVersionOrTag?: string } {
201
262
  if (input.startsWith("@")) {
202
263
  const secondAt = input.indexOf("@", 1)
@@ -220,8 +281,16 @@ function splitNpmNameAndVersion(input: string): { packageName: string; requested
220
281
  }
221
282
 
222
283
  function validateNpmPackageName(name: string): void {
223
- if (!name || name.includes(" ")) {
224
- throw new Error(`Invalid npm package name: \"${name}\".`)
284
+ if (!name) throw new Error('Invalid npm package name: name is required.')
285
+ if (name.length > 214) throw new Error(`Invalid npm package name: "${name}" exceeds 214 characters.`)
286
+ if (name !== name.toLowerCase() && !name.startsWith('@')) {
287
+ throw new Error(`Invalid npm package name: "${name}" must be lowercase.`)
288
+ }
289
+ if (name.startsWith('.') || name.startsWith('_')) {
290
+ throw new Error(`Invalid npm package name: "${name}" cannot start with . or _.`)
291
+ }
292
+ if (/[~'!()*]/.test(name) || name.includes(' ') || encodeURIComponent(name).replace(/%40/g, '@').replace(/%2[Ff]/g, '/') !== name) {
293
+ throw new Error(`Invalid npm package name: "${name}" contains invalid characters.`)
225
294
  }
226
295
  }
227
296
 
@@ -237,18 +306,27 @@ function splitRefSuffix(value: string): [string, string | undefined] {
237
306
  return [value.slice(0, hash), value.slice(hash + 1) || undefined]
238
307
  }
239
308
 
240
- function githubHeaders(): HeadersInit {
241
- const token = process.env.GITHUB_TOKEN?.trim()
242
- const headers: Record<string, string> = {
243
- Accept: "application/vnd.github+json",
244
- "User-Agent": "agentikit-registry",
309
+ function findGitRepoRoot(startDir: string): string | undefined {
310
+ let current = path.resolve(startDir)
311
+ while (true) {
312
+ if (fs.existsSync(path.join(current, ".git"))) {
313
+ return current
314
+ }
315
+ const parent = path.dirname(current)
316
+ if (parent === current) return undefined
317
+ current = parent
245
318
  }
246
- if (token) headers.Authorization = `Bearer ${token}`
247
- return headers
319
+ }
320
+
321
+ function readGitValue(repoRoot: string, ...args: string[]): string | undefined {
322
+ const result = spawnSync("git", ["-C", repoRoot, ...args], { encoding: "utf8" })
323
+ if (result.status !== 0) return undefined
324
+ const value = result.stdout.trim()
325
+ return value || undefined
248
326
  }
249
327
 
250
328
  async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
251
- const response = await fetch(url, { headers })
329
+ const response = await fetchWithTimeout(url, { headers })
252
330
  if (!response.ok) {
253
331
  throw new Error(`Request failed (${response.status}) for ${url}`)
254
332
  }
@@ -256,17 +334,8 @@ async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
256
334
  }
257
335
 
258
336
  async function tryFetchJson<T>(url: string, headers?: HeadersInit): Promise<T | null> {
259
- const response = await fetch(url, { headers })
337
+ const response = await fetchWithTimeout(url, { headers })
260
338
  if (!response.ok) return null
261
339
  return await response.json() as T
262
340
  }
263
341
 
264
- function asRecord(value: unknown): Record<string, unknown> {
265
- return typeof value === "object" && value !== null && !Array.isArray(value)
266
- ? value as Record<string, unknown>
267
- : {}
268
- }
269
-
270
- function asString(value: unknown): string | undefined {
271
- return typeof value === "string" && value ? value : undefined
272
- }