agentikit 0.0.7 → 0.0.9

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 (98) hide show
  1. package/README.md +215 -76
  2. package/dist/index.d.ts +17 -3
  3. package/dist/index.js +10 -2
  4. package/dist/src/asset-spec.d.ts +14 -0
  5. package/dist/src/asset-spec.js +46 -0
  6. package/dist/src/cli.js +268 -57
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +37 -0
  10. package/dist/src/config.js +124 -0
  11. package/dist/src/embedder.d.ts +10 -0
  12. package/dist/src/embedder.js +87 -0
  13. package/dist/src/frontmatter.d.ts +30 -0
  14. package/dist/src/frontmatter.js +86 -0
  15. package/dist/src/indexer.d.ts +20 -2
  16. package/dist/src/indexer.js +212 -80
  17. package/dist/src/init.d.ts +19 -0
  18. package/dist/src/init.js +87 -0
  19. package/dist/src/llm.d.ts +15 -0
  20. package/dist/src/llm.js +91 -0
  21. package/dist/src/markdown.d.ts +18 -0
  22. package/dist/src/markdown.js +77 -0
  23. package/dist/src/metadata.d.ts +11 -2
  24. package/dist/src/metadata.js +161 -29
  25. package/dist/src/registry-install.d.ts +11 -0
  26. package/dist/src/registry-install.js +208 -0
  27. package/dist/src/registry-resolve.d.ts +3 -0
  28. package/dist/src/registry-resolve.js +231 -0
  29. package/dist/src/registry-search.d.ts +5 -0
  30. package/dist/src/registry-search.js +129 -0
  31. package/dist/src/registry-types.d.ts +55 -0
  32. package/dist/src/registry-types.js +1 -0
  33. package/dist/src/ripgrep-install.d.ts +12 -0
  34. package/dist/src/ripgrep-install.js +169 -0
  35. package/dist/src/ripgrep-resolve.d.ts +13 -0
  36. package/dist/src/ripgrep-resolve.js +68 -0
  37. package/dist/src/ripgrep.d.ts +3 -36
  38. package/dist/src/ripgrep.js +2 -262
  39. package/dist/src/similarity.d.ts +1 -2
  40. package/dist/src/similarity.js +11 -0
  41. package/dist/src/stash-add.d.ts +4 -0
  42. package/dist/src/stash-add.js +59 -0
  43. package/dist/src/stash-ref.d.ts +7 -0
  44. package/dist/src/stash-ref.js +33 -0
  45. package/dist/src/stash-registry.d.ts +18 -0
  46. package/dist/src/stash-registry.js +221 -0
  47. package/dist/src/stash-resolve.d.ts +2 -0
  48. package/dist/src/stash-resolve.js +45 -0
  49. package/dist/src/stash-search.d.ts +8 -0
  50. package/dist/src/stash-search.js +484 -0
  51. package/dist/src/stash-show.d.ts +5 -0
  52. package/dist/src/stash-show.js +114 -0
  53. package/dist/src/stash-types.d.ts +217 -0
  54. package/dist/src/stash-types.js +1 -0
  55. package/dist/src/stash.d.ts +10 -63
  56. package/dist/src/stash.js +6 -633
  57. package/dist/src/tool-runner.d.ts +35 -0
  58. package/dist/src/tool-runner.js +100 -0
  59. package/dist/src/walker.d.ts +19 -0
  60. package/dist/src/walker.js +47 -0
  61. package/package.json +8 -14
  62. package/src/asset-spec.ts +69 -0
  63. package/src/cli.ts +282 -46
  64. package/src/common.ts +58 -0
  65. package/src/config.ts +183 -0
  66. package/src/embedder.ts +117 -0
  67. package/src/frontmatter.ts +95 -0
  68. package/src/indexer.ts +244 -84
  69. package/src/init.ts +106 -0
  70. package/src/llm.ts +124 -0
  71. package/src/markdown.ts +106 -0
  72. package/src/metadata.ts +171 -27
  73. package/src/registry-install.ts +245 -0
  74. package/src/registry-resolve.ts +272 -0
  75. package/src/registry-search.ts +145 -0
  76. package/src/registry-types.ts +64 -0
  77. package/src/ripgrep-install.ts +200 -0
  78. package/src/ripgrep-resolve.ts +72 -0
  79. package/src/ripgrep.ts +3 -315
  80. package/src/similarity.ts +13 -1
  81. package/src/stash-add.ts +66 -0
  82. package/src/stash-ref.ts +41 -0
  83. package/src/stash-registry.ts +259 -0
  84. package/src/stash-resolve.ts +47 -0
  85. package/src/stash-search.ts +595 -0
  86. package/src/stash-show.ts +112 -0
  87. package/src/stash-types.ts +221 -0
  88. package/src/stash.ts +31 -760
  89. package/src/tool-runner.ts +129 -0
  90. package/src/walker.ts +53 -0
  91. package/.claude-plugin/plugin.json +0 -21
  92. package/commands/open.md +0 -11
  93. package/commands/run.md +0 -11
  94. package/commands/search.md +0 -11
  95. package/dist/src/plugin.d.ts +0 -2
  96. package/dist/src/plugin.js +0 -55
  97. package/skills/stash/SKILL.md +0 -73
  98. package/src/plugin.ts +0 -56
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared frontmatter parsing utilities.
3
+ *
4
+ * Provides a single, canonical YAML-subset frontmatter parser used by both
5
+ * the stash open logic and the metadata generator.
6
+ */
7
+ /**
8
+ * Parse YAML-subset frontmatter from a Markdown (or similar) string.
9
+ *
10
+ * Returns the parsed key-value data and the remaining body content.
11
+ */
12
+ export declare function parseFrontmatter(raw: string): {
13
+ data: Record<string, unknown>;
14
+ content: string;
15
+ frontmatter: string | null;
16
+ bodyStartLine: number;
17
+ };
18
+ export declare function parseFrontmatterBlock(raw: string): {
19
+ frontmatter: string;
20
+ content: string;
21
+ bodyStartLine: number;
22
+ } | null;
23
+ /**
24
+ * Parse a simple YAML scalar value (string, boolean, or number).
25
+ */
26
+ export declare function parseYamlScalar(value: string): unknown;
27
+ /**
28
+ * Coerce an unknown value to a trimmed string, or return undefined if empty/non-string.
29
+ */
30
+ export declare function toStringOrUndefined(value: unknown): string | undefined;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared frontmatter parsing utilities.
3
+ *
4
+ * Provides a single, canonical YAML-subset frontmatter parser used by both
5
+ * the stash open logic and the metadata generator.
6
+ */
7
+ /**
8
+ * Parse YAML-subset frontmatter from a Markdown (or similar) string.
9
+ *
10
+ * Returns the parsed key-value data and the remaining body content.
11
+ */
12
+ export function parseFrontmatter(raw) {
13
+ const parsedBlock = parseFrontmatterBlock(raw);
14
+ if (!parsedBlock) {
15
+ return { data: {}, content: raw, frontmatter: null, bodyStartLine: 1 };
16
+ }
17
+ const data = {};
18
+ let currentKey = null;
19
+ let nested = null;
20
+ for (const line of parsedBlock.frontmatter.split(/\r?\n/)) {
21
+ const indented = line.match(/^ (\w[\w-]*):\s*(.+)$/);
22
+ if (indented && currentKey && nested) {
23
+ nested[indented[1]] = parseYamlScalar(indented[2].trim());
24
+ continue;
25
+ }
26
+ const top = line.match(/^(\w[\w-]*):\s*(.*)$/);
27
+ if (!top) {
28
+ continue;
29
+ }
30
+ currentKey = top[1];
31
+ const value = top[2].trim();
32
+ if (value === "") {
33
+ nested = {};
34
+ data[currentKey] = nested;
35
+ }
36
+ else {
37
+ nested = null;
38
+ data[currentKey] = parseYamlScalar(value);
39
+ }
40
+ }
41
+ return {
42
+ data,
43
+ content: parsedBlock.content,
44
+ frontmatter: parsedBlock.frontmatter,
45
+ bodyStartLine: parsedBlock.bodyStartLine,
46
+ };
47
+ }
48
+ export function parseFrontmatterBlock(raw) {
49
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
50
+ if (!match)
51
+ return null;
52
+ return {
53
+ frontmatter: match[1],
54
+ content: match[2],
55
+ bodyStartLine: countLines(raw.slice(0, match[0].length - match[2].length)) + 1,
56
+ };
57
+ }
58
+ function countLines(text) {
59
+ if (text.length === 0)
60
+ return 0;
61
+ return text.split(/\r?\n/).length - 1;
62
+ }
63
+ /**
64
+ * Parse a simple YAML scalar value (string, boolean, or number).
65
+ */
66
+ export function parseYamlScalar(value) {
67
+ if (value === "")
68
+ return "";
69
+ if (value === "true")
70
+ return true;
71
+ if (value === "false")
72
+ return false;
73
+ const asNumber = Number(value);
74
+ if (!Number.isNaN(asNumber))
75
+ return asNumber;
76
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
77
+ return value.slice(1, -1);
78
+ }
79
+ return value;
80
+ }
81
+ /**
82
+ * Coerce an unknown value to a trimmed string, or return undefined if empty/non-string.
83
+ */
84
+ export function toStringOrUndefined(value) {
85
+ return typeof value === "string" && value.trim() ? value : undefined;
86
+ }
@@ -1,26 +1,44 @@
1
1
  import { type StashEntry } from "./metadata";
