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.
Files changed (80) hide show
  1. package/README.md +113 -77
  2. package/dist/index.d.ts +13 -3
  3. package/dist/index.js +7 -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 +154 -52
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +31 -0
  10. package/dist/src/config.js +74 -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 +10 -2
  24. package/dist/src/metadata.js +146 -30
  25. package/dist/src/ripgrep-install.d.ts +12 -0
  26. package/dist/src/ripgrep-install.js +169 -0
  27. package/dist/src/ripgrep-resolve.d.ts +13 -0
  28. package/dist/src/ripgrep-resolve.js +68 -0
  29. package/dist/src/ripgrep.d.ts +3 -36
  30. package/dist/src/ripgrep.js +2 -262
  31. package/dist/src/similarity.d.ts +1 -2
  32. package/dist/src/similarity.js +11 -0
  33. package/dist/src/stash-ref.d.ts +7 -0
  34. package/dist/src/stash-ref.js +33 -0
  35. package/dist/src/stash-resolve.d.ts +2 -0
  36. package/dist/src/stash-resolve.js +45 -0
  37. package/dist/src/stash-search.d.ts +6 -0
  38. package/dist/src/stash-search.js +269 -0
  39. package/dist/src/stash-show.d.ts +5 -0
  40. package/dist/src/stash-show.js +107 -0
  41. package/dist/src/stash-types.d.ts +53 -0
  42. package/dist/src/stash-types.js +1 -0
  43. package/dist/src/stash.d.ts +8 -63
  44. package/dist/src/stash.js +4 -633
  45. package/dist/src/tool-runner.d.ts +35 -0
  46. package/dist/src/tool-runner.js +100 -0
  47. package/dist/src/walker.d.ts +19 -0
  48. package/dist/src/walker.js +47 -0
  49. package/package.json +8 -14
  50. package/src/asset-spec.ts +69 -0
  51. package/src/cli.ts +164 -48
  52. package/src/common.ts +58 -0
  53. package/src/config.ts +124 -0
  54. package/src/embedder.ts +117 -0
  55. package/src/frontmatter.ts +95 -0
  56. package/src/indexer.ts +244 -84
  57. package/src/init.ts +106 -0
  58. package/src/llm.ts +124 -0
  59. package/src/markdown.ts +106 -0
  60. package/src/metadata.ts +157 -29
  61. package/src/ripgrep-install.ts +200 -0
  62. package/src/ripgrep-resolve.ts +72 -0
  63. package/src/ripgrep.ts +3 -315
  64. package/src/similarity.ts +13 -1
  65. package/src/stash-ref.ts +41 -0
  66. package/src/stash-resolve.ts +47 -0
  67. package/src/stash-search.ts +343 -0
  68. package/src/stash-show.ts +104 -0
  69. package/src/stash-types.ts +46 -0
  70. package/src/stash.ts +16 -760
  71. package/src/tool-runner.ts +129 -0
  72. package/src/walker.ts +53 -0
  73. package/.claude-plugin/plugin.json +0 -21
  74. package/commands/open.md +0 -11
  75. package/commands/run.md +0 -11
  76. package/commands/search.md +0 -11
  77. package/dist/src/plugin.d.ts +0 -2
  78. package/dist/src/plugin.js +0 -55
  79. package/skills/stash/SKILL.md +0 -73
  80. package/src/plugin.ts +0 -56
@@ -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,15 @@ 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[];
16
23
  }
17
24
  export interface StashFile {
18
25
  entries: StashEntry[];
@@ -21,7 +28,8 @@ export declare function stashFilePath(dirPath: string): string;
21
28
  export declare function loadStashFile(dirPath: string): StashFile | null;
22
29
  export declare function writeStashFile(dirPath: string, stash: StashFile): void;
23
30
  export declare function validateStashEntry(entry: unknown): StashEntry | null;
24
- export declare function generateMetadata(dirPath: string, assetType: AgentikitAssetType, files: string[]): StashFile;
31
+ export declare function generateMetadata(dirPath: string, assetType: AgentikitAssetType, files: string[], typeRoot?: string): StashFile;
32
+ export declare function generateIntents(description: string, tags: string[], name: string): string[];
25
33
  export declare function extractDescriptionFromComments(filePath: string): string | null;
26
34
  export declare function extractFrontmatterDescription(filePath: string): string | null;
27
35
  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,176 @@ 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
+ }
64
97
  return result;
