@fractary/codex 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ console.log(ref.path) // 'docs/api-guide.md'
40
40
 
41
41
  // Create storage and cache managers
42
42
  const storage = StorageManager.create()
43
- const cache = CacheManager.create({ cacheDir: '.codex-cache' })
43
+ const cache = CacheManager.create({ cacheDir: '.fractary/codex/cache' })
44
44
 
45
45
  // Fetch content with caching
46
46
  const content = await cache.get('codex://myorg/docs/api-guide.md')
@@ -80,7 +80,7 @@ const result = await storage.fetch('codex://org/project/file.md')
80
80
  import { createCacheManager } from '@fractary/codex'
81
81
 
82
82
  const cache = createCacheManager({
83
- cacheDir: '.codex-cache',
83
+ cacheDir: '.fractary/codex/cache',
84
84
  maxMemorySize: 50 * 1024 * 1024,
85
85
  defaultTtl: 3600
86
86
  })
@@ -96,7 +96,7 @@ import { createMcpServer } from '@fractary/codex'
96
96
  const server = createMcpServer({
97
97
  name: 'codex',
98
98
  version: '1.0.0',
99
- cacheDir: '.codex-cache'
99
+ cacheDir: '.fractary/codex/cache'
100
100
  })
101
101
 
102
102
  await server.start()
@@ -226,12 +226,9 @@ MIT - see [LICENSE](LICENSE)
226
226
 
227
227
  ## Documentation
228
228
 
229
- - [Command Reference](../../docs/guides/command-reference.md) - Complete command reference for all interfaces
230
- - [API Reference](../../docs/guides/api-reference.md) - Complete API documentation
231
229
  - [CLI Integration Guide](../../docs/guides/cli-integration.md) - How to integrate into CLI applications
230
+ - [API Reference](../../docs/guides/api-reference.md) - Complete API documentation
232
231
  - [Configuration Guide](../../docs/guides/configuration.md) - Configuration reference
233
- - [Naming Conventions](../../docs/guides/naming-conventions.md) - Naming standards across all interfaces
234
- - [MCP Migration Guide](../../docs/guides/mcp-migration-guide.md) - Migrating to new MCP tool names
235
232
  - [Troubleshooting](../../docs/guides/troubleshooting.md) - Common issues and solutions
236
233
  - [Examples](../../docs/examples/) - Real-world usage patterns
237
234
 
package/dist/index.cjs CHANGED
@@ -185,10 +185,10 @@ function parseReference(uri, options = {}) {
185
185
  path: filePath
186
186
  };
187
187
  }