2
+ import { type SerializedTfIdf } from "./similarity";
3
+ import type { EmbeddingVector } from "./embedder";
2
4
  export interface IndexedEntry {
3
5
  entry: StashEntry;
4
6
  path: string;
5
7
  dirPath: string;
8
+ embedding?: EmbeddingVector;
6
9
  }
7
10
  export interface SearchIndex {
8
11
  version: number;
9
12
  builtAt: string;
10
13
  stashDir: string;
14
+ /** All stash directories that were indexed (primary + additional) */
15
+ stashDirs?: string[];
11
16
  entries: IndexedEntry[];
12
17
  /** Serialized TF-IDF state (term frequencies, idf values) */
13
- tfidf?: unknown;
18
+ tfidf?: SerializedTfIdf;
19
+ /** Whether embeddings are included in entries */
20
+ hasEmbeddings?: boolean;
14
21
  }
15
22
  export interface IndexResponse {
16
23
  stashDir: string;
17
24
  totalEntries: number;
18
25
  generatedMetadata: number;
19
26
  indexPath: string;
27
+ mode: "full" | "incremental";
28
+ directoriesScanned: number;
29
+ directoriesSkipped: number;
30
+ /** Timing counters in milliseconds */
31
+ timing?: {
32
+ totalMs: number;
33
+ walkMs: number;
34
+ embedMs: number;
35
+ tfidfMs: number;
36
+ };
20
37
  }
