@fractary/codex 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
+ import path3 from 'path';
2
+ import { execSync } from 'child_process';
3
+ import micromatch2, { isMatch } from 'micromatch';
1
4
  import { z } from 'zod';
2
5
  import yaml from 'js-yaml';
3
- import { isMatch } from 'micromatch';
6
+ import fs2 from 'fs/promises';
4
7
 
5
8
  // src/errors/CodexError.ts
6
9
  var CodexError = class _CodexError extends Error {
@@ -28,6 +31,664 @@ var ValidationError = class _ValidationError extends CodexError {
28
31
  Object.setPrototypeOf(this, _ValidationError.prototype);
29
32
  }
30
33
  };
34
+ var FORBIDDEN_PATTERNS = [
35
+ /^\//,
36
+ // Absolute paths
37
+ /\.\./,
38
+ // Parent directory traversal
39
+ /^~/,
40
+ // Home directory expansion
41
+ /\0/
42
+ // Null bytes
43
+ ];
44
+ var FORBIDDEN_PROTOCOLS = ["file:", "http:", "https:", "ftp:", "data:", "javascript:"];
45
+ function validatePath(filePath) {
46
+ if (!filePath || filePath.trim() === "") {
47
+ return false;
48
+ }
49
+ for (const pattern of FORBIDDEN_PATTERNS) {
50
+ if (pattern.test(filePath)) {
51
+ return false;
52
+ }
53
+ }
54
+ const lowerPath = filePath.toLowerCase();
55
+ for (const protocol of FORBIDDEN_PROTOCOLS) {
56
+ if (lowerPath.startsWith(protocol)) {
57
+ return false;
58
+ }
59
+ }
60
+ const normalized = path3.normalize(filePath);
61
+ if (normalized.startsWith("..") || normalized.includes("/../") || normalized.includes("\\..\\")) {
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+ function sanitizePath(filePath) {
67
+ if (!filePath) {
68
+ return "";
69
+ }
70
+ let sanitized = filePath;
71
+ for (const protocol of FORBIDDEN_PROTOCOLS) {
72
+ if (sanitized.toLowerCase().startsWith(protocol)) {
73
+ sanitized = sanitized.slice(protocol.length);
74
+ }
75
+ }
76
+ sanitized = sanitized.replace(/^\/+/, "");
77
+ sanitized = sanitized.replace(/^~\//, "");
78
+ sanitized = path3.normalize(sanitized);
79
+ sanitized = sanitized.replace(/\.\.\//g, "").replace(/\.\./g, "");
80
+ sanitized = sanitized.replace(/^\.\//, "");
81
+ sanitized = sanitized.replace(/\0/g, "");
82
+ return sanitized;
83
+ }
84
+ function validateOrg(org) {
85
+ if (!org || org.trim() === "") {
86
+ return false;
87
+ }
88
+ const orgPattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
89
+ return orgPattern.test(org) && org.length <= 39;
90
+ }
91
+ function validateProject(project) {
92
+ if (!project || project.trim() === "") {
93
+ return false;
94
+ }
95
+ const projectPattern = /^[a-zA-Z0-9._-]+$/;
96
+ return projectPattern.test(project) && project.length <= 100 && !project.includes("..");
97
+ }
98
+ function validateUri(uri) {
99
+ if (!uri || !uri.startsWith("codex://")) {
100
+ return false;
101
+ }
102
+ const pathPart = uri.slice("codex://".length);
103
+ const parts = pathPart.split("/");
104
+ if (parts.length < 2) {
105
+ return false;
106
+ }
107
+ const [org, project, ...rest] = parts;
108
+ if (!org || !validateOrg(org)) {
109
+ return false;
110
+ }
111
+ if (!project || !validateProject(project)) {
112
+ return false;
113
+ }
114
+ if (rest.length > 0) {
115
+ const filePath = rest.join("/");
116
+ if (!validatePath(filePath)) {
117
+ return false;
118
+ }
119
+ }
120
+ return true;
121
+ }
122
+
123
+ // src/references/parser.ts
124
+ var CODEX_URI_PREFIX = "codex://";
125
+ var LEGACY_REF_PREFIX = "@codex/";
126
+ function isValidUri(uri) {
127
+ return validateUri(uri);
128
+ }
129
+ function isLegacyReference(ref) {
130
+ return ref.startsWith(LEGACY_REF_PREFIX);
131
+ }
132
+ function convertLegacyReference(ref, defaultOrg) {
133
+ if (!isLegacyReference(ref)) {
134
+ return null;
135
+ }
136
+ const pathPart = ref.slice(LEGACY_REF_PREFIX.length);
137
+ const parts = pathPart.split("/");
138
+ if (parts.length < 1 || !parts[0]) {
139
+ return null;
140
+ }
141
+ const project = parts[0];
142
+ const filePath = parts.slice(1).join("/");
143
+ if (filePath) {
144
+ return `${CODEX_URI_PREFIX}${defaultOrg}/${project}/${filePath}`;
145
+ }
146
+ return `${CODEX_URI_PREFIX}${defaultOrg}/${project}`;
147
+ }
148
+ function parseReference(uri, options = {}) {
149
+ const { sanitize = true, strict = true } = options;
150
+ if (!uri.startsWith(CODEX_URI_PREFIX)) {
151
+ return null;
152
+ }
153
+ if (strict && !validateUri(uri)) {
154
+ return null;
155
+ }
156
+ const pathPart = uri.slice(CODEX_URI_PREFIX.length);
157
+ const parts = pathPart.split("/");
158
+ if (parts.length < 2) {
159
+ return null;
160
+ }
161
+ const org = parts[0];
162
+ const project = parts[1];
163
+ let filePath = parts.slice(2).join("/");
164
+ if (!org || !project) {
165
+ return null;
166
+ }
167
+ if (sanitize && filePath) {
168
+ if (!validatePath(filePath)) {
169
+ filePath = sanitizePath(filePath);
170
+ }
171
+ }
172
+ return {
173
+ uri,
174
+ org,
175
+ project,
176
+ path: filePath
177
+ };
178
+ }
179
+ function buildUri(org, project, path5) {
180
+ const base = `${CODEX_URI_PREFIX}${org}/${project}`;
181
+ if (path5) {
182
+ const cleanPath = path5.startsWith("/") ? path5.slice(1) : path5;
183
+ return `${base}/${cleanPath}`;
184
+ }
185
+ return base;
186
+ }
187
+ function getExtension(uri) {
188
+ const parsed = parseReference(uri, { strict: false });
189
+ if (!parsed || !parsed.path) {
190
+ return "";
191
+ }
192
+ const lastDot = parsed.path.lastIndexOf(".");
193
+ if (lastDot === -1 || lastDot === parsed.path.length - 1) {
194
+ return "";
195
+ }
196
+ return parsed.path.slice(lastDot + 1).toLowerCase();
197
+ }
198
+ function getFilename(uri) {
199
+ const parsed = parseReference(uri, { strict: false });
200
+ if (!parsed || !parsed.path) {
201
+ return "";
202
+ }
203
+ const lastSlash = parsed.path.lastIndexOf("/");
204
+ if (lastSlash === -1) {
205
+ return parsed.path;
206
+ }
207
+ return parsed.path.slice(lastSlash + 1);
208
+ }
209
+ function getDirectory(uri) {
210
+ const parsed = parseReference(uri, { strict: false });
211
+ if (!parsed || !parsed.path) {
212
+ return "";
213
+ }
214
+ const lastSlash = parsed.path.lastIndexOf("/");
215
+ if (lastSlash === -1) {
216
+ return "";
217
+ }
218
+ return parsed.path.slice(0, lastSlash);
219
+ }
220
+ var DEFAULT_CACHE_DIR = ".fractary/plugins/codex/cache";
221
+ function detectCurrentProject(cwd) {
222
+ try {
223
+ const options = cwd ? { cwd, encoding: "utf-8" } : { encoding: "utf-8" };
224
+ const stdout = execSync("git remote get-url origin", options);
225
+ const url = stdout.toString().trim();
226
+ const patterns = [
227
+ /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/,
228
+ /gitlab\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/,
229
+ /bitbucket\.org[:/]([^/]+)\/([^/]+?)(?:\.git)?$/
230
+ ];
231
+ for (const pattern of patterns) {
232
+ const match = url.match(pattern);
233
+ if (match && match[1] && match[2]) {
234
+ return { org: match[1], project: match[2] };
235
+ }
236
+ }
237
+ } catch {
238
+ }
239
+ return { org: null, project: null };
240
+ }
241
+ function getCurrentContext(options = {}) {
242
+ if (options.currentOrg && options.currentProject) {
243
+ return { org: options.currentOrg, project: options.currentProject };
244
+ }
245
+ const envOrg = process.env.CODEX_CURRENT_ORG;
246
+ const envProject = process.env.CODEX_CURRENT_PROJECT;
247
+ if (envOrg && envProject) {
248
+ return { org: envOrg, project: envProject };
249
+ }
250
+ return detectCurrentProject(options.cwd);
251
+ }
252
+ function calculateCachePath(org, project, filePath, cacheDir) {
253
+ return path3.join(cacheDir, org, project, filePath);
254
+ }
255
+ function resolveReference(uri, options = {}) {
256
+ const parsed = parseReference(uri);
257
+ if (!parsed) {
258
+ return null;
259
+ }
260
+ const cacheDir = options.cacheDir || DEFAULT_CACHE_DIR;
261
+ const currentContext = getCurrentContext(options);
262
+ const cachePath = parsed.path ? calculateCachePath(parsed.org, parsed.project, parsed.path, cacheDir) : path3.join(cacheDir, parsed.org, parsed.project);
263
+ const isCurrentProject = currentContext.org === parsed.org && currentContext.project === parsed.project;
264
+ const resolved = {
265
+ ...parsed,
266
+ cachePath,
267
+ isCurrentProject
268
+ };
269
+ if (isCurrentProject && parsed.path) {
270
+ resolved.localPath = parsed.path;
271
+ }
272
+ return resolved;
273
+ }
274
+ function resolveReferences(uris, options = {}) {
275
+ return uris.map((uri) => resolveReference(uri, options)).filter((r) => r !== null);
276
+ }
277
+ function isCurrentProjectUri(uri, options = {}) {
278
+ const resolved = resolveReference(uri, options);
279
+ return resolved?.isCurrentProject ?? false;
280
+ }
281
+ function getRelativeCachePath(uri) {
282
+ const parsed = parseReference(uri);
283
+ if (!parsed) {
284
+ return null;
285
+ }
286
+ if (parsed.path) {
287
+ return path3.join(parsed.org, parsed.project, parsed.path);
288
+ }
289
+ return path3.join(parsed.org, parsed.project);
290
+ }
291
+
292
+ // src/types/built-in.ts
293
+ var TTL = {
294
+ ONE_HOUR: 3600,
295
+ ONE_DAY: 86400,
296
+ ONE_WEEK: 604800,
297
+ TWO_WEEKS: 1209600,
298
+ ONE_MONTH: 2592e3,
299
+ ONE_YEAR: 31536e3
300
+ };
301
+ var BUILT_IN_TYPES = {
302
+ docs: {
303
+ name: "docs",
304
+ description: "Documentation files",
305
+ patterns: ["docs/**", "README.md", "CLAUDE.md", "*.md"],
306
+ defaultTtl: TTL.ONE_WEEK,
307
+ archiveAfterDays: 365,
308
+ archiveStorage: "cloud"
309
+ },
310
+ specs: {
311
+ name: "specs",
312
+ description: "Technical specifications",
313
+ patterns: ["specs/**", "SPEC-*.md", "**/specs/**"],
314
+ defaultTtl: TTL.TWO_WEEKS,
315
+ archiveAfterDays: null,
316
+ // Never auto-archive specs
317
+ archiveStorage: null
318
+ },
319
+ logs: {
320
+ name: "logs",
321
+ description: "Session and workflow logs",
322
+ patterns: [".fractary/**/logs/**", "logs/**", "*.log"],
323
+ defaultTtl: TTL.ONE_DAY,
324
+ archiveAfterDays: 30,
325
+ archiveStorage: "cloud"
326
+ },
327
+ standards: {
328
+ name: "standards",
329
+ description: "Organization standards and guides",
330
+ patterns: ["standards/**", "guides/**", ".fractary/standards/**"],
331
+ defaultTtl: TTL.ONE_MONTH,
332
+ archiveAfterDays: null,
333
+ // Never auto-archive standards
334
+ archiveStorage: null
335
+ },
336
+ templates: {
337
+ name: "templates",
338
+ description: "Reusable templates",
339
+ patterns: ["templates/**", ".templates/**", ".fractary/templates/**"],
340
+ defaultTtl: TTL.TWO_WEEKS,
341
+ archiveAfterDays: 180,
342
+ archiveStorage: "cloud"
343
+ },
344
+ state: {
345
+ name: "state",
346
+ description: "Workflow state files",
347
+ patterns: [".fractary/**/state.json", ".fractary/**/state/**", "**/state.json"],
348
+ defaultTtl: TTL.ONE_HOUR,
349
+ archiveAfterDays: 7,
350
+ archiveStorage: "local"
351
+ },
352
+ config: {
353
+ name: "config",
354
+ description: "Configuration files",
355
+ patterns: [
356
+ ".fractary/**/*.json",
357
+ "*.config.js",
358
+ "*.config.ts",
359
+ "tsconfig.json",
360
+ "package.json"
361
+ ],
362
+ defaultTtl: TTL.ONE_DAY,
363
+ archiveAfterDays: null,
364
+ archiveStorage: null
365
+ },
366
+ systems: {
367
+ name: "systems",
368
+ description: "System definitions",
369
+ patterns: [".fractary/systems/**", "systems/**"],
370
+ defaultTtl: TTL.TWO_WEEKS,
371
+ archiveAfterDays: null,
372
+ archiveStorage: null
373
+ }
374
+ };
375
+ var DEFAULT_TYPE = {
376
+ name: "default",
377
+ description: "Default type for unmatched files",
378
+ patterns: ["**/*"],
379
+ defaultTtl: TTL.ONE_WEEK,
380
+ archiveAfterDays: 90,
381
+ archiveStorage: "cloud"
382
+ };
383
+ function getBuiltInType(name) {
384
+ return BUILT_IN_TYPES[name];
385
+ }
386
+ function getBuiltInTypeNames() {
387
+ return Object.keys(BUILT_IN_TYPES);
388
+ }
389
+ function isBuiltInType(name) {
390
+ return name in BUILT_IN_TYPES;
391
+ }
392
+ var TypeRegistry = class {
393
+ types = /* @__PURE__ */ new Map();
394
+ patternCache = /* @__PURE__ */ new Map();
395
+ constructor(options = {}) {
396
+ const { includeBuiltIn = true, customTypes = {} } = options;
397
+ if (includeBuiltIn) {
398
+ for (const [name, type] of Object.entries(BUILT_IN_TYPES)) {
399
+ this.types.set(name, type);
400
+ }
401
+ }
402
+ for (const [name, partialType] of Object.entries(customTypes)) {
403
+ this.register({
404
+ ...DEFAULT_TYPE,
405
+ ...partialType,
406
+ name
407
+ });
408
+ }
409
+ }
410
+ /**
411
+ * Get a type by name
412
+ *
413
+ * Custom types take precedence over built-in types with the same name.
414
+ *
415
+ * @param name - Type name
416
+ * @returns The type or undefined
417
+ */
418
+ get(name) {
419
+ return this.types.get(name);
420
+ }
421
+ /**
422
+ * Register a custom type
423
+ *
424
+ * @param type - The type to register
425
+ */
426
+ register(type) {
427
+ this.types.set(type.name, type);
428
+ this.patternCache.clear();
429
+ }
430
+ /**
431
+ * Unregister a type
432
+ *
433
+ * Note: Built-in types can be unregistered but this is not recommended.
434
+ *
435
+ * @param name - Type name to unregister
436
+ * @returns true if the type was removed
437
+ */
438
+ unregister(name) {
439
+ const result = this.types.delete(name);
440
+ if (result) {
441
+ this.patternCache.clear();
442
+ }
443
+ return result;
444
+ }
445
+ /**
446
+ * Check if a type exists
447
+ *
448
+ * @param name - Type name
449
+ * @returns true if the type exists
450
+ */
451
+ has(name) {
452
+ return this.types.has(name);
453
+ }
454
+ /**
455
+ * List all registered types
456
+ *
457
+ * @returns Array of all types
458
+ */
459
+ list() {
460
+ return Array.from(this.types.values());
461
+ }
462
+ /**
463
+ * List all type names
464
+ *
465
+ * @returns Array of type names
466
+ */
467
+ listNames() {
468
+ return Array.from(this.types.keys());
469
+ }
470
+ /**
471
+ * Detect the type of a file based on its path
472
+ *
473
+ * Types are checked in order of specificity (most specific patterns first).
474
+ * Returns 'default' if no type matches.
475
+ *
476
+ * @param filePath - File path to check
477
+ * @returns The detected type name
478
+ */
479
+ detectType(filePath) {
480
+ const cached = this.patternCache.get(filePath);
481
+ if (cached) {
482
+ return cached;
483
+ }
484
+ const normalizedPath = filePath.replace(/\\/g, "/");
485
+ const sortedTypes = this.list().sort((a, b) => {
486
+ const aMaxLen = Math.max(...a.patterns.map((p) => p.length));
487
+ const bMaxLen = Math.max(...b.patterns.map((p) => p.length));
488
+ return bMaxLen - aMaxLen;
489
+ });
490
+ for (const type of sortedTypes) {
491
+ if (type.name === "default") continue;
492
+ for (const pattern of type.patterns) {
493
+ if (micromatch2.isMatch(normalizedPath, pattern)) {
494
+ this.patternCache.set(filePath, type.name);
495
+ return type.name;
496
+ }
497
+ }
498
+ }
499
+ this.patternCache.set(filePath, "default");
500
+ return "default";
501
+ }
502
+ /**
503
+ * Get the TTL for a file based on its type
504
+ *
505
+ * @param filePath - File path
506
+ * @param override - Optional TTL override
507
+ * @returns TTL in seconds
508
+ */
509
+ getTtl(filePath, override) {
510
+ if (override !== void 0) {
511
+ return override;
512
+ }
513
+ const typeName = this.detectType(filePath);
514
+ const type = this.get(typeName) || DEFAULT_TYPE;
515
+ return type.defaultTtl;
516
+ }
517
+ /**
518
+ * Get archive configuration for a file
519
+ *
520
+ * @param filePath - File path
521
+ * @returns Archive configuration or null if archiving is disabled
522
+ */
523
+ getArchiveConfig(filePath) {
524
+ const typeName = this.detectType(filePath);
525
+ const type = this.get(typeName) || DEFAULT_TYPE;
526
+ if (type.archiveAfterDays === null || type.archiveStorage === null) {
527
+ return null;
528
+ }
529
+ return {
530
+ afterDays: type.archiveAfterDays,
531
+ storage: type.archiveStorage
532
+ };
533
+ }
534
+ /**
535
+ * Get all files that match a type's patterns
536
+ *
537
+ * @param typeName - Type name
538
+ * @param files - Array of file paths to filter
539
+ * @returns Filtered file paths
540
+ */
541
+ filterByType(typeName, files) {
542
+ const type = this.get(typeName);
543
+ if (!type) {
544
+ return [];
545
+ }
546
+ return files.filter((file) => {
547
+ const normalized = file.replace(/\\/g, "/");
548
+ return type.patterns.some((pattern) => micromatch2.isMatch(normalized, pattern));
549
+ });
550
+ }
551
+ /**
552
+ * Check if a type is a built-in type
553
+ *
554
+ * @param name - Type name
555
+ * @returns true if it's a built-in type
556
+ */
557
+ isBuiltIn(name) {
558
+ return isBuiltInType(name);
559
+ }
560
+ /**
561
+ * Get the number of registered types
562
+ */
563
+ get size() {
564
+ return this.types.size;
565
+ }
566
+ /**
567
+ * Clear pattern cache
568
+ *
569
+ * Call this if you need to force re-detection of types.
570
+ */
571
+ clearCache() {
572
+ this.patternCache.clear();
573
+ }
574
+ };
575
+ function createDefaultRegistry() {
576
+ return new TypeRegistry();
577
+ }
578
+ var CustomTypeSchema = z.object({
579
+ description: z.string().optional(),
580
+ patterns: z.array(z.string()).min(1),
581
+ defaultTtl: z.number().positive().optional(),
582
+ archiveAfterDays: z.number().positive().nullable().optional(),
583
+ archiveStorage: z.enum(["local", "cloud", "drive"]).nullable().optional(),
584
+ syncPatterns: z.array(z.string()).optional(),
585
+ excludePatterns: z.array(z.string()).optional(),
586
+ permissions: z.object({
587
+ include: z.array(z.string()),
588
+ exclude: z.array(z.string())
589
+ }).optional()
590
+ });
591
+ var TypesConfigSchema = z.object({
592
+ custom: z.record(z.string(), CustomTypeSchema).optional()
593
+ });
594
+ function parseTtl(value) {
595
+ if (typeof value === "number") {
596
+ return value;
597
+ }
598
+ const match = value.match(/^(\d+)(s|m|h|d|w)$/);
599
+ if (!match || !match[1] || !match[2]) {
600
+ return TTL.ONE_WEEK;
601
+ }
602
+ const num = parseInt(match[1], 10);
603
+ const unit = match[2];
604
+ switch (unit) {
605
+ case "s":
606
+ return num;
607
+ case "m":
608
+ return num * 60;
609
+ case "h":
610
+ return num * 3600;
611
+ case "d":
612
+ return num * 86400;
613
+ case "w":
614
+ return num * 604800;
615
+ default:
616
+ return TTL.ONE_WEEK;
617
+ }
618
+ }
619
+ function validateCustomTypes(config) {
620
+ const result = {
621
+ valid: true,
622
+ errors: []
623
+ };
624
+ if (!config || typeof config !== "object") {
625
+ return result;
626
+ }
627
+ const typesConfig = config;
628
+ const customTypes = typesConfig.custom;
629
+ if (!customTypes) {
630
+ return result;
631
+ }
632
+ for (const [name, typeConfig] of Object.entries(customTypes)) {
633
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
634
+ result.valid = false;
635
+ result.errors.push({
636
+ name,
637
+ field: "name",
638
+ message: "Type name must start with a letter and contain only lowercase letters, numbers, hyphens, and underscores"
639
+ });
640
+ }
641
+ const parseResult = CustomTypeSchema.safeParse(typeConfig);
642
+ if (!parseResult.success) {
643
+ result.valid = false;
644
+ for (const issue of parseResult.error.issues) {
645
+ result.errors.push({
646
+ name,
647
+ field: issue.path.join("."),
648
+ message: issue.message
649
+ });
650
+ }
651
+ }
652
+ }
653
+ return result;
654
+ }
655
+ function loadCustomTypes(config) {
656
+ const types = /* @__PURE__ */ new Map();
657
+ if (!config.custom) {
658
+ return types;
659
+ }
660
+ for (const [name, typeConfig] of Object.entries(config.custom)) {
661
+ const type = {
662
+ name,
663
+ description: typeConfig.description || `Custom type: ${name}`,
664
+ patterns: typeConfig.patterns,
665
+ defaultTtl: typeConfig.defaultTtl ?? DEFAULT_TYPE.defaultTtl,
666
+ archiveAfterDays: typeConfig.archiveAfterDays ?? DEFAULT_TYPE.archiveAfterDays,
667
+ archiveStorage: typeConfig.archiveStorage ?? DEFAULT_TYPE.archiveStorage,
668
+ syncPatterns: typeConfig.syncPatterns,
669
+ excludePatterns: typeConfig.excludePatterns,
670
+ permissions: typeConfig.permissions
671
+ };
672
+ types.set(name, type);
673
+ }
674
+ return types;
675
+ }
676
+ function mergeTypes(builtIn, custom) {
677
+ const merged = /* @__PURE__ */ new Map();
678
+ for (const [name, type] of Object.entries(builtIn)) {
679
+ merged.set(name, type);
680
+ }
681
+ for (const [name, type] of custom) {
682
+ merged.set(name, type);
683
+ }
684
+ return merged;
685
+ }
686
+ function extendType(baseName, overrides) {
687
+ return {
688
+ ...overrides,
689
+ name: baseName
690
+ };
691
+ }
31
692
  var MetadataSchema = z.object({
32
693
  // Organizational
33
694
  org: z.string().optional(),
@@ -35,6 +696,7 @@ var MetadataSchema = z.object({
35
696
  // Sync rules
36
697
  codex_sync_include: z.array(z.string()).optional(),
37
698
  codex_sync_exclude: z.array(z.string()).optional(),
699
+ codex_sync_custom: z.array(z.string()).optional(),
38
700
  // Metadata
39
701
  title: z.string().optional(),
40
702
  description: z.string().optional(),
@@ -383,6 +1045,3163 @@ function getTargetRepos(options) {
383
1045
  );
384
1046
  }
385
1047
 
386
- export { AutoSyncPatternSchema, CodexConfigSchema, CodexError, ConfigurationError, MetadataSchema, SyncRulesSchema, ValidationError, evaluatePatterns, extractOrgFromRepoName, extractRawFrontmatter, filterByPatterns, getDefaultConfig, getDefaultDirectories, getDefaultRules, getTargetRepos, hasFrontmatter, loadConfig, matchAnyPattern, matchPattern, parseMetadata, resolveOrganization, shouldSyncToRepo, validateMetadata };
1048
+ // src/core/custom/destinations.ts
1049
+ function parseCustomDestination(value) {
1050
+ if (typeof value !== "string" || !value) {
1051
+ throw new ValidationError("Custom destination must be a non-empty string");
1052
+ }
1053
+ const colonIndex = value.indexOf(":");
1054
+ if (colonIndex === -1) {
1055
+ throw new ValidationError(
1056
+ `Invalid custom destination format: "${value}". Expected format: "repo:path"`
1057
+ );
1058
+ }
1059
+ const repo = value.substring(0, colonIndex).trim();
1060
+ const path5 = value.substring(colonIndex + 1).trim();
1061
+ if (!repo) {
1062
+ throw new ValidationError(
1063
+ `Invalid custom destination: repository name cannot be empty in "${value}"`
1064
+ );
1065
+ }
1066
+ if (!path5) {
1067
+ throw new ValidationError(
1068
+ `Invalid custom destination: path cannot be empty in "${value}"`
1069
+ );
1070
+ }
1071
+ return { repo, path: path5 };
1072
+ }
1073
+ function getCustomSyncDestinations(metadata) {
1074
+ const customDestinations = metadata.codex_sync_custom;
1075
+ if (!customDestinations || !Array.isArray(customDestinations)) {
1076
+ return [];
1077
+ }
1078
+ const results = [];
1079
+ for (const destination of customDestinations) {
1080
+ try {
1081
+ const parsed = parseCustomDestination(destination);
1082
+ results.push(parsed);
1083
+ } catch (error) {
1084
+ continue;
1085
+ }
1086
+ }
1087
+ return results;
1088
+ }
1089
+
1090
+ // src/storage/provider.ts
1091
+ var DEFAULT_FETCH_OPTIONS = {
1092
+ timeout: 3e4,
1093
+ // 30 seconds
1094
+ maxRetries: 3,
1095
+ token: "",
1096
+ branch: "main",
1097
+ followRedirects: true,
1098
+ maxSize: 10 * 1024 * 1024
1099
+ // 10MB
1100
+ };
1101
+ function mergeFetchOptions(options) {
1102
+ return {
1103
+ ...DEFAULT_FETCH_OPTIONS,
1104
+ ...options
1105
+ };
1106
+ }
1107
+ function detectContentType(path5) {
1108
+ const ext = path5.split(".").pop()?.toLowerCase();
1109
+ const mimeTypes = {
1110
+ md: "text/markdown",
1111
+ markdown: "text/markdown",
1112
+ txt: "text/plain",
1113
+ json: "application/json",
1114
+ yaml: "application/x-yaml",
1115
+ yml: "application/x-yaml",
1116
+ xml: "application/xml",
1117
+ html: "text/html",
1118
+ htm: "text/html",
1119
+ css: "text/css",
1120
+ js: "application/javascript",
1121
+ ts: "application/typescript",
1122
+ py: "text/x-python",
1123
+ rb: "text/x-ruby",
1124
+ go: "text/x-go",
1125
+ rs: "text/x-rust",
1126
+ java: "text/x-java",
1127
+ c: "text/x-c",
1128
+ cpp: "text/x-c++",
1129
+ h: "text/x-c",
1130
+ hpp: "text/x-c++",
1131
+ sh: "application/x-sh",
1132
+ bash: "application/x-sh",
1133
+ zsh: "application/x-sh",
1134
+ pdf: "application/pdf",
1135
+ png: "image/png",
1136
+ jpg: "image/jpeg",
1137
+ jpeg: "image/jpeg",
1138
+ gif: "image/gif",
1139
+ svg: "image/svg+xml",
1140
+ webp: "image/webp"
1141
+ };
1142
+ return mimeTypes[ext || ""] || "application/octet-stream";
1143
+ }
1144
+ var LocalStorage = class {
1145
+ name = "local";
1146
+ type = "local";
1147
+ baseDir;
1148
+ constructor(options = {}) {
1149
+ this.baseDir = options.baseDir || process.cwd();
1150
+ }
1151
+ /**
1152
+ * Check if this provider can handle the reference
1153
+ *
1154
+ * Local provider handles references that are in the current project
1155
+ * and have a local path.
1156
+ */
1157
+ canHandle(reference) {
1158
+ return reference.isCurrentProject && !!reference.localPath;
1159
+ }
1160
+ /**
1161
+ * Fetch content from local filesystem
1162
+ */
1163
+ async fetch(reference, options) {
1164
+ const opts = mergeFetchOptions(options);
1165
+ if (!reference.localPath) {
1166
+ throw new Error(`No local path for reference: ${reference.uri}`);
1167
+ }
1168
+ const fullPath = path3.isAbsolute(reference.localPath) ? reference.localPath : path3.join(this.baseDir, reference.localPath);
1169
+ try {
1170
+ await fs2.access(fullPath);
1171
+ } catch {
1172
+ throw new Error(`File not found: ${fullPath}`);
1173
+ }
1174
+ const stats = await fs2.stat(fullPath);
1175
+ if (stats.size > opts.maxSize) {
1176
+ throw new Error(
1177
+ `File too large: ${stats.size} bytes (max: ${opts.maxSize} bytes)`
1178
+ );
1179
+ }
1180
+ const content = await fs2.readFile(fullPath);
1181
+ return {
1182
+ content,
1183
+ contentType: detectContentType(reference.localPath),
1184
+ size: content.length,
1185
+ source: "local",
1186
+ metadata: {
1187
+ path: fullPath,
1188
+ mtime: stats.mtime.toISOString()
1189
+ }
1190
+ };
1191
+ }
1192
+ /**
1193
+ * Check if file exists locally
1194
+ */
1195
+ async exists(reference) {
1196
+ if (!reference.localPath) {
1197
+ return false;
1198
+ }
1199
+ const fullPath = path3.isAbsolute(reference.localPath) ? reference.localPath : path3.join(this.baseDir, reference.localPath);
1200
+ try {
1201
+ await fs2.access(fullPath);
1202
+ return true;
1203
+ } catch {
1204
+ return false;
1205
+ }
1206
+ }
1207
+ /**
1208
+ * Read file content as string
1209
+ */
1210
+ async readText(filePath) {
1211
+ const fullPath = path3.isAbsolute(filePath) ? filePath : path3.join(this.baseDir, filePath);
1212
+ return fs2.readFile(fullPath, "utf-8");
1213
+ }
1214
+ /**
1215
+ * Write content to file
1216
+ */
1217
+ async write(filePath, content) {
1218
+ const fullPath = path3.isAbsolute(filePath) ? filePath : path3.join(this.baseDir, filePath);
1219
+ await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
1220
+ await fs2.writeFile(fullPath, content);
1221
+ }
1222
+ /**
1223
+ * Delete a file
1224
+ */
1225
+ async delete(filePath) {
1226
+ const fullPath = path3.isAbsolute(filePath) ? filePath : path3.join(this.baseDir, filePath);
1227
+ try {
1228
+ await fs2.unlink(fullPath);
1229
+ return true;
1230
+ } catch {
1231
+ return false;
1232
+ }
1233
+ }
1234
+ /**
1235
+ * List files in a directory
1236
+ */
1237
+ async list(dirPath) {
1238
+ const fullPath = path3.isAbsolute(dirPath) ? dirPath : path3.join(this.baseDir, dirPath);
1239
+ try {
1240
+ const entries = await fs2.readdir(fullPath, { withFileTypes: true });
1241
+ return entries.filter((e) => e.isFile()).map((e) => path3.join(dirPath, e.name));
1242
+ } catch {
1243
+ return [];
1244
+ }
1245
+ }
1246
+ };
1247
+ function createLocalStorage(options) {
1248
+ return new LocalStorage(options);
1249
+ }
1250
+
1251
+ // src/storage/github.ts
1252
+ var GitHubStorage = class {
1253
+ name = "github";
1254
+ type = "github";
1255
+ apiBaseUrl;
1256
+ rawBaseUrl;
1257
+ defaultBranch;
1258
+ token;
1259
+ constructor(options = {}) {
1260
+ this.apiBaseUrl = options.apiBaseUrl || "https://api.github.com";
1261
+ this.rawBaseUrl = options.rawBaseUrl || "https://raw.githubusercontent.com";
1262
+ this.defaultBranch = options.defaultBranch || "main";
1263
+ this.token = options.token || (options.tokenEnv ? process.env[options.tokenEnv] : void 0);
1264
+ if (!this.token) {
1265
+ this.token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
1266
+ }
1267
+ }
1268
+ /**
1269
+ * Check if this provider can handle the reference
1270
+ *
1271
+ * GitHub provider handles references that are NOT in the current project
1272
+ * (those need to be fetched from GitHub).
1273
+ */
1274
+ canHandle(reference) {
1275
+ return !reference.isCurrentProject;
1276
+ }
1277
+ /**
1278
+ * Fetch content from GitHub
1279
+ */
1280
+ async fetch(reference, options) {
1281
+ const opts = mergeFetchOptions(options);
1282
+ const token = opts.token || this.token;
1283
+ const branch = opts.branch || this.defaultBranch;
1284
+ if (!reference.path) {
1285
+ throw new Error(`No path specified for reference: ${reference.uri}`);
1286
+ }
1287
+ try {
1288
+ return await this.fetchRaw(reference, branch, opts, token);
1289
+ } catch (error) {
1290
+ if (token) {
1291
+ return await this.fetchApi(reference, branch, opts, token);
1292
+ }
1293
+ throw error;
1294
+ }
1295
+ }
1296
+ /**
1297
+ * Fetch using raw.githubusercontent.com
1298
+ */
1299
+ async fetchRaw(reference, branch, opts, token) {
1300
+ const url = `${this.rawBaseUrl}/${reference.org}/${reference.project}/${branch}/${reference.path}`;
1301
+ const headers = {
1302
+ "User-Agent": "fractary-codex"
1303
+ };
1304
+ if (token) {
1305
+ headers["Authorization"] = `token ${token}`;
1306
+ }
1307
+ const controller = new AbortController();
1308
+ const timeoutId = setTimeout(() => controller.abort(), opts.timeout);
1309
+ try {
1310
+ const response = await fetch(url, {
1311
+ headers,
1312
+ signal: controller.signal,
1313
+ redirect: opts.followRedirects ? "follow" : "manual"
1314
+ });
1315
+ if (!response.ok) {
1316
+ throw new Error(`GitHub raw fetch failed: ${response.status} ${response.statusText}`);
1317
+ }
1318
+ const arrayBuffer = await response.arrayBuffer();
1319
+ const content = Buffer.from(arrayBuffer);
1320
+ if (content.length > opts.maxSize) {
1321
+ throw new Error(`Content too large: ${content.length} bytes (max: ${opts.maxSize} bytes)`);
1322
+ }
1323
+ return {
1324
+ content,
1325
+ contentType: response.headers.get("content-type") || detectContentType(reference.path),
1326
+ size: content.length,
1327
+ source: "github-raw",
1328
+ metadata: {
1329
+ url,
1330
+ branch
1331
+ }
1332
+ };
1333
+ } finally {
1334
+ clearTimeout(timeoutId);
1335
+ }
1336
+ }
1337
+ /**
1338
+ * Fetch using GitHub API (for private repos)
1339
+ */
1340
+ async fetchApi(reference, branch, opts, token) {
1341
+ const url = `${this.apiBaseUrl}/repos/${reference.org}/${reference.project}/contents/${reference.path}?ref=${branch}`;
1342
+ const headers = {
1343
+ "User-Agent": "fractary-codex",
1344
+ Accept: "application/vnd.github.v3+json",
1345
+ Authorization: `token ${token}`
1346
+ };
1347
+ const controller = new AbortController();
1348
+ const timeoutId = setTimeout(() => controller.abort(), opts.timeout);
1349
+ try {
1350
+ const response = await fetch(url, {
1351
+ headers,
1352
+ signal: controller.signal
1353
+ });
1354
+ if (!response.ok) {
1355
+ throw new Error(`GitHub API fetch failed: ${response.status} ${response.statusText}`);
1356
+ }
1357
+ const data = await response.json();
1358
+ if (data.type !== "file") {
1359
+ throw new Error(`Reference is not a file: ${reference.uri}`);
1360
+ }
1361
+ let content;
1362
+ if (data.content && data.encoding === "base64") {
1363
+ content = Buffer.from(data.content, "base64");
1364
+ } else if (data.download_url) {
1365
+ const downloadResponse = await fetch(data.download_url, {
1366
+ headers: { "User-Agent": "fractary-codex" },
1367
+ signal: controller.signal
1368
+ });
1369
+ const arrayBuffer = await downloadResponse.arrayBuffer();
1370
+ content = Buffer.from(arrayBuffer);
1371
+ } else {
1372
+ throw new Error(`No content available for: ${reference.uri}`);
1373
+ }
1374
+ if (content.length > opts.maxSize) {
1375
+ throw new Error(`Content too large: ${content.length} bytes (max: ${opts.maxSize} bytes)`);
1376
+ }
1377
+ return {
1378
+ content,
1379
+ contentType: detectContentType(reference.path),
1380
+ size: content.length,
1381
+ source: "github-api",
1382
+ metadata: {
1383
+ sha: data.sha,
1384
+ url,
1385
+ branch
1386
+ }
1387
+ };
1388
+ } finally {
1389
+ clearTimeout(timeoutId);
1390
+ }
1391
+ }
1392
+ /**
1393
+ * Check if file exists on GitHub
1394
+ */
1395
+ async exists(reference, options) {
1396
+ const opts = mergeFetchOptions(options);
1397
+ const token = opts.token || this.token;
1398
+ const branch = opts.branch || this.defaultBranch;
1399
+ if (!reference.path) {
1400
+ return false;
1401
+ }
1402
+ const url = `${this.rawBaseUrl}/${reference.org}/${reference.project}/${branch}/${reference.path}`;
1403
+ const headers = {
1404
+ "User-Agent": "fractary-codex"
1405
+ };
1406
+ if (token) {
1407
+ headers["Authorization"] = `token ${token}`;
1408
+ }
1409
+ try {
1410
+ const response = await fetch(url, {
1411
+ method: "HEAD",
1412
+ headers
1413
+ });
1414
+ return response.ok;
1415
+ } catch {
1416
+ return false;
1417
+ }
1418
+ }
1419
+ /**
1420
+ * Get repository metadata
1421
+ */
1422
+ async getRepoInfo(org, project) {
1423
+ const url = `${this.apiBaseUrl}/repos/${org}/${project}`;
1424
+ const headers = {
1425
+ "User-Agent": "fractary-codex",
1426
+ Accept: "application/vnd.github.v3+json"
1427
+ };
1428
+ if (this.token) {
1429
+ headers["Authorization"] = `token ${this.token}`;
1430
+ }
1431
+ try {
1432
+ const response = await fetch(url, { headers });
1433
+ if (!response.ok) {
1434
+ return null;
1435
+ }
1436
+ const data = await response.json();
1437
+ return {
1438
+ defaultBranch: data.default_branch,
1439
+ private: data.private,
1440
+ size: data.size
1441
+ };
1442
+ } catch {
1443
+ return null;
1444
+ }
1445
+ }
1446
+ };
1447
+ function createGitHubStorage(options) {
1448
+ return new GitHubStorage(options);
1449
+ }
1450
+
1451
+ // src/storage/http.ts
1452
+ var HttpStorage = class {
1453
+ name = "http";
1454
+ type = "http";
1455
+ defaultTimeout;
1456
+ defaultMaxSize;
1457
+ defaultHeaders;
1458
+ constructor(options = {}) {
1459
+ this.defaultTimeout = options.timeout || 3e4;
1460
+ this.defaultMaxSize = options.maxSize || 10 * 1024 * 1024;
1461
+ this.defaultHeaders = {
1462
+ "User-Agent": options.userAgent || "fractary-codex",
1463
+ ...options.headers
1464
+ };
1465
+ }
1466
+ /**
1467
+ * Check if this provider can handle the reference
1468
+ *
1469
+ * HTTP provider is a fallback - it can handle any reference
1470
+ * but should have lower priority than specialized providers.
1471
+ */
1472
+ canHandle(_reference) {
1473
+ return true;
1474
+ }
1475
+ /**
1476
+ * Fetch content from HTTP URL
1477
+ *
1478
+ * This method constructs a URL from the reference and fetches it.
1479
+ * The URL pattern is: https://raw.githubusercontent.com/{org}/{project}/{branch}/{path}
1480
+ *
1481
+ * For custom URL schemes, use the fetchUrl method directly.
1482
+ */
1483
+ async fetch(reference, options) {
1484
+ const opts = mergeFetchOptions(options);
1485
+ const branch = opts.branch || "main";
1486
+ if (!reference.path) {
1487
+ throw new Error(`No path specified for reference: ${reference.uri}`);
1488
+ }
1489
+ const url = `https://raw.githubusercontent.com/${reference.org}/${reference.project}/${branch}/${reference.path}`;
1490
+ return this.fetchUrl(url, opts);
1491
+ }
1492
+ /**
1493
+ * Fetch content from any URL
1494
+ */
1495
+ async fetchUrl(url, options) {
1496
+ const opts = mergeFetchOptions(options);
1497
+ const timeout = opts.timeout || this.defaultTimeout;
1498
+ const maxSize = opts.maxSize || this.defaultMaxSize;
1499
+ const headers = {
1500
+ ...this.defaultHeaders
1501
+ };
1502
+ if (opts.token) {
1503
+ headers["Authorization"] = `Bearer ${opts.token}`;
1504
+ }
1505
+ const controller = new AbortController();
1506
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1507
+ try {
1508
+ const response = await fetch(url, {
1509
+ headers,
1510
+ signal: controller.signal,
1511
+ redirect: opts.followRedirects ? "follow" : "manual"
1512
+ });
1513
+ if (!response.ok) {
1514
+ throw new Error(`HTTP fetch failed: ${response.status} ${response.statusText}`);
1515
+ }
1516
+ const contentLength = response.headers.get("content-length");
1517
+ if (contentLength && parseInt(contentLength, 10) > maxSize) {
1518
+ throw new Error(`Content too large: ${contentLength} bytes (max: ${maxSize} bytes)`);
1519
+ }
1520
+ const arrayBuffer = await response.arrayBuffer();
1521
+ const content = Buffer.from(arrayBuffer);
1522
+ if (content.length > maxSize) {
1523
+ throw new Error(`Content too large: ${content.length} bytes (max: ${maxSize} bytes)`);
1524
+ }
1525
+ const pathname = new URL(url).pathname;
1526
+ const filename = pathname.split("/").pop() || "";
1527
+ return {
1528
+ content,
1529
+ contentType: response.headers.get("content-type") || detectContentType(filename),
1530
+ size: content.length,
1531
+ source: "http",
1532
+ metadata: {
1533
+ url,
1534
+ status: response.status,
1535
+ headers: Object.fromEntries(response.headers.entries())
1536
+ }
1537
+ };
1538
+ } finally {
1539
+ clearTimeout(timeoutId);
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Check if URL exists (HEAD request)
1544
+ */
1545
+ async exists(reference, options) {
1546
+ const opts = mergeFetchOptions(options);
1547
+ const branch = opts.branch || "main";
1548
+ if (!reference.path) {
1549
+ return false;
1550
+ }
1551
+ const url = `https://raw.githubusercontent.com/${reference.org}/${reference.project}/${branch}/${reference.path}`;
1552
+ return this.urlExists(url, opts);
1553
+ }
1554
+ /**
1555
+ * Check if any URL exists
1556
+ */
1557
+ async urlExists(url, options) {
1558
+ const opts = mergeFetchOptions(options);
1559
+ const timeout = opts.timeout || this.defaultTimeout;
1560
+ const headers = {
1561
+ ...this.defaultHeaders
1562
+ };
1563
+ if (opts.token) {
1564
+ headers["Authorization"] = `Bearer ${opts.token}`;
1565
+ }
1566
+ const controller = new AbortController();
1567
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1568
+ try {
1569
+ const response = await fetch(url, {
1570
+ method: "HEAD",
1571
+ headers,
1572
+ signal: controller.signal,
1573
+ redirect: "follow"
1574
+ });
1575
+ return response.ok;
1576
+ } catch {
1577
+ return false;
1578
+ } finally {
1579
+ clearTimeout(timeoutId);
1580
+ }
1581
+ }
1582
+ };
1583
+ function createHttpStorage(options) {
1584
+ return new HttpStorage(options);
1585
+ }
1586
+
1587
+ // src/storage/manager.ts
1588
+ var StorageManager = class {
1589
+ providers = /* @__PURE__ */ new Map();
1590
+ priority;
1591
+ constructor(config = {}) {
1592
+ this.providers.set("local", new LocalStorage(config.local));
1593
+ this.providers.set("github", new GitHubStorage(config.github));
1594
+ this.providers.set("http", new HttpStorage(config.http));
1595
+ this.priority = config.priority || ["local", "github", "http"];
1596
+ }
1597
+ /**
1598
+ * Register a custom storage provider
1599
+ */
1600
+ registerProvider(provider) {
1601
+ this.providers.set(provider.type, provider);
1602
+ }
1603
+ /**
1604
+ * Remove a storage provider
1605
+ */
1606
+ removeProvider(type) {
1607
+ return this.providers.delete(type);
1608
+ }
1609
+ /**
1610
+ * Get a provider by type
1611
+ */
1612
+ getProvider(type) {
1613
+ return this.providers.get(type);
1614
+ }
1615
+ /**
1616
+ * Get all registered providers
1617
+ */
1618
+ getProviders() {
1619
+ return Array.from(this.providers.values());
1620
+ }
1621
+ /**
1622
+ * Find the best provider for a reference
1623
+ */
1624
+ findProvider(reference) {
1625
+ for (const type of this.priority) {
1626
+ const provider = this.providers.get(type);
1627
+ if (provider && provider.canHandle(reference)) {
1628
+ return provider;
1629
+ }
1630
+ }
1631
+ return null;
1632
+ }
1633
+ /**
1634
+ * Fetch content for a reference
1635
+ *
1636
+ * Tries providers in priority order until one succeeds.
1637
+ */
1638
+ async fetch(reference, options) {
1639
+ const errors = [];
1640
+ for (const type of this.priority) {
1641
+ const provider = this.providers.get(type);
1642
+ if (!provider || !provider.canHandle(reference)) {
1643
+ continue;
1644
+ }
1645
+ try {
1646
+ return await provider.fetch(reference, options);
1647
+ } catch (error) {
1648
+ errors.push(error instanceof Error ? error : new Error(String(error)));
1649
+ }
1650
+ }
1651
+ if (errors.length === 0) {
1652
+ throw new Error(`No provider can handle reference: ${reference.uri}`);
1653
+ }
1654
+ const firstError = errors[0];
1655
+ if (firstError) {
1656
+ if (errors.length > 1) {
1657
+ throw new Error(`All providers failed for ${reference.uri}. First error: ${firstError.message}`);
1658
+ }
1659
+ throw firstError;
1660
+ }
1661
+ throw new Error(`Unknown error fetching ${reference.uri}`);
1662
+ }
1663
+ /**
1664
+ * Check if content exists for a reference
1665
+ *
1666
+ * Returns true if any provider reports the content exists.
1667
+ */
1668
+ async exists(reference, options) {
1669
+ for (const type of this.priority) {
1670
+ const provider = this.providers.get(type);
1671
+ if (!provider || !provider.canHandle(reference)) {
1672
+ continue;
1673
+ }
1674
+ try {
1675
+ if (await provider.exists(reference, options)) {
1676
+ return true;
1677
+ }
1678
+ } catch {
1679
+ }
1680
+ }
1681
+ return false;
1682
+ }
1683
+ /**
1684
+ * Fetch content using a specific provider
1685
+ */
1686
+ async fetchWith(type, reference, options) {
1687
+ const provider = this.providers.get(type);
1688
+ if (!provider) {
1689
+ throw new Error(`Provider not found: ${type}`);
1690
+ }
1691
+ return provider.fetch(reference, options);
1692
+ }
1693
+ /**
1694
+ * Fetch multiple references in parallel
1695
+ */
1696
+ async fetchMany(references, options) {
1697
+ const results = /* @__PURE__ */ new Map();
1698
+ const promises = references.map(async (ref) => {
1699
+ try {
1700
+ const result = await this.fetch(ref, options);
1701
+ results.set(ref.uri, result);
1702
+ } catch (error) {
1703
+ results.set(ref.uri, error instanceof Error ? error : new Error(String(error)));
1704
+ }
1705
+ });
1706
+ await Promise.all(promises);
1707
+ return results;
1708
+ }
1709
+ /**
1710
+ * Get provider status (for diagnostics)
1711
+ */
1712
+ getStatus() {
1713
+ return this.priority.map((type, index) => {
1714
+ const provider = this.providers.get(type);
1715
+ return {
1716
+ type,
1717
+ name: provider?.name || type,
1718
+ priority: index
1719
+ };
1720
+ });
1721
+ }
1722
+ };
1723
+ function createStorageManager(config) {
1724
+ return new StorageManager(config);
1725
+ }
1726
+ var defaultManager = null;
1727
+ function getDefaultStorageManager() {
1728
+ if (!defaultManager) {
1729
+ defaultManager = createStorageManager();
1730
+ }
1731
+ return defaultManager;
1732
+ }
1733
+ function setDefaultStorageManager(manager) {
1734
+ defaultManager = manager;
1735
+ }
1736
+
1737
+ // src/cache/entry.ts
1738
+ function calculateContentHash(content) {
1739
+ let hash = 2166136261;
1740
+ for (let i = 0; i < content.length; i++) {
1741
+ hash ^= content[i] ?? 0;
1742
+ hash = Math.imul(hash, 16777619);
1743
+ }
1744
+ return (hash >>> 0).toString(16).padStart(8, "0");
1745
+ }
1746
+ function createCacheEntry(uri, result, ttl) {
1747
+ const now = Date.now();
1748
+ const metadata = {
1749
+ uri,
1750
+ cachedAt: now,
1751
+ expiresAt: now + ttl * 1e3,
1752
+ ttl,
1753
+ contentHash: calculateContentHash(result.content),
1754
+ size: result.size,
1755
+ contentType: result.contentType,
1756
+ source: result.source,
1757
+ accessCount: 1,
1758
+ lastAccessedAt: now,
1759
+ providerMetadata: result.metadata
1760
+ };
1761
+ if (result.metadata?.etag && typeof result.metadata.etag === "string") {
1762
+ metadata.etag = result.metadata.etag;
1763
+ }
1764
+ if (result.metadata?.lastModified && typeof result.metadata.lastModified === "string") {
1765
+ metadata.lastModified = result.metadata.lastModified;
1766
+ }
1767
+ return {
1768
+ metadata,
1769
+ content: result.content
1770
+ };
1771
+ }
1772
+ function getCacheEntryStatus(entry) {
1773
+ const now = Date.now();
1774
+ if (now < entry.metadata.expiresAt) {
1775
+ return "fresh";
1776
+ }
1777
+ const staleWindow = 5 * 60 * 1e3;
1778
+ if (now < entry.metadata.expiresAt + staleWindow) {
1779
+ return "stale";
1780
+ }
1781
+ return "expired";
1782
+ }
1783
+ function isCacheEntryValid(entry) {
1784
+ const status = getCacheEntryStatus(entry);
1785
+ return status === "fresh" || status === "stale";
1786
+ }
1787
+ function isCacheEntryFresh(entry) {
1788
+ return getCacheEntryStatus(entry) === "fresh";
1789
+ }
1790
+ function touchCacheEntry(entry) {
1791
+ entry.metadata.accessCount++;
1792
+ entry.metadata.lastAccessedAt = Date.now();
1793
+ }
1794
+ function serializeCacheEntry(entry) {
1795
+ return {
1796
+ metadata: entry.metadata,
1797
+ content: entry.content.toString("base64")
1798
+ };
1799
+ }
1800
+ function deserializeCacheEntry(serialized) {
1801
+ return {
1802
+ metadata: serialized.metadata,
1803
+ content: Buffer.from(serialized.content, "base64")
1804
+ };
1805
+ }
1806
+ function getRemainingTtl(entry) {
1807
+ const remaining = entry.metadata.expiresAt - Date.now();
1808
+ return Math.max(0, Math.floor(remaining / 1e3));
1809
+ }
1810
+ function hasContentChanged(entry, newContent) {
1811
+ return entry.metadata.contentHash !== calculateContentHash(newContent);
1812
+ }
1813
+ function getCacheEntryAge(entry) {
1814
+ return Math.floor((Date.now() - entry.metadata.cachedAt) / 1e3);
1815
+ }
1816
+ var CachePersistence = class {
1817
+ cacheDir;
1818
+ extension;
1819
+ atomicWrites;
1820
+ constructor(options) {
1821
+ this.cacheDir = options.cacheDir;
1822
+ this.extension = options.extension || ".cache";
1823
+ this.atomicWrites = options.atomicWrites ?? true;
1824
+ }
1825
+ /**
1826
+ * Get the cache file path for a URI
1827
+ */
1828
+ getCachePath(uri) {
1829
+ const match = uri.match(/^codex:\/\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
1830
+ if (!match || !match[1] || !match[2]) {
1831
+ throw new Error(`Invalid codex URI: ${uri}`);
1832
+ }
1833
+ const [, org, project, filePath] = match;
1834
+ const relativePath = filePath || "index";
1835
+ return path3.join(this.cacheDir, org, project, relativePath + this.extension);
1836
+ }
1837
+ /**
1838
+ * Get the metadata file path for a URI
1839
+ */
1840
+ getMetadataPath(uri) {
1841
+ return this.getCachePath(uri).replace(this.extension, ".meta.json");
1842
+ }
1843
+ /**
1844
+ * Read a cache entry from disk
1845
+ */
1846
+ async read(uri) {
1847
+ const cachePath = this.getCachePath(uri);
1848
+ const metadataPath = this.getMetadataPath(uri);
1849
+ try {
1850
+ const [metadataJson, content] = await Promise.all([
1851
+ fs2.readFile(metadataPath, "utf-8"),
1852
+ fs2.readFile(cachePath)
1853
+ ]);
1854
+ const metadata = JSON.parse(metadataJson);
1855
+ return {
1856
+ metadata,
1857
+ content
1858
+ };
1859
+ } catch (error) {
1860
+ if (error.code === "ENOENT") {
1861
+ return null;
1862
+ }
1863
+ throw error;
1864
+ }
1865
+ }
1866
+ /**
1867
+ * Write a cache entry to disk
1868
+ */
1869
+ async write(entry) {
1870
+ const cachePath = this.getCachePath(entry.metadata.uri);
1871
+ const metadataPath = this.getMetadataPath(entry.metadata.uri);
1872
+ await fs2.mkdir(path3.dirname(cachePath), { recursive: true });
1873
+ if (this.atomicWrites) {
1874
+ const tempCachePath = cachePath + ".tmp";
1875
+ const tempMetadataPath = metadataPath + ".tmp";
1876
+ try {
1877
+ await Promise.all([
1878
+ fs2.writeFile(tempCachePath, entry.content),
1879
+ fs2.writeFile(tempMetadataPath, JSON.stringify(entry.metadata, null, 2))
1880
+ ]);
1881
+ await Promise.all([
1882
+ fs2.rename(tempCachePath, cachePath),
1883
+ fs2.rename(tempMetadataPath, metadataPath)
1884
+ ]);
1885
+ } catch (error) {
1886
+ await Promise.all([
1887
+ fs2.unlink(tempCachePath).catch(() => {
1888
+ }),
1889
+ fs2.unlink(tempMetadataPath).catch(() => {
1890
+ })
1891
+ ]);
1892
+ throw error;
1893
+ }
1894
+ } else {
1895
+ await Promise.all([
1896
+ fs2.writeFile(cachePath, entry.content),
1897
+ fs2.writeFile(metadataPath, JSON.stringify(entry.metadata, null, 2))
1898
+ ]);
1899
+ }
1900
+ }
1901
+ /**
1902
+ * Delete a cache entry
1903
+ */
1904
+ async delete(uri) {
1905
+ const cachePath = this.getCachePath(uri);
1906
+ const metadataPath = this.getMetadataPath(uri);
1907
+ try {
1908
+ await Promise.all([fs2.unlink(cachePath), fs2.unlink(metadataPath)]);
1909
+ return true;
1910
+ } catch (error) {
1911
+ if (error.code === "ENOENT") {
1912
+ return false;
1913
+ }
1914
+ throw error;
1915
+ }
1916
+ }
1917
+ /**
1918
+ * Check if a cache entry exists
1919
+ */
1920
+ async exists(uri) {
1921
+ const cachePath = this.getCachePath(uri);
1922
+ try {
1923
+ await fs2.access(cachePath);
1924
+ return true;
1925
+ } catch {
1926
+ return false;
1927
+ }
1928
+ }
1929
+ /**
1930
+ * List all cached URIs
1931
+ */
1932
+ async list() {
1933
+ const uris = [];
1934
+ try {
1935
+ const orgs = await fs2.readdir(this.cacheDir);
1936
+ for (const org of orgs) {
1937
+ const orgPath = path3.join(this.cacheDir, org);
1938
+ const orgStat = await fs2.stat(orgPath);
1939
+ if (!orgStat.isDirectory()) continue;
1940
+ const projects = await fs2.readdir(orgPath);
1941
+ for (const project of projects) {
1942
+ const projectPath = path3.join(orgPath, project);
1943
+ const projectStat = await fs2.stat(projectPath);
1944
+ if (!projectStat.isDirectory()) continue;
1945
+ const files = await this.listFilesRecursive(projectPath);
1946
+ for (const file of files) {
1947
+ if (file.endsWith(this.extension)) {
1948
+ const relativePath = path3.relative(projectPath, file);
1949
+ const filePath = relativePath.slice(0, -this.extension.length);
1950
+ uris.push(`codex://${org}/${project}/${filePath}`);
1951
+ }
1952
+ }
1953
+ }
1954
+ }
1955
+ } catch (error) {
1956
+ if (error.code === "ENOENT") {
1957
+ return [];
1958
+ }
1959
+ throw error;
1960
+ }
1961
+ return uris;
1962
+ }
1963
+ /**
1964
+ * Recursively list files in a directory
1965
+ */
1966
+ async listFilesRecursive(dir) {
1967
+ const files = [];
1968
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
1969
+ for (const entry of entries) {
1970
+ const fullPath = path3.join(dir, entry.name);
1971
+ if (entry.isDirectory()) {
1972
+ files.push(...await this.listFilesRecursive(fullPath));
1973
+ } else if (entry.isFile()) {
1974
+ files.push(fullPath);
1975
+ }
1976
+ }
1977
+ return files;
1978
+ }
1979
+ /**
1980
+ * Clear all cache entries
1981
+ */
1982
+ async clear() {
1983
+ let count = 0;
1984
+ try {
1985
+ const orgs = await fs2.readdir(this.cacheDir);
1986
+ for (const org of orgs) {
1987
+ const orgPath = path3.join(this.cacheDir, org);
1988
+ const stat = await fs2.stat(orgPath);
1989
+ if (stat.isDirectory()) {
1990
+ await fs2.rm(orgPath, { recursive: true });
1991
+ count++;
1992
+ }
1993
+ }
1994
+ } catch (error) {
1995
+ if (error.code !== "ENOENT") {
1996
+ throw error;
1997
+ }
1998
+ }
1999
+ return count;
2000
+ }
2001
+ /**
2002
+ * Clear expired entries
2003
+ */
2004
+ async clearExpired() {
2005
+ const uris = await this.list();
2006
+ let cleared = 0;
2007
+ for (const uri of uris) {
2008
+ const entry = await this.read(uri);
2009
+ if (entry && entry.metadata.expiresAt < Date.now()) {
2010
+ if (await this.delete(uri)) {
2011
+ cleared++;
2012
+ }
2013
+ }
2014
+ }
2015
+ return cleared;
2016
+ }
2017
+ /**
2018
+ * Get cache statistics
2019
+ */
2020
+ async getStats() {
2021
+ const stats = {
2022
+ entryCount: 0,
2023
+ totalSize: 0,
2024
+ freshCount: 0,
2025
+ staleCount: 0,
2026
+ expiredCount: 0
2027
+ };
2028
+ const uris = await this.list();
2029
+ const now = Date.now();
2030
+ const staleWindow = 5 * 60 * 1e3;
2031
+ for (const uri of uris) {
2032
+ const entry = await this.read(uri);
2033
+ if (entry) {
2034
+ stats.entryCount++;
2035
+ stats.totalSize += entry.metadata.size;
2036
+ if (now < entry.metadata.expiresAt) {
2037
+ stats.freshCount++;
2038
+ } else if (now < entry.metadata.expiresAt + staleWindow) {
2039
+ stats.staleCount++;
2040
+ } else {
2041
+ stats.expiredCount++;
2042
+ }
2043
+ }
2044
+ }
2045
+ return stats;
2046
+ }
2047
+ /**
2048
+ * Ensure cache directory exists
2049
+ */
2050
+ async ensureDir() {
2051
+ await fs2.mkdir(this.cacheDir, { recursive: true });
2052
+ }
2053
+ /**
2054
+ * Get cache directory path
2055
+ */
2056
+ getCacheDir() {
2057
+ return this.cacheDir;
2058
+ }
2059
+ };
2060
+ function createCachePersistence(options) {
2061
+ return new CachePersistence(options);
2062
+ }
2063
+
2064
+ // src/cache/manager.ts
2065
+ var CacheManager = class {
2066
+ memoryCache = /* @__PURE__ */ new Map();
2067
+ persistence;
2068
+ storage = null;
2069
+ config;
2070
+ // LRU tracking
2071
+ accessOrder = [];
2072
+ // Background refresh tracking
2073
+ refreshPromises = /* @__PURE__ */ new Map();
2074
+ constructor(config) {
2075
+ this.config = {
2076
+ cacheDir: config.cacheDir,
2077
+ defaultTtl: config.defaultTtl ?? 3600,
2078
+ // 1 hour default
2079
+ maxMemoryEntries: config.maxMemoryEntries ?? 1e3,
2080
+ maxMemorySize: config.maxMemorySize ?? 50 * 1024 * 1024,
2081
+ // 50MB
2082
+ enablePersistence: config.enablePersistence ?? true,
2083
+ staleWhileRevalidate: config.staleWhileRevalidate ?? true,
2084
+ backgroundRefreshThreshold: config.backgroundRefreshThreshold ?? 60
2085
+ // 1 minute
2086
+ };
2087
+ this.persistence = this.config.enablePersistence ? createCachePersistence({ cacheDir: this.config.cacheDir }) : null;
2088
+ }
2089
+ /**
2090
+ * Set the storage manager for fetching
2091
+ */
2092
+ setStorageManager(storage) {
2093
+ this.storage = storage;
2094
+ }
2095
+ /**
2096
+ * Get content for a reference
2097
+ *
2098
+ * Implements cache-first strategy with stale-while-revalidate.
2099
+ */
2100
+ async get(reference, options) {
2101
+ const ttl = options?.ttl ?? this.config.defaultTtl;
2102
+ let entry = this.memoryCache.get(reference.uri);
2103
+ if (!entry && this.persistence) {
2104
+ entry = await this.persistence.read(reference.uri) ?? void 0;
2105
+ if (entry) {
2106
+ this.setMemoryEntry(reference.uri, entry);
2107
+ }
2108
+ }
2109
+ if (entry) {
2110
+ const status = getCacheEntryStatus(entry);
2111
+ if (status === "fresh") {
2112
+ touchCacheEntry(entry);
2113
+ return this.entryToResult(entry);
2114
+ }
2115
+ if (status === "stale" && this.config.staleWhileRevalidate) {
2116
+ touchCacheEntry(entry);
2117
+ const result = this.entryToResult(entry);
2118
+ this.backgroundRefresh(reference, ttl, options);
2119
+ return result;
2120
+ }
2121
+ }
2122
+ return this.fetchAndCache(reference, ttl, options);
2123
+ }
2124
+ /**
2125
+ * Check if content is cached
2126
+ */
2127
+ async has(uri) {
2128
+ if (this.memoryCache.has(uri)) {
2129
+ return true;
2130
+ }
2131
+ if (this.persistence) {
2132
+ return this.persistence.exists(uri);
2133
+ }
2134
+ return false;
2135
+ }
2136
+ /**
2137
+ * Get cache entry without fetching
2138
+ */
2139
+ async lookup(uri) {
2140
+ let entry = this.memoryCache.get(uri);
2141
+ if (entry) {
2142
+ return {
2143
+ entry,
2144
+ hit: true,
2145
+ fresh: isCacheEntryFresh(entry),
2146
+ source: "memory"
2147
+ };
2148
+ }
2149
+ if (this.persistence) {
2150
+ entry = await this.persistence.read(uri) ?? void 0;
2151
+ if (entry) {
2152
+ this.setMemoryEntry(uri, entry);
2153
+ return {
2154
+ entry,
2155
+ hit: true,
2156
+ fresh: isCacheEntryFresh(entry),
2157
+ source: "disk"
2158
+ };
2159
+ }
2160
+ }
2161
+ return {
2162
+ entry: null,
2163
+ hit: false,
2164
+ fresh: false,
2165
+ source: "none"
2166
+ };
2167
+ }
2168
+ /**
2169
+ * Store content in cache
2170
+ */
2171
+ async set(uri, result, ttl) {
2172
+ const actualTtl = ttl ?? this.config.defaultTtl;
2173
+ const entry = createCacheEntry(uri, result, actualTtl);
2174
+ this.setMemoryEntry(uri, entry);
2175
+ if (this.persistence) {
2176
+ await this.persistence.write(entry);
2177
+ }
2178
+ return entry;
2179
+ }
2180
+ /**
2181
+ * Invalidate a cache entry
2182
+ */
2183
+ async invalidate(uri) {
2184
+ let removed = false;
2185
+ if (this.memoryCache.has(uri)) {
2186
+ this.memoryCache.delete(uri);
2187
+ this.removeFromAccessOrder(uri);
2188
+ removed = true;
2189
+ }
2190
+ if (this.persistence) {
2191
+ const diskRemoved = await this.persistence.delete(uri);
2192
+ removed = removed || diskRemoved;
2193
+ }
2194
+ return removed;
2195
+ }
2196
+ /**
2197
+ * Invalidate all entries matching a pattern
2198
+ */
2199
+ async invalidatePattern(pattern) {
2200
+ let count = 0;
2201
+ for (const uri of this.memoryCache.keys()) {
2202
+ if (pattern.test(uri)) {
2203
+ this.memoryCache.delete(uri);
2204
+ this.removeFromAccessOrder(uri);
2205
+ count++;
2206
+ }
2207
+ }
2208
+ if (this.persistence) {
2209
+ const uris = await this.persistence.list();
2210
+ for (const uri of uris) {
2211
+ if (pattern.test(uri)) {
2212
+ await this.persistence.delete(uri);
2213
+ count++;
2214
+ }
2215
+ }
2216
+ }
2217
+ return count;
2218
+ }
2219
+ /**
2220
+ * Clear all cache entries
2221
+ */
2222
+ async clear() {
2223
+ this.memoryCache.clear();
2224
+ this.accessOrder = [];
2225
+ if (this.persistence) {
2226
+ await this.persistence.clear();
2227
+ }
2228
+ }
2229
+ /**
2230
+ * Clear expired entries
2231
+ */
2232
+ async clearExpired() {
2233
+ let count = 0;
2234
+ const now = Date.now();
2235
+ for (const [uri, entry] of this.memoryCache) {
2236
+ if (entry.metadata.expiresAt < now) {
2237
+ this.memoryCache.delete(uri);
2238
+ this.removeFromAccessOrder(uri);
2239
+ count++;
2240
+ }
2241
+ }
2242
+ if (this.persistence) {
2243
+ count += await this.persistence.clearExpired();
2244
+ }
2245
+ return count;
2246
+ }
2247
+ /**
2248
+ * Get cache statistics
2249
+ */
2250
+ async getStats() {
2251
+ let diskStats = {
2252
+ entryCount: 0,
2253
+ totalSize: 0,
2254
+ freshCount: 0,
2255
+ staleCount: 0,
2256
+ expiredCount: 0
2257
+ };
2258
+ if (this.persistence) {
2259
+ diskStats = await this.persistence.getStats();
2260
+ }
2261
+ let memorySize = 0;
2262
+ for (const entry of this.memoryCache.values()) {
2263
+ memorySize += entry.metadata.size;
2264
+ }
2265
+ return {
2266
+ ...diskStats,
2267
+ memoryEntries: this.memoryCache.size,
2268
+ memorySize
2269
+ };
2270
+ }
2271
+ /**
2272
+ * Preload content into cache
2273
+ */
2274
+ async preload(references, options) {
2275
+ if (!this.storage) {
2276
+ throw new Error("Storage manager not set");
2277
+ }
2278
+ await Promise.all(
2279
+ references.map(async (ref) => {
2280
+ try {
2281
+ await this.get(ref, options);
2282
+ } catch {
2283
+ }
2284
+ })
2285
+ );
2286
+ }
2287
+ /**
2288
+ * Get metadata for a cached entry
2289
+ */
2290
+ async getMetadata(uri) {
2291
+ const result = await this.lookup(uri);
2292
+ return result.entry?.metadata ?? null;
2293
+ }
2294
+ /**
2295
+ * Get remaining TTL for an entry
2296
+ */
2297
+ async getTtl(uri) {
2298
+ const result = await this.lookup(uri);
2299
+ if (result.entry) {
2300
+ return getRemainingTtl(result.entry);
2301
+ }
2302
+ return null;
2303
+ }
2304
+ /**
2305
+ * Fetch content and store in cache
2306
+ */
2307
+ async fetchAndCache(reference, ttl, options) {
2308
+ if (!this.storage) {
2309
+ throw new Error("Storage manager not set");
2310
+ }
2311
+ const result = await this.storage.fetch(reference, options);
2312
+ await this.set(reference.uri, result, ttl);
2313
+ return result;
2314
+ }
2315
+ /**
2316
+ * Background refresh for stale-while-revalidate
2317
+ */
2318
+ backgroundRefresh(reference, ttl, options) {
2319
+ const uri = reference.uri;
2320
+ if (this.refreshPromises.has(uri)) {
2321
+ return;
2322
+ }
2323
+ const promise = this.fetchAndCache(reference, ttl, options).then(() => {
2324
+ const entry = this.memoryCache.get(uri);
2325
+ return entry ?? null;
2326
+ }).catch(() => null).finally(() => {
2327
+ this.refreshPromises.delete(uri);
2328
+ });
2329
+ this.refreshPromises.set(uri, promise);
2330
+ }
2331
+ /**
2332
+ * Set entry in memory cache with LRU eviction
2333
+ */
2334
+ setMemoryEntry(uri, entry) {
2335
+ this.removeFromAccessOrder(uri);
2336
+ this.accessOrder.push(uri);
2337
+ this.memoryCache.set(uri, entry);
2338
+ this.evictIfNeeded();
2339
+ }
2340
+ /**
2341
+ * Evict entries if over limits
2342
+ */
2343
+ evictIfNeeded() {
2344
+ while (this.memoryCache.size > this.config.maxMemoryEntries) {
2345
+ this.evictOldest();
2346
+ }
2347
+ let totalSize = 0;
2348
+ for (const entry of this.memoryCache.values()) {
2349
+ totalSize += entry.metadata.size;
2350
+ }
2351
+ while (totalSize > this.config.maxMemorySize && this.memoryCache.size > 0) {
2352
+ const evicted = this.evictOldest();
2353
+ if (evicted) {
2354
+ totalSize -= evicted.metadata.size;
2355
+ } else {
2356
+ break;
2357
+ }
2358
+ }
2359
+ }
2360
+ /**
2361
+ * Evict the oldest entry (LRU)
2362
+ */
2363
+ evictOldest() {
2364
+ if (this.accessOrder.length === 0) {
2365
+ return null;
2366
+ }
2367
+ const oldest = this.accessOrder.shift();
2368
+ if (oldest) {
2369
+ const entry = this.memoryCache.get(oldest);
2370
+ this.memoryCache.delete(oldest);
2371
+ return entry ?? null;
2372
+ }
2373
+ return null;
2374
+ }
2375
+ /**
2376
+ * Remove URI from access order
2377
+ */
2378
+ removeFromAccessOrder(uri) {
2379
+ const index = this.accessOrder.indexOf(uri);
2380
+ if (index !== -1) {
2381
+ this.accessOrder.splice(index, 1);
2382
+ }
2383
+ }
2384
+ /**
2385
+ * Convert cache entry to fetch result
2386
+ */
2387
+ entryToResult(entry) {
2388
+ return {
2389
+ content: entry.content,
2390
+ contentType: entry.metadata.contentType,
2391
+ size: entry.metadata.size,
2392
+ source: entry.metadata.source,
2393
+ metadata: entry.metadata.providerMetadata
2394
+ };
2395
+ }
2396
+ };
2397
+ function createCacheManager(config) {
2398
+ return new CacheManager(config);
2399
+ }
2400
+ var defaultCacheManager = null;
2401
+ function getDefaultCacheManager() {
2402
+ if (!defaultCacheManager) {
2403
+ defaultCacheManager = createCacheManager({
2404
+ cacheDir: ".fractary/plugins/codex/cache"
2405
+ });
2406
+ }
2407
+ return defaultCacheManager;
2408
+ }
2409
+ function setDefaultCacheManager(manager) {
2410
+ defaultCacheManager = manager;
2411
+ }
2412
+
2413
+ // src/mcp/tools.ts
2414
+ var CODEX_TOOLS = [
2415
+ {
2416
+ name: "codex_fetch",
2417
+ description: "Fetch a document from the Codex knowledge base by URI. Returns the document content.",
2418
+ inputSchema: {
2419
+ type: "object",
2420
+ properties: {
2421
+ uri: {
2422
+ type: "string",
2423
+ description: "Codex URI in format: codex://org/project/path/to/file.md"
2424
+ },
2425
+ branch: {
2426
+ type: "string",
2427
+ description: "Git branch to fetch from (default: main)"
2428
+ },
2429
+ noCache: {
2430
+ type: "boolean",
2431
+ description: "Bypass cache and fetch fresh content"
2432
+ }
2433
+ },
2434
+ required: ["uri"]
2435
+ }
2436
+ },
2437
+ {
2438
+ name: "codex_search",
2439
+ description: "Search for documents in the Codex knowledge base.",
2440
+ inputSchema: {
2441
+ type: "object",
2442
+ properties: {
2443
+ query: {
2444
+ type: "string",
2445
+ description: "Search query string"
2446
+ },
2447
+ org: {
2448
+ type: "string",
2449
+ description: "Filter by organization"
2450
+ },
2451
+ project: {
2452
+ type: "string",
2453
+ description: "Filter by project"
2454
+ },
2455
+ limit: {
2456
+ type: "number",
2457
+ description: "Maximum number of results (default: 10)"
2458
+ },
2459
+ type: {
2460
+ type: "string",
2461
+ description: "Filter by artifact type (e.g., docs, specs, logs)"
2462
+ }
2463
+ },
2464
+ required: ["query"]
2465
+ }
2466
+ },
2467
+ {
2468
+ name: "codex_list",
2469
+ description: "List documents in the Codex cache.",
2470
+ inputSchema: {
2471
+ type: "object",
2472
+ properties: {
2473
+ org: {
2474
+ type: "string",
2475
+ description: "Filter by organization"
2476
+ },
2477
+ project: {
2478
+ type: "string",
2479
+ description: "Filter by project"
2480
+ },
2481
+ includeExpired: {
2482
+ type: "boolean",
2483
+ description: "Include expired cache entries"
2484
+ }
2485
+ }
2486
+ }
2487
+ },
2488
+ {
2489
+ name: "codex_invalidate",
2490
+ description: "Invalidate cached documents matching a pattern.",
2491
+ inputSchema: {
2492
+ type: "object",
2493
+ properties: {
2494
+ pattern: {
2495
+ type: "string",
2496
+ description: "URI pattern to invalidate (supports regex)"
2497
+ }
2498
+ },
2499
+ required: ["pattern"]
2500
+ }
2501
+ }
2502
+ ];
2503
+ function textResult(text, isError = false) {
2504
+ return {
2505
+ content: [{ type: "text", text }],
2506
+ isError
2507
+ };
2508
+ }
2509
+ function resourceResult(uri, content, mimeType) {
2510
+ return {
2511
+ content: [
2512
+ {
2513
+ type: "resource",
2514
+ resource: {
2515
+ uri,
2516
+ mimeType,
2517
+ text: content
2518
+ }
2519
+ }
2520
+ ]
2521
+ };
2522
+ }
2523
+ async function handleFetch(args, ctx) {
2524
+ const { uri, branch, noCache } = args;
2525
+ const ref = resolveReference(uri);
2526
+ if (!ref) {
2527
+ return textResult(`Invalid codex URI: ${uri}`, true);
2528
+ }
2529
+ try {
2530
+ let result;
2531
+ if (noCache) {
2532
+ result = await ctx.storage.fetch(ref, { branch });
2533
+ await ctx.cache.set(uri, result);
2534
+ } else {
2535
+ result = await ctx.cache.get(ref, { branch });
2536
+ }
2537
+ const content = result.content.toString("utf-8");
2538
+ return resourceResult(uri, content, result.contentType);
2539
+ } catch (error) {
2540
+ const message = error instanceof Error ? error.message : String(error);
2541
+ return textResult(`Failed to fetch ${uri}: ${message}`, true);
2542
+ }
2543
+ }
2544
+ async function handleSearch(args, ctx) {
2545
+ const { query, org, project, limit = 10 } = args;
2546
+ const stats = await ctx.cache.getStats();
2547
+ if (stats.entryCount === 0) {
2548
+ return textResult("No documents in cache. Use codex_fetch to load documents first.");
2549
+ }
2550
+ const message = `Search functionality requires a search index.
2551
+ Query: "${query}"
2552
+ Filters: org=${org || "any"}, project=${project || "any"}
2553
+ Limit: ${limit}
2554
+
2555
+ To fetch documents, use codex_fetch with a specific URI like:
2556
+ codex://org/project/docs/file.md`;
2557
+ return textResult(message);
2558
+ }
2559
+ async function handleList(args, ctx) {
2560
+ const { org, project, includeExpired } = args;
2561
+ const stats = await ctx.cache.getStats();
2562
+ let message = `Cache Statistics:
2563
+ - Total entries: ${stats.entryCount}
2564
+ - Memory entries: ${stats.memoryEntries}
2565
+ - Memory size: ${formatBytes(stats.memorySize)}
2566
+ - Total size: ${formatBytes(stats.totalSize)}
2567
+ - Fresh: ${stats.freshCount}
2568
+ - Stale: ${stats.staleCount}
2569
+ - Expired: ${stats.expiredCount}`;
2570
+ if (org) {
2571
+ message += `
2572
+
2573
+ Filtered by org: ${org}`;
2574
+ }
2575
+ if (project) {
2576
+ message += `
2577
+ Filtered by project: ${project}`;
2578
+ }
2579
+ if (includeExpired) {
2580
+ message += `
2581
+ Including expired entries`;
2582
+ }
2583
+ return textResult(message);
2584
+ }
2585
+ async function handleInvalidate(args, ctx) {
2586
+ const { pattern } = args;
2587
+ try {
2588
+ const regex = new RegExp(pattern);
2589
+ const count = await ctx.cache.invalidatePattern(regex);
2590
+ return textResult(`Invalidated ${count} cache entries matching pattern: ${pattern}`);
2591
+ } catch (error) {
2592
+ const message = error instanceof Error ? error.message : String(error);
2593
+ return textResult(`Invalid pattern: ${message}`, true);
2594
+ }
2595
+ }
2596
+ async function handleToolCall(name, args, ctx) {
2597
+ switch (name) {
2598
+ case "codex_fetch":
2599
+ return handleFetch(args, ctx);
2600
+ case "codex_search":
2601
+ return handleSearch(args, ctx);
2602
+ case "codex_list":
2603
+ return handleList(args, ctx);
2604
+ case "codex_invalidate":
2605
+ return handleInvalidate(args, ctx);
2606
+ default:
2607
+ return textResult(`Unknown tool: ${name}`, true);
2608
+ }
2609
+ }
2610
+ function formatBytes(bytes) {
2611
+ if (bytes === 0) return "0 B";
2612
+ const k = 1024;
2613
+ const sizes = ["B", "KB", "MB", "GB"];
2614
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2615
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
2616
+ }
2617
+
2618
+ // src/mcp/server.ts
2619
+ var McpServer = class {
2620
+ config;
2621
+ toolContext;
2622
+ constructor(config) {
2623
+ this.config = {
2624
+ name: config.name ?? "codex",
2625
+ version: config.version ?? "1.0.0",
2626
+ cache: config.cache,
2627
+ storage: config.storage
2628
+ };
2629
+ this.toolContext = {
2630
+ cache: config.cache,
2631
+ storage: config.storage
2632
+ };
2633
+ }
2634
+ /**
2635
+ * Get server info
2636
+ */
2637
+ getServerInfo() {
2638
+ return {
2639
+ name: this.config.name,
2640
+ version: this.config.version,
2641
+ capabilities: this.getCapabilities()
2642
+ };
2643
+ }
2644
+ /**
2645
+ * Get server capabilities
2646
+ */
2647
+ getCapabilities() {
2648
+ return {
2649
+ tools: {
2650
+ listChanged: false
2651
+ },
2652
+ resources: {
2653
+ subscribe: false,
2654
+ listChanged: false
2655
+ }
2656
+ };
2657
+ }
2658
+ /**
2659
+ * List available tools
2660
+ */
2661
+ listTools() {
2662
+ return CODEX_TOOLS;
2663
+ }
2664
+ /**
2665
+ * Call a tool
2666
+ */
2667
+ async callTool(name, args) {
2668
+ return handleToolCall(name, args, this.toolContext);
2669
+ }
2670
+ /**
2671
+ * List available resources
2672
+ */
2673
+ async listResources() {
2674
+ const resources = [];
2675
+ const stats = await this.config.cache.getStats();
2676
+ resources.push({
2677
+ uri: "codex://cache/summary",
2678
+ name: "Cache Summary",
2679
+ description: `${stats.entryCount} cached documents`,
2680
+ mimeType: "text/plain"
2681
+ });
2682
+ return resources;
2683
+ }
2684
+ /**
2685
+ * List resource templates
2686
+ */
2687
+ listResourceTemplates() {
2688
+ return [
2689
+ {
2690
+ uriTemplate: "codex://{org}/{project}/{path}",
2691
+ name: "Codex Document",
2692
+ description: "Fetch a document from the Codex knowledge base",
2693
+ mimeType: "text/markdown"
2694
+ }
2695
+ ];
2696
+ }
2697
+ /**
2698
+ * Read a resource
2699
+ */
2700
+ async readResource(uri) {
2701
+ if (uri === "codex://cache/summary") {
2702
+ const stats = await this.config.cache.getStats();
2703
+ return [
2704
+ {
2705
+ uri,
2706
+ mimeType: "text/plain",
2707
+ text: `Cache Statistics:
2708
+ - Total entries: ${stats.entryCount}
2709
+ - Memory entries: ${stats.memoryEntries}
2710
+ - Fresh: ${stats.freshCount}
2711
+ - Stale: ${stats.staleCount}
2712
+ - Expired: ${stats.expiredCount}`
2713
+ }
2714
+ ];
2715
+ }
2716
+ const ref = resolveReference(uri);
2717
+ if (!ref) {
2718
+ throw new Error(`Invalid codex URI: ${uri}`);
2719
+ }
2720
+ const result = await this.config.cache.get(ref);
2721
+ return [
2722
+ {
2723
+ uri,
2724
+ mimeType: result.contentType,
2725
+ text: result.content.toString("utf-8")
2726
+ }
2727
+ ];
2728
+ }
2729
+ /**
2730
+ * Handle JSON-RPC request
2731
+ *
2732
+ * This method handles the low-level MCP protocol messages.
2733
+ */
2734
+ async handleRequest(method, params) {
2735
+ switch (method) {
2736
+ case "initialize":
2737
+ return {
2738
+ protocolVersion: "2024-11-05",
2739
+ serverInfo: this.getServerInfo(),
2740
+ capabilities: this.getCapabilities()
2741
+ };
2742
+ case "tools/list":
2743
+ return { tools: this.listTools() };
2744
+ case "tools/call": {
2745
+ const { name, arguments: args } = params;
2746
+ return await this.callTool(name, args);
2747
+ }
2748
+ case "resources/list":
2749
+ return { resources: await this.listResources() };
2750
+ case "resources/templates/list":
2751
+ return { resourceTemplates: this.listResourceTemplates() };
2752
+ case "resources/read": {
2753
+ const { uri } = params;
2754
+ return { contents: await this.readResource(uri) };
2755
+ }
2756
+ case "prompts/list":
2757
+ return { prompts: [] };
2758
+ default:
2759
+ throw new Error(`Unknown method: ${method}`);
2760
+ }
2761
+ }
2762
+ /**
2763
+ * Process a JSON-RPC message
2764
+ */
2765
+ async processMessage(message) {
2766
+ let id = null;
2767
+ try {
2768
+ const request = JSON.parse(message);
2769
+ id = request.id;
2770
+ const { method, params } = request;
2771
+ const result = await this.handleRequest(method, params);
2772
+ return JSON.stringify({
2773
+ jsonrpc: "2.0",
2774
+ id,
2775
+ result
2776
+ });
2777
+ } catch (error) {
2778
+ const errorMessage = error instanceof Error ? error.message : String(error);
2779
+ return JSON.stringify({
2780
+ jsonrpc: "2.0",
2781
+ id,
2782
+ error: {
2783
+ code: -32603,
2784
+ message: errorMessage
2785
+ }
2786
+ });
2787
+ }
2788
+ }
2789
+ };
2790
+ function createMcpServer(config) {
2791
+ return new McpServer(config);
2792
+ }
2793
+
2794
+ // src/sync/types.ts
2795
+ var DEFAULT_SYNC_CONFIG = {
2796
+ defaultDirection: "to-codex",
2797
+ rules: [],
2798
+ defaultExcludes: [
2799
+ "**/node_modules/**",
2800
+ "**/.git/**",
2801
+ "**/.DS_Store",
2802
+ "**/Thumbs.db",
2803
+ "**/*.log",
2804
+ "**/.env*",
2805
+ "**/dist/**",
2806
+ "**/build/**",
2807
+ "**/coverage/**"
2808
+ ],
2809
+ deleteOrphans: false,
2810
+ conflictStrategy: "newest"
2811
+ };
2812
+ function evaluatePath(path5, rules, direction, defaultExcludes = []) {
2813
+ for (const pattern of defaultExcludes) {
2814
+ if (micromatch2.isMatch(path5, pattern)) {
2815
+ return {
2816
+ path: path5,
2817
+ shouldSync: false,
2818
+ reason: `Excluded by default pattern: ${pattern}`
2819
+ };
2820
+ }
2821
+ }
2822
+ const sortedRules = [...rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
2823
+ for (const rule of sortedRules) {
2824
+ if (rule.direction && rule.direction !== direction) {
2825
+ continue;
2826
+ }
2827
+ if (micromatch2.isMatch(path5, rule.pattern)) {
2828
+ return {
2829
+ path: path5,
2830
+ shouldSync: rule.include,
2831
+ matchedRule: rule,
2832
+ reason: rule.include ? `Included by rule: ${rule.pattern}` : `Excluded by rule: ${rule.pattern}`
2833
+ };
2834
+ }
2835
+ }
2836
+ return {
2837
+ path: path5,
2838
+ shouldSync: true,
2839
+ reason: "No matching rule, included by default"
2840
+ };
2841
+ }
2842
+ function evaluatePaths(paths, rules, direction, defaultExcludes = []) {
2843
+ const results = /* @__PURE__ */ new Map();
2844
+ for (const path5 of paths) {
2845
+ results.set(path5, evaluatePath(path5, rules, direction, defaultExcludes));
2846
+ }
2847
+ return results;
2848
+ }
2849
+ function filterSyncablePaths(paths, rules, direction, defaultExcludes = []) {
2850
+ return paths.filter(
2851
+ (path5) => evaluatePath(path5, rules, direction, defaultExcludes).shouldSync
2852
+ );
2853
+ }
2854
+ function createRulesFromPatterns(include = [], exclude = []) {
2855
+ const rules = [];
2856
+ for (const pattern of exclude) {
2857
+ rules.push({
2858
+ pattern,
2859
+ include: false,
2860
+ priority: 100
2861
+ });
2862
+ }
2863
+ for (const pattern of include) {
2864
+ rules.push({
2865
+ pattern,
2866
+ include: true,
2867
+ priority: 50
2868
+ });
2869
+ }
2870
+ return rules;
2871
+ }
2872
+ function mergeRules(...ruleSets) {
2873
+ return ruleSets.flat();
2874
+ }
2875
+ function summarizeEvaluations(results) {
2876
+ const summary = {
2877
+ total: results.size,
2878
+ included: 0,
2879
+ excluded: 0,
2880
+ byReason: {}
2881
+ };
2882
+ for (const result of results.values()) {
2883
+ if (result.shouldSync) {
2884
+ summary.included++;
2885
+ } else {
2886
+ summary.excluded++;
2887
+ }
2888
+ summary.byReason[result.reason] = (summary.byReason[result.reason] ?? 0) + 1;
2889
+ }
2890
+ return summary;
2891
+ }
2892
+ function validateRules(rules) {
2893
+ const errors = [];
2894
+ for (let i = 0; i < rules.length; i++) {
2895
+ const rule = rules[i];
2896
+ if (!rule) continue;
2897
+ if (!rule.pattern || rule.pattern.trim() === "") {
2898
+ errors.push(`Rule ${i}: pattern is empty`);
2899
+ continue;
2900
+ }
2901
+ try {
2902
+ micromatch2.isMatch("test", rule.pattern);
2903
+ } catch {
2904
+ errors.push(`Rule ${i}: invalid pattern "${rule.pattern}"`);
2905
+ }
2906
+ if (rule.direction && !["to-codex", "from-codex", "bidirectional"].includes(rule.direction)) {
2907
+ errors.push(`Rule ${i}: invalid direction "${rule.direction}"`);
2908
+ }
2909
+ }
2910
+ return errors;
2911
+ }
2912
+
2913
+ // src/sync/planner.ts
2914
+ function createSyncPlan(sourceFiles, targetFiles, options, config) {
2915
+ const direction = options.direction ?? config.defaultDirection;
2916
+ const targetMap = new Map(targetFiles.map((f) => [f.path, f]));
2917
+ const sourceMap = new Map(sourceFiles.map((f) => [f.path, f]));
2918
+ const files = [];
2919
+ const conflicts = [];
2920
+ const skipped = [];
2921
+ const rules = [
2922
+ ...config.rules,
2923
+ ...(options.include ?? []).map((p) => ({ pattern: p, include: true, priority: 200 })),
2924
+ ...(options.exclude ?? []).map((p) => ({ pattern: p, include: false, priority: 200 }))
2925
+ ];
2926
+ const excludes = [...config.defaultExcludes];
2927
+ for (const sourceFile of sourceFiles) {
2928
+ const evaluation = evaluatePath(sourceFile.path, rules, direction, excludes);
2929
+ if (!evaluation.shouldSync) {
2930
+ skipped.push({
2931
+ path: sourceFile.path,
2932
+ operation: "skip",
2933
+ size: sourceFile.size,
2934
+ reason: evaluation.reason
2935
+ });
2936
+ continue;
2937
+ }
2938
+ const targetFile = targetMap.get(sourceFile.path);
2939
+ if (!targetFile) {
2940
+ files.push({
2941
+ path: sourceFile.path,
2942
+ operation: "create",
2943
+ size: sourceFile.size,
2944
+ mtime: sourceFile.mtime,
2945
+ hash: sourceFile.hash
2946
+ });
2947
+ } else {
2948
+ const isDifferent = sourceFile.hash !== targetFile.hash;
2949
+ if (isDifferent) {
2950
+ if (options.force) {
2951
+ files.push({
2952
+ path: sourceFile.path,
2953
+ operation: "update",
2954
+ size: sourceFile.size,
2955
+ mtime: sourceFile.mtime,
2956
+ hash: sourceFile.hash
2957
+ });
2958
+ } else if (targetFile.mtime > sourceFile.mtime) {
2959
+ conflicts.push({
2960
+ path: sourceFile.path,
2961
+ operation: "conflict",
2962
+ size: sourceFile.size,
2963
+ mtime: sourceFile.mtime,
2964
+ reason: "Target file is newer than source"
2965
+ });
2966
+ } else {
2967
+ files.push({
2968
+ path: sourceFile.path,
2969
+ operation: "update",
2970
+ size: sourceFile.size,
2971
+ mtime: sourceFile.mtime,
2972
+ hash: sourceFile.hash
2973
+ });
2974
+ }
2975
+ } else {
2976
+ skipped.push({
2977
+ path: sourceFile.path,
2978
+ operation: "skip",
2979
+ size: sourceFile.size,
2980
+ reason: "Files are identical"
2981
+ });
2982
+ }
2983
+ }
2984
+ }
2985
+ if (options.delete) {
2986
+ for (const targetFile of targetFiles) {
2987
+ if (!sourceMap.has(targetFile.path)) {
2988
+ const evaluation = evaluatePath(targetFile.path, rules, direction, excludes);
2989
+ if (evaluation.shouldSync) {
2990
+ files.push({
2991
+ path: targetFile.path,
2992
+ operation: "delete",
2993
+ size: targetFile.size
2994
+ });
2995
+ }
2996
+ }
2997
+ }
2998
+ }
2999
+ let limitedFiles = files;
3000
+ if (options.maxFiles && files.length > options.maxFiles) {
3001
+ limitedFiles = files.slice(0, options.maxFiles);
3002
+ for (const file of files.slice(options.maxFiles)) {
3003
+ skipped.push({
3004
+ ...file,
3005
+ operation: "skip",
3006
+ reason: "Exceeded max files limit"
3007
+ });
3008
+ }
3009
+ }
3010
+ const totalBytes = limitedFiles.reduce((sum, f) => sum + (f.size ?? 0), 0);
3011
+ return {
3012
+ direction,
3013
+ source: direction === "from-codex" ? "codex" : "local",
3014
+ target: direction === "from-codex" ? "local" : "codex",
3015
+ files: limitedFiles,
3016
+ totalFiles: limitedFiles.length,
3017
+ totalBytes,
3018
+ conflicts,
3019
+ skipped
3020
+ };
3021
+ }
3022
+ function estimateSyncTime(plan, bytesPerSecond = 1024 * 1024) {
3023
+ const transferTime = plan.totalBytes / bytesPerSecond * 1e3;
3024
+ const overheadPerFile = 50;
3025
+ const overhead = plan.totalFiles * overheadPerFile;
3026
+ return Math.ceil(transferTime + overhead);
3027
+ }
3028
+ function createEmptySyncPlan(direction = "to-codex") {
3029
+ return {
3030
+ direction,
3031
+ source: direction === "from-codex" ? "codex" : "local",
3032
+ target: direction === "from-codex" ? "local" : "codex",
3033
+ files: [],
3034
+ totalFiles: 0,
3035
+ totalBytes: 0,
3036
+ conflicts: [],
3037
+ skipped: []
3038
+ };
3039
+ }
3040
+ function filterPlanOperations(plan, operations) {
3041
+ const filtered = plan.files.filter(
3042
+ (f) => operations.includes(f.operation)
3043
+ );
3044
+ return {
3045
+ ...plan,
3046
+ files: filtered,
3047
+ totalFiles: filtered.length,
3048
+ totalBytes: filtered.reduce((sum, f) => sum + (f.size ?? 0), 0)
3049
+ };
3050
+ }
3051
+ function getPlanStats(plan) {
3052
+ return {
3053
+ creates: plan.files.filter((f) => f.operation === "create").length,
3054
+ updates: plan.files.filter((f) => f.operation === "update").length,
3055
+ deletes: plan.files.filter((f) => f.operation === "delete").length,
3056
+ skips: plan.skipped.length,
3057
+ conflicts: plan.conflicts.length,
3058
+ totalBytes: plan.totalBytes
3059
+ };
3060
+ }
3061
+ function formatPlanSummary(plan) {
3062
+ const stats = getPlanStats(plan);
3063
+ const lines = [
3064
+ `Sync Plan: ${plan.source} \u2192 ${plan.target}`,
3065
+ `Direction: ${plan.direction}`,
3066
+ "",
3067
+ `Operations:`,
3068
+ ` Create: ${stats.creates} files`,
3069
+ ` Update: ${stats.updates} files`,
3070
+ ` Delete: ${stats.deletes} files`,
3071
+ ` Skip: ${stats.skips} files`,
3072
+ ""
3073
+ ];
3074
+ if (stats.conflicts > 0) {
3075
+ lines.push(`\u26A0\uFE0F Conflicts: ${stats.conflicts} files`);
3076
+ lines.push("");
3077
+ }
3078
+ lines.push(`Total: ${plan.totalFiles} files (${formatBytes2(stats.totalBytes)})`);
3079
+ return lines.join("\n");
3080
+ }
3081
+ function formatBytes2(bytes) {
3082
+ if (bytes === 0) return "0 B";
3083
+ const k = 1024;
3084
+ const sizes = ["B", "KB", "MB", "GB"];
3085
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3086
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
3087
+ }
3088
+
3089
+ // src/sync/manager.ts
3090
+ var SyncManager = class {
3091
+ localStorage;
3092
+ config;
3093
+ manifestPath;
3094
+ manifest = null;
3095
+ constructor(options) {
3096
+ this.localStorage = options.localStorage;
3097
+ this.config = {
3098
+ ...DEFAULT_SYNC_CONFIG,
3099
+ ...options.config
3100
+ };
3101
+ this.manifestPath = options.manifestPath ?? ".fractary/codex-sync-manifest.json";
3102
+ }
3103
+ /**
3104
+ * Load the sync manifest
3105
+ */
3106
+ async loadManifest() {
3107
+ try {
3108
+ const content = await this.localStorage.readText(this.manifestPath);
3109
+ this.manifest = JSON.parse(content);
3110
+ return this.manifest;
3111
+ } catch {
3112
+ return null;
3113
+ }
3114
+ }
3115
+ /**
3116
+ * Save the sync manifest
3117
+ */
3118
+ async saveManifest(manifest) {
3119
+ this.manifest = manifest;
3120
+ await this.localStorage.write(this.manifestPath, JSON.stringify(manifest, null, 2));
3121
+ }
3122
+ /**
3123
+ * Get or create manifest
3124
+ */
3125
+ async getOrCreateManifest(org, project) {
3126
+ let manifest = await this.loadManifest();
3127
+ if (!manifest || manifest.org !== org || manifest.project !== project) {
3128
+ manifest = {
3129
+ version: 1,
3130
+ org,
3131
+ project,
3132
+ lastSync: 0,
3133
+ entries: {}
3134
+ };
3135
+ }
3136
+ return manifest;
3137
+ }
3138
+ /**
3139
+ * List local files
3140
+ */
3141
+ async listLocalFiles(directory) {
3142
+ const files = await this.localStorage.list(directory);
3143
+ const fileInfos = [];
3144
+ for (const file of files) {
3145
+ try {
3146
+ const content = await this.localStorage.readText(file);
3147
+ const buffer = Buffer.from(content);
3148
+ fileInfos.push({
3149
+ path: file,
3150
+ size: buffer.length,
3151
+ mtime: Date.now(),
3152
+ // Would need fs.stat for real mtime
3153
+ hash: calculateContentHash(buffer)
3154
+ });
3155
+ } catch {
3156
+ }
3157
+ }
3158
+ return fileInfos;
3159
+ }
3160
+ /**
3161
+ * Create a sync plan
3162
+ *
3163
+ * @param _org - Organization (reserved for future use)
3164
+ * @param _project - Project (reserved for future use)
3165
+ * @param sourceDir - Source directory to sync from
3166
+ * @param targetFiles - Files currently in the target
3167
+ * @param options - Sync options
3168
+ */
3169
+ async createPlan(_org, _project, sourceDir, targetFiles, options) {
3170
+ const sourceFiles = await this.listLocalFiles(sourceDir);
3171
+ const plan = createSyncPlan(
3172
+ sourceFiles,
3173
+ targetFiles,
3174
+ options ?? {},
3175
+ this.config
3176
+ );
3177
+ plan.estimatedTime = estimateSyncTime(plan);
3178
+ return plan;
3179
+ }
3180
+ /**
3181
+ * Execute a sync plan (dry run)
3182
+ *
3183
+ * In a real implementation, this would actually copy files.
3184
+ * For the SDK, we provide the plan and let consumers execute.
3185
+ */
3186
+ async executePlan(plan, options) {
3187
+ const startTime = Date.now();
3188
+ const errors = [];
3189
+ let synced = 0;
3190
+ let failed = 0;
3191
+ if (options?.dryRun) {
3192
+ return {
3193
+ success: true,
3194
+ plan,
3195
+ synced: plan.totalFiles,
3196
+ failed: 0,
3197
+ skipped: plan.skipped.length,
3198
+ errors: [],
3199
+ duration: Date.now() - startTime,
3200
+ timestamp: Date.now()
3201
+ };
3202
+ }
3203
+ for (let i = 0; i < plan.files.length; i++) {
3204
+ const file = plan.files[i];
3205
+ if (!file) continue;
3206
+ try {
3207
+ if (options?.onProgress) {
3208
+ options.onProgress(i + 1, plan.totalFiles, file.path);
3209
+ }
3210
+ synced++;
3211
+ } catch (error) {
3212
+ failed++;
3213
+ errors.push({
3214
+ path: file.path,
3215
+ error: error instanceof Error ? error.message : String(error)
3216
+ });
3217
+ }
3218
+ }
3219
+ return {
3220
+ success: failed === 0,
3221
+ plan,
3222
+ synced,
3223
+ failed,
3224
+ skipped: plan.skipped.length,
3225
+ errors,
3226
+ duration: Date.now() - startTime,
3227
+ timestamp: Date.now()
3228
+ };
3229
+ }
3230
+ /**
3231
+ * Update manifest after sync
3232
+ */
3233
+ async updateManifest(org, project, syncedFiles) {
3234
+ const manifest = await this.getOrCreateManifest(org, project);
3235
+ const now = Date.now();
3236
+ for (const file of syncedFiles) {
3237
+ if (file.operation === "delete") {
3238
+ delete manifest.entries[file.path];
3239
+ } else if (file.operation === "create" || file.operation === "update") {
3240
+ manifest.entries[file.path] = {
3241
+ path: file.path,
3242
+ hash: file.hash ?? "",
3243
+ size: file.size ?? 0,
3244
+ syncedAt: now,
3245
+ source: "local"
3246
+ };
3247
+ }
3248
+ }
3249
+ manifest.lastSync = now;
3250
+ await this.saveManifest(manifest);
3251
+ }
3252
+ /**
3253
+ * Get sync status for a file
3254
+ */
3255
+ async getFileStatus(path5) {
3256
+ const manifest = await this.loadManifest();
3257
+ return manifest?.entries[path5] ?? null;
3258
+ }
3259
+ /**
3260
+ * Check if a file is synced
3261
+ */
3262
+ async isFileSynced(path5) {
3263
+ const status = await this.getFileStatus(path5);
3264
+ return status !== null;
3265
+ }
3266
+ /**
3267
+ * Get last sync timestamp
3268
+ */
3269
+ async getLastSyncTime() {
3270
+ const manifest = await this.loadManifest();
3271
+ return manifest?.lastSync ?? null;
3272
+ }
3273
+ /**
3274
+ * Clear sync manifest
3275
+ */
3276
+ async clearManifest() {
3277
+ try {
3278
+ await this.localStorage.delete(this.manifestPath);
3279
+ this.manifest = null;
3280
+ } catch {
3281
+ }
3282
+ }
3283
+ /**
3284
+ * Get sync configuration
3285
+ */
3286
+ getConfig() {
3287
+ return { ...this.config };
3288
+ }
3289
+ /**
3290
+ * Update sync configuration
3291
+ */
3292
+ updateConfig(updates) {
3293
+ this.config = {
3294
+ ...this.config,
3295
+ ...updates
3296
+ };
3297
+ }
3298
+ };
3299
+ function createSyncManager(config) {
3300
+ return new SyncManager(config);
3301
+ }
3302
+
3303
+ // src/permissions/types.ts
3304
+ var DEFAULT_PERMISSION_CONFIG = {
3305
+ defaultLevel: "read",
3306
+ defaultAllow: true,
3307
+ rules: [],
3308
+ enforced: false
3309
+ };
3310
+ var PERMISSION_LEVEL_ORDER = ["none", "read", "write", "admin"];
3311
+ function levelGrants(granted, required) {
3312
+ const grantedIndex = PERMISSION_LEVEL_ORDER.indexOf(granted);
3313
+ const requiredIndex = PERMISSION_LEVEL_ORDER.indexOf(required);
3314
+ return grantedIndex >= requiredIndex;
3315
+ }
3316
+ function maxLevel(a, b) {
3317
+ const aIndex = PERMISSION_LEVEL_ORDER.indexOf(a);
3318
+ const bIndex = PERMISSION_LEVEL_ORDER.indexOf(b);
3319
+ return PERMISSION_LEVEL_ORDER[Math.max(aIndex, bIndex)] ?? "none";
3320
+ }
3321
+ function minLevel(a, b) {
3322
+ const aIndex = PERMISSION_LEVEL_ORDER.indexOf(a);
3323
+ const bIndex = PERMISSION_LEVEL_ORDER.indexOf(b);
3324
+ return PERMISSION_LEVEL_ORDER[Math.min(aIndex, bIndex)] ?? "none";
3325
+ }
3326
+ function ruleMatchesContext(rule, context) {
3327
+ if (rule.enabled === false) {
3328
+ return false;
3329
+ }
3330
+ switch (rule.scope) {
3331
+ case "org":
3332
+ if (rule.org && context.org !== rule.org) {
3333
+ return false;
3334
+ }
3335
+ break;
3336
+ case "project":
3337
+ if (rule.org && context.org !== rule.org) {
3338
+ return false;
3339
+ }
3340
+ if (rule.project && context.project !== rule.project) {
3341
+ return false;
3342
+ }
3343
+ break;
3344
+ }
3345
+ return true;
3346
+ }
3347
+ function ruleMatchesPath(rule, path5) {
3348
+ return micromatch2.isMatch(path5, rule.pattern);
3349
+ }
3350
+ function ruleMatchesAction(rule, action) {
3351
+ return rule.actions.includes(action);
3352
+ }
3353
+ function evaluatePermission(path5, action, context, config) {
3354
+ const sortedRules = [...config.rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
3355
+ for (const rule of sortedRules) {
3356
+ if (!ruleMatchesContext(rule, context)) {
3357
+ continue;
3358
+ }
3359
+ if (!ruleMatchesPath(rule, path5)) {
3360
+ continue;
3361
+ }
3362
+ if (!ruleMatchesAction(rule, action)) {
3363
+ continue;
3364
+ }
3365
+ return {
3366
+ allowed: rule.level !== "none",
3367
+ level: rule.level,
3368
+ matchedRule: rule,
3369
+ reason: `Matched rule: ${rule.description ?? rule.pattern}`
3370
+ };
3371
+ }
3372
+ return {
3373
+ allowed: config.defaultAllow,
3374
+ level: config.defaultLevel,
3375
+ reason: config.defaultAllow ? "Allowed by default" : "Denied by default"
3376
+ };
3377
+ }
3378
+ function isAllowed(path5, action, context, config) {
3379
+ const result = evaluatePermission(path5, action, context, config);
3380
+ return result.allowed;
3381
+ }
3382
+ function hasPermission(path5, action, requiredLevel, context, config) {
3383
+ const result = evaluatePermission(path5, action, context, config);
3384
+ return levelGrants(result.level, requiredLevel);
3385
+ }
3386
+ function evaluatePermissions(paths, action, context, config) {
3387
+ const results = /* @__PURE__ */ new Map();
3388
+ for (const path5 of paths) {
3389
+ results.set(path5, evaluatePermission(path5, action, context, config));
3390
+ }
3391
+ return results;
3392
+ }
3393
+ function filterByPermission(paths, action, context, config, requiredLevel = "read") {
3394
+ return paths.filter((path5) => {
3395
+ const result = evaluatePermission(path5, action, context, config);
3396
+ return levelGrants(result.level, requiredLevel);
3397
+ });
3398
+ }
3399
+ function validateRules2(rules) {
3400
+ const errors = [];
3401
+ for (let i = 0; i < rules.length; i++) {
3402
+ const rule = rules[i];
3403
+ if (!rule) continue;
3404
+ if (!rule.pattern || rule.pattern.trim() === "") {
3405
+ errors.push(`Rule ${i}: pattern is empty`);
3406
+ continue;
3407
+ }
3408
+ try {
3409
+ micromatch2.isMatch("test", rule.pattern);
3410
+ } catch {
3411
+ errors.push(`Rule ${i}: invalid pattern "${rule.pattern}"`);
3412
+ }
3413
+ if (!rule.actions || rule.actions.length === 0) {
3414
+ errors.push(`Rule ${i}: actions are empty`);
3415
+ }
3416
+ if (rule.scope === "org" && !rule.org) {
3417
+ errors.push(`Rule ${i}: org scope requires org field`);
3418
+ }
3419
+ if (rule.scope === "project" && !rule.project) {
3420
+ errors.push(`Rule ${i}: project scope requires project field`);
3421
+ }
3422
+ }
3423
+ return errors;
3424
+ }
3425
+ function createRule(options) {
3426
+ return {
3427
+ pattern: options.pattern,
3428
+ actions: options.actions,
3429
+ level: options.level,
3430
+ scope: options.scope ?? "global",
3431
+ org: options.org,
3432
+ project: options.project,
3433
+ priority: options.priority ?? 0,
3434
+ description: options.description,
3435
+ enabled: true
3436
+ };
3437
+ }
3438
+ var CommonRules = {
3439
+ /** Allow all actions for docs */
3440
+ allowDocs: () => createRule({
3441
+ pattern: "docs/**",
3442
+ actions: ["fetch", "cache", "sync"],
3443
+ level: "read",
3444
+ description: "Allow read access to docs"
3445
+ }),
3446
+ /** Deny access to private files */
3447
+ denyPrivate: () => createRule({
3448
+ pattern: "**/.private/**",
3449
+ actions: ["fetch", "cache", "sync"],
3450
+ level: "none",
3451
+ priority: 100,
3452
+ description: "Deny access to private files"
3453
+ }),
3454
+ /** Read-only access to specs */
3455
+ readOnlySpecs: () => createRule({
3456
+ pattern: "specs/**",
3457
+ actions: ["fetch", "cache"],
3458
+ level: "read",
3459
+ description: "Read-only access to specs"
3460
+ }),
3461
+ /** Admin access for management */
3462
+ adminManage: () => createRule({
3463
+ pattern: "**",
3464
+ actions: ["manage", "invalidate"],
3465
+ level: "admin",
3466
+ description: "Admin access for management operations"
3467
+ })
3468
+ };
3469
+
3470
+ // src/permissions/manager.ts
3471
+ var PermissionManager = class {
3472
+ config;
3473
+ defaultContext;
3474
+ constructor(options = {}) {
3475
+ this.config = {
3476
+ ...DEFAULT_PERMISSION_CONFIG,
3477
+ ...options.config,
3478
+ // Create a new rules array to avoid shared state
3479
+ rules: options.config?.rules ? [...options.config.rules] : []
3480
+ };
3481
+ this.defaultContext = options.defaultContext ?? {};
3482
+ }
3483
+ /**
3484
+ * Check if an action is allowed for a path
3485
+ */
3486
+ isAllowed(path5, action, context) {
3487
+ const result = this.evaluate(path5, action, context);
3488
+ return result.allowed;
3489
+ }
3490
+ /**
3491
+ * Check if a permission level is granted
3492
+ */
3493
+ hasPermission(path5, action, requiredLevel, context) {
3494
+ const result = this.evaluate(path5, action, context);
3495
+ return levelGrants(result.level, requiredLevel);
3496
+ }
3497
+ /**
3498
+ * Evaluate permission for a path and action
3499
+ */
3500
+ evaluate(path5, action, context) {
3501
+ const mergedContext = { ...this.defaultContext, ...context };
3502
+ if (!this.config.enforced) {
3503
+ return {
3504
+ allowed: true,
3505
+ level: "admin",
3506
+ reason: "Permissions not enforced"
3507
+ };
3508
+ }
3509
+ return evaluatePermission(path5, action, mergedContext, this.config);
3510
+ }
3511
+ /**
3512
+ * Filter paths by permission
3513
+ */
3514
+ filterAllowed(paths, action, context, requiredLevel = "read") {
3515
+ const mergedContext = { ...this.defaultContext, ...context };
3516
+ if (!this.config.enforced) {
3517
+ return paths;
3518
+ }
3519
+ return filterByPermission(paths, action, mergedContext, this.config, requiredLevel);
3520
+ }
3521
+ /**
3522
+ * Add a permission rule
3523
+ */
3524
+ addRule(rule) {
3525
+ const errors = validateRules2([rule]);
3526
+ if (errors.length > 0) {
3527
+ throw new Error(`Invalid rule: ${errors.join(", ")}`);
3528
+ }
3529
+ this.config.rules.push(rule);
3530
+ }
3531
+ /**
3532
+ * Remove a permission rule by ID
3533
+ */
3534
+ removeRule(id) {
3535
+ const index = this.config.rules.findIndex((r) => r.id === id);
3536
+ if (index === -1) {
3537
+ return false;
3538
+ }
3539
+ this.config.rules.splice(index, 1);
3540
+ return true;
3541
+ }
3542
+ /**
3543
+ * Get all rules
3544
+ */
3545
+ getRules() {
3546
+ return [...this.config.rules];
3547
+ }
3548
+ /**
3549
+ * Clear all rules
3550
+ */
3551
+ clearRules() {
3552
+ this.config.rules = [];
3553
+ }
3554
+ /**
3555
+ * Set rules (replace all)
3556
+ */
3557
+ setRules(rules) {
3558
+ const errors = validateRules2(rules);
3559
+ if (errors.length > 0) {
3560
+ throw new Error(`Invalid rules: ${errors.join(", ")}`);
3561
+ }
3562
+ this.config.rules = [...rules];
3563
+ }
3564
+ /**
3565
+ * Get configuration
3566
+ */
3567
+ getConfig() {
3568
+ return { ...this.config, rules: [...this.config.rules] };
3569
+ }
3570
+ /**
3571
+ * Update configuration
3572
+ */
3573
+ updateConfig(updates) {
3574
+ this.config = {
3575
+ ...this.config,
3576
+ ...updates
3577
+ };
3578
+ }
3579
+ /**
3580
+ * Set default context
3581
+ */
3582
+ setDefaultContext(context) {
3583
+ this.defaultContext = { ...context };
3584
+ }
3585
+ /**
3586
+ * Get default context
3587
+ */
3588
+ getDefaultContext() {
3589
+ return { ...this.defaultContext };
3590
+ }
3591
+ /**
3592
+ * Enable permission enforcement
3593
+ */
3594
+ enable() {
3595
+ this.config.enforced = true;
3596
+ }
3597
+ /**
3598
+ * Disable permission enforcement
3599
+ */
3600
+ disable() {
3601
+ this.config.enforced = false;
3602
+ }
3603
+ /**
3604
+ * Check if permissions are enforced
3605
+ */
3606
+ isEnforced() {
3607
+ return this.config.enforced;
3608
+ }
3609
+ /**
3610
+ * Assert permission (throws if denied)
3611
+ */
3612
+ assertPermission(path5, action, requiredLevel = "read", context) {
3613
+ const result = this.evaluate(path5, action, context);
3614
+ if (!levelGrants(result.level, requiredLevel)) {
3615
+ throw new PermissionDeniedError(
3616
+ `Permission denied for ${action} on ${path5}: ${result.reason}`,
3617
+ path5,
3618
+ action,
3619
+ result
3620
+ );
3621
+ }
3622
+ }
3623
+ };
3624
+ var PermissionDeniedError = class extends Error {
3625
+ constructor(message, path5, action, result) {
3626
+ super(message);
3627
+ this.path = path5;
3628
+ this.action = action;
3629
+ this.result = result;
3630
+ this.name = "PermissionDeniedError";
3631
+ }
3632
+ };
3633
+ function createPermissionManager(config) {
3634
+ return new PermissionManager(config);
3635
+ }
3636
+ var defaultManager2 = null;
3637
+ function getDefaultPermissionManager() {
3638
+ if (!defaultManager2) {
3639
+ defaultManager2 = createPermissionManager();
3640
+ }
3641
+ return defaultManager2;
3642
+ }
3643
+ function setDefaultPermissionManager(manager) {
3644
+ defaultManager2 = manager;
3645
+ }
3646
+
3647
+ // src/migration/detector.ts
3648
+ function detectVersion(config) {
3649
+ if (!config || typeof config !== "object") {
3650
+ return {
3651
+ version: "unknown",
3652
+ confidence: "high",
3653
+ reason: "Invalid configuration object"
3654
+ };
3655
+ }
3656
+ const obj = config;
3657
+ if (obj.version === "3.0") {
3658
+ return {
3659
+ version: "3.0",
3660
+ confidence: "high",
3661
+ reason: "Explicit version field found"
3662
+ };
3663
+ }
3664
+ if (isModernConfig(config)) {
3665
+ return {
3666
+ version: "3.0",
3667
+ confidence: "medium",
3668
+ reason: "Modern structure detected (organization object, sync.patterns)"
3669
+ };
3670
+ }
3671
+ if (isLegacyConfig(config)) {
3672
+ return {
3673
+ version: "2.x",
3674
+ confidence: "high",
3675
+ reason: "Legacy structure detected (org/defaultOrg/codexRepo at top level)"
3676
+ };
3677
+ }
3678
+ const knownFields = [
3679
+ "org",
3680
+ "defaultOrg",
3681
+ "codexRepo",
3682
+ "autoSync",
3683
+ "syncRules",
3684
+ "organization",
3685
+ "sync",
3686
+ "cache"
3687
+ ];
3688
+ const hasKnownFields = knownFields.some((field) => field in obj);
3689
+ if (hasKnownFields) {
3690
+ return {
3691
+ version: "2.x",
3692
+ confidence: "low",
3693
+ reason: "Some known fields found, assuming v2.x"
3694
+ };
3695
+ }
3696
+ return {
3697
+ version: "unknown",
3698
+ confidence: "high",
3699
+ reason: "No recognizable configuration structure"
3700
+ };
3701
+ }
3702
+ function isModernConfig(config) {
3703
+ if (!config || typeof config !== "object") {
3704
+ return false;
3705
+ }
3706
+ const obj = config;
3707
+ if (!obj.organization || typeof obj.organization !== "object") {
3708
+ return false;
3709
+ }
3710
+ const org = obj.organization;
3711
+ if (typeof org.name !== "string" || typeof org.codexRepo !== "string") {
3712
+ return false;
3713
+ }
3714
+ if (!obj.sync || typeof obj.sync !== "object") {
3715
+ return false;
3716
+ }
3717
+ const sync = obj.sync;
3718
+ if (!Array.isArray(sync.patterns)) {
3719
+ return false;
3720
+ }
3721
+ return true;
3722
+ }
3723
+ function isLegacyConfig(config) {
3724
+ if (!config || typeof config !== "object") {
3725
+ return false;
3726
+ }
3727
+ const obj = config;
3728
+ const legacyMarkers = ["org", "defaultOrg", "codexRepo", "codexPath"];
3729
+ const hasLegacyMarker = legacyMarkers.some(
3730
+ (marker) => marker in obj && typeof obj[marker] === "string"
3731
+ );
3732
+ if (hasLegacyMarker) {
3733
+ return true;
3734
+ }
3735
+ if (Array.isArray(obj.autoSync)) {
3736
+ return true;
3737
+ }
3738
+ if (obj.syncRules && typeof obj.syncRules === "object") {
3739
+ const syncRules = obj.syncRules;
3740
+ if (Array.isArray(syncRules.include) || Array.isArray(syncRules.exclude)) {
3741
+ return true;
3742
+ }
3743
+ }
3744
+ return false;
3745
+ }
3746
+ function needsMigration(config) {
3747
+ const detection = detectVersion(config);
3748
+ return detection.version === "2.x";
3749
+ }
3750
+ function getMigrationRequirements(config) {
3751
+ const requirements = [];
3752
+ if (!config || typeof config !== "object") {
3753
+ requirements.push("Valid configuration object required");
3754
+ return requirements;
3755
+ }
3756
+ const obj = config;
3757
+ if (!obj.org && !obj.defaultOrg) {
3758
+ requirements.push("Organization name is required (org or defaultOrg)");
3759
+ }
3760
+ if (!obj.codexRepo && !obj.codexPath) {
3761
+ requirements.push("Codex repository name is required (codexRepo or codexPath)");
3762
+ }
3763
+ return requirements;
3764
+ }
3765
+
3766
+ // src/migration/migrator.ts
3767
+ var DEFAULT_MIGRATION_OPTIONS = {
3768
+ preserveUnknown: false,
3769
+ strict: false,
3770
+ defaultOrg: "organization",
3771
+ defaultCodexRepo: "codex"
3772
+ };
3773
+ function migrateConfig(config, options = {}) {
3774
+ const mergedOptions = { ...DEFAULT_MIGRATION_OPTIONS, ...options };
3775
+ const result = {
3776
+ success: false,
3777
+ warnings: [],
3778
+ errors: [],
3779
+ changes: []
3780
+ };
3781
+ const detection = detectVersion(config);
3782
+ if (detection.version === "3.0") {
3783
+ if (isModernConfig(config)) {
3784
+ result.success = true;
3785
+ result.config = config;
3786
+ result.warnings.push("Configuration is already v3.0, no migration needed");
3787
+ return result;
3788
+ }
3789
+ }
3790
+ if (detection.version === "unknown") {
3791
+ result.errors.push(`Cannot migrate: ${detection.reason}`);
3792
+ return result;
3793
+ }
3794
+ if (!isLegacyConfig(config)) {
3795
+ result.errors.push("Configuration does not match expected v2.x format");
3796
+ return result;
3797
+ }
3798
+ try {
3799
+ const migrated = performMigration(config, mergedOptions, result);
3800
+ result.config = migrated;
3801
+ result.success = true;
3802
+ } catch (error) {
3803
+ result.errors.push(
3804
+ `Migration failed: ${error instanceof Error ? error.message : String(error)}`
3805
+ );
3806
+ }
3807
+ return result;
3808
+ }
3809
+ function performMigration(legacy, options, result) {
3810
+ const orgName = legacy.org || legacy.defaultOrg || options.defaultOrg;
3811
+ if (legacy.org && legacy.defaultOrg && legacy.org !== legacy.defaultOrg) {
3812
+ result.warnings.push(
3813
+ `Both 'org' and 'defaultOrg' specified with different values. Using 'org': ${legacy.org}`
3814
+ );
3815
+ }
3816
+ if (!legacy.org && !legacy.defaultOrg) {
3817
+ result.changes.push({
3818
+ type: "added",
3819
+ path: "organization.name",
3820
+ newValue: orgName,
3821
+ description: `Added default organization name: ${orgName}`
3822
+ });
3823
+ } else {
3824
+ const fieldUsed = legacy.org ? "org" : "defaultOrg";
3825
+ result.changes.push({
3826
+ type: "renamed",
3827
+ path: "organization.name",
3828
+ oldValue: fieldUsed,
3829
+ newValue: "organization.name",
3830
+ description: `Renamed '${fieldUsed}' to 'organization.name'`
3831
+ });
3832
+ }
3833
+ const codexRepo = legacy.codexRepo || legacy.codexPath || options.defaultCodexRepo;
3834
+ if (legacy.codexRepo && legacy.codexPath && legacy.codexRepo !== legacy.codexPath) {
3835
+ result.warnings.push(
3836
+ `Both 'codexRepo' and 'codexPath' specified with different values. Using 'codexRepo': ${legacy.codexRepo}`
3837
+ );
3838
+ }
3839
+ if (!legacy.codexRepo && !legacy.codexPath) {
3840
+ result.changes.push({
3841
+ type: "added",
3842
+ path: "organization.codexRepo",
3843
+ newValue: codexRepo,
3844
+ description: `Added default codex repository: ${codexRepo}`
3845
+ });
3846
+ } else {
3847
+ const fieldUsed = legacy.codexRepo ? "codexRepo" : "codexPath";
3848
+ result.changes.push({
3849
+ type: "renamed",
3850
+ path: "organization.codexRepo",
3851
+ oldValue: fieldUsed,
3852
+ newValue: "organization.codexRepo",
3853
+ description: `Renamed '${fieldUsed}' to 'organization.codexRepo'`
3854
+ });
3855
+ }
3856
+ const patterns = [];
3857
+ if (legacy.autoSync && legacy.autoSync.length > 0) {
3858
+ for (const legacyPattern of legacy.autoSync) {
3859
+ const modernPattern = migrateSyncPattern(legacyPattern, result);
3860
+ patterns.push(modernPattern);
3861
+ }
3862
+ result.changes.push({
3863
+ type: "transformed",
3864
+ path: "sync.patterns",
3865
+ oldValue: "autoSync",
3866
+ newValue: "sync.patterns",
3867
+ description: `Converted ${legacy.autoSync.length} auto-sync pattern(s) to modern format`
3868
+ });
3869
+ }
3870
+ const include = [];
3871
+ const exclude = [];
3872
+ if (legacy.syncRules) {
3873
+ if (legacy.syncRules.include) {
3874
+ include.push(...legacy.syncRules.include);
3875
+ result.changes.push({
3876
+ type: "renamed",
3877
+ path: "sync.include",
3878
+ oldValue: "syncRules.include",
3879
+ newValue: "sync.include",
3880
+ description: "Moved syncRules.include to sync.include"
3881
+ });
3882
+ }
3883
+ if (legacy.syncRules.exclude) {
3884
+ exclude.push(...legacy.syncRules.exclude);
3885
+ result.changes.push({
3886
+ type: "renamed",
3887
+ path: "sync.exclude",
3888
+ oldValue: "syncRules.exclude",
3889
+ newValue: "sync.exclude",
3890
+ description: "Moved syncRules.exclude to sync.exclude"
3891
+ });
3892
+ }
3893
+ }
3894
+ const modern = {
3895
+ version: "3.0",
3896
+ organization: {
3897
+ name: orgName,
3898
+ codexRepo
3899
+ },
3900
+ sync: {
3901
+ patterns,
3902
+ include,
3903
+ exclude,
3904
+ defaultDirection: "to-codex"
3905
+ }
3906
+ };
3907
+ if (legacy.environments) {
3908
+ modern.environments = {};
3909
+ for (const [envName, envConfig] of Object.entries(legacy.environments)) {
3910
+ const envResult = {
3911
+ success: false,
3912
+ warnings: [],
3913
+ errors: [],
3914
+ changes: []
3915
+ };
3916
+ try {
3917
+ const migratedEnv = performMigration(
3918
+ { ...legacy, ...envConfig },
3919
+ { ...options },
3920
+ envResult
3921
+ );
3922
+ modern.environments[envName] = {
3923
+ organization: migratedEnv.organization,
3924
+ sync: migratedEnv.sync
3925
+ };
3926
+ result.changes.push({
3927
+ type: "transformed",
3928
+ path: `environments.${envName}`,
3929
+ description: `Migrated environment '${envName}'`
3930
+ });
3931
+ result.warnings.push(...envResult.warnings.map((w) => `[${envName}] ${w}`));
3932
+ } catch {
3933
+ result.warnings.push(`Failed to migrate environment '${envName}', skipping`);
3934
+ }
3935
+ }
3936
+ }
3937
+ result.changes.push({
3938
+ type: "added",
3939
+ path: "version",
3940
+ newValue: "3.0",
3941
+ description: "Added version field"
3942
+ });
3943
+ return modern;
3944
+ }
3945
+ function migrateSyncPattern(legacy, result) {
3946
+ const modern = {
3947
+ pattern: legacy.pattern
3948
+ };
3949
+ if (legacy.destination) {
3950
+ modern.target = legacy.destination;
3951
+ }
3952
+ if (legacy.bidirectional) {
3953
+ modern.direction = "bidirectional";
3954
+ }
3955
+ if (legacy.targets && legacy.targets.length > 0) {
3956
+ result.warnings.push(
3957
+ `Pattern '${legacy.pattern}' had multiple targets (${legacy.targets.join(", ")}). In v3.0, each target should be a separate pattern.`
3958
+ );
3959
+ if (!modern.target && legacy.targets[0]) {
3960
+ modern.target = legacy.targets[0];
3961
+ }
3962
+ }
3963
+ return modern;
3964
+ }
3965
+ function validateMigratedConfig(config) {
3966
+ const errors = [];
3967
+ if (config.version !== "3.0") {
3968
+ errors.push(`Invalid version: ${config.version}, expected '3.0'`);
3969
+ }
3970
+ if (!config.organization) {
3971
+ errors.push("Missing organization configuration");
3972
+ } else {
3973
+ if (!config.organization.name) {
3974
+ errors.push("Missing organization.name");
3975
+ }
3976
+ if (!config.organization.codexRepo) {
3977
+ errors.push("Missing organization.codexRepo");
3978
+ }
3979
+ }
3980
+ if (!config.sync) {
3981
+ errors.push("Missing sync configuration");
3982
+ } else {
3983
+ if (!Array.isArray(config.sync.patterns)) {
3984
+ errors.push("sync.patterns must be an array");
3985
+ }
3986
+ if (!Array.isArray(config.sync.include)) {
3987
+ errors.push("sync.include must be an array");
3988
+ }
3989
+ if (!Array.isArray(config.sync.exclude)) {
3990
+ errors.push("sync.exclude must be an array");
3991
+ }
3992
+ }
3993
+ return errors;
3994
+ }
3995
+ function generateMigrationReport(result) {
3996
+ const lines = ["# Migration Report", ""];
3997
+ if (result.success) {
3998
+ lines.push("Status: SUCCESS");
3999
+ } else {
4000
+ lines.push("Status: FAILED");
4001
+ }
4002
+ lines.push("");
4003
+ if (result.changes.length > 0) {
4004
+ lines.push("## Changes", "");
4005
+ for (const change of result.changes) {
4006
+ lines.push(`- [${change.type.toUpperCase()}] ${change.path}: ${change.description}`);
4007
+ }
4008
+ lines.push("");
4009
+ }
4010
+ if (result.warnings.length > 0) {
4011
+ lines.push("## Warnings", "");
4012
+ for (const warning of result.warnings) {
4013
+ lines.push(`- ${warning}`);
4014
+ }
4015
+ lines.push("");
4016
+ }
4017
+ if (result.errors.length > 0) {
4018
+ lines.push("## Errors", "");
4019
+ for (const error of result.errors) {
4020
+ lines.push(`- ${error}`);
4021
+ }
4022
+ lines.push("");
4023
+ }
4024
+ return lines.join("\n");
4025
+ }
4026
+ function createEmptyModernConfig(org, codexRepo) {
4027
+ return {
4028
+ version: "3.0",
4029
+ organization: {
4030
+ name: org,
4031
+ codexRepo
4032
+ },
4033
+ sync: {
4034
+ patterns: [],
4035
+ include: ["**/*.md", "CLAUDE.md"],
4036
+ exclude: ["**/node_modules/**", "**/.git/**"],
4037
+ defaultDirection: "to-codex"
4038
+ }
4039
+ };
4040
+ }
4041
+
4042
+ // src/migration/references.ts
4043
+ var LEGACY_PATTERNS = {
4044
+ /** @codex: style references */
4045
+ CODEX_TAG: /@codex:\s*([^\s\]]+)/g,
4046
+ /** [codex:...] style references */
4047
+ CODEX_BRACKET: /\[codex:\s*([^\]]+)\]/g,
4048
+ /** {{codex:...}} style references */
4049
+ CODEX_MUSTACHE: /\{\{codex:\s*([^}]+)\}\}/g,
4050
+ /** Simple path references in specific contexts */
4051
+ SIMPLE_PATH: /codex\/([a-zA-Z0-9_-]+\/[a-zA-Z0-9_\-/.]+)/g
4052
+ };
4053
+ function convertLegacyReferences(text, options = {}) {
4054
+ const result = {
4055
+ original: text,
4056
+ converted: text,
4057
+ references: [],
4058
+ modified: false
4059
+ };
4060
+ result.converted = processPattern(
4061
+ result.converted,
4062
+ LEGACY_PATTERNS.CODEX_TAG,
4063
+ "codex-tag",
4064
+ options,
4065
+ result.references
4066
+ );
4067
+ result.converted = processPattern(
4068
+ result.converted,
4069
+ LEGACY_PATTERNS.CODEX_BRACKET,
4070
+ "codex-bracket",
4071
+ options,
4072
+ result.references
4073
+ );
4074
+ result.converted = processPattern(
4075
+ result.converted,
4076
+ LEGACY_PATTERNS.CODEX_MUSTACHE,
4077
+ "codex-mustache",
4078
+ options,
4079
+ result.references
4080
+ );
4081
+ result.converted = processPattern(
4082
+ result.converted,
4083
+ LEGACY_PATTERNS.SIMPLE_PATH,
4084
+ "simple-path",
4085
+ options,
4086
+ result.references
4087
+ );
4088
+ result.modified = result.original !== result.converted;
4089
+ return result;
4090
+ }
4091
+ function processPattern(text, pattern, format, options, references) {
4092
+ pattern.lastIndex = 0;
4093
+ return text.replace(pattern, (match, captured, offset) => {
4094
+ const uri = convertToUri(captured.trim(), options);
4095
+ references.push({
4096
+ original: match,
4097
+ uri,
4098
+ position: offset,
4099
+ format
4100
+ });
4101
+ if (options.preserveWrapper) {
4102
+ switch (format) {
4103
+ case "codex-tag":
4104
+ return `@codex: ${uri}`;
4105
+ case "codex-bracket":
4106
+ return `[${uri}]`;
4107
+ case "codex-mustache":
4108
+ return `{{${uri}}}`;
4109
+ case "simple-path":
4110
+ return uri;
4111
+ }
4112
+ }
4113
+ return uri;
4114
+ });
4115
+ }
4116
+ function convertToUri(reference, options = {}) {
4117
+ const trimmed = reference.trim();
4118
+ if (trimmed.startsWith("codex://")) {
4119
+ return trimmed;
4120
+ }
4121
+ const parts = parseReference2(trimmed);
4122
+ const org = parts.org || options.defaultOrg || "_";
4123
+ const project = parts.project || options.defaultProject || "_";
4124
+ const path5 = parts.path;
4125
+ return `codex://${org}/${project}/${path5}`;
4126
+ }
4127
+ function parseReference2(reference) {
4128
+ const trimmed = reference.trim();
4129
+ if (trimmed.startsWith("./") || trimmed.startsWith("../")) {
4130
+ return { path: trimmed };
4131
+ }
4132
+ const parts = trimmed.split("/");
4133
+ if (parts.length >= 3) {
4134
+ const [org, project, ...rest] = parts;
4135
+ return {
4136
+ org: org || void 0,
4137
+ project: project || void 0,
4138
+ path: rest.join("/")
4139
+ };
4140
+ }
4141
+ if (parts.length === 2) {
4142
+ return { path: trimmed };
4143
+ }
4144
+ return { path: trimmed };
4145
+ }
4146
+ function findLegacyReferences(text) {
4147
+ const references = [];
4148
+ for (const [name, pattern] of Object.entries(LEGACY_PATTERNS)) {
4149
+ pattern.lastIndex = 0;
4150
+ let match;
4151
+ while ((match = pattern.exec(text)) !== null) {
4152
+ const format = name.toLowerCase().replace("_", "-");
4153
+ references.push({
4154
+ original: match[0],
4155
+ uri: match[1] || "",
4156
+ position: match.index,
4157
+ format
4158
+ });
4159
+ }
4160
+ }
4161
+ references.sort((a, b) => a.position - b.position);
4162
+ return references;
4163
+ }
4164
+ function hasLegacyReferences(text) {
4165
+ for (const pattern of Object.values(LEGACY_PATTERNS)) {
4166
+ pattern.lastIndex = 0;
4167
+ if (pattern.test(text)) {
4168
+ return true;
4169
+ }
4170
+ }
4171
+ return false;
4172
+ }
4173
+ function migrateFileReferences(content, options = {}) {
4174
+ return convertLegacyReferences(content, options);
4175
+ }
4176
+ function generateReferenceMigrationSummary(results) {
4177
+ const lines = ["# Reference Migration Summary", ""];
4178
+ let totalRefs = 0;
4179
+ let modifiedFiles = 0;
4180
+ for (const result of results) {
4181
+ if (result.modified) {
4182
+ modifiedFiles++;
4183
+ }
4184
+ totalRefs += result.references.length;
4185
+ }
4186
+ lines.push(`- Total files processed: ${results.length}`);
4187
+ lines.push(`- Files with changes: ${modifiedFiles}`);
4188
+ lines.push(`- Total references converted: ${totalRefs}`);
4189
+ lines.push("");
4190
+ const byFormat = /* @__PURE__ */ new Map();
4191
+ for (const result of results) {
4192
+ for (const ref of result.references) {
4193
+ byFormat.set(ref.format, (byFormat.get(ref.format) || 0) + 1);
4194
+ }
4195
+ }
4196
+ if (byFormat.size > 0) {
4197
+ lines.push("## References by Format", "");
4198
+ for (const [format, count] of byFormat.entries()) {
4199
+ lines.push(`- ${format}: ${count}`);
4200
+ }
4201
+ }
4202
+ return lines.join("\n");
4203
+ }
4204
+
4205
+ export { AutoSyncPatternSchema, BUILT_IN_TYPES, CODEX_TOOLS, CODEX_URI_PREFIX, CacheManager, CachePersistence, CodexConfigSchema, CodexError, CommonRules, ConfigurationError, CustomTypeSchema, DEFAULT_CACHE_DIR, DEFAULT_FETCH_OPTIONS, DEFAULT_MIGRATION_OPTIONS, DEFAULT_PERMISSION_CONFIG, DEFAULT_SYNC_CONFIG, DEFAULT_TYPE, GitHubStorage, HttpStorage, LEGACY_PATTERNS, LEGACY_REF_PREFIX, LocalStorage, McpServer, MetadataSchema, PERMISSION_LEVEL_ORDER, PermissionDeniedError, PermissionManager, StorageManager, SyncManager, SyncRulesSchema, TTL, TypeRegistry, TypesConfigSchema, ValidationError, buildUri, calculateCachePath, calculateContentHash, convertLegacyReference, convertLegacyReferences, convertToUri, createCacheEntry, createCacheManager, createCachePersistence, createDefaultRegistry, createEmptyModernConfig, createEmptySyncPlan, createGitHubStorage, createHttpStorage, createLocalStorage, createMcpServer, createPermissionManager, createRule, createRulesFromPatterns, createStorageManager, createSyncManager, createSyncPlan, deserializeCacheEntry, detectContentType, detectCurrentProject, detectVersion, estimateSyncTime, evaluatePath, evaluatePaths, evaluatePatterns, evaluatePermission, evaluatePermissions, extendType, extractOrgFromRepoName, extractRawFrontmatter, filterByPatterns, filterByPermission, filterPlanOperations, filterSyncablePaths, findLegacyReferences, formatPlanSummary, generateMigrationReport, generateReferenceMigrationSummary, getBuiltInType, getBuiltInTypeNames, getCacheEntryAge, getCacheEntryStatus, getCurrentContext, getCustomSyncDestinations, getDefaultCacheManager, getDefaultConfig, getDefaultDirectories, getDefaultPermissionManager, getDefaultRules, getDefaultStorageManager, getDirectory, getExtension, getFilename, getMigrationRequirements, getPlanStats, getRelativeCachePath, getRemainingTtl, getTargetRepos, handleFetch, handleInvalidate, handleList, handleSearch, handleToolCall, hasContentChanged, hasFrontmatter, hasLegacyReferences, hasPermission as hasPermissionLevel, isBuiltInType, isCacheEntryFresh, isCacheEntryValid, isCurrentProjectUri, isLegacyConfig, isLegacyReference, isModernConfig, isAllowed as isPermissionAllowed, isValidUri, levelGrants, loadConfig, loadCustomTypes, matchAnyPattern, matchPattern, maxLevel, mergeFetchOptions, mergeRules, mergeTypes, migrateConfig, migrateFileReferences, minLevel, needsMigration, parseCustomDestination, parseMetadata, parseReference, parseTtl, resolveOrganization, resolveReference, resolveReferences, ruleMatchesAction, ruleMatchesContext, ruleMatchesPath, sanitizePath, serializeCacheEntry, setDefaultCacheManager, setDefaultPermissionManager, setDefaultStorageManager, shouldSyncToRepo, summarizeEvaluations, touchCacheEntry, validateCustomTypes, validateMetadata, validateMigratedConfig, validateOrg, validatePath, validateRules2 as validatePermissionRules, validateProject, validateRules, validateUri };
387
4206
  //# sourceMappingURL=index.js.map
388
4207
  //# sourceMappingURL=index.js.map