agentikit 0.0.3

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/src/stash.ts ADDED
@@ -0,0 +1,695 @@
1
+ import { spawnSync } from "node:child_process"
2
+ import fs from "node:fs"
3
+ import path from "node:path"
4
+ import { loadSearchIndex, buildSearchText } from "./indexer"
5
+ import { TfIdfAdapter, type ScoredEntry } from "./similarity"
6
+
7
+ export type AgentikitAssetType = "tool" | "skill" | "command" | "agent"
8
+ export type AgentikitSearchType = AgentikitAssetType | "any"
9
+
10
+ export interface SearchHit {
11
+ type: AgentikitAssetType
12
+ name: string
13
+ path: string
14
+ openRef: string
15
+ summary?: string
16
+ description?: string
17
+ tags?: string[]
18
+ score?: number
19
+ runCmd?: string
20
+ kind?: "bash" | "bun" | "powershell" | "cmd"
21
+ }
22
+
23
+ export interface SearchResponse {
24
+ stashDir: string
25
+ hits: SearchHit[]
26
+ tip?: string
27
+ }
28
+
29
+ export interface OpenResponse {
30
+ type: AgentikitAssetType
31
+ name: string
32
+ path: string
33
+ content?: string
34
+ template?: string
35
+ prompt?: string
36
+ description?: string
37
+ toolPolicy?: unknown
38
+ modelHint?: unknown
39
+ runCmd?: string
40
+ kind?: "bash" | "bun" | "powershell" | "cmd"
41
+ }
42
+
43
+ export interface RunResponse {
44
+ type: "tool"
45
+ name: string
46
+ path: string
47
+ output: string
48
+ exitCode: number
49
+ }
50
+
51
+ type IndexedAsset = {
52
+ type: AgentikitAssetType
53
+ name: string
54
+ path: string
55
+ }
56
+
57
+ interface ToolExecution {
58
+ command: string
59
+ args: string[]
60
+ cwd?: string
61
+ }
62
+
63
+ interface ToolInfo {
64
+ runCmd: string
65
+ kind: "bash" | "bun" | "powershell" | "cmd"
66
+ install?: ToolExecution
67
+ execute: ToolExecution
68
+ }
69
+
70
+ const IS_WINDOWS = process.platform === "win32"
71
+ const TOOL_EXTENSIONS = new Set([".sh", ".ts", ".js", ".ps1", ".cmd", ".bat"])
72
+ const DEFAULT_LIMIT = 20
73
+
74
+ export function resolveStashDir(): string {
75
+ const raw = process.env.AGENTIKIT_STASH_DIR?.trim()
76
+ if (!raw) {
77
+ throw new Error("AGENTIKIT_STASH_DIR is not set. Set it to your Agentikit stash path.")
78
+ }
79
+ const stashDir = path.resolve(raw)
80
+ let stat: fs.Stats
81
+ try {
82
+ stat = fs.statSync(stashDir)
83
+ } catch {
84
+ throw new Error(`Unable to read AGENTIKIT_STASH_DIR at "${stashDir}".`)
85
+ }
86
+ if (!stat.isDirectory()) {
87
+ throw new Error(`AGENTIKIT_STASH_DIR must point to a directory: "${stashDir}".`)
88
+ }
89
+ return stashDir
90
+ }
91
+
92
+ export function agentikitSearch(input: {
93
+ query: string
94
+ type?: AgentikitSearchType
95
+ limit?: number
96
+ }): SearchResponse {
97
+ const query = input.query.trim().toLowerCase()
98
+ const searchType = input.type ?? "any"
99
+ const limit = normalizeLimit(input.limit)
100
+ const stashDir = resolveStashDir()
101
+
102
+ // Try semantic search via persisted index
103
+ const semanticHits = trySemanticSearch(query, searchType, limit, stashDir)
104
+ if (semanticHits) {
105
+ return {
106
+ stashDir,
107
+ hits: semanticHits,
108
+ tip: semanticHits.length === 0 ? "No matching stash assets were found. Try running 'agentikit index' to rebuild." : undefined,
109
+ }
110
+ }
111
+
112
+ // Fallback: substring matching (no index built yet)
113
+ const assets = indexAssets(stashDir, searchType)
114
+ const hits = assets
115
+ .filter((asset) => asset.name.toLowerCase().includes(query))
116
+ .sort(compareAssets)
117
+ .slice(0, limit)
118
+ .map((asset): SearchHit => assetToSearchHit(asset, stashDir))
119
+
120
+ return {
121
+ stashDir,
122
+ hits,
123
+ tip: hits.length === 0 ? "No matching stash assets were found." : undefined,
124
+ }
125
+ }
126
+
127
+ function trySemanticSearch(
128
+ query: string,
129
+ searchType: AgentikitSearchType,
130
+ limit: number,
131
+ stashDir: string,
132
+ ): SearchHit[] | null {
133
+ const index = loadSearchIndex()
134
+ if (!index || !index.entries || index.entries.length === 0) return null
135
+ if (index.stashDir !== stashDir) return null
136
+
137
+ const scoredEntries: ScoredEntry[] = index.entries.map((ie) => ({
138
+ id: `${ie.entry.type}:${ie.entry.name}`,
139
+ text: buildSearchText(ie.entry),
140
+ entry: ie.entry,
141
+ path: ie.path,
142
+ }))
143
+
144
+ let adapter: TfIdfAdapter
145
+ if (index.tfidf) {
146
+ adapter = TfIdfAdapter.deserialize(index.tfidf as any, scoredEntries)
147
+ } else {
148
+ adapter = new TfIdfAdapter()
149
+ adapter.buildIndex(scoredEntries)
150
+ }
151
+
152
+ const typeFilter = searchType === "any" ? undefined : searchType
153
+ const results = adapter.search(query, limit, typeFilter)
154
+
155
+ return results.map((r): SearchHit => {
156
+ const hit: SearchHit = {
157
+ type: r.entry.type,
158
+ name: r.entry.name,
159
+ path: r.path,
160
+ openRef: makeOpenRef(r.entry.type, r.entry.name),
161
+ description: r.entry.description,
162
+ tags: r.entry.tags,
163
+ score: r.score,
164
+ }
165
+
166
+ if (r.entry.type === "tool") {
167
+ try {
168
+ const toolInfo = buildToolInfo(stashDir, r.path)
169
+ hit.runCmd = toolInfo.runCmd
170
+ hit.kind = toolInfo.kind
171
+ } catch {
172
+ // Tool file may have been removed since indexing
173
+ }
174
+ }
175
+
176
+ return hit
177
+ })
178
+ }
179
+
180
+ export function agentikitOpen(input: { ref: string }): OpenResponse {
181
+ const parsed = parseOpenRef(input.ref)
182
+ const stashDir = resolveStashDir()
183
+ const assetPath = resolveAssetPath(stashDir, parsed.type, parsed.name)
184
+ const content = fs.readFileSync(assetPath, "utf8")
185
+
186
+ switch (parsed.type) {
187
+ case "skill":
188
+ return {
189
+ type: "skill",
190
+ name: parsed.name,
191
+ path: assetPath,
192
+ content,
193
+ }
194
+ case "command": {
195
+ const parsedMd = parseFrontmatter(content)
196
+ return {
197
+ type: "command",
198
+ name: parsed.name,
199
+ path: assetPath,
200
+ description: toStringOrUndefined(parsedMd.data.description),
201
+ template: parsedMd.content,
202
+ }
203
+ }
204
+ case "agent": {
205
+ const parsedMd = parseFrontmatter(content)
206
+ return {
207
+ type: "agent",
208
+ name: parsed.name,
209
+ path: assetPath,
210
+ description: toStringOrUndefined(parsedMd.data.description),
211
+ prompt: parsedMd.content,
212
+ toolPolicy: parsedMd.data.tools,
213
+ modelHint: parsedMd.data.model,
214
+ }
215
+ }
216
+ case "tool": {
217
+ const toolInfo = buildToolInfo(stashDir, assetPath)
218
+ return {
219
+ type: "tool",
220
+ name: parsed.name,
221
+ path: assetPath,
222
+ runCmd: toolInfo.runCmd,
223
+ kind: toolInfo.kind,
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ export function agentikitRun(input: { ref: string }): RunResponse {
230
+ const parsed = parseOpenRef(input.ref)
231
+ if (parsed.type !== "tool") {
232
+ throw new Error(`agentikitRun only supports tool refs. Got: "${parsed.type}".`)
233
+ }
234
+ const stashDir = resolveStashDir()
235
+ const assetPath = resolveAssetPath(stashDir, "tool", parsed.name)
236
+ const toolInfo = buildToolInfo(stashDir, assetPath)
237
+
238
+ if (toolInfo.install) {
239
+ const installResult = runToolExecution(toolInfo.install)
240
+ if (installResult.exitCode !== 0) {
241
+ return {
242
+ type: "tool",
243
+ name: parsed.name,
244
+ path: assetPath,
245
+ output: installResult.output,
246
+ exitCode: installResult.exitCode,
247
+ }
248
+ }
249
+ }
250
+
251
+ const runResult = runToolExecution(toolInfo.execute)
252
+
253
+ return {
254
+ type: "tool",
255
+ name: parsed.name,
256
+ path: assetPath,
257
+ output: runResult.output,
258
+ exitCode: runResult.exitCode,
259
+ }
260
+ }
261
+
262
+ function assetToSearchHit(asset: IndexedAsset, stashDir: string): SearchHit {
263
+ if (asset.type !== "tool") {
264
+ return {
265
+ type: asset.type,
266
+ name: asset.name,
267
+ path: asset.path,
268
+ openRef: makeOpenRef(asset.type, asset.name),
269
+ }
270
+ }
271
+ const toolInfo = buildToolInfo(stashDir, asset.path)
272
+ return {
273
+ type: "tool",
274
+ name: asset.name,
275
+ path: asset.path,
276
+ openRef: makeOpenRef("tool", asset.name),
277
+ runCmd: toolInfo.runCmd,
278
+ kind: toolInfo.kind,
279
+ }
280
+ }
281
+
282
+ function normalizeLimit(limit?: number): number {
283
+ if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
284
+ return DEFAULT_LIMIT
285
+ }
286
+ return Math.min(Math.floor(limit), 200)
287
+ }
288
+
289
+ const ASSET_INDEXERS: Record<AgentikitAssetType, { dir: string; collect: (root: string, file: string) => IndexedAsset | undefined }> = {
290
+ tool: {
291
+ dir: "tools",
292
+ collect(root, file) {
293
+ if (!TOOL_EXTENSIONS.has(path.extname(file).toLowerCase())) return undefined
294
+ return { type: "tool", name: toPosix(path.relative(root, file)), path: file }
295
+ },
296
+ },
297
+ skill: {
298
+ dir: "skills",
299
+ collect(root, file) {
300
+ if (path.basename(file) !== "SKILL.md") return undefined
301
+ const relDir = toPosix(path.dirname(path.relative(root, file)))
302
+ if (!relDir || relDir === ".") return undefined
303
+ return { type: "skill", name: relDir, path: file }
304
+ },
305
+ },
306
+ command: {
307
+ dir: "commands",
308
+ collect(root, file) {
309
+ if (path.extname(file).toLowerCase() !== ".md") return undefined
310
+ return { type: "command", name: toPosix(path.relative(root, file)), path: file }
311
+ },
312
+ },
313
+ agent: {
314
+ dir: "agents",
315
+ collect(root, file) {
316
+ if (path.extname(file).toLowerCase() !== ".md") return undefined
317
+ return { type: "agent", name: toPosix(path.relative(root, file)), path: file }
318
+ },
319
+ },
320
+ }
321
+
322
+ function indexAssets(stashDir: string, type: AgentikitSearchType): IndexedAsset[] {
323
+ const assets: IndexedAsset[] = []
324
+ const types = type === "any" ? (Object.keys(ASSET_INDEXERS) as AgentikitAssetType[]) : [type]
325
+ for (const assetType of types) {
326
+ const indexer = ASSET_INDEXERS[assetType]
327
+ const root = path.join(stashDir, indexer.dir)
328
+ walkFiles(root, (file) => {
329
+ const asset = indexer.collect(root, file)
330
+ if (asset) assets.push(asset)
331
+ })
332
+ }
333
+ return assets
334
+ }
335
+
336
+ function walkFiles(root: string, onFile: (file: string) => void): void {
337
+ if (!fs.existsSync(root)) return
338
+ const stack = [root]
339
+ while (stack.length > 0) {
340
+ const current = stack.pop()
341
+ if (!current) continue
342
+ const entries = fs.readdirSync(current, { withFileTypes: true })
343
+ for (const entry of entries) {
344
+ const fullPath = path.join(current, entry.name)
345
+ if (entry.isDirectory()) {
346
+ stack.push(fullPath)
347
+ } else if (entry.isFile()) {
348
+ onFile(fullPath)
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ function compareAssets(a: IndexedAsset, b: IndexedAsset): number {
355
+ if (a.type !== b.type) return a.type.localeCompare(b.type)
356
+ return a.name.localeCompare(b.name)
357
+ }
358
+
359
+ function parseOpenRef(ref: string): { type: AgentikitAssetType; name: string } {
360
+ const separator = ref.indexOf(":")
361
+ if (separator <= 0) {
362
+ throw new Error("Invalid open ref. Expected format '<type>:<name>'.")
363
+ }
364
+ const rawType = ref.slice(0, separator)
365
+ const rawName = ref.slice(separator + 1)
366
+ if (!isAssetType(rawType)) {
367
+ throw new Error(`Invalid open ref type: "${rawType}".`)
368
+ }
369
+ let name: string
370
+ try {
371
+ name = decodeURIComponent(rawName)
372
+ } catch {
373
+ throw new Error("Invalid open ref encoding.")
374
+ }
375
+ const normalized = path.posix.normalize(name.replace(/\\/g, "/"))
376
+ if (
377
+ !name
378
+ || name.includes("\0")
379
+ || /^[A-Za-z]:/.test(name)
380
+ || path.posix.isAbsolute(normalized)
381
+ || normalized === ".."
382
+ || normalized.startsWith("../")
383
+ ) {
384
+ throw new Error("Invalid open ref name.")
385
+ }
386
+ return { type: rawType, name: normalized }
387
+ }
388
+
389
+ function makeOpenRef(type: AgentikitAssetType, name: string): string {
390
+ return `${type}:${encodeURIComponent(name)}`
391
+ }
392
+
393
+ function resolveAssetPath(stashDir: string, type: AgentikitAssetType, name: string): string {
394
+ const root = path.join(stashDir, type === "tool" ? "tools" : `${type}s`)
395
+ const target = type === "skill" ? path.join(root, name, "SKILL.md") : path.join(root, name)
396
+ const resolvedRoot = resolveAndValidateTypeRoot(root, type, name)
397
+ const resolvedTarget = path.resolve(target)
398
+ if (!isWithin(resolvedTarget, resolvedRoot)) {
399
+ throw new Error("Ref resolves outside the stash root.")
400
+ }
401
+ if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isFile()) {
402
+ throw new Error(`Stash asset not found for ref: ${type}:${name}`)
403
+ }
404
+ const realTarget = fs.realpathSync(resolvedTarget)
405
+ if (!isWithin(realTarget, resolvedRoot)) {
406
+ throw new Error("Ref resolves outside the stash root.")
407
+ }
408
+ if (type === "tool" && !TOOL_EXTENSIONS.has(path.extname(resolvedTarget).toLowerCase())) {
409
+ throw new Error("Tool ref must resolve to a .sh, .ts, .js, .ps1, .cmd, or .bat file.")
410
+ }
411
+ return realTarget
412
+ }
413
+
414
+ function resolveAndValidateTypeRoot(root: string, type: AgentikitAssetType, name: string): string {
415
+ const rootStat = readTypeRootStat(root, type, name)
416
+ if (!rootStat.isDirectory()) {
417
+ throw new Error(`Stash type root is not a directory for ref: ${type}:${name}`)
418
+ }
419
+ return fs.realpathSync(root)
420
+ }
421
+
422
+ function readTypeRootStat(root: string, type: AgentikitAssetType, name: string): fs.Stats {
423
+ try {
424
+ return fs.statSync(root)
425
+ } catch (error: unknown) {
426
+ if (hasErrnoCode(error, "ENOENT")) {
427
+ throw new Error(`Stash type root not found for ref: ${type}:${name}`)
428
+ }
429
+ throw error
430
+ }
431
+ }
432
+
433
+ function buildToolInfo(stashDir: string, filePath: string): ToolInfo {
434
+ const ext = path.extname(filePath).toLowerCase()
435
+
436
+ if (ext === ".sh") {
437
+ return {
438
+ runCmd: `bash ${shellQuote(filePath)}`,
439
+ kind: "bash",
440
+ execute: { command: "bash", args: [filePath] },
441
+ }
442
+ }
443
+
444
+ if (ext === ".ps1") {
445
+ return {
446
+ runCmd: `powershell -ExecutionPolicy Bypass -File ${shellQuote(filePath)}`,
447
+ kind: "powershell",
448
+ execute: { command: "powershell", args: ["-ExecutionPolicy", "Bypass", "-File", filePath] },
449
+ }
450
+ }
451
+
452
+ if (ext === ".cmd" || ext === ".bat") {
453
+ return {
454
+ runCmd: `cmd /c ${shellQuote(filePath)}`,
455
+ kind: "cmd",
456
+ execute: { command: "cmd", args: ["/c", filePath] },
457
+ }
458
+ }
459
+
460
+ if (ext !== ".ts" && ext !== ".js") {
461
+ throw new Error(`Unsupported tool extension: ${ext}`)
462
+ }
463
+
464
+ const toolsRoot = path.resolve(path.join(stashDir, "tools"))
465
+ const pkgDir = findNearestPackageDir(path.dirname(filePath), toolsRoot)
466
+ if (!pkgDir) {
467
+ return {
468
+ runCmd: `bun ${shellQuote(filePath)}`,
469
+ kind: "bun",
470
+ execute: { command: "bun", args: [filePath] },
471
+ }
472
+ }
473
+ const installFlag = process.env.AGENTIKIT_BUN_INSTALL
474
+ const shouldInstall = installFlag === "1" || installFlag === "true" || installFlag === "yes"
475
+
476
+ const quotedPkgDir = shellQuote(pkgDir)
477
+ const quotedFilePath = shellQuote(filePath)
478
+ const cdCmd = IS_WINDOWS ? `cd /d ${quotedPkgDir}` : `cd ${quotedPkgDir}`
479
+ const chain = IS_WINDOWS ? " & " : " && "
480
+ return {
481
+ runCmd: shouldInstall
482
+ ? `${cdCmd}${chain}bun install${chain}bun ${quotedFilePath}`
483
+ : `${cdCmd}${chain}bun ${quotedFilePath}`,
484
+ kind: "bun",
485
+ install: shouldInstall ? { command: "bun", args: ["install"], cwd: pkgDir } : undefined,
486
+ execute: { command: "bun", args: [filePath], cwd: pkgDir },
487
+ }
488
+ }
489
+
490
+ function findNearestPackageDir(startDir: string, toolsRoot: string): string | undefined {
491
+ let current = path.resolve(startDir)
492
+ const root = path.resolve(toolsRoot)
493
+ while (isWithin(current, root)) {
494
+ if (fs.existsSync(path.join(current, "package.json"))) {
495
+ return current
496
+ }
497
+ if (current === root) return undefined
498
+ current = path.dirname(current)
499
+ }
500
+ return undefined
501
+ }
502
+
503
+ function isWithin(candidate: string, root: string): boolean {
504
+ const normalizedRoot = normalizeFsPathForComparison(path.resolve(root))
505
+ const normalizedCandidate = normalizeFsPathForComparison(path.resolve(candidate))
506
+ const rel = path.relative(normalizedRoot, normalizedCandidate)
507
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
508
+ }
509
+
510
+ function normalizeFsPathForComparison(value: string): string {
511
+ return process.platform === "win32" ? value.toLowerCase() : value
512
+ }
513
+
514
+ function toPosix(input: string): string {
515
+ return input.split(path.sep).join("/")
516
+ }
517
+
518
+ function parseFrontmatter(raw: string): { data: Record<string, unknown>; content: string } {
519
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
520
+ if (!match) {
521
+ return { data: {}, content: raw }
522
+ }
523
+
524
+ const data: Record<string, unknown> = {}
525
+ let currentKey: string | null = null
526
+ let nested: Record<string, unknown> | null = null
527
+
528
+ for (const line of match[1].split(/\r?\n/)) {
529
+ const indented = line.match(/^ (\w[\w-]*):\s*(.+)$/)
530
+ if (indented && currentKey && nested) {
531
+ nested[indented[1]] = parseYamlScalar(indented[2].trim())
532
+ continue
533
+ }
534
+
535
+ const top = line.match(/^(\w[\w-]*):\s*(.*)$/)
536
+ if (!top) {
537
+ continue
538
+ }
539
+
540
+ currentKey = top[1]
541
+ const value = top[2].trim()
542
+ if (value === "") {
543
+ nested = {}
544
+ data[currentKey] = nested
545
+ } else {
546
+ nested = null
547
+ data[currentKey] = parseYamlScalar(value)
548
+ }
549
+ }
550
+ return { data, content: match[2] }
551
+ }
552
+
553
+ function parseYamlScalar(value: string): unknown {
554
+ if (value === "") return ""
555
+ if (value === "true") return true
556
+ if (value === "false") return false
557
+ const asNumber = Number(value)
558
+ if (!Number.isNaN(asNumber)) return asNumber
559
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
560
+ return value.slice(1, -1)
561
+ }
562
+ return value
563
+ }
564
+
565
+ function isAssetType(type: string): type is AgentikitAssetType {
566
+ return type === "tool" || type === "skill" || type === "command" || type === "agent"
567
+ }
568
+
569
+ function toStringOrUndefined(value: unknown): string | undefined {
570
+ return typeof value === "string" && value.trim() ? value : undefined
571
+ }
572
+
573
+ function shellQuote(input: string): string {
574
+ if (/[\r\n\t\0]/.test(input)) {
575
+ throw new Error("Unsupported control characters in stash path.")
576
+ }
577
+ if (IS_WINDOWS) {
578
+ return `"${input.replace(/"/g, '""')}"`
579
+ }
580
+ const escaped = input
581
+ .replace(/\\/g, "\\\\")
582
+ .replace(/"/g, '\\"')
583
+ .replace(/\$/g, "\\$")
584
+ .replace(/`/g, "\\`")
585
+ return `"${escaped}"`
586
+ }
587
+
588
+ function runToolExecution(execution: ToolExecution): { output: string; exitCode: number } {
589
+ const result = spawnSync(execution.command, execution.args, {
590
+ cwd: execution.cwd,
591
+ encoding: "utf8",
592
+ timeout: 60_000,
593
+ })
594
+
595
+ const stdout = typeof result.stdout === "string" ? result.stdout : ""
596
+ const stderr = typeof result.stderr === "string" ? result.stderr : ""
597
+ const combinedOutput = combineProcessOutput(stdout, stderr)
598
+ if (typeof result.status === "number") {
599
+ return { output: combinedOutput, exitCode: result.status }
600
+ }
601
+ if (result.error) {
602
+ return {
603
+ output: `${combinedOutput}${result.error.message ? `\n${result.error.message}` : ""}`.trim(),
604
+ exitCode: 1,
605
+ }
606
+ }
607
+ return {
608
+ output: combinedOutput || `Unexpected process termination while running "${execution.command}": no status code or error information available.`,
609
+ exitCode: 1,
610
+ }
611
+ }
612
+
613
+ function combineProcessOutput(stdout: string, stderr: string): string {
614
+ if (stdout && stderr) {
615
+ return `stdout:\n${stdout.trim()}\n\nstderr:\n${stderr.trim()}`
616
+ }
617
+ return `${stdout}${stderr}`.trim()
618
+ }
619
+
620
+ export interface InitResponse {
621
+ stashDir: string
622
+ created: boolean
623
+ envSet: boolean
624
+ profileUpdated?: string
625
+ }
626
+
627
+ export function agentikitInit(): InitResponse {
628
+ let stashDir: string
629
+ const home = process.env.HOME || ""
630
+ if (IS_WINDOWS) {
631
+ const docs = process.env.USERPROFILE
632
+ ? path.join(process.env.USERPROFILE, "Documents")
633
+ : ""
634
+ if (!docs) {
635
+ throw new Error("Unable to determine Documents folder. Ensure USERPROFILE is set.")
636
+ }
637
+ stashDir = path.join(docs, "agentikit")
638
+ } else {
639
+ if (!home) {
640
+ throw new Error("Unable to determine home directory. Set HOME.")
641
+ }
642
+ stashDir = path.join(home, "agentikit")
643
+ }
644
+
645
+ let created = false
646
+ if (!fs.existsSync(stashDir)) {
647
+ fs.mkdirSync(stashDir, { recursive: true })
648
+ created = true
649
+ }
650
+
651
+ for (const sub of ["tools", "skills", "commands", "agents"]) {
652
+ const subDir = path.join(stashDir, sub)
653
+ if (!fs.existsSync(subDir)) {
654
+ fs.mkdirSync(subDir, { recursive: true })
655
+ }
656
+ }
657
+
658
+ let envSet = false
659
+ let profileUpdated: string | undefined
660
+
661
+ if (IS_WINDOWS) {
662
+ const result = spawnSync("setx", ["AGENTIKIT_STASH_DIR", stashDir], {
663
+ encoding: "utf8",
664
+ timeout: 10_000,
665
+ })
666
+ envSet = result.status === 0
667
+ } else {
668
+ const shell = process.env.SHELL || ""
669
+ let profile: string
670
+ if (shell.endsWith("/zsh")) {
671
+ profile = path.join(home, ".zshrc")
672
+ } else if (shell.endsWith("/bash")) {
673
+ profile = path.join(home, ".bashrc")
674
+ } else {
675
+ profile = path.join(home, ".profile")
676
+ }
677
+
678
+ const exportLine = `export AGENTIKIT_STASH_DIR="${stashDir}"`
679
+ const existing = fs.existsSync(profile) ? fs.readFileSync(profile, "utf8") : ""
680
+ if (!existing.includes("AGENTIKIT_STASH_DIR")) {
681
+ fs.appendFileSync(profile, `\n# Agentikit stash directory\n${exportLine}\n`)
682
+ envSet = true
683
+ profileUpdated = profile
684
+ }
685
+ }
686
+
687
+ process.env.AGENTIKIT_STASH_DIR = stashDir
688
+
689
+ return { stashDir, created, envSet, profileUpdated }
690
+ }
691
+
692
+ function hasErrnoCode(error: unknown, code: string): boolean {
693
+ if (typeof error !== "object" || error === null || !("code" in error)) return false
694
+ return (error as Record<string, unknown>).code === code
695
+ }