21
38
  export declare function getIndexPath(): string;
22
39
  export declare function loadSearchIndex(): SearchIndex | null;
23
40
  export declare function agentikitIndex(options?: {
24
41
  stashDir?: string;
25
- }): IndexResponse;
42
+ full?: boolean;
43
+ }): Promise<IndexResponse>;
26
44
  export declare function buildSearchText(entry: StashEntry): string;
@@ -1,16 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { resolveStashDir } from "./common";
4
+ import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec";
3
5
  import { loadStashFile, writeStashFile, generateMetadata, } from "./metadata";
4
6
  import { TfIdfAdapter } from "./similarity";
7
+ import { walkStash } from "./walker";
5
8
  // ── Constants ───────────────────────────────────────────────────────────────
6
- const INDEX_VERSION = 1;
7
- const SCRIPT_EXTENSIONS = new Set([".sh", ".ts", ".js", ".ps1", ".cmd", ".bat"]);
8
- const TYPE_DIRS = {
9
- tool: "tools",
10
- skill: "skills",
11
- command: "commands",
12
- agent: "agents",
13
- };
9
+ const INDEX_VERSION = 4;
14
10
  // ── Index Path ──────────────────────────────────────────────────────────────
15
11
  export function getIndexPath() {
16
12
  const cacheDir = process.env.XDG_CACHE_HOME
@@ -32,37 +28,98 @@ export function loadSearchIndex() {
32
28
  }
33
29
  }