65
98
  }
66
- function isValidType(type) {
67
- return type === "tool" || type === "skill" || type === "command" || type === "agent";
68
- }
69
99
  // ── Metadata Generation ─────────────────────────────────────────────────────
70
- const SCRIPT_EXTENSIONS = new Set([".sh", ".ts", ".js", ".ps1", ".cmd", ".bat"]);
71
- export function generateMetadata(dirPath, assetType, files) {
100
+ export function generateMetadata(dirPath, assetType, files, typeRoot = dirPath) {
72
101
  const entries = [];
102
+ const pkgMeta = extractPackageMetadata(dirPath);
73
103
  for (const file of files) {
74
104
  const ext = path.extname(file).toLowerCase();
75
105
  const baseName = path.basename(file, ext);
106
+ const fileName = path.basename(file);
76
107
  // 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")
108
+ if (!isRelevantAssetFile(assetType, fileName))
82
109
  continue;
110
+ const canonicalName = assetType === "skill"
111
+ ? deriveCanonicalAssetName(assetType, typeRoot, file) ?? baseName
112
+ : baseName;
83
113
  const entry = {
84
- name: baseName,
114
+ name: canonicalName,
85
115
  type: assetType,
86
116
  generated: true,
117
+ quality: "generated",
118
+ confidence: 0.55,
119
+ source: "filename",
87
120
  };
88
- // Priority 3: package.json metadata
89
- const pkgMeta = extractPackageMetadata(dirPath);
121
+ // Priority 1: package.json metadata
90
122
  if (pkgMeta) {
91
- if (pkgMeta.description && !entry.description)
123
+ if (pkgMeta.description && !entry.description) {
92
124
  entry.description = pkgMeta.description;
125
+ entry.source = "package";
126
+ entry.confidence = 0.8;
127
+ }
93
128
  if (pkgMeta.keywords && pkgMeta.keywords.length > 0)
94
- entry.tags = pkgMeta.keywords;
129
+ entry.tags = normalizeTerms(pkgMeta.keywords);
95
130
  }
96
- // Priority 2: Frontmatter (for .md files)
131
+ // Priority 2: Frontmatter (for .md files — overrides package.json description)
97
132
  if (ext === ".md") {
98
133
  const fm = extractFrontmatterDescription(file);
99
- if (fm)
134
+ if (fm) {
100
135
  entry.description = fm;
136
+ entry.source = "frontmatter";
137
+ entry.confidence = 0.9;
138
+ }
139
+ }
140
+ // Knowledge entries: generate TOC from headings
141
+ if (assetType === "knowledge") {
142
+ try {
143
+ const mdContent = fs.readFileSync(file, "utf8");
144
+ const toc = parseMarkdownToc(mdContent);
145
+ if (toc.headings.length > 0)
146
+ entry.toc = toc.headings;
147
+ }
148
+ catch {
149
+ // Non-fatal: skip TOC if file can't be read
150
+ }
101
151
  }
102
- // Priority 4: Code comments (for script files)
152
+ // Priority 3: Code comments (for script files)
103
153
  if (SCRIPT_EXTENSIONS.has(ext) && ext !== ".md") {
104
154
  const commentDesc = extractDescriptionFromComments(file);
105
- if (commentDesc && !entry.description)
155
+ if (commentDesc && !entry.description) {
106
156
  entry.description = commentDesc;
157
+ entry.source = "comments";
158
+ entry.confidence = 0.7;
159
+ }
107
160
  }
108
- // Priority 5: Filename heuristics (fallback)
161
+ // Priority 4: Filename heuristics (fallback)
109
162
  if (!entry.description) {
110
163
  entry.description = fileNameToDescription(baseName);
164
+ entry.source = "filename";
165
+ entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
111
166
  }
112
167
  if (!entry.tags || entry.tags.length === 0) {
113
168
  entry.tags = extractTagsFromPath(file, dirPath);
114
169
  }
170
+ entry.tags = normalizeTerms(entry.tags ?? []);
171
+ entry.aliases = buildAliases(canonicalName, entry.tags);
172
+ // Intents are only generated when LLM is configured (via enhanceStashWithLlm)
173
+ // Heuristic intents are too noisy to be useful for search quality
115
174
  entry.entry = path.basename(file);
116
175
  entries.push(entry);
117
176
  }
118
177
  return { entries };
119
178
  }
179
+ function normalizeTerms(values) {
180
+ const normalized = new Set();
181
+ for (const value of values) {
182
+ const cleaned = value.toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
183
+ if (!cleaned)
184
+ continue;
185
+ normalized.add(cleaned);
186
+ if (cleaned.endsWith("s") && cleaned.length > 3) {
187
+ normalized.add(cleaned.slice(0, -1));
188
+ }
189
+ }
190
+ return Array.from(normalized);
191
+ }
192
+ function buildAliases(name, tags) {
193
+ const aliases = new Set();
194
+ const spaced = name.replace(/[-_]+/g, " ").trim().toLowerCase();
195
+ if (spaced && spaced !== name.toLowerCase())
196
+ aliases.add(spaced);
197
+ if (tags.length > 1)
198
+ aliases.add(tags.join(" "));
199
+ return Array.from(aliases);
200
+ }
201
+ // ── Intent Generation ────────────────────────────────────────────────────────
202
+ export function generateIntents(description, tags, name) {
203
+ const intents = new Set();
204
+ // Split name on separators to extract tokens and potential verb
205
+ const nameTokens = name
206
+ .replace(/[-_]+/g, " ")
207
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
208
+ .toLowerCase()
209
+ .trim()
210
+ .split(/\s+/)
211
+ .filter((t) => t.length > 1);
212
+ // Intent from name as phrase (e.g. "summarize diff")
213
+ const namePhrase = nameTokens.join(" ");
214
+ if (namePhrase.length > 2)
215
+ intents.add(namePhrase);
216
+ // Intent from description (lowercased)
217
+ const desc = description.toLowerCase().trim();
218
+ if (desc.length > 2)
219
+ intents.add(desc);
220
+ // Combine first name token (potential verb) with tags
221
+ // e.g. name "summarize-diff", tags ["git"] → "summarize git diff"
222
+ if (nameTokens.length >= 1 && tags.length > 0) {
223
+ const verb = nameTokens[0];
224
+ const rest = nameTokens.slice(1).join(" ");
225
+ for (const tag of tags) {
226
+ const tagLower = tag.toLowerCase();
227
+ // verb + tag + rest (e.g. "summarize git diff")
228
+ const parts = [verb, tagLower, rest].filter((p) => p.length > 0);
229
+ const phrase = parts.join(" ");
230
+ if (phrase !== namePhrase && phrase.length > 2)
231
+ intents.add(phrase);
232
+ }
233
+ }
234
+ // Join tag pairs (e.g. ["git", "diff"] → "git diff")
235
+ if (tags.length >= 2) {
236
+ const tagPhrase = tags.map((t) => t.toLowerCase()).join(" ");
237
+ if (tagPhrase.length > 2)
238
+ intents.add(tagPhrase);
239
+ }
240
+ // Cap at 8 intents
241
+ return Array.from(intents).slice(0, 8);
242
+ }
120
243
  export function extractDescriptionFromComments(filePath) {
121
244
  let content;
122
245
  try {
@@ -170,15 +293,8 @@ export function extractFrontmatterDescription(filePath) {
170
293
  catch {
171
294
  return null;
172
295
  }
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;
296
+ const parsed = parseFrontmatter(content);
297
+ return toStringOrUndefined(parsed.data.description) ?? null;
182
298
  }
183
299
  export function extractPackageMetadata(dirPath) {
184
300
  const pkgPath = path.join(dirPath, "package.json");
@@ -0,0 +1,12 @@
1
+ export interface EnsureRgResult {
2
+ rgPath: string;
3
+ installed: boolean;
4
+ version: string;
5
+ }
6
+ /**
7
+ * Ensure ripgrep is available. If not found on PATH or in stash/bin,
8
+ * download and install it to stash/bin.
9
+ *
10
+ * Returns the path to the ripgrep binary and whether it was newly installed.
11
+ */
12
+ export declare function ensureRg(stashDir: string): EnsureRgResult;
@@ -0,0 +1,169 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { IS_WINDOWS } from "./common";
5
+ import { RG_BINARY, resolveRg } from "./ripgrep-resolve";
6
+ /**
7
+ * Platform and architecture detection for ripgrep binary downloads.
8
+ */
9
+ function getRgPlatformTarget() {
10
+ const platform = process.platform;
11
+ const arch = process.arch;
12
+ if (platform === "linux" && arch === "x64") {
13
+ return { platform: "x86_64-unknown-linux-musl", arch: "x64", ext: ".tar.gz" };
14
+ }
15
+ if (platform === "linux" && arch === "arm64") {
16
+ return { platform: "aarch64-unknown-linux-gnu", arch: "arm64", ext: ".tar.gz" };
17
+ }
18
+ if (platform === "darwin" && arch === "x64") {
19
+ return { platform: "x86_64-apple-darwin", arch: "x64", ext: ".tar.gz" };
20
+ }
21
+ if (platform === "darwin" && arch === "arm64") {
22
+ return { platform: "aarch64-apple-darwin", arch: "arm64", ext: ".tar.gz" };
23
+ }
24
+ if (platform === "win32" && arch === "x64") {
25
+ return { platform: "x86_64-pc-windows-msvc", arch: "x64", ext: ".zip" };
26
+ }
27
+ return null;
28
+ }
29
+ const RG_VERSION = "14.1.1";
30
+ /**
31
+ * Ensure ripgrep is available. If not found on PATH or in stash/bin,
32
+ * download and install it to stash/bin.
33
+ *
34
+ * Returns the path to the ripgrep binary and whether it was newly installed.
35
+ */
36
+ export function ensureRg(stashDir) {
37
+ // Already available?
38
+ const existing = resolveRg(stashDir);
39
+ if (existing) {
40
+ return { rgPath: existing, installed: false, version: getRgVersion(existing) };
41
+ }
42
+ // Determine platform
43
+ const target = getRgPlatformTarget();
44
+ if (!target) {
45
+ throw new Error(`Unsupported platform for ripgrep auto-install: ${process.platform}/${process.arch}. ` +
46
+ `Install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`);
47
+ }
48
+ const binDir = path.join(stashDir, "bin");
49
+ if (!fs.existsSync(binDir)) {
50
+ fs.mkdirSync(binDir, { recursive: true });
51
+ }
52
+ const archiveName = `ripgrep-${RG_VERSION}-${target.platform}`;
53
+ const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${archiveName}${target.ext}`;
54
+ const destBinary = path.join(binDir, RG_BINARY);
55
+ if (target.ext === ".tar.gz") {
56
+ downloadAndExtractTarGz(url, archiveName, destBinary);
57
+ }
58
+ else {
59
+ downloadAndExtractZip(url, archiveName, destBinary);
60
+ }
61
+ // Make executable
62
+ if (!IS_WINDOWS) {
63
+ fs.chmodSync(destBinary, 0o755);
64
+ }
65
+ return { rgPath: destBinary, installed: true, version: RG_VERSION };
66
+ }
67
+ function downloadAndExtractTarGz(url, archiveName, destBinary) {
68
+ const destDir = path.dirname(destBinary);
69
+ const tmpTarGz = path.join(destDir, "rg-download.tar.gz");
70
+ try {
71
+ // Download archive to a temporary file without using a shell
72
+ const curlResult = spawnSync("curl", ["-fsSL", "-o", tmpTarGz, url], {
73
+ encoding: "utf8",
74
+ timeout: 60_000,
75
+ });
76
+ if (curlResult.status !== 0) {
77
+ const err = curlResult.stderr?.trim() || curlResult.error?.message || "unknown error";
78
+ throw new Error(`Failed to download ripgrep from ${url}: ${err}`);
79
+ }
80
+ // Extract the specific binary from the archive into destDir
81
+ const tarResult = spawnSync("tar", [
82
+ "xzf",
83
+ tmpTarGz,
84
+ "--strip-components=1",
85
+ "-C",
86
+ destDir,
87
+ `${archiveName}/rg`,
88
+ ], {
89
+ encoding: "utf8",
90
+ timeout: 60_000,
91
+ });
92
+ if (tarResult.status !== 0) {
93
+ const err = tarResult.stderr?.trim() || tarResult.error?.message || "unknown error";
94
+ throw new Error(`Failed to extract ripgrep from ${url}: ${err}`);
95
+ }
96
+ if (!fs.existsSync(destBinary)) {
97
+ throw new Error(`ripgrep binary not found at ${destBinary} after extraction`);
98
+ }
99
+ }
100
+ finally {
101
+ // Best-effort cleanup of temporary archive
102
+ try {
103
+ if (fs.existsSync(tmpTarGz)) {
104
+ fs.unlinkSync(tmpTarGz);
105
+ }
106
+ }
107
+ catch {
108
+ // ignore cleanup errors
109
+ }
110
+ }
111
+ }
112
+ function downloadAndExtractZip(url, archiveName, destBinary) {
113
+ const destDir = path.dirname(destBinary);
114
+ const tmpZip = path.join(destDir, "rg-download.zip");
115
+ const expandedDir = path.join(destDir, archiveName);
116
+ try {
117
+ // Download
118
+ const dlResult = spawnSync("curl", ["-fsSL", "-o", tmpZip, url], {
119
+ encoding: "utf8",
120
+ timeout: 60_000,
121
+ });
122
+ if (dlResult.status !== 0) {
123
+ throw new Error(dlResult.stderr?.trim() || "download failed");
124
+ }
125
+ // Extract the zip archive using separate spawnSync calls with argument arrays
126
+ // to avoid shell injection via path interpolation in PowerShell -Command strings
127
+ const expandResult = spawnSync("powershell", [
128
+ "-Command",
129
+ "Expand-Archive",
130
+ "-Path", tmpZip,
131
+ "-DestinationPath", destDir,
132
+ "-Force",
133
+ ], {
134
+ encoding: "utf8",
135
+ timeout: 60_000,
136
+ });
137
+ if (expandResult.status !== 0) {
138
+ throw new Error(expandResult.stderr?.trim() || "extraction failed");
139
+ }
140
+ const srcRgExe = path.join(destDir, archiveName, "rg.exe");
141
+ const moveResult = spawnSync("powershell", [
142
+ "-Command",
143
+ "Move-Item",
144
+ "-Force",
145
+ "-Path", srcRgExe,
146
+ "-Destination", destBinary,
147
+ ], {
148
+ encoding: "utf8",
149
+ timeout: 60_000,
150
+ });
151
+ if (moveResult.status !== 0) {
152
+ throw new Error(moveResult.stderr?.trim() || "move failed");
153
+ }
154
+ }
155
+ finally {
156
+ if (fs.existsSync(tmpZip))
157
+ fs.unlinkSync(tmpZip);
158
+ if (fs.existsSync(expandedDir))
159
+ fs.rmSync(expandedDir, { recursive: true, force: true });
160
+ }
161
+ }
162
+ function getRgVersion(rgPath) {
163
+ const result = spawnSync(rgPath, ["--version"], { encoding: "utf8", timeout: 5_000 });
164
+ if (result.status === 0 && result.stdout) {
165
+ const match = result.stdout.match(/ripgrep\s+([\d.]+)/);
166
+ return match ? match[1] : "unknown";
167
+ }
168
+ return "unknown";
169
+ }
@@ -0,0 +1,13 @@
1
+ export declare const RG_BINARY: string;
2
+ /**
3
+ * Resolve the path to a usable ripgrep binary.
4
+ * Checks in order:
5
+ * 1. stashDir/bin/rg
6
+ * 2. system PATH (rg)
7
+ * Returns null if ripgrep is not available.
8
+ */
9
+ export declare function resolveRg(stashDir?: string): string | null;
10
+ /**
11
+ * Check if ripgrep is available (either in stash/bin or system PATH).
12
+ */
13
+ export declare function isRgAvailable(stashDir?: string): boolean;
@@ -0,0 +1,68 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { IS_WINDOWS } from "./common";
4
+ export const RG_BINARY = IS_WINDOWS ? "rg.exe" : "rg";
5
+ function canExecute(filePath) {
6
+ if (!fs.existsSync(filePath))
7
+ return false;
8
+ if (IS_WINDOWS)
9
+ return true;
10
+ try {
11
+ fs.accessSync(filePath, fs.constants.X_OK);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function resolveFromPath() {
19
+ const rawPath = process.env.PATH;
20
+ if (!rawPath)
21
+ return null;
22
+ const pathEntries = rawPath.split(path.delimiter).filter(Boolean);
23
+ if (IS_WINDOWS) {
24
+ const pathext = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
25
+ .split(";")
26
+ .filter(Boolean)
27
+ .map((ext) => ext.toLowerCase());
28
+ for (const entry of pathEntries) {
29
+ const directCandidate = path.join(entry, "rg");
30
+ if (canExecute(directCandidate))
31
+ return directCandidate;
32
+ for (const ext of pathext) {
33
+ const candidate = path.join(entry, `rg${ext}`);
34
+ if (canExecute(candidate))
35
+ return candidate;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ for (const entry of pathEntries) {
41
+ const candidate = path.join(entry, "rg");
42
+ if (canExecute(candidate))
43
+ return candidate;
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * Resolve the path to a usable ripgrep binary.
49
+ * Checks in order:
50
+ * 1. stashDir/bin/rg
51
+ * 2. system PATH (rg)
52
+ * Returns null if ripgrep is not available.
53
+ */
54
+ export function resolveRg(stashDir) {
55
+ // Check stash bin directory first
56
+ if (stashDir) {
57
+ const stashRg = path.join(stashDir, "bin", RG_BINARY);
58
+ if (canExecute(stashRg))
59
+ return stashRg;
60
+ }
61
+ return resolveFromPath();
62
+ }
63
+ /**
64
+ * Check if ripgrep is available (either in stash/bin or system PATH).
65
+ */
66
+ export function isRgAvailable(stashDir) {
67
+ return resolveRg(stashDir) !== null;
68
+ }
@@ -1,36 +1,3 @@
1
- /**
2
- * Resolve the path to a usable ripgrep binary.
3
- * Checks in order:
4
- * 1. stashDir/bin/rg
5
- * 2. system PATH (rg)
6
- * Returns null if ripgrep is not available.
7
- */
8
- export declare function resolveRg(stashDir?: string): string | null;
9
- /**
10
- * Check if ripgrep is available (either in stash/bin or system PATH).
11
- */
12
- export declare function isRgAvailable(stashDir?: string): boolean;
13
- export interface RgCandidateResult {
14
- matchedFiles: string[];
15
- usedRg: boolean;
16
- }
17
- /**
18
- * Use ripgrep to find .stash.json files that match query tokens.
19
- * Returns paths to matching .stash.json files.
20
- *
21
- * If ripgrep is not available or the query is empty, returns null
22
- * to signal that the caller should skip pre-filtering.
23
- */
24
- export declare function rgFilterCandidates(query: string, searchDir: string, stashDir?: string): RgCandidateResult | null;
25
- export interface EnsureRgResult {
26
- rgPath: string;
27
- installed: boolean;
28
- version: string;
29
- }
30
- /**
31
- * Ensure ripgrep is available. If not found on PATH or in stash/bin,
32
- * download and install it to stash/bin.
33
- *
34
- * Returns the path to the ripgrep binary and whether it was newly installed.
35
- */
36
- export declare function ensureRg(stashDir: string): EnsureRgResult;
1
+ export { resolveRg, isRgAvailable } from "./ripgrep-resolve";
2
+ export { ensureRg } from "./ripgrep-install";
3
+ export type { EnsureRgResult } from "./ripgrep-install";