agentikit 0.0.8 → 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.
- package/README.md +135 -117
- package/dist/index.d.ts +13 -3
- package/dist/index.js +7 -1
- package/dist/src/asset-spec.d.ts +2 -0
- package/dist/src/asset-spec.js +22 -3
- package/dist/src/asset-type-handler.d.ts +27 -0
- package/dist/src/asset-type-handler.js +33 -0
- package/dist/src/cli.js +335 -100
- package/dist/src/common.d.ts +6 -1
- package/dist/src/common.js +18 -4
- package/dist/src/config-cli.d.ts +9 -0
- package/dist/src/config-cli.js +473 -0
- package/dist/src/config.d.ts +25 -6
- package/dist/src/config.js +188 -28
- package/dist/src/db.d.ts +46 -0
- package/dist/src/db.js +299 -0
- package/dist/src/embedder.js +12 -7
- package/dist/src/github.d.ts +4 -0
- package/dist/src/github.js +19 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +26 -0
- package/dist/src/handlers/command-handler.d.ts +2 -0
- package/dist/src/handlers/command-handler.js +23 -0
- package/dist/src/handlers/index.d.ts +6 -0
- package/dist/src/handlers/index.js +23 -0
- package/dist/src/handlers/knowledge-handler.d.ts +2 -0
- package/dist/src/handlers/knowledge-handler.js +56 -0
- package/dist/src/handlers/markdown-helpers.d.ts +7 -0
- package/dist/src/handlers/markdown-helpers.js +15 -0
- package/dist/src/handlers/script-handler.d.ts +2 -0
- package/dist/src/handlers/script-handler.js +78 -0
- package/dist/src/handlers/skill-handler.d.ts +2 -0
- package/dist/src/handlers/skill-handler.js +30 -0
- package/dist/src/handlers/tool-handler.d.ts +2 -0
- package/dist/src/handlers/tool-handler.js +58 -0
- package/dist/src/indexer.d.ts +1 -23
- package/dist/src/indexer.js +162 -155
- package/dist/src/init.d.ts +2 -2
- package/dist/src/init.js +21 -9
- package/dist/src/llm.js +4 -3
- package/dist/src/metadata.d.ts +1 -1
- package/dist/src/metadata.js +22 -64
- package/dist/src/origin-resolve.d.ts +19 -0
- package/dist/src/origin-resolve.js +53 -0
- package/dist/src/registry-install.d.ts +11 -0
- package/dist/src/registry-install.js +315 -0
- package/dist/src/registry-resolve.d.ts +3 -0
- package/dist/src/registry-resolve.js +299 -0
- package/dist/src/registry-search.d.ts +27 -0
- package/dist/src/registry-search.js +263 -0
- package/dist/src/registry-types.d.ts +62 -0
- package/dist/src/registry-types.js +1 -0
- package/dist/src/stash-add.d.ts +4 -0
- package/dist/src/stash-add.js +59 -0
- package/dist/src/stash-clone.d.ts +22 -0
- package/dist/src/stash-clone.js +83 -0
- package/dist/src/stash-ref.d.ts +27 -3
- package/dist/src/stash-ref.js +63 -24
- package/dist/src/stash-registry.d.ts +18 -0
- package/dist/src/stash-registry.js +221 -0
- package/dist/src/stash-resolve.js +3 -0
- package/dist/src/stash-search.d.ts +3 -1
- package/dist/src/stash-search.js +357 -138
- package/dist/src/stash-show.d.ts +1 -1
- package/dist/src/stash-show.js +28 -89
- package/dist/src/stash-source.d.ts +24 -0
- package/dist/src/stash-source.js +81 -0
- package/dist/src/stash-types.d.ts +175 -1
- package/dist/src/stash.d.ts +9 -1
- package/dist/src/stash.js +5 -0
- package/dist/src/tool-runner.d.ts +1 -1
- package/dist/src/tool-runner.js +18 -5
- package/package.json +7 -2
- package/src/asset-spec.ts +20 -4
- package/src/asset-type-handler.ts +77 -0
- package/src/cli.ts +354 -103
- package/src/common.ts +23 -5
- package/src/config-cli.ts +499 -0
- package/src/config.ts +218 -37
- package/src/db.ts +411 -0
- package/src/embedder.ts +22 -11
- package/src/github.ts +21 -0
- package/src/handlers/agent-handler.ts +32 -0
- package/src/handlers/command-handler.ts +29 -0
- package/src/handlers/index.ts +25 -0
- package/src/handlers/knowledge-handler.ts +62 -0
- package/src/handlers/markdown-helpers.ts +19 -0
- package/src/handlers/script-handler.ts +92 -0
- package/src/handlers/skill-handler.ts +37 -0
- package/src/handlers/tool-handler.ts +71 -0
- package/src/indexer.ts +208 -187
- package/src/init.ts +17 -9
- package/src/llm.ts +4 -3
- package/src/metadata.ts +21 -65
- package/src/origin-resolve.ts +67 -0
- package/src/registry-install.ts +361 -0
- package/src/registry-resolve.ts +341 -0
- package/src/registry-search.ts +335 -0
- package/src/registry-types.ts +72 -0
- package/src/stash-add.ts +63 -0
- package/src/stash-clone.ts +127 -0
- package/src/stash-ref.ts +84 -26
- package/src/stash-registry.ts +259 -0
- package/src/stash-resolve.ts +3 -0
- package/src/stash-search.ts +425 -155
- package/src/stash-show.ts +33 -82
- package/src/stash-source.ts +103 -0
- package/src/stash-types.ts +186 -1
- package/src/stash.ts +23 -0
- package/src/tool-runner.ts +18 -5
- package/dist/src/similarity.d.ts +0 -34
- package/src/similarity.ts +0 -271
package/src/init.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agentikit initialization logic.
|
|
3
3
|
*
|
|
4
|
-
* Creates the stash directory structure, sets the
|
|
4
|
+
* Creates the working stash directory structure, sets the AKM_STASH_DIR
|
|
5
5
|
* environment variable, and ensures ripgrep is available.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -25,7 +25,7 @@ export interface InitResponse {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function agentikitInit(): InitResponse {
|
|
28
|
+
export async function agentikitInit(): Promise<InitResponse> {
|
|
29
29
|
let stashDir: string
|
|
30
30
|
if (IS_WINDOWS) {
|
|
31
31
|
const userProfile = process.env.USERPROFILE?.trim()
|
|
@@ -59,7 +59,7 @@ export function agentikitInit(): InitResponse {
|
|
|
59
59
|
let profileUpdated: string | undefined
|
|
60
60
|
|
|
61
61
|
if (IS_WINDOWS) {
|
|
62
|
-
const result = spawnSync("setx", ["
|
|
62
|
+
const result = spawnSync("setx", ["AKM_STASH_DIR", stashDir], {
|
|
63
63
|
encoding: "utf8",
|
|
64
64
|
timeout: 10_000,
|
|
65
65
|
})
|
|
@@ -76,22 +76,30 @@ export function agentikitInit(): InitResponse {
|
|
|
76
76
|
profile = path.join(homeDir, ".profile")
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const exportLine = `export
|
|
79
|
+
const exportLine = `export AKM_STASH_DIR="${stashDir}"`
|
|
80
80
|
const existing = fs.existsSync(profile) ? fs.readFileSync(profile, "utf8") : ""
|
|
81
|
-
if (!existing.includes("
|
|
82
|
-
|
|
81
|
+
if (!existing.includes("AKM_STASH_DIR")) {
|
|
82
|
+
const updated = existing + `\n# Agentikit working stash directory\n${exportLine}\n`
|
|
83
|
+
const tmpPath = profile + `.tmp.${process.pid}`
|
|
84
|
+
try {
|
|
85
|
+
fs.writeFileSync(tmpPath, updated, "utf8")
|
|
86
|
+
fs.renameSync(tmpPath, profile)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
try { fs.unlinkSync(tmpPath) } catch { /* ignore */ }
|
|
89
|
+
throw err
|
|
90
|
+
}
|
|
83
91
|
envSet = true
|
|
84
92
|
profileUpdated = profile
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
// Create default config.json if it doesn't exist
|
|
89
|
-
const configPath = getConfigPath(
|
|
97
|
+
const configPath = getConfigPath()
|
|
90
98
|
if (!fs.existsSync(configPath)) {
|
|
91
|
-
saveConfig(DEFAULT_CONFIG
|
|
99
|
+
saveConfig(DEFAULT_CONFIG)
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
process.env.
|
|
102
|
+
process.env.AKM_STASH_DIR = stashDir
|
|
95
103
|
|
|
96
104
|
// Ensure ripgrep is available (install to stash/bin if needed)
|
|
97
105
|
let ripgrep: InitResponse["ripgrep"]
|
package/src/llm.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LlmConnectionConfig } from "./config"
|
|
2
|
+
import { fetchWithTimeout } from "./common"
|
|
2
3
|
import type { StashEntry } from "./metadata"
|
|
3
4
|
|
|
4
5
|
// ── OpenAI-compatible chat completions ──────────────────────────────────────
|
|
@@ -21,14 +22,14 @@ async function chatCompletion(
|
|
|
21
22
|
headers["Authorization"] = `Bearer ${config.apiKey}`
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const response = await
|
|
25
|
+
const response = await fetchWithTimeout(config.endpoint, {
|
|
25
26
|
method: "POST",
|
|
26
27
|
headers,
|
|
27
28
|
body: JSON.stringify({
|
|
28
29
|
model: config.model,
|
|
29
30
|
messages,
|
|
30
|
-
temperature: 0.3,
|
|
31
|
-
max_tokens: 512,
|
|
31
|
+
temperature: config.temperature ?? 0.3,
|
|
32
|
+
max_tokens: config.maxTokens ?? 512,
|
|
32
33
|
}),
|
|
33
34
|
})
|
|
34
35
|
|
package/src/metadata.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type AgentikitAssetType, isAssetType } from "./common"
|
|
|
4
4
|
import { SCRIPT_EXTENSIONS, isRelevantAssetFile, deriveCanonicalAssetName } from "./asset-spec"
|
|
5
5
|
import { parseFrontmatter, toStringOrUndefined } from "./frontmatter"
|
|
6
6
|
import { parseMarkdownToc, type TocHeading } from "./markdown"
|
|
7
|
+
import { tryGetHandler } from "./asset-type-handler"
|
|
7
8
|
|
|
8
9
|
// ── Schema ──────────────────────────────────────────────────────────────────
|
|
9
10
|
|
|
@@ -28,6 +29,7 @@ export interface StashEntry {
|
|
|
28
29
|
source?: "package" | "frontmatter" | "comments" | "filename" | "manual" | "llm"
|
|
29
30
|
aliases?: string[]
|
|
30
31
|
toc?: TocHeading[]
|
|
32
|
+
usage?: string[]
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export interface StashFile {
|
|
@@ -111,10 +113,25 @@ export function validateStashEntry(entry: unknown): StashEntry | null {
|
|
|
111
113
|
)
|
|
112
114
|
if (validated.length > 0) result.toc = validated
|
|
113
115
|
}
|
|
116
|
+
const usage = normalizeNonEmptyStringList(e.usage)
|
|
117
|
+
if (usage) result.usage = usage
|
|
114
118
|
|
|
115
119
|
return result
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
function normalizeNonEmptyStringList(value: unknown): string[] | undefined {
|
|
123
|
+
if (typeof value === "string") {
|
|
124
|
+
const trimmed = value.trim()
|
|
125
|
+
return trimmed ? [trimmed] : undefined
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(value)) return undefined
|
|
128
|
+
const filtered = value
|
|
129
|
+
.filter((item): item is string => typeof item === "string")
|
|
130
|
+
.map((item) => item.trim())
|
|
131
|
+
.filter((item) => item.length > 0)
|
|
132
|
+
return filtered.length > 0 ? filtered : undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
119
136
|
|
|
120
137
|
export function generateMetadata(
|
|
@@ -167,25 +184,10 @@ export function generateMetadata(
|
|
|
167
184
|
}
|
|
168
185
|
}
|
|
169
186
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const toc = parseMarkdownToc(mdContent)
|
|
175
|
-
if (toc.headings.length > 0) entry.toc = toc.headings
|
|
176
|
-
} catch {
|
|
177
|
-
// Non-fatal: skip TOC if file can't be read
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Priority 3: Code comments (for script files)
|
|
182
|
-
if (SCRIPT_EXTENSIONS.has(ext) && ext !== ".md") {
|
|
183
|
-
const commentDesc = extractDescriptionFromComments(file)
|
|
184
|
-
if (commentDesc && !entry.description) {
|
|
185
|
-
entry.description = commentDesc
|
|
186
|
-
entry.source = "comments"
|
|
187
|
-
entry.confidence = 0.7
|
|
188
|
-
}
|
|
187
|
+
// Type-specific metadata extraction (e.g. TOC for knowledge, comments for tools/scripts)
|
|
188
|
+
const handler = tryGetHandler(assetType)
|
|
189
|
+
if (handler?.extractTypeMetadata) {
|
|
190
|
+
handler.extractTypeMetadata(entry, file, ext)
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
// Priority 4: Filename heuristics (fallback)
|
|
@@ -233,52 +235,6 @@ function buildAliases(name: string, tags: string[]): string[] {
|
|
|
233
235
|
return Array.from(aliases)
|
|
234
236
|
}
|
|
235
237
|
|
|
236
|
-
// ── Intent Generation ────────────────────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
export function generateIntents(description: string, tags: string[], name: string): string[] {
|
|
239
|
-
const intents = new Set<string>()
|
|
240
|
-
|
|
241
|
-
// Split name on separators to extract tokens and potential verb
|
|
242
|
-
const nameTokens = name
|
|
243
|
-
.replace(/[-_]+/g, " ")
|
|
244
|
-
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
245
|
-
.toLowerCase()
|
|
246
|
-
.trim()
|
|
247
|
-
.split(/\s+/)
|
|
248
|
-
.filter((t) => t.length > 1)
|
|
249
|
-
|
|
250
|
-
// Intent from name as phrase (e.g. "summarize diff")
|
|
251
|
-
const namePhrase = nameTokens.join(" ")
|
|
252
|
-
if (namePhrase.length > 2) intents.add(namePhrase)
|
|
253
|
-
|
|
254
|
-
// Intent from description (lowercased)
|
|
255
|
-
const desc = description.toLowerCase().trim()
|
|
256
|
-
if (desc.length > 2) intents.add(desc)
|
|
257
|
-
|
|
258
|
-
// Combine first name token (potential verb) with tags
|
|
259
|
-
// e.g. name "summarize-diff", tags ["git"] → "summarize git diff"
|
|
260
|
-
if (nameTokens.length >= 1 && tags.length > 0) {
|
|
261
|
-
const verb = nameTokens[0]
|
|
262
|
-
const rest = nameTokens.slice(1).join(" ")
|
|
263
|
-
for (const tag of tags) {
|
|
264
|
-
const tagLower = tag.toLowerCase()
|
|
265
|
-
// verb + tag + rest (e.g. "summarize git diff")
|
|
266
|
-
const parts = [verb, tagLower, rest].filter((p) => p.length > 0)
|
|
267
|
-
const phrase = parts.join(" ")
|
|
268
|
-
if (phrase !== namePhrase && phrase.length > 2) intents.add(phrase)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Join tag pairs (e.g. ["git", "diff"] → "git diff")
|
|
273
|
-
if (tags.length >= 2) {
|
|
274
|
-
const tagPhrase = tags.map((t) => t.toLowerCase()).join(" ")
|
|
275
|
-
if (tagPhrase.length > 2) intents.add(tagPhrase)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Cap at 8 intents
|
|
279
|
-
return Array.from(intents).slice(0, 8)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
238
|
export function extractDescriptionFromComments(filePath: string): string | null {
|
|
283
239
|
let content: string
|
|
284
240
|
try {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { fetchWithTimeout, isWithin, TYPE_DIRS } from "./common"
|
|
5
|
+
import { loadConfig, saveConfig, type AgentikitConfig } from "./config"
|
|
6
|
+
import { parseRegistryRef, resolveRegistryArtifact } from "./registry-resolve"
|
|
7
|
+
import type { ParsedGitRef, RegistryInstallResult, RegistryInstalledEntry, RegistrySource } from "./registry-types"
|
|
8
|
+
|
|
9
|
+
const REGISTRY_STASH_DIR_NAMES = new Set<string>(Object.values(TYPE_DIRS))
|
|
10
|
+
|
|
11
|
+
export interface InstallRegistryRefOptions {
|
|
12
|
+
cacheRootDir?: string
|
|
13
|
+
now?: Date
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function installRegistryRef(ref: string, options?: InstallRegistryRefOptions): Promise<RegistryInstallResult> {
|
|
17
|
+
const parsed = parseRegistryRef(ref)
|
|
18
|
+
if (parsed.source === "git") {
|
|
19
|
+
return installGitRegistryRef(parsed, options)
|
|
20
|
+
}
|
|
21
|
+
const resolved = await resolveRegistryArtifact(parsed)
|
|
22
|
+
|
|
23
|
+
const installedAt = (options?.now ?? new Date()).toISOString()
|
|
24
|
+
const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir()
|
|
25
|
+
const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id)
|
|
26
|
+
const archivePath = path.join(cacheDir, "artifact.tar.gz")
|
|
27
|
+
const extractedDir = path.join(cacheDir, "extracted")
|
|
28
|
+
|
|
29
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
30
|
+
|
|
31
|
+
await downloadArchive(resolved.artifactUrl, archivePath)
|
|
32
|
+
extractTarGzSecure(archivePath, extractedDir)
|
|
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
|
+
|
|
70
|
+
const stashRoot = detectStashRoot(extractedDir)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: resolved.id,
|
|
74
|
+
source: resolved.source,
|
|
75
|
+
ref: resolved.ref,
|
|
76
|
+
artifactUrl: resolved.artifactUrl,
|
|
77
|
+
resolvedVersion: resolved.resolvedVersion,
|
|
78
|
+
resolvedRevision: resolved.resolvedRevision,
|
|
79
|
+
installedAt,
|
|
80
|
+
cacheDir,
|
|
81
|
+
extractedDir,
|
|
82
|
+
stashRoot,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function upsertInstalledRegistryEntry(entry: RegistryInstalledEntry): AgentikitConfig {
|
|
87
|
+
const current = loadConfig()
|
|
88
|
+
const currentInstalled = current.registry?.installed ?? []
|
|
89
|
+
const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id)
|
|
90
|
+
const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)]
|
|
91
|
+
|
|
92
|
+
const nextConfig: AgentikitConfig = {
|
|
93
|
+
...current,
|
|
94
|
+
registry: { installed: nextInstalled },
|
|
95
|
+
}
|
|
96
|
+
saveConfig(nextConfig)
|
|
97
|
+
return nextConfig
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function removeInstalledRegistryEntry(id: string): AgentikitConfig {
|
|
101
|
+
const current = loadConfig()
|
|
102
|
+
const currentInstalled = current.registry?.installed ?? []
|
|
103
|
+
const nextInstalled = currentInstalled.filter((item) => item.id !== id)
|
|
104
|
+
|
|
105
|
+
const nextConfig: AgentikitConfig = {
|
|
106
|
+
...current,
|
|
107
|
+
registry: nextInstalled.length > 0 ? { installed: nextInstalled } : undefined,
|
|
108
|
+
}
|
|
109
|
+
saveConfig(nextConfig)
|
|
110
|
+
return nextConfig
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getRegistryCacheRootDir(): string {
|
|
114
|
+
const xdgCache = process.env.XDG_CACHE_HOME?.trim()
|
|
115
|
+
if (xdgCache) {
|
|
116
|
+
return path.join(path.resolve(xdgCache), "agentikit", "registry")
|
|
117
|
+
}
|
|
118
|
+
const home = process.env.HOME?.trim()
|
|
119
|
+
if (!home) {
|
|
120
|
+
throw new Error("Unable to determine cache directory. Set XDG_CACHE_HOME or HOME.")
|
|
121
|
+
}
|
|
122
|
+
return path.join(path.resolve(home), ".cache", "agentikit", "registry")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function detectStashRoot(extractedDir: string): string {
|
|
126
|
+
const root = path.resolve(extractedDir)
|
|
127
|
+
|
|
128
|
+
const rootDotStash = path.join(root, ".stash")
|
|
129
|
+
if (isDirectory(rootDotStash)) {
|
|
130
|
+
return root
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (hasStashDirs(root)) {
|
|
134
|
+
return root
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const opencodeDir = path.join(root, "opencode")
|
|
138
|
+
if (hasStashDirs(opencodeDir)) {
|
|
139
|
+
return opencodeDir
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const shallowest = findShallowestDotStashRoot(root)
|
|
143
|
+
if (shallowest) return shallowest
|
|
144
|
+
|
|
145
|
+
return root
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildInstallCacheDir(cacheRootDir: string, source: RegistrySource, id: string): string {
|
|
149
|
+
const slug = `${source}-${id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "")}`
|
|
150
|
+
const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
151
|
+
return path.join(cacheRootDir, slug || source, stamp)
|
|
152
|
+
}
|
|
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
|
+
|
|
169
|
+
async function downloadArchive(url: string, destination: string): Promise<void> {
|
|
170
|
+
const response = await fetchWithTimeout(url, undefined, 120_000)
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error(`Failed to download archive (${response.status}) from ${url}`)
|
|
173
|
+
}
|
|
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
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function extractTarGzSecure(archivePath: string, destinationDir: string): void {
|
|
188
|
+
const listResult = spawnSync("tar", ["tzf", archivePath], { encoding: "utf8" })
|
|
189
|
+
if (listResult.status !== 0) {
|
|
190
|
+
const err = listResult.stderr?.trim() || listResult.error?.message || "unknown error"
|
|
191
|
+
throw new Error(`Failed to inspect archive ${archivePath}: ${err}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
validateTarEntries(listResult.stdout)
|
|
195
|
+
|
|
196
|
+
fs.rmSync(destinationDir, { recursive: true, force: true })
|
|
197
|
+
fs.mkdirSync(destinationDir, { recursive: true })
|
|
198
|
+
|
|
199
|
+
const extractResult = spawnSync("tar", ["xzf", archivePath, "--strip-components=1", "-C", destinationDir], {
|
|
200
|
+
encoding: "utf8",
|
|
201
|
+
})
|
|
202
|
+
if (extractResult.status !== 0) {
|
|
203
|
+
const err = extractResult.stderr?.trim() || extractResult.error?.message || "unknown error"
|
|
204
|
+
throw new Error(`Failed to extract archive ${archivePath}: ${err}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateTarEntries(listOutput: string): void {
|
|
209
|
+
const lines = listOutput.split(/\r?\n/).filter(Boolean)
|
|
210
|
+
for (const rawLine of lines) {
|
|
211
|
+
const entry = rawLine.trim()
|
|
212
|
+
if (!entry || entry.includes("\0")) {
|
|
213
|
+
throw new Error(`Archive contains an invalid entry: ${JSON.stringify(rawLine)}`)
|
|
214
|
+
}
|
|
215
|
+
if (entry.startsWith("/")) {
|
|
216
|
+
throw new Error(`Archive contains an absolute path entry: ${entry}`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const normalized = path.posix.normalize(entry)
|
|
220
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
221
|
+
throw new Error(`Archive contains a path traversal entry: ${entry}`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const parts = normalized.split("/").filter(Boolean)
|
|
225
|
+
const stripped = parts.slice(1).join("/")
|
|
226
|
+
if (!stripped) continue
|
|
227
|
+
const normalizedStripped = path.posix.normalize(stripped)
|
|
228
|
+
if (normalizedStripped === ".." || normalizedStripped.startsWith("../") || path.posix.isAbsolute(normalizedStripped)) {
|
|
229
|
+
throw new Error(`Archive contains an unsafe entry after strip-components: ${entry}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isDirectory(target: string): boolean {
|
|
235
|
+
try {
|
|
236
|
+
return fs.statSync(target).isDirectory()
|
|
237
|
+
} catch {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
}
|
|
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
|
+
|
|
325
|
+
function hasStashDirs(dirPath: string): boolean {
|
|
326
|
+
if (!isDirectory(dirPath)) return false
|
|
327
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
328
|
+
return entries.some((entry) => entry.isDirectory() && REGISTRY_STASH_DIR_NAMES.has(entry.name))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function findShallowestDotStashRoot(root: string): string | undefined {
|
|
332
|
+
const queue: string[] = [root]
|
|
333
|
+
while (queue.length > 0) {
|
|
334
|
+
const current = queue.shift()!
|
|
335
|
+
const dotStash = path.join(current, ".stash")
|
|
336
|
+
if (isDirectory(dotStash)) {
|
|
337
|
+
return current
|
|
338
|
+
}
|
|
339
|
+
let children: fs.Dirent[]
|
|
340
|
+
try {
|
|
341
|
+
children = fs.readdirSync(current, { withFileTypes: true })
|
|
342
|
+
} catch {
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
for (const child of children) {
|
|
346
|
+
if (!child.isDirectory()) continue
|
|
347
|
+
if (child.name === ".git" || child.name === "node_modules") continue
|
|
348
|
+
queue.push(path.join(current, child.name))
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return undefined
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function normalizeInstalledEntry(entry: RegistryInstalledEntry): RegistryInstalledEntry {
|
|
355
|
+
return {
|
|
356
|
+
...entry,
|
|
357
|
+
stashRoot: path.resolve(entry.stashRoot),
|
|
358
|
+
cacheDir: path.resolve(entry.cacheDir),
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|