@jmylchreest/aide-plugin 0.0.54 → 0.0.56

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.
@@ -25,7 +25,8 @@
25
25
  * Stop (blocking) → session.idle re-prompts via session.prompt() for persistence
26
26
  */
27
27
 
28
- import { execFileSync, execSync } from "child_process";
28
+ import { execFileSync } from "child_process";
29
+ import which from "which";
29
30
  import { join } from "path";
30
31
  import { findAideBinary } from "../core/aide-client.js";
31
32
  import {
@@ -48,6 +49,8 @@ import { trackToolUse, updateToolStats } from "../core/tool-tracking.js";
48
49
  import { evaluateToolUse, isToolDenied } from "../core/tool-enforcement.js";
49
50
  import { checkPersistence, getActiveMode } from "../core/persistence-logic.js";
50
51
  import { checkWriteGuard } from "../core/write-guard.js";
52
+ import { checkSmartReadHint } from "../core/context-guard.js";
53
+ import { recordFileRead } from "../core/read-tracking.js";
51
54
  import {
52
55
  checkComments,
53
56
  getCheckableFilePath,
@@ -226,18 +229,9 @@ function createConfigHandler(
226
229
  }
227
230
  // Skip skills requiring binaries not on PATH
228
231
  if (skill.requires_binary && skill.requires_binary.length > 0) {
229
- const allPresent = skill.requires_binary.every((bin) => {
230
- try {
231
- const cmd =
232
- process.platform === "win32"
233
- ? `where ${bin}`
234
- : `command -v ${bin}`;
235
- execSync(cmd, { stdio: "ignore", timeout: 2000 });
236
- return true;
237
- } catch {
238
- return false;
239
- }
240
- });
232
+ const allPresent = skill.requires_binary.every(
233
+ (bin) => which.sync(bin, { nothrow: true }) !== null,
234
+ );
241
235
  if (!allPresent) continue;
242
236
  }
243
237
  const commandName = `aide:${skill.name}`;
@@ -730,6 +724,21 @@ function createToolBeforeHandler(
730
724
  debug(SOURCE, `Tool enforcement check failed (non-fatal): ${err}`);
731
725
  }
732
726
 
727
+ // Smart read hint: suggest code index for re-reads of unchanged files
728
+ try {
729
+ const hintResult = checkSmartReadHint(
730
+ input.tool,
731
+ (_output.args || {}) as Record<string, unknown>,
732
+ state.cwd,
733
+ state.binary,
734
+ );
735
+ if (hintResult.shouldHint && hintResult.hint) {
736
+ debug(SOURCE, `Smart read hint for ${input.tool}: ${hintResult.hint}`);
737
+ }
738
+ } catch (err) {
739
+ debug(SOURCE, `Smart read hint check failed (non-fatal): ${err}`);
740
+ }
741
+
733
742
  // Track tool use
734
743
  if (!state.binary) return;
735
744
 
@@ -773,6 +782,27 @@ function createToolAfterHandler(
773
782
  debug(SOURCE, `Partial memory write failed (non-fatal): ${err}`);
774
783
  }
775
784
 
785
+ // Record file reads for smart-read-hint feature
786
+ try {
787
+ const toolArgs = (_output.metadata?.args || {}) as Record<
788
+ string,
789
+ unknown
790
+ >;
791
+ if (
792
+ input.tool.toLowerCase() === "read" &&
793
+ toolArgs.file_path &&
794
+ state.binary
795
+ ) {
796
+ recordFileRead(
797
+ state.binary,
798
+ state.cwd,
799
+ toolArgs.file_path as string,
800
+ );
801
+ }
802
+ } catch (err) {
803
+ debug(SOURCE, `Read tracking failed (non-fatal): ${err}`);
804
+ }
805
+
776
806
  // Context pruning: dedup/supersede/purge tool outputs
777
807
  try {
778
808
  const toolArgs = (_output.metadata?.args || {}) as Record<
@@ -40,6 +40,7 @@ import { basename, dirname, join, resolve } from "path";
40
40
  import { fileURLToPath } from "url";
41
41
  import { existsSync, readFileSync, statSync } from "fs";
42
42
  import { createHooks } from "./hooks.js";
43
+ import { isDebugEnabled } from "../lib/logger.js";
43
44
  import type { Plugin, PluginInput, Hooks } from "./types.js";
44
45
 
45
46
  // Resolve the plugin package root so we can find bundled skills.
@@ -218,7 +219,7 @@ export const AidePlugin: Plugin = async (ctx: PluginInput): Promise<Hooks> => {
218
219
  }
219
220
 
220
221
  // Also log to stderr when debugging
221
- if (process.env.AIDE_DEBUG === "1") console.error(rawLog);
222
+ if (isDebugEnabled()) console.error(rawLog);
222
223
 
223
224
  const resolved = resolveProjectRoot(ctx);
224
225
 
@@ -231,7 +232,7 @@ export const AidePlugin: Plugin = async (ctx: PluginInput): Promise<Hooks> => {
231
232
  } catch {
232
233
  // non-fatal
233
234
  }
234
- if (process.env.AIDE_DEBUG === "1") console.error(resolvedLog);
235
+ if (isDebugEnabled()) console.error(resolvedLog);
235
236
 
236
237
  return createHooks(resolved.root, ctx.worktree, ctx.client, pluginRoot, {
237
238
  skipInit: !resolved.hasProjectRoot,
@@ -1,159 +0,0 @@
1
- #!/bin/bash
2
- # aide-wrapper.sh - Ensures aide binary exists before executing
3
- #
4
- # This wrapper is called by an assistant's MCP server configuration.
5
- # It finds the aide binary, downloads it if missing, then delegates to it.
6
- #
7
- # Plugin root resolution order:
8
- # 1. AIDE_PLUGIN_ROOT (canonical, platform-agnostic)
9
- # 2. CLAUDE_PLUGIN_ROOT (set by Claude Code)
10
- # 3. SCRIPT_DIR/.. (fallback: infer from wrapper location)
11
- #
12
- # Lives at: <plugin-root>/bin/aide-wrapper.sh
13
- # Binary at: <plugin-root>/bin/aide
14
- #
15
- # Logs written to: .aide/_logs/wrapper.log
16
-
17
- set -eo pipefail
18
-
19
- # Resolve symlinks so that invoking via node_modules/.bin/aide-wrapper
20
- # (which is a symlink to the real package) gives us the real package dir.
21
- REAL_SCRIPT="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
22
- SCRIPT_DIR="$(cd "$(dirname "$REAL_SCRIPT")" && pwd)"
23
- PLUGIN_ROOT="${AIDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}"
24
-
25
- # Binary extension
26
- EXT=""
27
- if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
28
- EXT=".exe"
29
- fi
30
-
31
- BINARY="$PLUGIN_ROOT/bin/aide${EXT}"
32
- BIN_DIR="$PLUGIN_ROOT/bin"
33
-
34
- # Setup logging
35
- LOG_DIR="$PLUGIN_ROOT/.aide/_logs"
36
- LOG_FILE="$LOG_DIR/wrapper.log"
37
- mkdir -p "$LOG_DIR" 2>/dev/null || true
38
-
39
- log() {
40
- local timestamp
41
- timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
42
- local msg="[$timestamp] [aide-wrapper] $*"
43
- echo "$msg" >&2
44
- echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
45
- }
46
-
47
- PARENT_PID=$PPID
48
- PARENT_CMD=$(ps -o comm= -p "$PPID" 2>/dev/null || echo "unknown")
49
- PARENT_ARGS=$(ps -o args= -p "$PPID" 2>/dev/null || echo "unknown")
50
- log "Starting wrapper (pid=$$, ppid=$PARENT_PID, parent=$PARENT_CMD, parent_args=$PARENT_ARGS, args=$*)"
51
- log "PLUGIN_ROOT=$PLUGIN_ROOT"
52
- log "BINARY=$BINARY"
53
-
54
- # Version comparison: returns 0 (true) if $1 >= $2 (semver base only)
55
- version_gte() {
56
- local IFS=.
57
- local a=($1) b=($2)
58
- for i in 0 1 2; do
59
- local va=${a[$i]:-0} vb=${b[$i]:-0}
60
- if (( va > vb )); then return 0; fi
61
- if (( va < vb )); then return 1; fi
62
- done
63
- return 0
64
- }
65
-
66
- # Determine if we need to download
67
- NEEDS_DOWNLOAD=false
68
-
69
- if [[ ! -x "$BINARY" ]]; then
70
- NEEDS_DOWNLOAD=true
71
- log "Binary not found or not executable"
72
- else
73
- # Check if this is a dev build that should be preserved
74
- BINARY_VERSION=$("$BINARY" version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.+]+)?' | head -1 || true)
75
- log "Binary version: ${BINARY_VERSION:-unknown}"
76
-
77
- if [[ -n "$BINARY_VERSION" && "$BINARY_VERSION" == *"-dev."* ]]; then
78
- # Dev build — check base version against plugin version
79
- BASE_VERSION="${BINARY_VERSION%%-*}"
80
- # Read plugin version: try .claude-plugin/plugin.json (dev), fall back to package.json (npm)
81
- PLUGIN_VERSION=$(grep -oP '"version"\s*:\s*"\K[0-9]+\.[0-9]+\.[0-9]+' "$PLUGIN_ROOT/.claude-plugin/plugin.json" 2>/dev/null \
82
- || grep -oP '"version"\s*:\s*"\K[0-9]+\.[0-9]+\.[0-9]+' "$PLUGIN_ROOT/package.json" 2>/dev/null \
83
- || true)
84
-
85
- if [[ -n "$PLUGIN_VERSION" ]] && version_gte "$BASE_VERSION" "$PLUGIN_VERSION"; then
86
- log "Dev build v$BINARY_VERSION (base $BASE_VERSION >= plugin v$PLUGIN_VERSION), using local build"
87
- else
88
- NEEDS_DOWNLOAD=true
89
- log "Dev build v$BINARY_VERSION is older than plugin v${PLUGIN_VERSION:-unknown}, re-downloading"
90
- fi
91
- else
92
- log "Release binary v${BINARY_VERSION:-unknown}"
93
- fi
94
- fi
95
-
96
- if [[ "$NEEDS_DOWNLOAD" == "true" ]]; then
97
- LOCKFILE="$BIN_DIR/.aide-download.lock"
98
-
99
- # Resolve the downloader script — prefer src/ (dev) but fall back to dist/ (npm install)
100
- if [[ -f "$PLUGIN_ROOT/src/lib/aide-downloader.ts" ]]; then
101
- DOWNLOADER="$PLUGIN_ROOT/src/lib/aide-downloader.ts"
102
- elif [[ -f "$PLUGIN_ROOT/dist/lib/aide-downloader.js" ]]; then
103
- DOWNLOADER="$PLUGIN_ROOT/dist/lib/aide-downloader.js"
104
- else
105
- log "ERROR: Cannot find aide-downloader in src/ or dist/ under $PLUGIN_ROOT"
106
- exit 1
107
- fi
108
-
109
- # Remove the stale/outdated binary before entering the lock so that
110
- # the re-check inside the critical section can distinguish between
111
- # "no binary yet" and "a concurrent process just downloaded the right one".
112
- if [[ -f "$BINARY" ]]; then
113
- log "Removing outdated binary before download"
114
- rm -f "$BINARY" 2>/dev/null || true
115
- fi
116
-
117
- # Use flock to ensure only one process downloads at a time.
118
- # If another process holds the lock, we wait for it to finish.
119
- (
120
- flock -w 60 9 || { log "ERROR: Timed out waiting for download lock"; exit 1; }
121
-
122
- # Re-check after acquiring lock — another process may have finished the download
123
- if [[ -x "$BINARY" ]]; then
124
- log "Binary appeared while waiting for lock (downloaded by another process)"
125
- else
126
- log "Downloading binary..."
127
- log "Using downloader: $DOWNLOADER"
128
-
129
- # Use bun/tsx for .ts files (dev), node for .js files (npm install)
130
- if [[ "$DOWNLOADER" == *.ts ]]; then
131
- if command -v bun &>/dev/null; then
132
- RUNNER="bun"
133
- elif command -v tsx &>/dev/null; then
134
- RUNNER="tsx"
135
- else
136
- RUNNER="npx --yes tsx"
137
- fi
138
- else
139
- RUNNER="node"
140
- fi
141
-
142
- if ! $RUNNER "$DOWNLOADER" --dest "$BIN_DIR" 2>&1 | tee -a "$LOG_FILE" >&2; then
143
- log "ERROR: Downloader failed"
144
- exit 1
145
- fi
146
-
147
- if [[ ! -x "$BINARY" ]]; then
148
- log "ERROR: Binary not found after download"
149
- exit 1
150
- fi
151
- fi
152
- ) 9>"$LOCKFILE"
153
-
154
- rm -f "$LOCKFILE"
155
- log "Binary ready at $BINARY"
156
- fi
157
-
158
- log "Executing: $BINARY $*"
159
- exec "$BINARY" "$@"
@@ -1,362 +0,0 @@
1
- /**
2
- * Skills Registry (skills.sh integration)
3
- *
4
- * STATUS: UTILITY LIBRARY - Not yet integrated into hooks
5
- *
6
- * This library provides programmatic management of skills from the skills.sh
7
- * marketplace. Skills are markdown files with YAML frontmatter that define
8
- * triggers and behaviors for the skill-injector hook.
9
- *
10
- * Currently, skill discovery and loading is done directly in skill-injector.ts.
11
- * This registry library is intended for future use cases:
12
- *
13
- * Future integration:
14
- * - CLI commands for `aide skill install/uninstall/update`
15
- * - Automatic skill updates on session start
16
- * - Skill marketplace browsing and search
17
- *
18
- * The skill-injector hook currently handles skill discovery inline because:
19
- * 1. It only needs to read local skill files, not manage them
20
- * 2. It needs to be fast (runs on every user prompt)
21
- * 3. Registry features (install, update) are not needed at runtime
22
- */
23
-
24
- import { execFileSync } from "child_process";
25
- import {
26
- existsSync,
27
- readFileSync,
28
- writeFileSync,
29
- mkdirSync,
30
- readdirSync,
31
- copyFileSync,
32
- unlinkSync,
33
- } from "fs";
34
- import { join, basename, resolve } from "path";
35
- import { homedir } from "os";
36
-
37
- export interface SkillMetadata {
38
- name: string;
39
- version: string;
40
- source: string;
41
- installedAt: string;
42
- updatedAt?: string;
43
- }
44
-
45
- export interface SkillsRegistry {
46
- installed: SkillMetadata[];
47
- autoUpdate: boolean;
48
- syncInterval: string;
49
- lastSync?: string;
50
- }
51
-
52
- const REGISTRY_FILE = ".aide/skills/registry.json";
53
- const SKILLS_DIR = ".aide/skills";
54
- const GLOBAL_SKILLS_DIR = join(homedir(), ".aide", "skills");
55
-
56
- /**
57
- * Validate and sanitize a URL for safe fetching
58
- * Prevents command injection by ensuring only valid http/https URLs
59
- */
60
- function validateUrl(url: string): string | null {
61
- try {
62
- const parsed = new URL(url);
63
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
64
- return null;
65
- }
66
- // Return the properly parsed URL (sanitized)
67
- return parsed.toString();
68
- } catch {
69
- return null;
70
- }
71
- }
72
-
73
- /**
74
- * Safely fetch content from a URL using curl
75
- * Uses execFileSync with argument array to prevent command injection
76
- */
77
- function safeFetch(url: string): string | null {
78
- const validUrl = validateUrl(url);
79
- if (!validUrl) {
80
- console.error(`Invalid URL: ${url}`);
81
- return null;
82
- }
83
- try {
84
- return execFileSync("curl", ["-sL", validUrl], {
85
- encoding: "utf-8",
86
- timeout: 30000,
87
- });
88
- } catch (error) {
89
- console.error(`Failed to fetch ${validUrl}: ${error}`);
90
- return null;
91
- }
92
- }
93
-
94
- /**
95
- * Load skills registry
96
- */
97
- export function loadRegistry(cwd: string): SkillsRegistry {
98
- const registryPath = join(cwd, REGISTRY_FILE);
99
- if (existsSync(registryPath)) {
100
- try {
101
- return JSON.parse(readFileSync(registryPath, "utf-8"));
102
- } catch {
103
- // Return default
104
- }
105
- }
106
- return {
107
- installed: [],
108
- autoUpdate: true,
109
- syncInterval: "24h",
110
- };
111
- }
112
-
113
- /**
114
- * Save skills registry
115
- */
116
- export function saveRegistry(cwd: string, registry: SkillsRegistry): void {
117
- const skillsDir = join(cwd, SKILLS_DIR);
118
- if (!existsSync(skillsDir)) {
119
- mkdirSync(skillsDir, { recursive: true });
120
- }
121
- writeFileSync(join(cwd, REGISTRY_FILE), JSON.stringify(registry, null, 2));
122
- }
123
-
124
- /**
125
- * Sanitize a skill name to prevent path traversal.
126
- * Strips path separators and rejects names that would escape the target directory.
127
- */
128
- function sanitizeSkillName(name: string): string {
129
- // Take only the basename to strip any directory components
130
- const safe = basename(name).replace(/[^a-zA-Z0-9._-]/g, "_");
131
- if (!safe || safe === "." || safe === "..") {
132
- throw new Error(`Invalid skill name: ${name}`);
133
- }
134
- return safe;
135
- }
136
-
137
- /**
138
- * Validate that a resolved path stays within the expected directory.
139
- */
140
- function assertWithinDir(filePath: string, dir: string): void {
141
- const resolved = resolve(filePath);
142
- const resolvedDir = resolve(dir);
143
- if (!resolved.startsWith(resolvedDir + "/") && resolved !== resolvedDir) {
144
- throw new Error(`Path traversal detected: ${filePath} escapes ${dir}`);
145
- }
146
- }
147
-
148
- /**
149
- * Install a skill from skills.sh or a URL
150
- *
151
- * Formats:
152
- * - skills.sh/<author>/<skill>
153
- * - https://github.com/<owner>/<repo>/blob/main/<path>.md
154
- * - Local file path
155
- */
156
- export async function installSkill(
157
- cwd: string,
158
- source: string,
159
- options: { global?: boolean } = {},
160
- ): Promise<SkillMetadata | null> {
161
- const targetDir = options.global ? GLOBAL_SKILLS_DIR : join(cwd, SKILLS_DIR);
162
-
163
- // Ensure target directory exists
164
- if (!existsSync(targetDir)) {
165
- mkdirSync(targetDir, { recursive: true });
166
- }
167
-
168
- let content: string;
169
- let name: string;
170
- let version = "1.0.0";
171
-
172
- // Handle different source formats
173
- if (
174
- source.startsWith("skills.sh/") ||
175
- source.startsWith("https://skills.sh/")
176
- ) {
177
- // skills.sh marketplace format: skills.sh/author/skill
178
- const skillPath = source
179
- .replace("skills.sh/", "")
180
- .replace("https://skills.sh/", "");
181
- const url = `https://skills.sh/api/skills/${skillPath}/raw`;
182
-
183
- const fetched = safeFetch(url);
184
- if (!fetched) {
185
- console.error(`Failed to fetch skill from skills.sh`);
186
- return null;
187
- }
188
- content = fetched;
189
- name = skillPath.split("/").pop() || "unknown";
190
- } else if (source.startsWith("https://github.com/")) {
191
- // GitHub raw file
192
- const rawUrl = source
193
- .replace("github.com", "raw.githubusercontent.com")
194
- .replace("/blob/", "/");
195
-
196
- const fetched = safeFetch(rawUrl);
197
- if (!fetched) {
198
- console.error(`Failed to fetch skill from GitHub`);
199
- return null;
200
- }
201
- content = fetched;
202
- name = basename(source, ".md");
203
- } else if (source.startsWith("https://") || source.startsWith("http://")) {
204
- // Direct URL
205
- const fetched = safeFetch(source);
206
- if (!fetched) {
207
- console.error(`Failed to fetch skill`);
208
- return null;
209
- }
210
- content = fetched;
211
- name = basename(source, ".md");
212
- } else if (existsSync(source)) {
213
- // Local file
214
- content = readFileSync(source, "utf-8");
215
- name = basename(source, ".md");
216
- } else {
217
- console.error(`Invalid skill source: ${source}`);
218
- return null;
219
- }
220
-
221
- // Parse skill to extract metadata
222
- const meta = parseSkillFrontmatter(content);
223
- if (meta) {
224
- name = meta.name || name;
225
- version = meta.version || version;
226
- }
227
-
228
- // Sanitize name and write skill file
229
- name = sanitizeSkillName(name);
230
- const skillPath = join(targetDir, `${name}.md`);
231
- assertWithinDir(skillPath, targetDir);
232
- writeFileSync(skillPath, content);
233
-
234
- // Update registry
235
- const registry = loadRegistry(cwd);
236
- const existing = registry.installed.findIndex((s) => s.name === name);
237
-
238
- const metadata: SkillMetadata = {
239
- name,
240
- version,
241
- source,
242
- installedAt: new Date().toISOString(),
243
- };
244
-
245
- if (existing >= 0) {
246
- metadata.installedAt = registry.installed[existing].installedAt;
247
- metadata.updatedAt = new Date().toISOString();
248
- registry.installed[existing] = metadata;
249
- } else {
250
- registry.installed.push(metadata);
251
- }
252
-
253
- saveRegistry(cwd, registry);
254
-
255
- console.log(`Installed skill: ${name} (${version})`);
256
- return metadata;
257
- }
258
-
259
- /**
260
- * Uninstall a skill
261
- */
262
- export function uninstallSkill(cwd: string, name: string): boolean {
263
- const registry = loadRegistry(cwd);
264
- const index = registry.installed.findIndex((s) => s.name === name);
265
-
266
- if (index < 0) {
267
- console.error(`Skill not installed: ${name}`);
268
- return false;
269
- }
270
-
271
- // Sanitize name and remove file
272
- const safeName = sanitizeSkillName(name);
273
- const skillPath = join(cwd, SKILLS_DIR, `${safeName}.md`);
274
- assertWithinDir(skillPath, join(cwd, SKILLS_DIR));
275
- if (existsSync(skillPath)) {
276
- try {
277
- unlinkSync(skillPath);
278
- } catch {
279
- // Ignore
280
- }
281
- }
282
-
283
- // Update registry
284
- registry.installed.splice(index, 1);
285
- saveRegistry(cwd, registry);
286
-
287
- console.log(`Uninstalled skill: ${name}`);
288
- return true;
289
- }
290
-
291
- /**
292
- * List installed skills
293
- */
294
- export function listSkills(cwd: string): SkillMetadata[] {
295
- const registry = loadRegistry(cwd);
296
- return registry.installed;
297
- }
298
-
299
- /**
300
- * Update all skills (if autoUpdate is enabled)
301
- */
302
- export async function updateSkills(cwd: string): Promise<void> {
303
- const registry = loadRegistry(cwd);
304
-
305
- if (!registry.autoUpdate) {
306
- console.log("Auto-update is disabled");
307
- return;
308
- }
309
-
310
- console.log("Updating skills...");
311
-
312
- for (const skill of registry.installed) {
313
- if (
314
- skill.source.startsWith("skills.sh/") ||
315
- skill.source.startsWith("https://")
316
- ) {
317
- await installSkill(cwd, skill.source);
318
- }
319
- }
320
-
321
- registry.lastSync = new Date().toISOString();
322
- saveRegistry(cwd, registry);
323
-
324
- console.log("Skills updated");
325
- }
326
-
327
- /**
328
- * Search for skills (placeholder - would query skills.sh API)
329
- */
330
- export async function searchSkills(
331
- query: string,
332
- ): Promise<Array<{ name: string; description: string; author: string }>> {
333
- // In a real implementation, this would query the skills.sh API
334
- console.log(`Searching for skills matching: ${query}`);
335
- console.log("Note: skills.sh integration requires API access");
336
-
337
- return [];
338
- }
339
-
340
- /**
341
- * Parse YAML frontmatter from skill content
342
- */
343
- function parseSkillFrontmatter(
344
- content: string,
345
- ): { name?: string; version?: string; description?: string } | null {
346
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
347
- if (!match) return null;
348
-
349
- const yaml = match[1];
350
- const result: { name?: string; version?: string; description?: string } = {};
351
-
352
- const nameMatch = yaml.match(/^name:\s*(.+)$/m);
353
- if (nameMatch) result.name = nameMatch[1].trim();
354
-
355
- const versionMatch = yaml.match(/^version:\s*(.+)$/m);
356
- if (versionMatch) result.version = versionMatch[1].trim();
357
-
358
- const descMatch = yaml.match(/^description:\s*(.+)$/m);
359
- if (descMatch) result.description = descMatch[1].trim();
360
-
361
- return result;
362
- }