188
- function buildUri(org, project, path5) {
188
+ function buildUri(org, project, path6) {
189
189
  const base = `${CODEX_URI_PREFIX}${org}/${project}`;
190
- if (path5) {
191
- const cleanPath = path5.startsWith("/") ? path5.slice(1) : path5;
190
+ if (path6) {
191
+ const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
192
192
  return `${base}/${cleanPath}`;
193
193
  }
194
194
  return base;
@@ -226,7 +226,7 @@ function getDirectory(uri) {
226
226
  }
227
227
  return parsed.path.slice(0, lastSlash);
228
228
  }
229
- var DEFAULT_CACHE_DIR = ".fractary/plugins/codex/cache";
229
+ var DEFAULT_CACHE_DIR = ".fractary/codex/cache";
230
230
  function detectCurrentProject(cwd) {
231
231
  try {
232
232
  const options = cwd ? { cwd, encoding: "utf-8" } : { encoding: "utf-8" };
@@ -1066,18 +1066,18 @@ function parseCustomDestination(value) {
1066
1066
  );
1067
1067
  }
1068
1068
  const repo = value.substring(0, colonIndex).trim();
1069
- const path5 = value.substring(colonIndex + 1).trim();
1069
+ const path6 = value.substring(colonIndex + 1).trim();
1070
1070
  if (!repo) {
1071
1071
  throw new ValidationError(
1072
1072
  `Invalid custom destination: repository name cannot be empty in "${value}"`
1073
1073
  );
1074
1074
  }
1075
- if (!path5) {
1075
+ if (!path6) {
1076
1076
  throw new ValidationError(
1077
1077
  `Invalid custom destination: path cannot be empty in "${value}"`
1078
1078
  );
1079
1079
  }
1080
- return { repo, path: path5 };
1080
+ return { repo, path: path6 };
1081
1081
  }
1082
1082
  function getCustomSyncDestinations(metadata) {
1083
1083
  const customDestinations = metadata.codex_sync_custom;
@@ -1113,8 +1113,8 @@ function mergeFetchOptions(options) {
1113
1113
  ...options
1114
1114
  };
1115
1115
  }
1116
- function detectContentType(path5) {
1117
- const ext = path5.split(".").pop()?.toLowerCase();
1116
+ function detectContentType(path6) {
1117
+ const ext = path6.split(".").pop()?.toLowerCase();
1118
1118
  const mimeTypes = {
1119
1119
  md: "text/markdown",
1120
1120
  markdown: "text/markdown",
@@ -2437,11 +2437,11 @@ var DEFAULT_SYNC_CONFIG = {
2437
2437
  deleteOrphans: false,
2438
2438
  conflictStrategy: "newest"
2439
2439
  };
2440
- function evaluatePath(path5, rules, direction, defaultExcludes = []) {
2440
+ function evaluatePath(path6, rules, direction, defaultExcludes = []) {
2441
2441
  for (const pattern of defaultExcludes) {
2442
- if (micromatch2__default.default.isMatch(path5, pattern)) {
2442
+ if (micromatch2__default.default.isMatch(path6, pattern)) {
2443
2443
  return {
2444
- path: path5,
2444
+ path: path6,
2445
2445
  shouldSync: false,
2446
2446
  reason: `Excluded by default pattern: ${pattern}`
2447
2447
  };
@@ -2452,9 +2452,9 @@ function evaluatePath(path5, rules, direction, defaultExcludes = []) {
2452
2452
  if (rule.direction && rule.direction !== direction) {
2453
2453
  continue;
2454
2454
  }
2455
- if (micromatch2__default.default.isMatch(path5, rule.pattern)) {
2455
+ if (micromatch2__default.default.isMatch(path6, rule.pattern)) {
2456
2456
  return {
2457
- path: path5,
2457
+ path: path6,
2458
2458
  shouldSync: rule.include,
2459
2459
  matchedRule: rule,
2460
2460
  reason: rule.include ? `Included by rule: ${rule.pattern}` : `Excluded by rule: ${rule.pattern}`
@@ -2462,21 +2462,21 @@ function evaluatePath(path5, rules, direction, defaultExcludes = []) {
2462
2462
  }
2463
2463
  }
2464
2464
  return {
2465
- path: path5,
2465
+ path: path6,
2466
2466
  shouldSync: true,
2467
2467
  reason: "No matching rule, included by default"
2468
2468
  };
2469
2469
  }
2470
2470
  function evaluatePaths(paths, rules, direction, defaultExcludes = []) {
2471
2471
  const results = /* @__PURE__ */ new Map();
2472
- for (const path5 of paths) {
2473
- results.set(path5, evaluatePath(path5, rules, direction, defaultExcludes));
2472
+ for (const path6 of paths) {
2473
+ results.set(path6, evaluatePath(path6, rules, direction, defaultExcludes));
2474
2474
  }
2475
2475
  return results;
2476
2476
  }
2477
2477
  function filterSyncablePaths(paths, rules, direction, defaultExcludes = []) {
2478
2478
  return paths.filter(
2479
- (path5) => evaluatePath(path5, rules, direction, defaultExcludes).shouldSync
2479
+ (path6) => evaluatePath(path6, rules, direction, defaultExcludes).shouldSync
2480
2480
  );
2481
2481
  }
2482
2482
  function createRulesFromPatterns(include = [], exclude = []) {
@@ -2713,6 +2713,123 @@ function formatBytes(bytes) {
2713
2713
  const i = Math.floor(Math.log(bytes) / Math.log(k));
2714
2714
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
2715
2715
  }
2716
+ async function scanCodexWithRouting(options) {
2717
+ const {
2718
+ codexDir,
2719
+ targetProject,
2720
+ org,
2721
+ rules,
2722
+ storage,
2723
+ skipNoFrontmatter = true,
2724
+ maxFileSize = 10 * 1024 * 1024
2725
+ // 10MB default
2726
+ } = options;
2727
+ const startTime = Date.now();
2728
+ const routedFiles = [];
2729
+ const sourceProjectsSet = /* @__PURE__ */ new Set();
2730
+ const errors = [];
2731
+ let totalScanned = 0;
2732
+ let totalSkipped = 0;
2733
+ const allFiles = await listAllFilesRecursive(codexDir);
2734
+ for (const filePath of allFiles) {
2735
+ totalScanned++;
2736
+ try {
2737
+ if (!filePath.endsWith(".md")) {
2738
+ totalSkipped++;
2739
+ continue;
2740
+ }
2741
+ const fullPath = path3__default.default.join(codexDir, filePath);
2742
+ const stats = await fs2__default.default.stat(fullPath);
2743
+ if (stats.size > maxFileSize) {
2744
+ totalSkipped++;
2745
+ errors.push({
2746
+ path: filePath,
2747
+ error: `File too large (${stats.size} bytes, max: ${maxFileSize})`
2748
+ });
2749
+ continue;
2750
+ }
2751
+ const content = await storage.readText(fullPath);
2752
+ const parseResult = parseMetadata(content, { strict: false });
2753
+ if (skipNoFrontmatter && Object.keys(parseResult.metadata).length === 0) {
2754
+ totalSkipped++;
2755
+ continue;
2756
+ }
2757
+ const sourceProject = extractProjectFromPath(filePath, org);
2758
+ const shouldSync = shouldSyncToRepo({
2759
+ filePath,
2760
+ fileMetadata: parseResult.metadata,
2761
+ targetRepo: targetProject,
2762
+ sourceRepo: sourceProject,
2763
+ rules
2764
+ });
2765
+ if (shouldSync) {
2766
+ const buffer = Buffer.from(content);
2767
+ const hash = calculateContentHash(buffer);
2768
+ routedFiles.push({
2769
+ path: filePath,
2770
+ size: buffer.length,
2771
+ mtime: stats.mtimeMs,
2772
+ hash,
2773
+ metadata: parseResult.metadata,
2774
+ sourceProject
2775
+ });
2776
+ sourceProjectsSet.add(sourceProject);
2777
+ } else {
2778
+ totalSkipped++;
2779
+ }
2780
+ } catch (error) {
2781
+ totalSkipped++;
2782
+ errors.push({
2783
+ path: filePath,
2784
+ error: error instanceof Error ? error.message : String(error)
2785
+ });
2786
+ }
2787
+ }
2788
+ const durationMs = Date.now() - startTime;
2789
+ return {
2790
+ files: routedFiles,
2791
+ stats: {
2792
+ totalScanned,
2793
+ totalMatched: routedFiles.length,
2794
+ totalSkipped,
2795
+ sourceProjects: Array.from(sourceProjectsSet).sort(),
2796
+ durationMs,
2797
+ errors
2798
+ }
2799
+ };
2800
+ }
2801
+ function extractProjectFromPath(filePath, org) {
2802
+ const normalizedPath = filePath.replace(/\\/g, "/");
2803
+ const withoutOrg = normalizedPath.startsWith(`${org}/`) ? normalizedPath.slice(org.length + 1) : normalizedPath;
2804
+ const firstSlash = withoutOrg.indexOf("/");
2805
+ if (firstSlash === -1) {
2806
+ return withoutOrg;
2807
+ }
2808
+ return withoutOrg.slice(0, firstSlash);
2809
+ }
2810
+ async function listAllFilesRecursive(dirPath) {
2811
+ const files = [];
2812
+ async function scanDirectory(currentPath, relativePath = "") {
2813
+ try {
2814
+ const entries = await fs2__default.default.readdir(currentPath, { withFileTypes: true });
2815
+ for (const entry of entries) {
2816
+ const entryPath = path3__default.default.join(currentPath, entry.name);
2817
+ const entryRelativePath = relativePath ? path3__default.default.join(relativePath, entry.name) : entry.name;
2818
+ if (entry.isDirectory()) {
2819
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
2820
+ continue;
2821
+ }
2822
+ await scanDirectory(entryPath, entryRelativePath);
2823
+ } else if (entry.isFile()) {
2824
+ files.push(entryRelativePath);
2825
+ }
2826
+ }
2827
+ } catch {
2828
+ }
2829
+ }
2830
+ await scanDirectory(dirPath);
2831
+ return files;
2832
+ }
2716
2833
 
2717
2834
  // src/sync/manager.ts
2718
2835
  var SyncManager = class {
@@ -2805,6 +2922,56 @@ var SyncManager = class {
2805
2922
  plan.estimatedTime = estimateSyncTime(plan);
2806
2923
  return plan;
2807
2924
  }
2925
+ /**
2926
+ * Create a routing-aware sync plan
2927
+ *
2928
+ * Scans entire codex repository and evaluates routing rules to find all files
2929
+ * that should sync to the target project based on codex_sync_include patterns.
2930
+ *
2931
+ * This enables cross-project knowledge sharing where files from multiple
2932
+ * projects can be synced to the target based on their frontmatter metadata.
2933
+ *
2934
+ * @param org - Organization name (e.g., "corthosai")
2935
+ * @param project - Target project name (e.g., "lake.corthonomy.ai")
2936
+ * @param codexDir - Path to codex repository directory
2937
+ * @param options - Sync options
2938
+ * @returns Sync plan with routing metadata
2939
+ *
2940
+ * @example
2941
+ * ```typescript
2942
+ * const plan = await manager.createRoutingAwarePlan(
2943
+ * 'corthosai',
2944
+ * 'lake.corthonomy.ai',
2945
+ * '/path/to/codex.corthos.ai',
2946
+ * { direction: 'from-codex' }
2947
+ * )
2948
+ *
2949
+ * console.log(`Found ${plan.totalFiles} files from ${plan.metadata.scannedProjects.length} projects`)
2950
+ * ```
2951
+ */
2952
+ async createRoutingAwarePlan(org, project, codexDir, options) {
2953
+ const routingScan = await scanCodexWithRouting({
2954
+ codexDir,
2955
+ targetProject: project,
2956
+ org,
2957
+ rules: void 0,
2958
+ // Use default routing rules (preventSelfSync, preventCodexSync, etc.)
2959
+ storage: this.localStorage
2960
+ });
2961
+ const sourceFiles = routingScan.files.map((rf) => ({
2962
+ path: rf.path,
2963
+ size: rf.size,
2964
+ mtime: rf.mtime,
2965
+ hash: rf.hash
2966
+ }));
2967
+ const targetFiles = await this.listLocalFiles(process.cwd());
2968
+ const plan = createSyncPlan(sourceFiles, targetFiles, options ?? {}, this.config);
2969
+ plan.estimatedTime = estimateSyncTime(plan);
2970
+ return {
2971
+ ...plan,
2972
+ routingScan
2973
+ };
2974
+ }
2808
2975
  /**
2809
2976
  * Execute a sync plan (dry run)
2810
2977
  *
@@ -2880,15 +3047,15 @@ var SyncManager = class {
2880
3047
  /**
2881
3048
  * Get sync status for a file
2882
3049
  */
2883
- async getFileStatus(path5) {
3050
+ async getFileStatus(path6) {
2884
3051
  const manifest = await this.loadManifest();
2885
- return manifest?.entries[path5] ?? null;
3052
+ return manifest?.entries[path6] ?? null;
2886
3053
  }
2887
3054
  /**
2888
3055
  * Check if a file is synced
2889
3056
  */
2890
- async isFileSynced(path5) {
2891
- const status = await this.getFileStatus(path5);
3057
+ async isFileSynced(path6) {
3058
+ const status = await this.getFileStatus(path6);
2892
3059
  return status !== null;
2893
3060
  }
2894
3061
  /**
@@ -2972,19 +3139,19 @@ function ruleMatchesContext(rule, context) {
2972
3139
  }
2973
3140
  return true;
2974
3141
  }
2975
- function ruleMatchesPath(rule, path5) {
2976
- return micromatch2__default.default.isMatch(path5, rule.pattern);
3142
+ function ruleMatchesPath(rule, path6) {
3143
+ return micromatch2__default.default.isMatch(path6, rule.pattern);
2977
3144
  }
2978
3145
  function ruleMatchesAction(rule, action) {
2979
3146
  return rule.actions.includes(action);
2980
3147
  }
2981
- function evaluatePermission(path5, action, context, config) {
3148
+ function evaluatePermission(path6, action, context, config) {
2982
3149
  const sortedRules = [...config.rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
2983
3150
  for (const rule of sortedRules) {
2984
3151
  if (!ruleMatchesContext(rule, context)) {
2985
3152
  continue;
2986
3153
  }
2987
- if (!ruleMatchesPath(rule, path5)) {
3154
+ if (!ruleMatchesPath(rule, path6)) {
2988
3155
  continue;
2989
3156
  }
2990
3157
  if (!ruleMatchesAction(rule, action)) {
@@ -3003,24 +3170,24 @@ function evaluatePermission(path5, action, context, config) {
3003
3170
  reason: config.defaultAllow ? "Allowed by default" : "Denied by default"
3004
3171
  };
3005
3172
  }
3006
- function isAllowed(path5, action, context, config) {
3007
- const result = evaluatePermission(path5, action, context, config);
3173
+ function isAllowed(path6, action, context, config) {
3174
+ const result = evaluatePermission(path6, action, context, config);
3008
3175
  return result.allowed;
3009
3176
  }
3010
- function hasPermission(path5, action, requiredLevel, context, config) {
3011
- const result = evaluatePermission(path5, action, context, config);
3177
+ function hasPermission(path6, action, requiredLevel, context, config) {
3178
+ const result = evaluatePermission(path6, action, context, config);
3012
3179
  return levelGrants(result.level, requiredLevel);
3013
3180
  }
3014
3181
  function evaluatePermissions(paths, action, context, config) {
3015
3182
  const results = /* @__PURE__ */ new Map();
3016
- for (const path5 of paths) {
3017
- results.set(path5, evaluatePermission(path5, action, context, config));
3183
+ for (const path6 of paths) {
3184
+ results.set(path6, evaluatePermission(path6, action, context, config));
3018
3185
  }
3019
3186
  return results;
3020
3187
  }
3021
3188
  function filterByPermission(paths, action, context, config, requiredLevel = "read") {
3022
- return paths.filter((path5) => {
3023
- const result = evaluatePermission(path5, action, context, config);
3189
+ return paths.filter((path6) => {
3190
+ const result = evaluatePermission(path6, action, context, config);
3024
3191
  return levelGrants(result.level, requiredLevel);
3025
3192
  });
3026
3193
  }
@@ -3111,21 +3278,21 @@ var PermissionManager = class {
3111
3278
  /**
3112
3279
  * Check if an action is allowed for a path
3113
3280
  */
3114
- isAllowed(path5, action, context) {
3115
- const result = this.evaluate(path5, action, context);
3281
+ isAllowed(path6, action, context) {
3282
+ const result = this.evaluate(path6, action, context);
3116
3283
  return result.allowed;
3117
3284
  }
3118
3285
  /**
3119
3286
  * Check if a permission level is granted
3120
3287
  */
3121
- hasPermission(path5, action, requiredLevel, context) {
3122
- const result = this.evaluate(path5, action, context);
3288
+ hasPermission(path6, action, requiredLevel, context) {
3289
+ const result = this.evaluate(path6, action, context);
3123
3290
  return levelGrants(result.level, requiredLevel);
3124
3291
  }
3125
3292
  /**
3126
3293
  * Evaluate permission for a path and action
3127
3294
  */
3128
- evaluate(path5, action, context) {
3295
+ evaluate(path6, action, context) {
3129
3296
  const mergedContext = { ...this.defaultContext, ...context };
3130
3297
  if (!this.config.enforced) {
3131
3298
  return {
@@ -3134,7 +3301,7 @@ var PermissionManager = class {
3134
3301
  reason: "Permissions not enforced"
3135
3302
  };
3136
3303
  }
3137
- return evaluatePermission(path5, action, mergedContext, this.config);
3304
+ return evaluatePermission(path6, action, mergedContext, this.config);
3138
3305
  }
3139
3306
  /**
3140
3307
  * Filter paths by permission
@@ -3237,12 +3404,12 @@ var PermissionManager = class {
3237
3404
  /**
3238
3405
  * Assert permission (throws if denied)
3239
3406
  */
3240
- assertPermission(path5, action, requiredLevel = "read", context) {
3241
- const result = this.evaluate(path5, action, context);
3407
+ assertPermission(path6, action, requiredLevel = "read", context) {
3408
+ const result = this.evaluate(path6, action, context);
3242
3409
  if (!levelGrants(result.level, requiredLevel)) {
3243
3410
  throw new PermissionDeniedError(
3244
- `Permission denied for ${action} on ${path5}: ${result.reason}`,
3245
- path5,
3411
+ `Permission denied for ${action} on ${path6}: ${result.reason}`,
3412
+ path6,
3246
3413
  action,
3247
3414
  result
3248
3415
  );
@@ -3250,9 +3417,9 @@ var PermissionManager = class {
3250
3417
  }
3251
3418
  };
3252
3419
  var PermissionDeniedError = class extends Error {
3253
- constructor(message, path5, action, result) {
3420
+ constructor(message, path6, action, result) {
3254
3421
  super(message);
3255
- this.path = path5;
3422
+ this.path = path6;
3256
3423
  this.action = action;
3257
3424
  this.result = result;
3258
3425
  this.name = "PermissionDeniedError";
@@ -3749,8 +3916,8 @@ function convertToUri(reference, options = {}) {
3749
3916
  const parts = parseReference2(trimmed);
3750
3917
  const org = parts.org || options.defaultOrg || "_";
3751
3918
  const project = parts.project || options.defaultProject || "_";
3752
- const path5 = parts.path;
3753
- return `codex://${org}/${project}/${path5}`;
3919
+ const path6 = parts.path;
3920
+ return `codex://${org}/${project}/${path6}`;
3754
3921
  }
3755
3922
  function parseReference2(reference) {
3756
3923
  const trimmed = reference.trim();