34
30
  // ── Indexer ──────────────────────────────────────────────────────────────────
35
- export function agentikitIndex(options) {
36
- const stashDir = options?.stashDir || resolveStashDirForIndex();
31
+ export async function agentikitIndex(options) {
32
+ const stashDir = options?.stashDir || resolveStashDir();
33
+ // Load config to get additional stash dirs and semantic search setting
34
+ const { loadConfig } = await import("./config.js");
35
+ const config = loadConfig(stashDir);
36
+ const allStashDirs = [stashDir];
37
+ for (const d of config.additionalStashDirs) {
38
+ try {
39
+ if (fs.statSync(d).isDirectory() && !allStashDirs.includes(path.resolve(d))) {
40
+ allStashDirs.push(path.resolve(d));
41
+ }
42
+ }
43
+ catch { /* skip nonexistent dirs */ }
44
+ }
45
+ const t0 = Date.now();
37
46
  const allEntries = [];
38
47
  let generatedCount = 0;
39
- for (const assetType of Object.keys(TYPE_DIRS)) {
40
- const typeRoot = path.join(stashDir, TYPE_DIRS[assetType]);
41
- if (!fs.existsSync(typeRoot) || !fs.statSync(typeRoot).isDirectory())
42
- continue;
43
- // Group files by their immediate parent directory
44
- const dirGroups = collectDirectoryGroups(typeRoot, assetType);
45
- for (const [dirPath, files] of dirGroups) {
46
- // Try loading existing .stash.json
47
- let stash = loadStashFile(dirPath);
48
- if (!stash) {
49
- // Generate metadata
50
- stash = generateMetadata(dirPath, assetType, files);
51
- if (stash.entries.length > 0) {
52
- writeStashFile(dirPath, stash);
53
- generatedCount += stash.entries.length;
54
- }
48
+ let scannedDirs = 0;
49
+ let skippedDirs = 0;
50
+ // Load previous index for incremental mode
51
+ const previousIndex = !options?.full ? loadSearchIndex() : null;
52
+ const isIncremental = previousIndex !== null && previousIndex.stashDir === stashDir;
53
+ const builtAtMs = isIncremental ? new Date(previousIndex.builtAt).getTime() : 0;
54
+ // Build lookup of previous entries by dirPath
55
+ const previousEntriesByDir = new Map();
56
+ if (isIncremental) {
57
+ for (const ie of previousIndex.entries) {
58
+ const list = previousEntriesByDir.get(ie.dirPath) || [];
59
+ list.push(ie);
60
+ previousEntriesByDir.set(ie.dirPath, list);
61
+ }
62
+ }
63
+ const seenPaths = new Set();
64
+ const tWalkStart = Date.now();
65
+ for (const currentStashDir of allStashDirs) {
66
+ for (const assetType of ASSET_TYPES) {
67
+ const typeRoot = path.join(currentStashDir, TYPE_DIRS[assetType]);
68
+ try {
69
+ if (!fs.statSync(typeRoot).isDirectory())
70
+ continue;
71
+ }
72
+ catch {
73
+ continue;
55
74
  }
56
- if (stash) {
57
- for (const entry of stash.entries) {
58
- const entryPath = entry.entry
59
- ? path.join(dirPath, entry.entry)
60
- : files[0] || dirPath;
61
- allEntries.push({ entry, path: entryPath, dirPath });
75
+ // Group files by their immediate parent directory
76
+ const dirGroups = walkStash(typeRoot, assetType);
77
+ for (const { dirPath, files } of dirGroups) {
78
+ // Deduplicate by dirPath across stash dirs
79
+ if (seenPaths.has(path.resolve(dirPath)))
80
+ continue;
81
+ seenPaths.add(path.resolve(dirPath));
82
+ // Incremental: skip directories that haven't changed
83
+ const prevEntries = previousEntriesByDir.get(dirPath);
84
+ if (isIncremental && prevEntries && !isDirStale(dirPath, files, prevEntries, builtAtMs)) {
85
+ allEntries.push(...prevEntries);
86
+ skippedDirs++;
87
+ continue;
88
+ }
89
+ scannedDirs++;
90
+ // Try loading existing .stash.json
91
+ let stash = loadStashFile(dirPath);
92
+ if (stash) {
93
+ const migration = migrateGeneratedSkillMetadata(stash, files, typeRoot);
94
+ if (migration.changed) {
95
+ stash = migration.stash;
96
+ writeStashFile(dirPath, stash);
97
+ }
98
+ }
99
+ if (!stash) {
100
+ // Generate metadata
101
+ stash = generateMetadata(dirPath, assetType, files, typeRoot);
102
+ // Enhance with LLM if configured
103
+ if (config.llm && stash.entries.length > 0) {
104
+ stash = await enhanceStashWithLlm(config.llm, stash, dirPath, files);
105
+ }
106
+ if (stash.entries.length > 0) {
107
+ writeStashFile(dirPath, stash);
108
+ generatedCount += stash.entries.length;
109
+ }
110
+ }
111
+ if (stash) {
112
+ for (const entry of stash.entries) {
113
+ const entryPath = entry.entry
114
+ ? path.join(dirPath, entry.entry)
115
+ : files[0] || dirPath;
116
+ allEntries.push({ entry, path: entryPath, dirPath });
117
+ }
62
118
  }
63
119
  }
64
120
  }
65
121
  }
122
+ const tWalkEnd = Date.now();
66
123
  // Build TF-IDF index
67
124
  const adapter = new TfIdfAdapter();
68
125
  const scoredEntries = allEntries.map((ie) => ({
@@ -72,6 +129,25 @@ export function agentikitIndex(options) {
72
129
  path: ie.path,
73
130
  }));
74
131
  adapter.buildIndex(scoredEntries);
132
+ const tTfidfEnd = Date.now();
133
+ // Generate embeddings if semantic search is enabled
134
+ let hasEmbeddings = false;
135
+ if (config.semanticSearch) {
136
+ try {
137
+ const { embed } = await import("./embedder.js");
138
+ for (const ie of allEntries) {
139
+ if (!ie.embedding) {
140
+ const text = buildSearchText(ie.entry);
141
+ ie.embedding = await embed(text, config.embedding);
142
+ }
143
+ }
144
+ hasEmbeddings = true;
145
+ }
146
+ catch {
147
+ // Embedding provider not available, continue without embeddings
148
+ }
149
+ }
150
+ const tEmbedEnd = Date.now();
75
151
  // Persist index
76
152
  const indexPath = getIndexPath();
77
153
  const indexDir = path.dirname(indexPath);
@@ -82,59 +158,119 @@ export function agentikitIndex(options) {
82
158
  version: INDEX_VERSION,
83
159
  builtAt: new Date().toISOString(),
84
160
  stashDir,
161
+ stashDirs: allStashDirs,
85
162
  entries: allEntries,
86
163
  tfidf: adapter.serialize(),
164
+ hasEmbeddings,
87
165
  };
88
166
  fs.writeFileSync(indexPath, JSON.stringify(index) + "\n", "utf8");
167
+ const tEnd = Date.now();
89
168
  return {
90
169
  stashDir,
91
170
  totalEntries: allEntries.length,
92
171
  generatedMetadata: generatedCount,
93
172
  indexPath,
173
+ mode: isIncremental ? "incremental" : "full",
174
+ directoriesScanned: scannedDirs,
175
+ directoriesSkipped: skippedDirs,
176
+ timing: {
177
+ totalMs: tEnd - t0,
178
+ walkMs: tWalkEnd - tWalkStart, // includes metadata generation (interleaved)
179
+ embedMs: tEmbedEnd - tTfidfEnd,
180
+ tfidfMs: tTfidfEnd - tWalkEnd,
181
+ },
94
182
  };
95
183
  }
96
184
  // ── Helpers ─────────────────────────────────────────────────────────────────
97
- function collectDirectoryGroups(typeRoot, assetType) {
98
- const groups = new Map();
99
- const walk = (dir) => {
100
- if (!fs.existsSync(dir))
101
- return;
102
- const entries = fs.readdirSync(dir, { withFileTypes: true });
103
- for (const entry of entries) {
104
- if (entry.name === ".stash.json")
105
- continue;
106
- const fullPath = path.join(dir, entry.name);
107
- if (entry.isDirectory()) {
108
- walk(fullPath);
109
- }
110
- else if (entry.isFile() && isRelevantFile(entry.name, assetType)) {
111
- const parentDir = path.dirname(fullPath);
112
- const existing = groups.get(parentDir);
113
- if (existing) {
114
- existing.push(fullPath);
115
- }
116
- else {
117
- groups.set(parentDir, [fullPath]);
118
- }
119
- }
185
+ function isDirStale(dirPath, currentFiles, previousEntries, builtAtMs) {
186
+ // Check if file set changed (additions or deletions)
187
+ const prevFileNames = new Set(previousEntries
188
+ .map((ie) => ie.entry.entry)
189
+ .filter((e) => !!e));
190
+ const currFileNames = new Set(currentFiles.map((f) => path.basename(f)));
191
+ if (prevFileNames.size !== currFileNames.size)
192
+ return true;
193
+ for (const name of currFileNames) {
194
+ if (!prevFileNames.has(name))
195
+ return true;
196
+ }
197
+ // Check modification times of current files
198
+ for (const file of currentFiles) {
199
+ try {
200
+ if (fs.statSync(file).mtimeMs > builtAtMs)
201
+ return true;
202
+ }
203
+ catch {
204
+ return true;
120
205
  }
206
+ }
207
+ // Check .stash.json modification time
208
+ const stashPath = path.join(dirPath, ".stash.json");
209
+ try {
210
+ if (fs.existsSync(stashPath) && fs.statSync(stashPath).mtimeMs > builtAtMs)
211
+ return true;
212
+ }
213
+ catch {
214
+ // ignore
215
+ }
216
+ return false;
217
+ }
218
+ function migrateGeneratedSkillMetadata(stash, files, typeRoot) {
219
+ const fileByBaseName = new Map(files.map((filePath) => [path.basename(filePath), filePath]));
220
+ let changed = false;
221
+ const entries = stash.entries.map((entry) => {
222
+ if (entry.type !== "skill" || entry.generated !== true)
223
+ return entry;
224
+ const hintedFilePath = entry.entry ? fileByBaseName.get(path.basename(entry.entry)) : undefined;
225
+ const skillFilePath = hintedFilePath ?? fileByBaseName.get("SKILL.md");
226
+ if (!skillFilePath)
227
+ return entry;
228
+ const canonicalName = deriveCanonicalAssetName("skill", typeRoot, skillFilePath);
229
+ if (!canonicalName || canonicalName === entry.name)
230
+ return entry;
231
+ changed = true;
232
+ return { ...entry, name: canonicalName };
233
+ });
234
+ if (!changed) {
235
+ return { stash, changed: false };
236
+ }
237
+ return {
238
+ stash: { entries },
239
+ changed: true,
121
240
  };
122
- walk(typeRoot);
123
- return groups;
124
241
  }
125
- function isRelevantFile(fileName, assetType) {
126
- const ext = path.extname(fileName).toLowerCase();
127
- switch (assetType) {
128
- case "tool":
129
- return SCRIPT_EXTENSIONS.has(ext);
130
- case "skill":
131
- return fileName === "SKILL.md";
132
- case "command":
133
- case "agent":
134
- return ext === ".md";
135
- default:
136
- return false;
242
+ async function enhanceStashWithLlm(llmConfig, stash, dirPath, files) {
243
+ const { enhanceMetadata } = await import("./llm.js");
244
+ const enhanced = [];
245
+ for (const entry of stash.entries) {
246
+ try {
247
+ // Find the file matching this entry for content context
248
+ const entryFile = entry.entry
249
+ ? files.find((f) => path.basename(f) === entry.entry) ?? files[0]
250
+ : files[0];
251
+ let fileContent;
252
+ if (entryFile) {
253
+ try {
254
+ fileContent = fs.readFileSync(entryFile, "utf8");
255
+ }
256
+ catch { /* ignore unreadable files */ }
257
+ }
258
+ const improvements = await enhanceMetadata(llmConfig, entry, fileContent);
259
+ const updated = { ...entry };
260
+ if (improvements.description)
261
+ updated.description = improvements.description;
262
+ if (improvements.intents?.length)
263
+ updated.intents = improvements.intents;
264
+ if (improvements.tags?.length)
265
+ updated.tags = improvements.tags;
266
+ enhanced.push(updated);
267
+ }
268
+ catch {
269
+ // LLM enhancement failed for this entry, keep original
270
+ enhanced.push(entry);
271
+ }
137
272
  }
273
+ return { entries: enhanced };
138
274
  }
139
275
  export function buildSearchText(entry) {
140
276
  const parts = [entry.name.replace(/[-_]/g, " ")];
@@ -144,6 +280,10 @@ export function buildSearchText(entry) {
144
280
  parts.push(entry.tags.join(" "));
145
281
  if (entry.examples)
146
282
  parts.push(entry.examples.join(" "));
283
+ if (entry.aliases)
284
+ parts.push(entry.aliases.join(" "));
285
+ if (entry.intents)
286
+ parts.push(entry.intents.join(" "));
147
287
  if (entry.intent) {
148
288
  if (entry.intent.when)
149
289
  parts.push(entry.intent.when);
@@ -152,16 +292,8 @@ export function buildSearchText(entry) {
152
292
  if (entry.intent.output)
153
293
  parts.push(entry.intent.output);
154
294
  }
155
- return parts.join(" ").toLowerCase();
156
- }
157
- function resolveStashDirForIndex() {
158
- const raw = process.env.AGENTIKIT_STASH_DIR?.trim();
159
- if (!raw) {
160
- throw new Error("AGENTIKIT_STASH_DIR is not set. Run 'agentikit init' first.");
295
+ if (entry.toc) {
296
+ parts.push(entry.toc.map((h) => h.text).join(" "));
161
297
  }
162
- const stashDir = path.resolve(raw);
163
- if (!fs.existsSync(stashDir) || !fs.statSync(stashDir).isDirectory()) {
164
- throw new Error(`AGENTIKIT_STASH_DIR does not exist or is not a directory: "${stashDir}"`);
165
- }
166
- return stashDir;
298
+ return parts.join(" ").toLowerCase();
167
299
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Agentikit initialization logic.
3
+ *
4
+ * Creates the stash directory structure, sets the AGENTIKIT_STASH_DIR
5
+ * environment variable, and ensures ripgrep is available.
6
+ */
7
+ export interface InitResponse {
8
+ stashDir: string;
9
+ created: boolean;
10
+ envSet: boolean;
11
+ profileUpdated?: string;
12
+ configPath: string;
13
+ ripgrep?: {
14
+ rgPath: string;
15
+ installed: boolean;
16
+ version: string;
17
+ };
18
+ }
19
+ export declare function agentikitInit(): InitResponse;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Agentikit initialization logic.
3
+ *
4
+ * Creates the stash directory structure, sets the AGENTIKIT_STASH_DIR
5
+ * environment variable, and ensures ripgrep is available.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { IS_WINDOWS, TYPE_DIRS } from "./common";
11
+ import { ensureRg } from "./ripgrep-install";
12
+ import { getConfigPath, saveConfig, DEFAULT_CONFIG } from "./config";
13
+ export function agentikitInit() {
14
+ let stashDir;
15
+ if (IS_WINDOWS) {
16
+ const userProfile = process.env.USERPROFILE?.trim();
17
+ if (!userProfile) {
18
+ throw new Error("Unable to determine Documents folder. Ensure USERPROFILE is set.");
19
+ }
20
+ const docs = path.join(userProfile, "Documents");
21
+ stashDir = path.join(docs, "agentikit");
22
+ }
23
+ else {
24
+ const home = process.env.HOME?.trim();
25
+ if (!home) {
26
+ throw new Error("Unable to determine home directory. Set HOME.");
27
+ }
28
+ stashDir = path.join(home, "agentikit");
29
+ }
30
+ let created = false;
31
+ if (!fs.existsSync(stashDir)) {
32
+ fs.mkdirSync(stashDir, { recursive: true });
33
+ created = true;
34
+ }
35
+ for (const sub of Object.values(TYPE_DIRS)) {
36
+ const subDir = path.join(stashDir, sub);
37
+ if (!fs.existsSync(subDir)) {
38
+ fs.mkdirSync(subDir, { recursive: true });
39
+ }
40
+ }
41
+ let envSet = false;
42
+ let profileUpdated;
43
+ if (IS_WINDOWS) {
44
+ const result = spawnSync("setx", ["AGENTIKIT_STASH_DIR", stashDir], {
45
+ encoding: "utf8",
46
+ timeout: 10_000,
47
+ });
48
+ envSet = result.status === 0;
49
+ }
50
+ else {
51
+ const shell = process.env.SHELL || "";
52
+ const homeDir = process.env.HOME; // already validated non-empty above
53
+ let profile;
54
+ if (shell.endsWith("/zsh")) {
55
+ profile = path.join(homeDir, ".zshrc");
56
+ }
57
+ else if (shell.endsWith("/bash")) {
58
+ profile = path.join(homeDir, ".bashrc");
59
+ }
60
+ else {
61
+ profile = path.join(homeDir, ".profile");
62
+ }
63
+ const exportLine = `export AGENTIKIT_STASH_DIR="${stashDir}"`;
64
+ const existing = fs.existsSync(profile) ? fs.readFileSync(profile, "utf8") : "";
65
+ if (!existing.includes("AGENTIKIT_STASH_DIR")) {
66
+ fs.appendFileSync(profile, `\n# Agentikit stash directory\n${exportLine}\n`);
67
+ envSet = true;
68
+ profileUpdated = profile;
69
+ }
70
+ }
71
+ // Create default config.json if it doesn't exist
72
+ const configPath = getConfigPath(stashDir);
73
+ if (!fs.existsSync(configPath)) {
74
+ saveConfig(DEFAULT_CONFIG, stashDir);
75
+ }
76
+ process.env.AGENTIKIT_STASH_DIR = stashDir;
77
+ // Ensure ripgrep is available (install to stash/bin if needed)
78
+ let ripgrep;
79
+ try {
80
+ const rgResult = ensureRg(stashDir);
81
+ ripgrep = rgResult;
82
+ }
83
+ catch {
84
+ // Non-fatal: ripgrep is optional, search works without it
85
+ }
86
+ return { stashDir, created, envSet, profileUpdated, configPath, ripgrep };
87
+ }
@@ -0,0 +1,15 @@
1
+ import type { LlmConnectionConfig } from "./config";
2
+ import type { StashEntry } from "./metadata";
3
+ /**
4
+ * Use an LLM to enhance a stash entry's metadata: improve description,
5
+ * generate intents, and suggest tags.
6
+ */
7
+ export declare function enhanceMetadata(config: LlmConnectionConfig, entry: StashEntry, fileContent?: string): Promise<{
8
+ description?: string;
9
+ intents?: string[];
10
+ tags?: string[];
11
+ }>;
12
+ /**
13
+ * Check if the LLM endpoint is reachable.
14
+ */
15
+ export declare function isLlmAvailable(config: LlmConnectionConfig): Promise<boolean>;