agentikit 0.0.7 → 0.0.8
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 +113 -77
- package/dist/index.d.ts +13 -3
- package/dist/index.js +7 -2
- package/dist/src/asset-spec.d.ts +14 -0
- package/dist/src/asset-spec.js +46 -0
- package/dist/src/cli.js +154 -52
- package/dist/src/common.d.ts +8 -0
- package/dist/src/common.js +46 -0
- package/dist/src/config.d.ts +31 -0
- package/dist/src/config.js +74 -0
- package/dist/src/embedder.d.ts +10 -0
- package/dist/src/embedder.js +87 -0
- package/dist/src/frontmatter.d.ts +30 -0
- package/dist/src/frontmatter.js +86 -0
- package/dist/src/indexer.d.ts +20 -2
- package/dist/src/indexer.js +212 -80
- package/dist/src/init.d.ts +19 -0
- package/dist/src/init.js +87 -0
- package/dist/src/llm.d.ts +15 -0
- package/dist/src/llm.js +91 -0
- package/dist/src/markdown.d.ts +18 -0
- package/dist/src/markdown.js +77 -0
- package/dist/src/metadata.d.ts +10 -2
- package/dist/src/metadata.js +146 -30
- package/dist/src/ripgrep-install.d.ts +12 -0
- package/dist/src/ripgrep-install.js +169 -0
- package/dist/src/ripgrep-resolve.d.ts +13 -0
- package/dist/src/ripgrep-resolve.js +68 -0
- package/dist/src/ripgrep.d.ts +3 -36
- package/dist/src/ripgrep.js +2 -262
- package/dist/src/similarity.d.ts +1 -2
- package/dist/src/similarity.js +11 -0
- package/dist/src/stash-ref.d.ts +7 -0
- package/dist/src/stash-ref.js +33 -0
- package/dist/src/stash-resolve.d.ts +2 -0
- package/dist/src/stash-resolve.js +45 -0
- package/dist/src/stash-search.d.ts +6 -0
- package/dist/src/stash-search.js +269 -0
- package/dist/src/stash-show.d.ts +5 -0
- package/dist/src/stash-show.js +107 -0
- package/dist/src/stash-types.d.ts +53 -0
- package/dist/src/stash-types.js +1 -0
- package/dist/src/stash.d.ts +8 -63
- package/dist/src/stash.js +4 -633
- package/dist/src/tool-runner.d.ts +35 -0
- package/dist/src/tool-runner.js +100 -0
- package/dist/src/walker.d.ts +19 -0
- package/dist/src/walker.js +47 -0
- package/package.json +8 -14
- package/src/asset-spec.ts +69 -0
- package/src/cli.ts +164 -48
- package/src/common.ts +58 -0
- package/src/config.ts +124 -0
- package/src/embedder.ts +117 -0
- package/src/frontmatter.ts +95 -0
- package/src/indexer.ts +244 -84
- package/src/init.ts +106 -0
- package/src/llm.ts +124 -0
- package/src/markdown.ts +106 -0
- package/src/metadata.ts +157 -29
- package/src/ripgrep-install.ts +200 -0
- package/src/ripgrep-resolve.ts +72 -0
- package/src/ripgrep.ts +3 -315
- package/src/similarity.ts +13 -1
- package/src/stash-ref.ts +41 -0
- package/src/stash-resolve.ts +47 -0
- package/src/stash-search.ts +343 -0
- package/src/stash-show.ts +104 -0
- package/src/stash-types.ts +46 -0
- package/src/stash.ts +16 -760
- package/src/tool-runner.ts +129 -0
- package/src/walker.ts +53 -0
- package/.claude-plugin/plugin.json +0 -21
- package/commands/open.md +0 -11
- package/commands/run.md +0 -11
- package/commands/search.md +0 -11
- package/dist/src/plugin.d.ts +0 -2
- package/dist/src/plugin.js +0 -55
- package/skills/stash/SKILL.md +0 -73
- package/src/plugin.ts +0 -56
package/dist/src/indexer.js
CHANGED
|
@@ -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 =
|
|
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 ||
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/src/init.js
ADDED
|
@@ -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>;
|
package/dist/src/llm.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
async function chatCompletion(config, messages) {
|
|
2
|
+
const headers = { "Content-Type": "application/json" };
|
|
3
|
+
if (config.apiKey) {
|
|
4
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
5
|
+
}
|
|
6
|
+
const response = await fetch(config.endpoint, {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers,
|
|
9
|
+
body: JSON.stringify({
|
|
10
|
+
model: config.model,
|
|
11
|
+
messages,
|
|
12
|
+
temperature: 0.3,
|
|
13
|
+
max_tokens: 512,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const body = await response.text().catch(() => "");
|
|
18
|
+
throw new Error(`LLM request failed (${response.status}): ${body}`);
|
|
19
|
+
}
|
|
20
|
+
const json = (await response.json());
|
|
21
|
+
return json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
22
|
+
}
|
|
23
|
+
// ── Metadata Enhancement ────────────────────────────────────────────────────
|
|
24
|
+
const SYSTEM_PROMPT = `You are a metadata generator for a developer tool registry. Given a tool/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
|
|
25
|
+
/**
|
|
26
|
+
* Use an LLM to enhance a stash entry's metadata: improve description,
|
|
27
|
+
* generate intents, and suggest tags.
|
|
28
|
+
*/
|
|
29
|
+
export async function enhanceMetadata(config, entry, fileContent) {
|
|
30
|
+
const contextParts = [
|
|
31
|
+
`Name: ${entry.name}`,
|
|
32
|
+
`Type: ${entry.type}`,
|
|
33
|
+
];
|
|
34
|
+
if (entry.description)
|
|
35
|
+
contextParts.push(`Current description: ${entry.description}`);
|
|
36
|
+
if (entry.tags?.length)
|
|
37
|
+
contextParts.push(`Current tags: ${entry.tags.join(", ")}`);
|
|
38
|
+
if (fileContent) {
|
|
39
|
+
// Limit content to first 2000 chars to stay within token limits
|
|
40
|
+
const truncated = fileContent.length > 2000
|
|
41
|
+
? fileContent.slice(0, 2000) + "\n... (truncated)"
|
|
42
|
+
: fileContent;
|
|
43
|
+
contextParts.push(`File content:\n${truncated}`);
|
|
44
|
+
}
|
|
45
|
+
const userPrompt = `${contextParts.join("\n")}
|
|
46
|
+
|
|
47
|
+
Generate improved metadata for this ${entry.type}. Return JSON with these fields:
|
|
48
|
+
- "description": a clear, concise one-sentence description of what this does
|
|
49
|
+
- "intents": an array of 3-6 natural language task phrases an agent might use to find this (e.g. "deploy a docker container", "run database migrations")
|
|
50
|
+
- "tags": an array of 3-8 relevant keyword tags
|
|
51
|
+
|
|
52
|
+
Return ONLY the JSON object, no explanation.`;
|
|
53
|
+
const raw = await chatCompletion(config, [
|
|
54
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
55
|
+
{ role: "user", content: userPrompt },
|
|
56
|
+
]);
|
|
57
|
+
try {
|
|
58
|
+
// Strip markdown code fences if present
|
|
59
|
+
const cleaned = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
|
|
60
|
+
const parsed = JSON.parse(cleaned);
|
|
61
|
+
const result = {};
|
|
62
|
+
if (typeof parsed.description === "string" && parsed.description) {
|
|
63
|
+
result.description = parsed.description;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(parsed.intents)) {
|
|
66
|
+
result.intents = parsed.intents.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 8);
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(parsed.tags)) {
|
|
69
|
+
result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// LLM returned unparseable output, return empty
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if the LLM endpoint is reachable.
|
|
80
|
+
*/
|
|
81
|
+
export async function isLlmAvailable(config) {
|
|
82
|
+
try {
|
|
83
|
+
const result = await chatCompletion(config, [
|
|
84
|
+
{ role: "user", content: "Respond with just the word: ok" },
|
|
85
|
+
]);
|
|
86
|
+
return result.length > 0;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface TocHeading {
|
|
2
|
+
level: number;
|
|
3
|
+
text: string;
|
|
4
|
+
line: number;
|
|
5
|
+
}
|
|
6
|
+
export interface KnowledgeToc {
|
|
7
|
+
headings: TocHeading[];
|
|
8
|
+
totalLines: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseMarkdownToc(content: string): KnowledgeToc;
|
|
11
|
+
export declare function extractSection(content: string, heading: string): {
|
|
12
|
+
content: string;
|
|
13
|
+
startLine: number;
|
|
14
|
+
endLine: number;
|
|
15
|
+
} | null;
|
|
16
|
+
export declare function extractLineRange(content: string, start: number, end: number): string;
|
|
17
|
+
export declare function extractFrontmatterOnly(content: string): string | null;
|
|
18
|
+
export declare function formatToc(toc: KnowledgeToc): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
2
|
+
// ── Parsing ─────────────────────────────────────────────────────────────────
|
|
3
|
+
export function parseMarkdownToc(content) {
|
|
4
|
+
const lines = content.split(/\r?\n/);
|
|
5
|
+
const headings = [];
|
|
6
|
+
const parsed = parseFrontmatter(content);
|
|
7
|
+
const start = parsed.frontmatter ? parsed.bodyStartLine - 1 : 0;
|
|
8
|
+
for (let i = start; i < lines.length; i++) {
|
|
9
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
10
|
+
if (match) {
|
|
11
|
+
headings.push({
|
|
12
|
+
level: match[1].length,
|
|
13
|
+
text: match[2].replace(/\s+#+\s*$/, "").trim(),
|
|
14
|
+
line: i + 1,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { headings, totalLines: lines.length };
|
|
19
|
+
}
|
|
20
|
+
// ── Extraction ──────────────────────────────────────────────────────────────
|
|
21
|
+
export function extractSection(content, heading) {
|
|
22
|
+
const lines = content.split(/\r?\n/);
|
|
23
|
+
const target = heading.toLowerCase();
|
|
24
|
+
let startIdx = -1;
|
|
25
|
+
let startLevel = 0;
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
28
|
+
if (!match)
|
|
29
|
+
continue;
|
|
30
|
+
const text = match[2].replace(/\s+#+\s*$/, "").trim();
|
|
31
|
+
if (text.toLowerCase() === target && startIdx === -1) {
|
|
32
|
+
startIdx = i;
|
|
33
|
+
startLevel = match[1].length;
|
|
34
|
+
}
|
|
35
|
+
else if (startIdx !== -1 && match[1].length <= startLevel) {
|
|
36
|
+
return {
|
|
37
|
+
content: lines.slice(startIdx, i).join("\n"),
|
|
38
|
+
startLine: startIdx + 1,
|
|
39
|
+
endLine: i,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (startIdx === -1)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
content: lines.slice(startIdx).join("\n"),
|
|
47
|
+
startLine: startIdx + 1,
|
|
48
|
+
endLine: lines.length,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function extractLineRange(content, start, end) {
|
|
52
|
+
const lines = content.split(/\r?\n/);
|
|
53
|
+
if (end < start)
|
|
54
|
+
return "";
|
|
55
|
+
const s = Math.max(1, Math.min(start, lines.length));
|
|
56
|
+
const e = Math.min(end, lines.length);
|
|
57
|
+
return lines.slice(s - 1, e).join("\n");
|
|
58
|
+
}
|
|
59
|
+
export function extractFrontmatterOnly(content) {
|
|
60
|
+
const parsed = parseFrontmatter(content);
|
|
61
|
+
return parsed.frontmatter;
|
|
62
|
+
}
|
|
63
|
+
// ── Formatting ──────────────────────────────────────────────────────────────
|
|
64
|
+
export function formatToc(toc) {
|
|
65
|
+
if (toc.headings.length === 0) {
|
|
66
|
+
return `(no headings found — ${toc.totalLines} lines total)`;
|
|
67
|
+
}
|
|
68
|
+
const lineWidth = String(toc.totalLines).length;
|
|
69
|
+
const parts = toc.headings.map((h) => {
|
|
70
|
+
const lineNum = `L${String(h.line).padStart(lineWidth)}`;
|
|
71
|
+
const indent = " ".repeat(h.level - 1);
|
|
72
|
+
const prefix = "#".repeat(h.level);
|
|
73
|
+
return `${lineNum} ${indent}${prefix} ${h.text}`;
|
|
74
|
+
});
|
|
75
|
+
parts.push(`\n${toc.totalLines} lines total`);
|
|
76
|
+
return parts.join("\n");
|
|
77
|
+
}
|