opencode-swarm-plugin 0.23.5 → 0.24.0

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.
@@ -0,0 +1,583 @@
1
+ /**
2
+ * Event Types for Beads Event Sourcing
3
+ *
4
+ * These events form an audit trail for all bead operations.
5
+ * Events are NOT replayed for state reconstruction (beads uses hybrid CRUD + audit trail).
6
+ * Events enable:
7
+ * - Full audit history
8
+ * - Debugging distributed swarm operations
9
+ * - Learning from bead lifecycle patterns
10
+ * - Integration with swarm-mail coordination
11
+ *
12
+ * Design notes:
13
+ * - 75% reusable infrastructure from swarm-mail
14
+ * - Events stay local (PGLite/SQLite), not written to JSONL
15
+ * - JSONL export happens from projection snapshots (proven git merge driver)
16
+ * - Follows same BaseEventSchema pattern as swarm-mail
17
+ */
18
+ import { z } from "zod";
19
+ import {
20
+ BeadDependencySchema,
21
+ BeadStatusSchema,
22
+ BeadTypeSchema,
23
+ } from "./bead.js";
24
+
25
+ // ============================================================================
26
+ // Base Event Schema (mirrors swarm-mail pattern)
27
+ // ============================================================================
28
+
29
+ /**
30
+ * Base fields present on all bead events
31
+ */
32
+ export const BaseBeadEventSchema = z.object({
33
+ /** Auto-generated event ID */
34
+ id: z.number().optional(),
35
+ /** Event type discriminator */
36
+ type: z.string(),
37
+ /** Project key (usually absolute path) */
38
+ project_key: z.string(),
39
+ /** Timestamp when event occurred */
40
+ timestamp: z.number(), // Unix ms
41
+ /** Sequence number for ordering */
42
+ sequence: z.number().optional(),
43
+ });
44
+
45
+ // ============================================================================
46
+ // Issue Lifecycle Events
47
+ // ============================================================================
48
+
49
+ /**
50
+ * Bead created
51
+ *
52
+ * Emitted when:
53
+ * - User calls `bd create`
54
+ * - Swarm epic decomposition creates subtasks
55
+ * - Agent spawns new beads during work
56
+ */
57
+ export const BeadCreatedEventSchema = BaseBeadEventSchema.extend({
58
+ type: z.literal("bead_created"),
59
+ bead_id: z.string(),
60
+ title: z.string(),
61
+ description: z.string().optional(),
62
+ issue_type: BeadTypeSchema,
63
+ priority: z.number().int().min(0).max(3),
64
+ parent_id: z.string().optional(),
65
+ /** Agent/user who created the bead */
66
+ created_by: z.string().optional(),
67
+ /** Metadata for tracking (e.g., epic context, swarm strategy) */
68
+ metadata: z.record(z.string(), z.unknown()).optional(),
69
+ });
70
+
71
+ /**
72
+ * Bead updated (generic field changes)
73
+ *
74
+ * Emitted for non-status field updates: title, description, priority
75
+ */
76
+ export const BeadUpdatedEventSchema = BaseBeadEventSchema.extend({
77
+ type: z.literal("bead_updated"),
78
+ bead_id: z.string(),
79
+ /** Agent/user who made the update */
80
+ updated_by: z.string().optional(),
81
+ /** Changed fields with old and new values */
82
+ changes: z.object({
83
+ title: z
84
+ .object({
85
+ old: z.string(),
86
+ new: z.string(),
87
+ })
88
+ .optional(),
89
+ description: z
90
+ .object({
91
+ old: z.string(),
92
+ new: z.string(),
93
+ })
94
+ .optional(),
95
+ priority: z
96
+ .object({
97
+ old: z.number(),
98
+ new: z.number(),
99
+ })
100
+ .optional(),
101
+ }),
102
+ });
103
+
104
+ /**
105
+ * Bead status changed
106
+ *
107
+ * Separate event for status transitions to enable workflow analysis.
108
+ * Tracks state machine: open → in_progress → (blocked | closed)
109
+ */
110
+ export const BeadStatusChangedEventSchema = BaseBeadEventSchema.extend({
111
+ type: z.literal("bead_status_changed"),
112
+ bead_id: z.string(),
113
+ from_status: BeadStatusSchema,
114
+ to_status: BeadStatusSchema,
115
+ /** Agent/user who changed status */
116
+ changed_by: z.string().optional(),
117
+ /** Optional reason (required for closed status) */
118
+ reason: z.string().optional(),
119
+ });
120
+
121
+ /**
122
+ * Bead closed
123
+ *
124
+ * Explicit close event (subset of status_changed for convenience).
125
+ * Includes closure reason for audit trail.
126
+ */
127
+ export const BeadClosedEventSchema = BaseBeadEventSchema.extend({
128
+ type: z.literal("bead_closed"),
129
+ bead_id: z.string(),
130
+ reason: z.string(),
131
+ /** Agent/user who closed */
132
+ closed_by: z.string().optional(),
133
+ /** Files touched during work (from swarm completion) */
134
+ files_touched: z.array(z.string()).optional(),
135
+ /** Duration in ms (if tracked by agent) */
136
+ duration_ms: z.number().optional(),
137
+ });
138
+
139
+ /**
140
+ * Bead reopened
141
+ *
142
+ * Emitted when closed bead is reopened.
143
+ */
144
+ export const BeadReopenedEventSchema = BaseBeadEventSchema.extend({
145
+ type: z.literal("bead_reopened"),
146
+ bead_id: z.string(),
147
+ reason: z.string().optional(),
148
+ /** Agent/user who reopened */
149
+ reopened_by: z.string().optional(),
150
+ });
151
+
152
+ /**
153
+ * Bead deleted
154
+ *
155
+ * Hard delete event (rare - beads are usually closed, not deleted).
156
+ * Useful for cleaning up spurious/duplicate beads.
157
+ */
158
+ export const BeadDeletedEventSchema = BaseBeadEventSchema.extend({
159
+ type: z.literal("bead_deleted"),
160
+ bead_id: z.string(),
161
+ reason: z.string().optional(),
162
+ /** Agent/user who deleted */
163
+ deleted_by: z.string().optional(),
164
+ });
165
+
166
+ // ============================================================================
167
+ // Dependency Events
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Dependency added between beads
172
+ *
173
+ * Supports 4 relationship types:
174
+ * - blocks: This bead blocks the target
175
+ * - blocked-by: This bead is blocked by the target
176
+ * - related: Informational link
177
+ * - discovered-from: Bead spawned from investigation of target
178
+ */
179
+ export const BeadDependencyAddedEventSchema = BaseBeadEventSchema.extend({
180
+ type: z.literal("bead_dependency_added"),
181
+ bead_id: z.string(),
182
+ /** Dependency relationship */
183
+ dependency: BeadDependencySchema,
184
+ /** Agent/user who added dependency */
185
+ added_by: z.string().optional(),
186
+ /** Optional reason (e.g., "needs auth service before OAuth implementation") */
187
+ reason: z.string().optional(),
188
+ });
189
+
190
+ /**
191
+ * Dependency removed
192
+ *
193
+ * Emitted when dependency is no longer relevant or was added in error.
194
+ */
195
+ export const BeadDependencyRemovedEventSchema = BaseBeadEventSchema.extend({
196
+ type: z.literal("bead_dependency_removed"),
197
+ bead_id: z.string(),
198
+ /** Dependency being removed */
199
+ dependency: BeadDependencySchema,
200
+ /** Agent/user who removed */
201
+ removed_by: z.string().optional(),
202
+ reason: z.string().optional(),
203
+ });
204
+
205
+ // ============================================================================
206
+ // Label Events
207
+ // ============================================================================
208
+
209
+ /**
210
+ * Label added to bead
211
+ *
212
+ * Labels are string tags for categorization/filtering.
213
+ * Common labels: "p0", "needs-review", "blocked-external", "tech-debt"
214
+ */
215
+ export const BeadLabelAddedEventSchema = BaseBeadEventSchema.extend({
216
+ type: z.literal("bead_label_added"),
217
+ bead_id: z.string(),
218
+ label: z.string(),
219
+ /** Agent/user who added label */
220
+ added_by: z.string().optional(),
221
+ });
222
+
223
+ /**
224
+ * Label removed from bead
225
+ */
226
+ export const BeadLabelRemovedEventSchema = BaseBeadEventSchema.extend({
227
+ type: z.literal("bead_label_removed"),
228
+ bead_id: z.string(),
229
+ label: z.string(),
230
+ /** Agent/user who removed label */
231
+ removed_by: z.string().optional(),
232
+ });
233
+
234
+ // ============================================================================
235
+ // Comment Events
236
+ // ============================================================================
237
+
238
+ /**
239
+ * Comment added to bead
240
+ *
241
+ * Supports agent-to-agent communication, human notes, and progress updates.
242
+ */
243
+ export const BeadCommentAddedEventSchema = BaseBeadEventSchema.extend({
244
+ type: z.literal("bead_comment_added"),
245
+ bead_id: z.string(),
246
+ /** Auto-generated comment ID */
247
+ comment_id: z.number().optional(),
248
+ author: z.string(),
249
+ body: z.string(),
250
+ /** Optional parent comment ID for threading */
251
+ parent_comment_id: z.number().optional(),
252
+ /** Comment metadata (e.g., attachments, mentions) */
253
+ metadata: z.record(z.string(), z.unknown()).optional(),
254
+ });
255
+
256
+ /**
257
+ * Comment updated (edit)
258
+ */
259
+ export const BeadCommentUpdatedEventSchema = BaseBeadEventSchema.extend({
260
+ type: z.literal("bead_comment_updated"),
261
+ bead_id: z.string(),
262
+ comment_id: z.number(),
263
+ old_body: z.string(),
264
+ new_body: z.string(),
265
+ updated_by: z.string(),
266
+ });
267
+
268
+ /**
269
+ * Comment deleted
270
+ */
271
+ export const BeadCommentDeletedEventSchema = BaseBeadEventSchema.extend({
272
+ type: z.literal("bead_comment_deleted"),
273
+ bead_id: z.string(),
274
+ comment_id: z.number(),
275
+ deleted_by: z.string(),
276
+ reason: z.string().optional(),
277
+ });
278
+
279
+ // ============================================================================
280
+ // Epic Events
281
+ // ============================================================================
282
+
283
+ /**
284
+ * Child bead added to epic
285
+ *
286
+ * Emitted when:
287
+ * - Epic created with subtasks (batch event for each child)
288
+ * - User manually adds child via `bd add-child`
289
+ * - Agent spawns additional subtask during work
290
+ */
291
+ export const BeadEpicChildAddedEventSchema = BaseBeadEventSchema.extend({
292
+ type: z.literal("bead_epic_child_added"),
293
+ /** Epic ID */
294
+ bead_id: z.string(),
295
+ /** Child bead ID */
296
+ child_id: z.string(),
297
+ /** Optional index for ordering */
298
+ child_index: z.number().optional(),
299
+ added_by: z.string().optional(),
300
+ });
301
+
302
+ /**
303
+ * Child bead removed from epic
304
+ *
305
+ * Rare - usually happens when subtask is merged/consolidated.
306
+ */
307
+ export const BeadEpicChildRemovedEventSchema = BaseBeadEventSchema.extend({
308
+ type: z.literal("bead_epic_child_removed"),
309
+ /** Epic ID */
310
+ bead_id: z.string(),
311
+ /** Child bead ID */
312
+ child_id: z.string(),
313
+ removed_by: z.string().optional(),
314
+ reason: z.string().optional(),
315
+ });
316
+
317
+ /**
318
+ * Epic eligible for closure
319
+ *
320
+ * Emitted when all child beads are closed.
321
+ * Triggers coordinator review for epic closure.
322
+ */
323
+ export const BeadEpicClosureEligibleEventSchema = BaseBeadEventSchema.extend({
324
+ type: z.literal("bead_epic_closure_eligible"),
325
+ bead_id: z.string(),
326
+ /** Child bead IDs (all closed) */
327
+ child_ids: z.array(z.string()),
328
+ /** Total duration across all children */
329
+ total_duration_ms: z.number().optional(),
330
+ /** Aggregate file changes */
331
+ all_files_touched: z.array(z.string()).optional(),
332
+ });
333
+
334
+ // ============================================================================
335
+ // Swarm Integration Events (bridge to swarm-mail)
336
+ // ============================================================================
337
+
338
+ /**
339
+ * Bead assigned to agent
340
+ *
341
+ * Links beads to swarm-mail's agent tracking.
342
+ * Emitted when agent calls `beads_start` or swarm spawns worker.
343
+ */
344
+ export const BeadAssignedEventSchema = BaseBeadEventSchema.extend({
345
+ type: z.literal("bead_assigned"),
346
+ bead_id: z.string(),
347
+ agent_name: z.string(),
348
+ /** Agent's task description for context */
349
+ task_description: z.string().optional(),
350
+ });
351
+
352
+ /**
353
+ * Bead work started
354
+ *
355
+ * Separate from status change to track actual work start time.
356
+ * Useful for duration/velocity metrics.
357
+ */
358
+ export const BeadWorkStartedEventSchema = BaseBeadEventSchema.extend({
359
+ type: z.literal("bead_work_started"),
360
+ bead_id: z.string(),
361
+ agent_name: z.string(),
362
+ /** Files reserved for this work */
363
+ reserved_files: z.array(z.string()).optional(),
364
+ });
365
+
366
+ /**
367
+ * Bead compacted
368
+ *
369
+ * Emitted when bead's event history is compressed (rare).
370
+ * Follows steveyegge/beads pattern - old events archived, projection preserved.
371
+ */
372
+ export const BeadCompactedEventSchema = BaseBeadEventSchema.extend({
373
+ type: z.literal("bead_compacted"),
374
+ bead_id: z.string(),
375
+ /** Number of events archived */
376
+ events_archived: z.number(),
377
+ /** New event store start sequence */
378
+ new_start_sequence: z.number(),
379
+ });
380
+
381
+ // ============================================================================
382
+ // Union Type
383
+ // ============================================================================
384
+
385
+ export const BeadEventSchema = z.discriminatedUnion("type", [
386
+ // Lifecycle
387
+ BeadCreatedEventSchema,
388
+ BeadUpdatedEventSchema,
389
+ BeadStatusChangedEventSchema,
390
+ BeadClosedEventSchema,
391
+ BeadReopenedEventSchema,
392
+ BeadDeletedEventSchema,
393
+
394
+ // Dependencies
395
+ BeadDependencyAddedEventSchema,
396
+ BeadDependencyRemovedEventSchema,
397
+
398
+ // Labels
399
+ BeadLabelAddedEventSchema,
400
+ BeadLabelRemovedEventSchema,
401
+
402
+ // Comments
403
+ BeadCommentAddedEventSchema,
404
+ BeadCommentUpdatedEventSchema,
405
+ BeadCommentDeletedEventSchema,
406
+
407
+ // Epic
408
+ BeadEpicChildAddedEventSchema,
409
+ BeadEpicChildRemovedEventSchema,
410
+ BeadEpicClosureEligibleEventSchema,
411
+
412
+ // Swarm Integration
413
+ BeadAssignedEventSchema,
414
+ BeadWorkStartedEventSchema,
415
+
416
+ // Maintenance
417
+ BeadCompactedEventSchema,
418
+ ]);
419
+
420
+ export type BeadEvent = z.infer<typeof BeadEventSchema>;
421
+
422
+ // ============================================================================
423
+ // Individual event types for convenience
424
+ // ============================================================================
425
+
426
+ export type BeadCreatedEvent = z.infer<typeof BeadCreatedEventSchema>;
427
+ export type BeadUpdatedEvent = z.infer<typeof BeadUpdatedEventSchema>;
428
+ export type BeadStatusChangedEvent = z.infer<
429
+ typeof BeadStatusChangedEventSchema
430
+ >;
431
+ export type BeadClosedEvent = z.infer<typeof BeadClosedEventSchema>;
432
+ export type BeadReopenedEvent = z.infer<typeof BeadReopenedEventSchema>;
433
+ export type BeadDeletedEvent = z.infer<typeof BeadDeletedEventSchema>;
434
+ export type BeadDependencyAddedEvent = z.infer<
435
+ typeof BeadDependencyAddedEventSchema
436
+ >;
437
+ export type BeadDependencyRemovedEvent = z.infer<
438
+ typeof BeadDependencyRemovedEventSchema
439
+ >;
440
+ export type BeadLabelAddedEvent = z.infer<typeof BeadLabelAddedEventSchema>;
441
+ export type BeadLabelRemovedEvent = z.infer<typeof BeadLabelRemovedEventSchema>;
442
+ export type BeadCommentAddedEvent = z.infer<typeof BeadCommentAddedEventSchema>;
443
+ export type BeadCommentUpdatedEvent = z.infer<
444
+ typeof BeadCommentUpdatedEventSchema
445
+ >;
446
+ export type BeadCommentDeletedEvent = z.infer<
447
+ typeof BeadCommentDeletedEventSchema
448
+ >;
449
+ export type BeadEpicChildAddedEvent = z.infer<
450
+ typeof BeadEpicChildAddedEventSchema
451
+ >;
452
+ export type BeadEpicChildRemovedEvent = z.infer<
453
+ typeof BeadEpicChildRemovedEventSchema
454
+ >;
455
+ export type BeadEpicClosureEligibleEvent = z.infer<
456
+ typeof BeadEpicClosureEligibleEventSchema
457
+ >;
458
+ export type BeadAssignedEvent = z.infer<typeof BeadAssignedEventSchema>;
459
+ export type BeadWorkStartedEvent = z.infer<typeof BeadWorkStartedEventSchema>;
460
+ export type BeadCompactedEvent = z.infer<typeof BeadCompactedEventSchema>;
461
+
462
+ // ============================================================================
463
+ // Event Helpers
464
+ // ============================================================================
465
+
466
+ /**
467
+ * Create a bead event with timestamp and validate
468
+ *
469
+ * Usage:
470
+ * ```typescript
471
+ * const event = createBeadEvent("bead_created", {
472
+ * project_key: "/path/to/repo",
473
+ * bead_id: "bd-123",
474
+ * title: "Add auth",
475
+ * issue_type: "feature",
476
+ * priority: 2
477
+ * });
478
+ * ```
479
+ */
480
+ export function createBeadEvent<T extends BeadEvent["type"]>(
481
+ type: T,
482
+ data: Omit<
483
+ Extract<BeadEvent, { type: T }>,
484
+ "type" | "timestamp" | "id" | "sequence"
485
+ >,
486
+ ): Extract<BeadEvent, { type: T }> {
487
+ const event = {
488
+ type,
489
+ timestamp: Date.now(),
490
+ ...data,
491
+ } as Extract<BeadEvent, { type: T }>;
492
+
493
+ // Validate
494
+ const result = BeadEventSchema.safeParse(event);
495
+ if (!result.success) {
496
+ throw new Error(`Invalid bead event: ${result.error.message}`);
497
+ }
498
+
499
+ return result.data as Extract<BeadEvent, { type: T }>;
500
+ }
501
+
502
+ /**
503
+ * Type guard for specific bead event types
504
+ *
505
+ * Usage:
506
+ * ```typescript
507
+ * if (isBeadEventType(event, "bead_closed")) {
508
+ * console.log(event.reason); // TypeScript knows this is BeadClosedEvent
509
+ * }
510
+ * ```
511
+ */
512
+ export function isBeadEventType<T extends BeadEvent["type"]>(
513
+ event: BeadEvent,
514
+ type: T,
515
+ ): event is Extract<BeadEvent, { type: T }> {
516
+ return event.type === type;
517
+ }
518
+
519
+ /**
520
+ * Extract bead ID from event (convenience helper)
521
+ *
522
+ * All bead events have bead_id field (or it's the epic's bead_id for epic events).
523
+ */
524
+ export function getBeadIdFromEvent(event: BeadEvent): string {
525
+ return event.bead_id;
526
+ }
527
+
528
+ /**
529
+ * Check if event represents a state transition
530
+ */
531
+ export function isStateTransitionEvent(
532
+ event: BeadEvent,
533
+ ): event is BeadStatusChangedEvent | BeadClosedEvent | BeadReopenedEvent {
534
+ return (
535
+ event.type === "bead_status_changed" ||
536
+ event.type === "bead_closed" ||
537
+ event.type === "bead_reopened"
538
+ );
539
+ }
540
+
541
+ /**
542
+ * Check if event represents an epic operation
543
+ */
544
+ export function isEpicEvent(
545
+ event: BeadEvent,
546
+ ): event is
547
+ | BeadEpicChildAddedEvent
548
+ | BeadEpicChildRemovedEvent
549
+ | BeadEpicClosureEligibleEvent {
550
+ return (
551
+ event.type === "bead_epic_child_added" ||
552
+ event.type === "bead_epic_child_removed" ||
553
+ event.type === "bead_epic_closure_eligible"
554
+ );
555
+ }
556
+
557
+ /**
558
+ * Check if event was triggered by an agent (vs human user)
559
+ */
560
+ export function isAgentEvent(event: BeadEvent): boolean {
561
+ // Agent events have agent_name field or *_by field containing agent signature
562
+ if ("agent_name" in event) return true;
563
+
564
+ const actorFields = [
565
+ "created_by",
566
+ "updated_by",
567
+ "changed_by",
568
+ "closed_by",
569
+ "deleted_by",
570
+ "added_by",
571
+ "removed_by",
572
+ "reopened_by",
573
+ ] as const;
574
+
575
+ return actorFields.some((field) => {
576
+ if (field in event) {
577
+ const value = (event as Record<string, unknown>)[field];
578
+ // Agent names are typically lowercase or have specific patterns
579
+ return typeof value === "string" && /^[a-z]+$/i.test(value);
580
+ }
581
+ return false;
582
+ });
583
+ }
@@ -143,3 +143,54 @@ export {
143
143
  type UpdateSwarmContextArgs,
144
144
  type QuerySwarmContextsArgs,
145
145
  } from "./swarm-context";
