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,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
+ }
@@ -1,4 +1,5 @@
1
- import type { AgentikitAssetType } from "./stash";
1
+ import { type AgentikitAssetType } from "./common";
2
+ import { type TocHeading } from "./markdown";
2
3
  export interface StashIntent {
3
4
  when?: string;
4
5
  input?: string;
@@ -10,9 +11,16 @@ export interface StashEntry {
10
11
  description?: string;
11
12
  tags?: string[];
12
13
  examples?: string[];
14
+ intents?: string[];
13
15
  intent?: StashIntent;
14
16
  entry?: string;
15
17
  generated?: boolean;
18
+ quality?: "generated" | "curated";
19
+ confidence?: number;
20
+ source?: "package" | "frontmatter" | "comments" | "filename" | "manual" | "llm";
21
+ aliases?: string[];
22
+ toc?: TocHeading[];
23
+ usage?: string[];
16
24
  }
17
25
  export interface StashFile {
18
26
  entries: StashEntry[];
@@ -21,7 +29,8 @@ export declare function stashFilePath(dirPath: string): string;
21
29
  export declare function loadStashFile(dirPath: string): StashFile | null;
22
30
  export declare function writeStashFile(dirPath: string, stash: StashFile): void;
23
31
  export declare function validateStashEntry(entry: unknown): StashEntry | null;
24
- export declare function generateMetadata(dirPath: string, assetType: AgentikitAssetType, files: string[]): StashFile;
32
+ export declare function generateMetadata(dirPath: string, assetType: AgentikitAssetType, files: string[], typeRoot?: string): StashFile;
33
+ export declare function generateIntents(description: string, tags: string[], name: string): string[];
25
34
  export declare function extractDescriptionFromComments(filePath: string): string | null;
26
35
  export declare function extractFrontmatterDescription(filePath: string): string | null;
27
36
  export declare function extractPackageMetadata(dirPath: string): {
@@ -1,5 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { isAssetType } from "./common";
4
+ import { SCRIPT_EXTENSIONS, isRelevantAssetFile, deriveCanonicalAssetName } from "./asset-spec";
5
+ import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
6
+ import { parseMarkdownToc } from "./markdown";
3
7
  // ── Load / Write ────────────────────────────────────────────────────────────
4
8
  const STASH_FILENAME = ".stash.json";
5
9
  export function stashFilePath(dirPath) {
@@ -35,7 +39,7 @@ export function validateStashEntry(entry) {
35
39
  const e = entry;
36
40
  if (typeof e.name !== "string" || !e.name)
37
41
  return null;
38
- if (typeof e.type !== "string" || !isValidType(e.type))
42
+ if (typeof e.type !== "string" || !isAssetType(e.type))
39
43
  return null;
40
44
  const result = {
41
45
  name: e.name,
@@ -47,6 +51,11 @@ export function validateStashEntry(entry) {
47
51
  result.tags = e.tags.filter((t) => typeof t === "string");
48
52
  if (Array.isArray(e.examples))
49
53
  result.examples = e.examples.filter((x) => typeof x === "string");
54
+ if (Array.isArray(e.intents)) {
55
+ const filtered = e.intents.filter((s) => typeof s === "string" && s.trim().length > 0);
56
+ if (filtered.length > 0)
57
+ result.intents = filtered;
58
+ }
50
59
  if (typeof e.intent === "object" && e.intent !== null) {
51
60
  const intent = e.intent;
52
61
  result.intent = {};
@@ -61,62 +70,192 @@ export function validateStashEntry(entry) {
61
70
  result.entry = e.entry;
62
71
  if (e.generated === true)
63
72
  result.generated = true;
73
+ if (e.quality === "generated" || e.quality === "curated")
74
+ result.quality = e.quality;
75
+ if (typeof e.confidence === "number" && Number.isFinite(e.confidence))
76
+ result.confidence = Math.max(0, Math.min(1, e.confidence));
77
+ if (typeof e.source === "string" && ["package", "frontmatter", "comments", "filename", "manual", "llm"].includes(e.source)) {
78
+ result.source = e.source;
79
+ }
80
+ if (Array.isArray(e.aliases)) {
81
+ const filtered = e.aliases.filter((a) => typeof a === "string" && a.trim().length > 0);
82
+ if (filtered.length > 0)
83
+ result.aliases = normalizeTerms(filtered);
84
+ }
85
+ if (Array.isArray(e.toc)) {
86
+ const validated = e.toc.filter((h) => {
87
+ if (typeof h !== "object" || h === null)
88
+ return false;
89
+ const rec = h;
90
+ return typeof rec.level === "number"
91
+ && typeof rec.text === "string"
92
+ && typeof rec.line === "number";
93
+ });
94
+ if (validated.length > 0)
95
+ result.toc = validated;
96
+ }
97
+ const usage = normalizeNonEmptyStringList(e.usage);
98
+ if (usage)
99
+ result.usage = usage;
64
100
  return result;
65
101
  }
66
- function isValidType(type) {
67
- return type === "tool" || type === "skill" || type === "command" || type === "agent";
102
+ function normalizeNonEmptyStringList(value) {
103
+ if (typeof value === "string") {
104
+ const trimmed = value.trim();
105
+ return trimmed ? [trimmed] : undefined;
106
+ }
107
+ if (!Array.isArray(value))
108
+ return undefined;
109
+ const filtered = value
110
+ .filter((item) => typeof item === "string")
111
+ .map((item) => item.trim())
112
+ .filter((item) => item.length > 0);
113
+ return filtered.length > 0 ? filtered : undefined;
68
114
  }
69
115
  // ── Metadata Generation ─────────────────────────────────────────────────────
70
- const SCRIPT_EXTENSIONS = new Set([".sh", ".ts", ".js", ".ps1", ".cmd", ".bat"]);
71
- export function generateMetadata(dirPath, assetType, files) {
116
+ export function generateMetadata(dirPath, assetType, files, typeRoot = dirPath) {
72
117
  const entries = [];
118
+ const pkgMeta = extractPackageMetadata(dirPath);
73
119
  for (const file of files) {
74
120
  const ext = path.extname(file).toLowerCase();
75
121
  const baseName = path.basename(file, ext);
122
+ const fileName = path.basename(file);
76
123
  // Skip non-relevant files
77
- if (assetType === "tool" && !SCRIPT_EXTENSIONS.has(ext))
78
- continue;
79
- if ((assetType === "command" || assetType === "agent") && ext !== ".md")
80
- continue;
81
- if (assetType === "skill" && path.basename(file) !== "SKILL.md")
124
+ if (!isRelevantAssetFile(assetType, fileName))
82
125
  continue;
126
+ const canonicalName = assetType === "skill"
127
+ ? deriveCanonicalAssetName(assetType, typeRoot, file) ?? baseName
128
+ : baseName;
83
129
  const entry = {
84
- name: baseName,
130
+ name: canonicalName,
85
131
  type: assetType,
86
132
  generated: true,
133
+ quality: "generated",
134
+ confidence: 0.55,
135
+ source: "filename",
87
136
  };
88
- // Priority 3: package.json metadata
89
- const pkgMeta = extractPackageMetadata(dirPath);
137
+ // Priority 1: package.json metadata
90
138
  if (pkgMeta) {
91
- if (pkgMeta.description && !entry.description)
139
+ if (pkgMeta.description && !entry.description) {
92
140
  entry.description = pkgMeta.description;
141
+ entry.source = "package";
142
+ entry.confidence = 0.8;
143
+ }
93
144
  if (pkgMeta.keywords && pkgMeta.keywords.length > 0)
94
- entry.tags = pkgMeta.keywords;
145
+ entry.tags = normalizeTerms(pkgMeta.keywords);
95
146
  }
96
- // Priority 2: Frontmatter (for .md files)
147
+ // Priority 2: Frontmatter (for .md files — overrides package.json description)
97
148
  if (ext === ".md") {
98
149
  const fm = extractFrontmatterDescription(file);
99
- if (fm)
150
+ if (fm) {
100
151
  entry.description = fm;
152
+ entry.source = "frontmatter";
153
+ entry.confidence = 0.9;
154
+ }
155
+ }
156
+ // Knowledge entries: generate TOC from headings
157
+ if (assetType === "knowledge") {
158
+ try {
159
+ const mdContent = fs.readFileSync(file, "utf8");
160
+ const toc = parseMarkdownToc(mdContent);
161
+ if (toc.headings.length > 0)
162
+ entry.toc = toc.headings;
163
+ }
164
+ catch {
165
+ // Non-fatal: skip TOC if file can't be read
166
+ }
101
167
  }
102
- // Priority 4: Code comments (for script files)
168
+ // Priority 3: Code comments (for script files)
103
169
  if (SCRIPT_EXTENSIONS.has(ext) && ext !== ".md") {
104
170
  const commentDesc = extractDescriptionFromComments(file);
105
- if (commentDesc && !entry.description)
171
+ if (commentDesc && !entry.description) {
106
172
  entry.description = commentDesc;
173
+ entry.source = "comments";
174
+ entry.confidence = 0.7;
175
+ }
107
176
  }
108
- // Priority 5: Filename heuristics (fallback)
177
+ // Priority 4: Filename heuristics (fallback)
109
178
  if (!entry.description) {
110
179
  entry.description = fileNameToDescription(baseName);
180
+ entry.source = "filename";
181
+ entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
111
182
  }
112
183
  if (!entry.tags || entry.tags.length === 0) {
113
184
  entry.tags = extractTagsFromPath(file, dirPath);
114
185
  }
186
+ entry.tags = normalizeTerms(entry.tags ?? []);
187
+ entry.aliases = buildAliases(canonicalName, entry.tags);
188
+ // Intents are only generated when LLM is configured (via enhanceStashWithLlm)
189
+ // Heuristic intents are too noisy to be useful for search quality
115
190
  entry.entry = path.basename(file);
116
191
  entries.push(entry);
117
192
  }
118
193
  return { entries };
119
194
  }
195
+ function normalizeTerms(values) {
196
+ const normalized = new Set();
197
+ for (const value of values) {
198
+ const cleaned = value.toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
199
+ if (!cleaned)
200
+ continue;
201
+ normalized.add(cleaned);
202
+ if (cleaned.endsWith("s") && cleaned.length > 3) {
203
+ normalized.add(cleaned.slice(0, -1));
204
+ }
205
+ }
206
+ return Array.from(normalized);
207
+ }
208
+ function buildAliases(name, tags) {
209
+ const aliases = new Set();
210
+ const spaced = name.replace(/[-_]+/g, " ").trim().toLowerCase();
211
+ if (spaced && spaced !== name.toLowerCase())
212
+ aliases.add(spaced);
213
+ if (tags.length > 1)
214
+ aliases.add(tags.join(" "));
215
+ return Array.from(aliases);
216
+ }
217
+ // ── Intent Generation ────────────────────────────────────────────────────────
218
+ export function generateIntents(description, tags, name) {
219
+ const intents = new Set();
220
+ // Split name on separators to extract tokens and potential verb
221
+ const nameTokens = name
222
+ .replace(/[-_]+/g, " ")
223
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
224
+ .toLowerCase()
225
+ .trim()
226
+ .split(/\s+/)
227
+ .filter((t) => t.length > 1);
228
+ // Intent from name as phrase (e.g. "summarize diff")
229
+ const namePhrase = nameTokens.join(" ");
230
+ if (namePhrase.length > 2)
231
+ intents.add(namePhrase);
232
+ // Intent from description (lowercased)
233
+ const desc = description.toLowerCase().trim();
234
+ if (desc.length > 2)
235
+ intents.add(desc);
236
+ // Combine first name token (potential verb) with tags
237
+ // e.g. name "summarize-diff", tags ["git"] → "summarize git diff"
238
+ if (nameTokens.length >= 1 && tags.length > 0) {
239
+ const verb = nameTokens[0];
240
+ const rest = nameTokens.slice(1).join(" ");
241
+ for (const tag of tags) {
242
+ const tagLower = tag.toLowerCase();
243
+ // verb + tag + rest (e.g. "summarize git diff")
244
+ const parts = [verb, tagLower, rest].filter((p) => p.length > 0);
245
+ const phrase = parts.join(" ");
246
+ if (phrase !== namePhrase && phrase.length > 2)
247
+ intents.add(phrase);
248
+ }
249
+ }
250
+ // Join tag pairs (e.g. ["git", "diff"] → "git diff")
251
+ if (tags.length >= 2) {
252
+ const tagPhrase = tags.map((t) => t.toLowerCase()).join(" ");
253
+ if (tagPhrase.length > 2)
254
+ intents.add(tagPhrase);
255
+ }
256
+ // Cap at 8 intents
257
+ return Array.from(intents).slice(0, 8);
258
+ }
120
259
  export function extractDescriptionFromComments(filePath) {
121
260
  let content;
122
261
  try {
@@ -170,15 +309,8 @@ export function extractFrontmatterDescription(filePath) {
170
309
  catch {
171
310
  return null;
172
311
  }
173
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
174
- if (!match)
175
- return null;
176
- for (const line of match[1].split(/\r?\n/)) {
177
- const m = line.match(/^description:\s*"?(.+?)"?\s*$/);
178
- if (m)
179
- return m[1];
180
- }
181
- return null;
312
+ const parsed = parseFrontmatter(content);
313
+ return toStringOrUndefined(parsed.data.description) ?? null;
182
314
  }
183
315
  export function extractPackageMetadata(dirPath) {
184
316
  const pkgPath = path.join(dirPath, "package.json");
@@ -0,0 +1,11 @@
1
+ import { type AgentikitConfig } from "./config";
2
+ import type { RegistryInstallResult, RegistryInstalledEntry } from "./registry-types";
3
+ export interface InstallRegistryRefOptions {
4
+ cacheRootDir?: string;
5
+ now?: Date;
6
+ }
7
+ export declare function installRegistryRef(ref: string, options?: InstallRegistryRefOptions): Promise<RegistryInstallResult>;
8
+ export declare function upsertInstalledRegistryEntry(entry: RegistryInstalledEntry, stashDir?: string): AgentikitConfig;
9
+ export declare function removeInstalledRegistryEntry(id: string, stashDir?: string): AgentikitConfig;
10
+ export declare function getRegistryCacheRootDir(): string;
11
+ export declare function detectStashRoot(extractedDir: string): string;