146
+
147
+ // Bead event schemas
148
+ export {
149
+ BaseBeadEventSchema,
150
+ BeadCreatedEventSchema,
151
+ BeadUpdatedEventSchema,
152
+ BeadStatusChangedEventSchema,
153
+ BeadClosedEventSchema,
154
+ BeadReopenedEventSchema,
155
+ BeadDeletedEventSchema,
156
+ BeadDependencyAddedEventSchema,
157
+ BeadDependencyRemovedEventSchema,
158
+ BeadLabelAddedEventSchema,
159
+ BeadLabelRemovedEventSchema,
160
+ BeadCommentAddedEventSchema,
161
+ BeadCommentUpdatedEventSchema,
162
+ BeadCommentDeletedEventSchema,
163
+ BeadEpicChildAddedEventSchema,
164
+ BeadEpicChildRemovedEventSchema,
165
+ BeadEpicClosureEligibleEventSchema,
166
+ BeadAssignedEventSchema,
167
+ BeadWorkStartedEventSchema,
168
+ BeadCompactedEventSchema,
169
+ BeadEventSchema,
170
+ createBeadEvent,
171
+ isBeadEventType,
172
+ getBeadIdFromEvent,
173
+ isStateTransitionEvent,
174
+ isEpicEvent,
175
+ isAgentEvent,
176
+ type BeadEvent,
177
+ type BeadCreatedEvent,
178
+ type BeadUpdatedEvent,
179
+ type BeadStatusChangedEvent,
180
+ type BeadClosedEvent,
181
+ type BeadReopenedEvent,
182
+ type BeadDeletedEvent,
183
+ type BeadDependencyAddedEvent,
184
+ type BeadDependencyRemovedEvent,
185
+ type BeadLabelAddedEvent,
186
+ type BeadLabelRemovedEvent,
187
+ type BeadCommentAddedEvent,
188
+ type BeadCommentUpdatedEvent,
189
+ type BeadCommentDeletedEvent,
190
+ type BeadEpicChildAddedEvent,
191
+ type BeadEpicChildRemovedEvent,
192
+ type BeadEpicClosureEligibleEvent,
193
+ type BeadAssignedEvent,
194
+ type BeadWorkStartedEvent,
195
+ type BeadCompactedEvent,
196
+ } from "./bead-events";
package/src/skills.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  isAbsolute,
36
36
  sep,
37
37
  } from "path";
38
+ import { fileURLToPath } from "url";
38
39
  import matter from "gray-matter";
39
40
 
40
41
  // =============================================================================
@@ -209,9 +210,15 @@ function getClaudeGlobalSkillsDir(): string {
209
210
  * Bundled skills from the package (lowest priority)
210
211
  */
211
212
  function getPackageSkillsDir(): string {
212
- // ES module equivalent of __dirname - resolve relative to this file
213
- const currentDir = new URL(".", import.meta.url).pathname;
214
- return join(currentDir, "..", "global-skills");
213
+ // Resolve relative to this file (handles URL-encoding like spaces)
214
+ try {
215
+ const currentFilePath = fileURLToPath(import.meta.url);
216
+ return join(dirname(currentFilePath), "..", "global-skills");
217
+ } catch {
218
+ // Fallback for non-file URLs (best-effort)
219
+ const currentDir = decodeURIComponent(new URL(".", import.meta.url).pathname);
220
+ return join(currentDir, "..", "global-skills");
221
+ }
215
222
  }
216
223
 
217
224
  /**