@fractary/codex 0.7.1 → 0.9.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/dist/index.cjs CHANGED
@@ -5,14 +5,33 @@ var path3 = require('path');
5
5
  var child_process = require('child_process');
6
6
  var zod = require('zod');
7
7
  var yaml = require('js-yaml');
8
- var fs2 = require('fs/promises');
8
+ var fs3 = require('fs/promises');
9
+ var util = require('util');
9
10
 
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
12
31
  var micromatch3__default = /*#__PURE__*/_interopDefault(micromatch3);
13
- var path3__default = /*#__PURE__*/_interopDefault(path3);
32
+ var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
14
33
  var yaml__default = /*#__PURE__*/_interopDefault(yaml);
15
- var fs2__default = /*#__PURE__*/_interopDefault(fs2);
34
+ var fs3__namespace = /*#__PURE__*/_interopNamespace(fs3);
16
35
 
17
36
  var __defProp = Object.defineProperty;
18
37
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -169,7 +188,7 @@ function validatePath(filePath) {
169
188
  return false;
170
189
  }
171
190
  }
172
- const normalized = path3__default.default.normalize(filePath);
191
+ const normalized = path3__namespace.default.normalize(filePath);
173
192
  if (normalized.startsWith("..") || normalized.includes("/../") || normalized.includes("\\..\\")) {
174
193
  return false;
175
194
  }
@@ -187,7 +206,7 @@ function sanitizePath(filePath) {
187
206
  }
188
207
  sanitized = sanitized.replace(/^\/+/, "");
189
208
  sanitized = sanitized.replace(/^~\//, "");
190
- sanitized = path3__default.default.normalize(sanitized);
209
+ sanitized = path3__namespace.default.normalize(sanitized);
191
210
  sanitized = sanitized.replace(/\.\.\//g, "").replace(/\.\./g, "");
192
211
  sanitized = sanitized.replace(/^\.\//, "");
193
212
  sanitized = sanitized.replace(/\0/g, "");
@@ -288,10 +307,10 @@ function parseReference(uri, options = {}) {
288
307
  path: filePath
289
308
  };
290
309
  }
291
- function buildUri(org, project, path6) {
310
+ function buildUri(org, project, path7) {
292
311
  const base = `${CODEX_URI_PREFIX}${org}/${project}`;
293
- if (path6) {
294
- const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
312
+ if (path7) {
313
+ const cleanPath = path7.startsWith("/") ? path7.slice(1) : path7;
295
314
  return `${base}/${cleanPath}`;
296
315
  }
297
316
  return base;
@@ -362,7 +381,7 @@ function getCurrentContext(options = {}) {
362
381
  return detectCurrentProject(options.cwd);
363
382
  }
364
383
  function calculateCachePath(org, project, filePath, cacheDir) {
365
- return path3__default.default.join(cacheDir, org, project, filePath);
384
+ return path3__namespace.default.join(cacheDir, org, project, filePath);
366
385
  }
367
386
  function resolveReference(uri, options = {}) {
368
387
  const parsed = parseReference(uri);
@@ -371,7 +390,7 @@ function resolveReference(uri, options = {}) {
371
390
  }
372
391
  const cacheDir = options.cacheDir || DEFAULT_CACHE_DIR;
373
392
  const currentContext = getCurrentContext(options);
374
- const cachePath = parsed.path ? calculateCachePath(parsed.org, parsed.project, parsed.path, cacheDir) : path3__default.default.join(cacheDir, parsed.org, parsed.project);
393
+ const cachePath = parsed.path ? calculateCachePath(parsed.org, parsed.project, parsed.path, cacheDir) : path3__namespace.default.join(cacheDir, parsed.org, parsed.project);
375
394
  const isCurrentProject = currentContext.org === parsed.org && currentContext.project === parsed.project;
376
395
  const resolved = {
377
396
  ...parsed,
@@ -380,9 +399,45 @@ function resolveReference(uri, options = {}) {
380
399
  };
381
400
  if (isCurrentProject && parsed.path) {
382
401
  resolved.localPath = parsed.path;
402
+ if (options.config) {
403
+ const fileSource = detectFilePluginSource(parsed.path, options.config);
404
+ if (fileSource) {
405
+ resolved.sourceType = "file-plugin";
406
+ resolved.filePluginSource = fileSource.name;
407
+ resolved.localPath = fileSource.fullPath;
408
+ }
409
+ }
383
410
  }
384
411
  return resolved;
385
412
  }
413
+ function detectFilePluginSource(filePath, config) {
414
+ if (!config?.file?.sources) {
415
+ return null;
416
+ }
417
+ for (const [sourceName, source] of Object.entries(config.file.sources)) {
418
+ const basePath = source.local?.base_path;
419
+ if (!basePath) {
420
+ continue;
421
+ }
422
+ const normalizedPath = filePath.replace(/^\.\//, "").replace(/^\//, "");
423
+ const normalizedBasePath = basePath.replace(/^\.\//, "").replace(/^\//, "").replace(/\/$/, "");
424
+ if (normalizedPath.startsWith(normalizedBasePath)) {
425
+ return {
426
+ name: sourceName,
427
+ fullPath: path3__namespace.default.join(basePath, normalizedPath.substring(normalizedBasePath.length).replace(/^\//, ""))
428
+ };
429
+ }
430
+ const sourceNameInPath = normalizedBasePath.split("/").pop();
431
+ if (sourceNameInPath && normalizedPath.startsWith(sourceNameInPath + "/")) {
432
+ const pathWithoutSource = normalizedPath.substring(sourceNameInPath.length + 1);
433
+ return {
434
+ name: sourceName,
435
+ fullPath: path3__namespace.default.join(basePath, pathWithoutSource)
436
+ };
437
+ }
438
+ }
439
+ return null;
440
+ }
386
441
  function resolveReferences(uris, options = {}) {
387
442
  return uris.map((uri) => resolveReference(uri, options)).filter((r) => r !== null);
388
443
  }
@@ -396,9 +451,9 @@ function getRelativeCachePath(uri) {
396
451
  return null;
397
452
  }
398
453
  if (parsed.path) {
399
- return path3__default.default.join(parsed.org, parsed.project, parsed.path);
454
+ return path3__namespace.default.join(parsed.org, parsed.project, parsed.path);
400
455
  }
401
- return path3__default.default.join(parsed.org, parsed.project);
456
+ return path3__namespace.default.join(parsed.org, parsed.project);
402
457
  }
403
458
 
404
459
  // src/types/built-in.ts
@@ -843,6 +898,16 @@ var DirectionalSyncSchema = zod.z.object({
843
898
  default_to_codex: zod.z.array(zod.z.string()).optional(),
844
899
  default_from_codex: zod.z.array(zod.z.string()).optional()
845
900
  });
901
+ var ArchiveProjectConfigSchema = zod.z.object({
902
+ enabled: zod.z.boolean(),
903
+ handler: zod.z.enum(["s3", "r2", "gcs", "local"]),
904
+ bucket: zod.z.string().optional(),
905
+ prefix: zod.z.string().optional(),
906
+ patterns: zod.z.array(zod.z.string()).optional()
907
+ });
908
+ var ArchiveConfigSchema = zod.z.object({
909
+ projects: zod.z.record(ArchiveProjectConfigSchema)
910
+ });
846
911
  var CodexConfigSchema = zod.z.object({
847
912
  organizationSlug: zod.z.string(),
848
913
  directories: zod.z.object({
@@ -852,8 +917,45 @@ var CodexConfigSchema = zod.z.object({
852
917
  }).optional(),
853
918
  rules: SyncRulesSchema.optional(),
854
919
  // Directional sync configuration
855
- sync: DirectionalSyncSchema.optional()
920
+ sync: DirectionalSyncSchema.optional(),
921
+ // Archive configuration
922
+ archive: ArchiveConfigSchema.optional()
856
923
  }).strict();
924
+ var FileSourceSchema = zod.z.object({
925
+ type: zod.z.enum(["s3", "r2", "gcs", "local"]),
926
+ bucket: zod.z.string().optional(),
927
+ prefix: zod.z.string().optional(),
928
+ region: zod.z.string().optional(),
929
+ local: zod.z.object({
930
+ base_path: zod.z.string()
931
+ }),
932
+ push: zod.z.object({
933
+ compress: zod.z.boolean().optional(),
934
+ keep_local: zod.z.boolean().optional()
935
+ }).optional(),
936
+ auth: zod.z.object({
937
+ profile: zod.z.string().optional()
938
+ }).optional()
939
+ }).refine(
940
+ (data) => {
941
+ if (data.type !== "local" && !data.bucket) {
942
+ return false;
943
+ }
944
+ return true;
945
+ },
946
+ {
947
+ message: "Bucket is required for s3, r2, and gcs storage types",
948
+ path: ["bucket"]
949
+ }
950
+ );
951
+ var FileConfigSchema = zod.z.object({
952
+ schema_version: zod.z.string(),
953
+ sources: zod.z.record(FileSourceSchema)
954
+ });
955
+ zod.z.object({
956
+ file: FileConfigSchema.optional(),
957
+ codex: CodexConfigSchema.optional()
958
+ });
857
959
  function parseMetadata(content, options = {}) {
858
960
  const { strict = true, normalize = true } = options;
859
961
  const normalizedContent = normalize ? content.replace(/\r\n/g, "\n") : content;
@@ -977,11 +1079,17 @@ function getDefaultRules() {
977
1079
  defaultExclude: []
978
1080
  };
979
1081
  }
1082
+ function getDefaultArchiveConfig() {
1083
+ return {
1084
+ projects: {}
1085
+ };
1086
+ }
980
1087
  function getDefaultConfig(orgSlug) {
981
1088
  return {
982
1089
  organizationSlug: orgSlug,
983
1090
  directories: getDefaultDirectories(orgSlug),
984
- rules: getDefaultRules()
1091
+ rules: getDefaultRules(),
1092
+ archive: getDefaultArchiveConfig()
985
1093
  };
986
1094
  }
987
1095
 
@@ -1177,18 +1285,18 @@ function parseCustomDestination(value) {
1177
1285
  );
1178
1286
  }
1179
1287
  const repo = value.substring(0, colonIndex).trim();
1180
- const path6 = value.substring(colonIndex + 1).trim();
1288
+ const path7 = value.substring(colonIndex + 1).trim();
1181
1289
  if (!repo) {
1182
1290
  throw new ValidationError(
1183
1291
  `Invalid custom destination: repository name cannot be empty in "${value}"`
1184
1292
  );
1185
1293
  }
1186
- if (!path6) {
1294
+ if (!path7) {
1187
1295
  throw new ValidationError(
1188
1296
  `Invalid custom destination: path cannot be empty in "${value}"`
1189
1297
  );
1190
1298
  }
1191
- return { repo, path: path6 };
1299
+ return { repo, path: path7 };
1192
1300
  }
1193
1301
  function getCustomSyncDestinations(metadata) {
1194
1302
  const customDestinations = metadata.codex_sync_custom;
@@ -1224,8 +1332,8 @@ function mergeFetchOptions(options) {
1224
1332
  ...options
1225
1333
  };
1226
1334
  }
1227
- function detectContentType(path6) {
1228
- const ext = path6.split(".").pop()?.toLowerCase();
1335
+ function detectContentType(path7) {
1336
+ const ext = path7.split(".").pop()?.toLowerCase();
1229
1337
  const mimeTypes = {
1230
1338
  md: "text/markdown",
1231
1339
  markdown: "text/markdown",
@@ -1285,19 +1393,19 @@ var LocalStorage = class {
1285
1393
  if (!reference.localPath) {
1286
1394
  throw new Error(`No local path for reference: ${reference.uri}`);
1287
1395
  }
1288
- const fullPath = path3__default.default.isAbsolute(reference.localPath) ? reference.localPath : path3__default.default.join(this.baseDir, reference.localPath);
1396
+ const fullPath = path3__namespace.default.isAbsolute(reference.localPath) ? reference.localPath : path3__namespace.default.join(this.baseDir, reference.localPath);
1289
1397
  try {
1290
- await fs2__default.default.access(fullPath);
1398
+ await fs3__namespace.default.access(fullPath);
1291
1399
  } catch {
1292
1400
  throw new Error(`File not found: ${fullPath}`);
1293
1401
  }
1294
- const stats = await fs2__default.default.stat(fullPath);
1402
+ const stats = await fs3__namespace.default.stat(fullPath);
1295
1403
  if (stats.size > opts.maxSize) {
1296
1404
  throw new Error(
1297
1405
  `File too large: ${stats.size} bytes (max: ${opts.maxSize} bytes)`
1298
1406
  );
1299
1407
  }
1300
- const content = await fs2__default.default.readFile(fullPath);
1408
+ const content = await fs3__namespace.default.readFile(fullPath);
1301
1409
  return {
1302
1410
  content,
1303
1411
  contentType: detectContentType(reference.localPath),
@@ -1316,9 +1424,9 @@ var LocalStorage = class {
1316
1424
  if (!reference.localPath) {
1317
1425
  return false;
1318
1426
  }
1319
- const fullPath = path3__default.default.isAbsolute(reference.localPath) ? reference.localPath : path3__default.default.join(this.baseDir, reference.localPath);
1427
+ const fullPath = path3__namespace.default.isAbsolute(reference.localPath) ? reference.localPath : path3__namespace.default.join(this.baseDir, reference.localPath);
1320
1428
  try {
1321
- await fs2__default.default.access(fullPath);
1429
+ await fs3__namespace.default.access(fullPath);
1322
1430
  return true;
1323
1431
  } catch {
1324
1432
  return false;
@@ -1328,24 +1436,24 @@ var LocalStorage = class {
1328
1436
  * Read file content as string
1329
1437
  */
1330
1438
  async readText(filePath) {
1331
- const fullPath = path3__default.default.isAbsolute(filePath) ? filePath : path3__default.default.join(this.baseDir, filePath);
1332
- return fs2__default.default.readFile(fullPath, "utf-8");
1439
+ const fullPath = path3__namespace.default.isAbsolute(filePath) ? filePath : path3__namespace.default.join(this.baseDir, filePath);
1440
+ return fs3__namespace.default.readFile(fullPath, "utf-8");
1333
1441
  }
1334
1442
  /**
1335
1443
  * Write content to file
1336
1444
  */
1337
1445
  async write(filePath, content) {
1338
- const fullPath = path3__default.default.isAbsolute(filePath) ? filePath : path3__default.default.join(this.baseDir, filePath);
1339
- await fs2__default.default.mkdir(path3__default.default.dirname(fullPath), { recursive: true });
1340
- await fs2__default.default.writeFile(fullPath, content);
1446
+ const fullPath = path3__namespace.default.isAbsolute(filePath) ? filePath : path3__namespace.default.join(this.baseDir, filePath);
1447
+ await fs3__namespace.default.mkdir(path3__namespace.default.dirname(fullPath), { recursive: true });
1448
+ await fs3__namespace.default.writeFile(fullPath, content);
1341
1449
  }
1342
1450
  /**
1343
1451
  * Delete a file
1344
1452
  */
1345
1453
  async delete(filePath) {
1346
- const fullPath = path3__default.default.isAbsolute(filePath) ? filePath : path3__default.default.join(this.baseDir, filePath);
1454
+ const fullPath = path3__namespace.default.isAbsolute(filePath) ? filePath : path3__namespace.default.join(this.baseDir, filePath);
1347
1455
  try {
1348
- await fs2__default.default.unlink(fullPath);
1456
+ await fs3__namespace.default.unlink(fullPath);
1349
1457
  return true;
1350
1458
  } catch {
1351
1459
  return false;
@@ -1355,10 +1463,10 @@ var LocalStorage = class {
1355
1463
  * List files in a directory
1356
1464
  */
1357
1465
  async list(dirPath) {
1358
- const fullPath = path3__default.default.isAbsolute(dirPath) ? dirPath : path3__default.default.join(this.baseDir, dirPath);
1466
+ const fullPath = path3__namespace.default.isAbsolute(dirPath) ? dirPath : path3__namespace.default.join(this.baseDir, dirPath);
1359
1467
  try {
1360
- const entries = await fs2__default.default.readdir(fullPath, { withFileTypes: true });
1361
- return entries.filter((e) => e.isFile()).map((e) => path3__default.default.join(dirPath, e.name));
1468
+ const entries = await fs3__namespace.default.readdir(fullPath, { withFileTypes: true });
1469
+ return entries.filter((e) => e.isFile()).map((e) => path3__namespace.default.join(dirPath, e.name));
1362
1470
  } catch {
1363
1471
  return [];
1364
1472
  }
@@ -1704,6 +1812,484 @@ function createHttpStorage(options) {
1704
1812
  return new HttpStorage(options);
1705
1813
  }
1706
1814
 
1815
+ // src/file-integration/source-resolver.ts
1816
+ var FileSourceResolver = class {
1817
+ constructor(config) {
1818
+ this.config = config;
1819
+ this.initializeSources();
1820
+ }
1821
+ sources = /* @__PURE__ */ new Map();
1822
+ /**
1823
+ * Initialize sources from config
1824
+ */
1825
+ initializeSources() {
1826
+ if (!this.config.file?.sources) {
1827
+ return;
1828
+ }
1829
+ for (const [name, sourceConfig] of Object.entries(this.config.file.sources)) {
1830
+ const resolved = {
1831
+ name,
1832
+ type: "file-plugin",
1833
+ localPath: sourceConfig.local.base_path,
1834
+ isCurrentProject: true,
1835
+ config: sourceConfig
1836
+ };
1837
+ if (sourceConfig.bucket && sourceConfig.type !== "local") {
1838
+ resolved.bucket = sourceConfig.bucket;
1839
+ resolved.prefix = sourceConfig.prefix;
1840
+ resolved.remotePath = this.buildRemotePath(sourceConfig);
1841
+ }
1842
+ this.sources.set(name, resolved);
1843
+ }
1844
+ }
1845
+ /**
1846
+ * Build remote path from source config
1847
+ */
1848
+ buildRemotePath(source) {
1849
+ const protocol = source.type === "s3" ? "s3://" : source.type === "r2" ? "r2://" : "gcs://";
1850
+ const bucket = source.bucket;
1851
+ const prefix = source.prefix ? `/${source.prefix}` : "";
1852
+ return `${protocol}${bucket}${prefix}`;
1853
+ }
1854
+ /**
1855
+ * Get all available file plugin sources
1856
+ *
1857
+ * @returns Array of resolved file sources
1858
+ */
1859
+ getAvailableSources() {
1860
+ return Array.from(this.sources.values());
1861
+ }
1862
+ /**
1863
+ * Resolve a source by name
1864
+ *
1865
+ * @param name - Source name (e.g., "specs", "logs")
1866
+ * @returns Resolved source or null if not found
1867
+ */
1868
+ resolveSource(name) {
1869
+ return this.sources.get(name) || null;
1870
+ }
1871
+ /**
1872
+ * Check if a path belongs to any file plugin source
1873
+ *
1874
+ * @param path - File path to check
1875
+ * @returns True if path matches any source's base_path
1876
+ */
1877
+ isFilePluginPath(path7) {
1878
+ return this.getSourceForPath(path7) !== null;
1879
+ }
1880
+ /**
1881
+ * Get the source for a given path
1882
+ *
1883
+ * Matches path against all source base_paths and returns the matching source.
1884
+ * Uses longest-match strategy if multiple sources match.
1885
+ *
1886
+ * @param path - File path to match
1887
+ * @returns Matching source or null
1888
+ */
1889
+ getSourceForPath(path7) {
1890
+ let bestMatch = null;
1891
+ let bestMatchLength = 0;
1892
+ for (const source of this.sources.values()) {
1893
+ const normalizedPath = this.normalizePath(path7);
1894
+ const normalizedBasePath = this.normalizePath(source.localPath);
1895
+ if (normalizedPath.startsWith(normalizedBasePath)) {
1896
+ const matchLength = normalizedBasePath.length;
1897
+ if (matchLength > bestMatchLength) {
1898
+ bestMatch = source;
1899
+ bestMatchLength = matchLength;
1900
+ }
1901
+ }
1902
+ }
1903
+ return bestMatch;
1904
+ }
1905
+ /**
1906
+ * Normalize path for comparison
1907
+ * - Remove leading "./" or "/"
1908
+ * - Remove trailing "/"
1909
+ * - Convert to lowercase for case-insensitive comparison
1910
+ */
1911
+ normalizePath(path7) {
1912
+ return path7.replace(/^\.\//, "").replace(/^\//, "").replace(/\/$/, "").toLowerCase();
1913
+ }
1914
+ /**
1915
+ * Get source names
1916
+ *
1917
+ * @returns Array of source names
1918
+ */
1919
+ getSourceNames() {
1920
+ return Array.from(this.sources.keys());
1921
+ }
1922
+ /**
1923
+ * Check if sources are configured
1924
+ *
1925
+ * @returns True if any file plugin sources are configured
1926
+ */
1927
+ hasSources() {
1928
+ return this.sources.size > 0;
1929
+ }
1930
+ };
1931
+
1932
+ // src/storage/errors.ts
1933
+ var FilePluginFileNotFoundError = class _FilePluginFileNotFoundError extends Error {
1934
+ constructor(filePath, sourceName, options) {
1935
+ const includeCloudSuggestions = options?.includeCloudSuggestions !== false;
1936
+ const storageType = options?.storageType;
1937
+ let message = `File not found: ${filePath}
1938
+
1939
+ `;
1940
+ if (includeCloudSuggestions) {
1941
+ if (storageType) {
1942
+ message += `This file may be in cloud storage (${storageType}).
1943
+
1944
+ `;
1945
+ } else {
1946
+ message += `This file may not have been synced from remote storage yet.
1947
+ `;
1948
+ }
1949
+ message += `To fetch from cloud storage, run:
1950
+ `;
1951
+ message += ` file pull ${sourceName}
1952
+
1953
+ `;
1954
+ message += `Or sync all sources:
1955
+ `;
1956
+ message += ` file sync`;
1957
+ } else {
1958
+ message += `Please ensure the file exists locally or pull it from cloud storage.`;
1959
+ }
1960
+ super(message);
1961
+ this.filePath = filePath;
1962
+ this.sourceName = sourceName;
1963
+ this.name = "FilePluginFileNotFoundError";
1964
+ if (Error.captureStackTrace) {
1965
+ Error.captureStackTrace(this, _FilePluginFileNotFoundError);
1966
+ }
1967
+ }
1968
+ };
1969
+
1970
+ // src/storage/file-plugin.ts
1971
+ var FilePluginStorage = class {
1972
+ constructor(options) {
1973
+ this.options = options;
1974
+ this.sourceResolver = new FileSourceResolver(options.config);
1975
+ this.baseDir = options.baseDir || process.cwd();
1976
+ }
1977
+ name = "file-plugin";
1978
+ type = "local";
1979
+ // Reuse local type for compatibility
1980
+ sourceResolver;
1981
+ baseDir;
1982
+ /**
1983
+ * Check if this provider can handle the reference
1984
+ *
1985
+ * Only handles:
1986
+ * - Current project references
1987
+ * - With sourceType === 'file-plugin'
1988
+ *
1989
+ * @param reference - Resolved reference
1990
+ * @returns True if this provider can handle the reference
1991
+ */
1992
+ canHandle(reference) {
1993
+ return reference.isCurrentProject && reference.sourceType === "file-plugin";
1994
+ }
1995
+ /**
1996
+ * Fetch content for a reference
1997
+ *
1998
+ * Reads from local filesystem based on the resolved local path.
1999
+ * If file not found and S3 fallback is enabled, throws helpful error.
2000
+ *
2001
+ * @param reference - Resolved reference
2002
+ * @param options - Fetch options (unused for local reads)
2003
+ * @returns Fetch result with content
2004
+ */
2005
+ async fetch(reference, _options) {
2006
+ if (!reference.localPath) {
2007
+ throw new Error(`File plugin reference missing localPath: ${reference.uri}`);
2008
+ }
2009
+ if (!reference.filePluginSource) {
2010
+ throw new Error(`File plugin reference missing source name: ${reference.uri}`);
2011
+ }
2012
+ const absolutePath = path3__namespace.isAbsolute(reference.localPath) ? path3__namespace.resolve(reference.localPath) : path3__namespace.resolve(this.baseDir, reference.localPath);
2013
+ const source = this.sourceResolver.resolveSource(reference.filePluginSource);
2014
+ if (source) {
2015
+ const allowedDir = path3__namespace.resolve(this.baseDir, source.localPath);
2016
+ if (!absolutePath.startsWith(allowedDir + path3__namespace.sep) && absolutePath !== allowedDir) {
2017
+ throw new Error(
2018
+ `Path traversal detected: ${reference.localPath} resolves outside allowed directory ${source.localPath}`
2019
+ );
2020
+ }
2021
+ }
2022
+ try {
2023
+ const content = await fs3__namespace.readFile(absolutePath);
2024
+ const contentType = this.detectContentType(absolutePath);
2025
+ return {
2026
+ content,
2027
+ contentType,
2028
+ size: content.length,
2029
+ source: "file-plugin",
2030
+ metadata: {
2031
+ filePluginSource: reference.filePluginSource,
2032
+ localPath: absolutePath
2033
+ }
2034
+ };
2035
+ } catch (error) {
2036
+ if (error.code === "ENOENT") {
2037
+ const source2 = this.sourceResolver.resolveSource(reference.filePluginSource);
2038
+ throw this.createFileNotFoundError(reference, source2);
2039
+ }
2040
+ throw error;
2041
+ }
2042
+ }
2043
+ /**
2044
+ * Check if a reference exists
2045
+ *
2046
+ * @param reference - Resolved reference
2047
+ * @param options - Fetch options (unused)
2048
+ * @returns True if file exists
2049
+ */
2050
+ async exists(reference, _options) {
2051
+ if (!reference.localPath) {
2052
+ return false;
2053
+ }
2054
+ const absolutePath = path3__namespace.isAbsolute(reference.localPath) ? path3__namespace.resolve(reference.localPath) : path3__namespace.resolve(this.baseDir, reference.localPath);
2055
+ if (reference.filePluginSource) {
2056
+ const source = this.sourceResolver.resolveSource(reference.filePluginSource);
2057
+ if (source) {
2058
+ const allowedDir = path3__namespace.resolve(this.baseDir, source.localPath);
2059
+ if (!absolutePath.startsWith(allowedDir + path3__namespace.sep) && absolutePath !== allowedDir) {
2060
+ return false;
2061
+ }
2062
+ }
2063
+ }
2064
+ try {
2065
+ await fs3__namespace.access(absolutePath);
2066
+ return true;
2067
+ } catch {
2068
+ return false;
2069
+ }
2070
+ }
2071
+ /**
2072
+ * Detect content type from file extension
2073
+ */
2074
+ detectContentType(filePath) {
2075
+ const ext = path3__namespace.extname(filePath).toLowerCase();
2076
+ const mimeTypes = {
2077
+ ".md": "text/markdown",
2078
+ ".txt": "text/plain",
2079
+ ".json": "application/json",
2080
+ ".yaml": "text/yaml",
2081
+ ".yml": "text/yaml",
2082
+ ".html": "text/html",
2083
+ ".xml": "application/xml",
2084
+ ".log": "text/plain",
2085
+ ".js": "application/javascript",
2086
+ ".ts": "application/typescript",
2087
+ ".py": "text/x-python",
2088
+ ".sh": "application/x-sh"
2089
+ };
2090
+ return mimeTypes[ext] || "application/octet-stream";
2091
+ }
2092
+ /**
2093
+ * Create a helpful error message when file is not found
2094
+ */
2095
+ createFileNotFoundError(reference, source) {
2096
+ const includeCloudSuggestions = this.options.enableS3Fallback !== false;
2097
+ const storageType = source?.config.type;
2098
+ return new FilePluginFileNotFoundError(
2099
+ reference.localPath || reference.path || "",
2100
+ reference.filePluginSource || "",
2101
+ {
2102
+ includeCloudSuggestions,
2103
+ storageType
2104
+ }
2105
+ );
2106
+ }
2107
+ };
2108
+ var execFileAsync = util.promisify(child_process.execFile);
2109
+ async function execFileNoThrow(command, args = [], options) {
2110
+ try {
2111
+ const { stdout, stderr } = await execFileAsync(command, args, {
2112
+ ...options,
2113
+ maxBuffer: options?.maxBuffer || 1024 * 1024 * 10
2114
+ // 10MB default
2115
+ });
2116
+ return {
2117
+ stdout: stdout || "",
2118
+ stderr: stderr || "",
2119
+ exitCode: 0
2120
+ };
2121
+ } catch (error) {
2122
+ const exitCode = typeof error.exitCode === "number" ? error.exitCode : 1;
2123
+ return {
2124
+ stdout: error.stdout || "",
2125
+ stderr: error.stderr || error.message || "",
2126
+ exitCode
2127
+ };
2128
+ }
2129
+ }
2130
+
2131
+ // src/storage/s3-archive.ts
2132
+ var S3ArchiveStorage = class {
2133
+ name = "s3-archive";
2134
+ type = "s3-archive";
2135
+ projects;
2136
+ fractaryCli;
2137
+ constructor(options = {}) {
2138
+ this.projects = options.projects || {};
2139
+ this.fractaryCli = options.fractaryCli || "fractary";
2140
+ }
2141
+ /**
2142
+ * Check if this provider can handle the reference
2143
+ *
2144
+ * S3 Archive provider handles references that:
2145
+ * 1. Are for the current project (same org/project)
2146
+ * 2. Have archive enabled in config
2147
+ * 3. Match configured patterns (if specified)
2148
+ */
2149
+ canHandle(reference) {
2150
+ if (!reference.isCurrentProject) {
2151
+ return false;
2152
+ }
2153
+ const projectKey = `${reference.org}/${reference.project}`;
2154
+ const config = this.projects[projectKey];
2155
+ if (!config || !config.enabled) {
2156
+ return false;
2157
+ }
2158
+ if (config.patterns && config.patterns.length > 0) {
2159
+ return this.matchesPatterns(reference.path, config.patterns);
2160
+ }
2161
+ return true;
2162
+ }
2163
+ /**
2164
+ * Fetch content from S3 archive via fractary-file CLI
2165
+ */
2166
+ async fetch(reference, options) {
2167
+ const opts = mergeFetchOptions(options);
2168
+ const projectKey = `${reference.org}/${reference.project}`;
2169
+ const config = this.projects[projectKey];
2170
+ if (!config) {
2171
+ throw new Error(`No archive config for project: ${projectKey}`);
2172
+ }
2173
+ const archivePath = this.calculateArchivePath(reference, config);
2174
+ try {
2175
+ const result = await execFileNoThrow(
2176
+ this.fractaryCli,
2177
+ [
2178
+ "file",
2179
+ "read",
2180
+ "--remote-path",
2181
+ archivePath,
2182
+ "--handler",
2183
+ config.handler,
2184
+ ...config.bucket ? ["--bucket", config.bucket] : []
2185
+ ],
2186
+ {
2187
+ timeout: opts.timeout
2188
+ }
2189
+ );
2190
+ if (result.exitCode !== 0) {
2191
+ throw new Error(`fractary-file read failed: ${result.stderr}`);
2192
+ }
2193
+ const content = Buffer.from(result.stdout);
2194
+ return {
2195
+ content,
2196
+ contentType: detectContentType(reference.path),
2197
+ size: content.length,
2198
+ source: "s3-archive",
2199
+ metadata: {
2200
+ archivePath,
2201
+ bucket: config.bucket,
2202
+ handler: config.handler
2203
+ }
2204
+ };
2205
+ } catch (error) {
2206
+ const message = error instanceof Error ? error.message : String(error);
2207
+ throw new Error(`Failed to fetch from archive: ${message}`);
2208
+ }
2209
+ }
2210
+ /**
2211
+ * Check if archived file exists
2212
+ *
2213
+ * Note: This currently downloads the file to check existence.
2214
+ * TODO: Optimize by using fractary-file 'stat' or 'head' command when available
2215
+ * to avoid downloading full file for existence checks.
2216
+ */
2217
+ async exists(reference, options) {
2218
+ const projectKey = `${reference.org}/${reference.project}`;
2219
+ const config = this.projects[projectKey];
2220
+ if (!config) {
2221
+ return false;
2222
+ }
2223
+ try {
2224
+ await this.fetch(reference, { ...options, timeout: 5e3 });
2225
+ return true;
2226
+ } catch {
2227
+ return false;
2228
+ }
2229
+ }
2230
+ /**
2231
+ * Calculate archive path from reference
2232
+ *
2233
+ * Pattern: {prefix}/{type}/{org}/{project}/{original-path}
2234
+ *
2235
+ * Examples (with default prefix "archive/"):
2236
+ * specs/WORK-123.md → archive/specs/org/project/specs/WORK-123.md
2237
+ * docs/api.md → archive/docs/org/project/docs/api.md
2238
+ *
2239
+ * Examples (with custom prefix "archived-docs/"):
2240
+ * specs/WORK-123.md → archived-docs/specs/org/project/specs/WORK-123.md
2241
+ */
2242
+ calculateArchivePath(reference, config) {
2243
+ const type = this.detectType(reference.path);
2244
+ const prefix = config.prefix || "archive/";
2245
+ const trimmedPrefix = prefix.trim();
2246
+ if (!trimmedPrefix) {
2247
+ throw new Error("Archive prefix cannot be empty or whitespace-only");
2248
+ }
2249
+ const normalizedPrefix = trimmedPrefix.endsWith("/") ? trimmedPrefix : `${trimmedPrefix}/`;
2250
+ return `${normalizedPrefix}${type}/${reference.org}/${reference.project}/${reference.path}`;
2251
+ }
2252
+ /**
2253
+ * Detect artifact type from path
2254
+ *
2255
+ * Used to organize archives by type
2256
+ */
2257
+ detectType(path7) {
2258
+ if (path7.startsWith("specs/")) return "specs";
2259
+ if (path7.startsWith("docs/")) return "docs";
2260
+ if (path7.includes("/logs/")) return "logs";
2261
+ return "misc";
2262
+ }
2263
+ /**
2264
+ * Check if path matches any of the patterns
2265
+ *
2266
+ * Supports glob-style patterns:
2267
+ * - specs/** (all files in specs/)
2268
+ * - *.md (all markdown files)
2269
+ * - docs/*.md (markdown files in docs/)
2270
+ */
2271
+ matchesPatterns(path7, patterns) {
2272
+ for (const pattern of patterns) {
2273
+ if (this.matchesPattern(path7, pattern)) {
2274
+ return true;
2275
+ }
2276
+ }
2277
+ return false;
2278
+ }
2279
+ /**
2280
+ * Check if path matches a single pattern
2281
+ */
2282
+ matchesPattern(path7, pattern) {
2283
+ const DOUBLE_STAR = "\0DOUBLE_STAR\0";
2284
+ let regexPattern = pattern.replace(/\*\*/g, DOUBLE_STAR);
2285
+ regexPattern = regexPattern.replace(/[.[\](){}+^$|\\]/g, "\\$&");
2286
+ regexPattern = regexPattern.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
2287
+ regexPattern = regexPattern.replace(new RegExp(DOUBLE_STAR, "g"), ".*");
2288
+ const regex = new RegExp(`^${regexPattern}$`);
2289
+ return regex.test(path7);
2290
+ }
2291
+ };
2292
+
1707
2293
  // src/storage/manager.ts
1708
2294
  var StorageManager = class {
1709
2295
  providers = /* @__PURE__ */ new Map();
@@ -1712,7 +2298,13 @@ var StorageManager = class {
1712
2298
  this.providers.set("local", new LocalStorage(config.local));
1713
2299
  this.providers.set("github", new GitHubStorage(config.github));
1714
2300
  this.providers.set("http", new HttpStorage(config.http));
1715
- this.priority = config.priority || ["local", "github", "http"];
2301
+ if (config.s3Archive) {
2302
+ this.providers.set("s3-archive", new S3ArchiveStorage(config.s3Archive));
2303
+ }
2304
+ if (config.filePlugin) {
2305
+ this.providers.set("file-plugin", new FilePluginStorage(config.filePlugin));
2306
+ }
2307
+ this.priority = config.priority || (config.filePlugin && config.s3Archive ? ["file-plugin", "local", "s3-archive", "github", "http"] : config.filePlugin ? ["file-plugin", "local", "github", "http"] : config.s3Archive ? ["local", "s3-archive", "github", "http"] : ["local", "github", "http"]);
1716
2308
  }
1717
2309
  /**
1718
2310
  * Register a custom storage provider
@@ -1952,7 +2544,7 @@ var CachePersistence = class {
1952
2544
  }
1953
2545
  const [, org, project, filePath] = match;
1954
2546
  const relativePath = filePath || "index";
1955
- return path3__default.default.join(this.cacheDir, org, project, relativePath + this.extension);
2547
+ return path3__namespace.default.join(this.cacheDir, org, project, relativePath + this.extension);
1956
2548
  }
1957
2549
  /**
1958
2550
  * Get the metadata file path for a URI
@@ -1968,8 +2560,8 @@ var CachePersistence = class {
1968
2560
  const metadataPath = this.getMetadataPath(uri);
1969
2561
  try {
1970
2562
  const [metadataJson, content] = await Promise.all([
1971
- fs2__default.default.readFile(metadataPath, "utf-8"),
1972
- fs2__default.default.readFile(cachePath)
2563
+ fs3__namespace.default.readFile(metadataPath, "utf-8"),
2564
+ fs3__namespace.default.readFile(cachePath)
1973
2565
  ]);
1974
2566
  const metadata = JSON.parse(metadataJson);
1975
2567
  return {
@@ -1989,32 +2581,32 @@ var CachePersistence = class {
1989
2581
  async write(entry) {
1990
2582
  const cachePath = this.getCachePath(entry.metadata.uri);
1991
2583
  const metadataPath = this.getMetadataPath(entry.metadata.uri);
1992
- await fs2__default.default.mkdir(path3__default.default.dirname(cachePath), { recursive: true });
2584
+ await fs3__namespace.default.mkdir(path3__namespace.default.dirname(cachePath), { recursive: true });
1993
2585
  if (this.atomicWrites) {
1994
2586
  const tempCachePath = cachePath + ".tmp";
1995
2587
  const tempMetadataPath = metadataPath + ".tmp";
1996
2588
  try {
1997
2589
  await Promise.all([
1998
- fs2__default.default.writeFile(tempCachePath, entry.content),
1999
- fs2__default.default.writeFile(tempMetadataPath, JSON.stringify(entry.metadata, null, 2))
2590
+ fs3__namespace.default.writeFile(tempCachePath, entry.content),
2591
+ fs3__namespace.default.writeFile(tempMetadataPath, JSON.stringify(entry.metadata, null, 2))
2000
2592
  ]);
2001
2593
  await Promise.all([
2002
- fs2__default.default.rename(tempCachePath, cachePath),
2003
- fs2__default.default.rename(tempMetadataPath, metadataPath)
2594
+ fs3__namespace.default.rename(tempCachePath, cachePath),
2595
+ fs3__namespace.default.rename(tempMetadataPath, metadataPath)
2004
2596
  ]);
2005
2597
  } catch (error) {
2006
2598
  await Promise.all([
2007
- fs2__default.default.unlink(tempCachePath).catch(() => {
2599
+ fs3__namespace.default.unlink(tempCachePath).catch(() => {
2008
2600
  }),
2009
- fs2__default.default.unlink(tempMetadataPath).catch(() => {
2601
+ fs3__namespace.default.unlink(tempMetadataPath).catch(() => {
2010
2602
  })
2011
2603
  ]);
2012
2604
  throw error;
2013
2605
  }
2014
2606
  } else {
2015
2607
  await Promise.all([
2016
- fs2__default.default.writeFile(cachePath, entry.content),
2017
- fs2__default.default.writeFile(metadataPath, JSON.stringify(entry.metadata, null, 2))
2608
+ fs3__namespace.default.writeFile(cachePath, entry.content),
2609
+ fs3__namespace.default.writeFile(metadataPath, JSON.stringify(entry.metadata, null, 2))
2018
2610
  ]);
2019
2611
  }
2020
2612
  }
@@ -2025,7 +2617,7 @@ var CachePersistence = class {
2025
2617
  const cachePath = this.getCachePath(uri);
2026
2618
  const metadataPath = this.getMetadataPath(uri);
2027
2619
  try {
2028
- await Promise.all([fs2__default.default.unlink(cachePath), fs2__default.default.unlink(metadataPath)]);
2620
+ await Promise.all([fs3__namespace.default.unlink(cachePath), fs3__namespace.default.unlink(metadataPath)]);
2029
2621
  return true;
2030
2622
  } catch (error) {
2031
2623
  if (error.code === "ENOENT") {
@@ -2040,7 +2632,7 @@ var CachePersistence = class {
2040
2632
  async exists(uri) {
2041
2633
  const cachePath = this.getCachePath(uri);
2042
2634
  try {
2043
- await fs2__default.default.access(cachePath);
2635
+ await fs3__namespace.default.access(cachePath);
2044
2636
  return true;
2045
2637
  } catch {
2046
2638
  return false;
@@ -2052,20 +2644,20 @@ var CachePersistence = class {
2052
2644
  async list() {
2053
2645
  const uris = [];
2054
2646
  try {
2055
- const orgs = await fs2__default.default.readdir(this.cacheDir);
2647
+ const orgs = await fs3__namespace.default.readdir(this.cacheDir);
2056
2648
  for (const org of orgs) {
2057
- const orgPath = path3__default.default.join(this.cacheDir, org);
2058
- const orgStat = await fs2__default.default.stat(orgPath);
2649
+ const orgPath = path3__namespace.default.join(this.cacheDir, org);
2650
+ const orgStat = await fs3__namespace.default.stat(orgPath);
2059
2651
  if (!orgStat.isDirectory()) continue;
2060
- const projects = await fs2__default.default.readdir(orgPath);
2652
+ const projects = await fs3__namespace.default.readdir(orgPath);
2061
2653
  for (const project of projects) {
2062
- const projectPath = path3__default.default.join(orgPath, project);
2063
- const projectStat = await fs2__default.default.stat(projectPath);
2654
+ const projectPath = path3__namespace.default.join(orgPath, project);
2655
+ const projectStat = await fs3__namespace.default.stat(projectPath);
2064
2656
  if (!projectStat.isDirectory()) continue;
2065
2657
  const files = await this.listFilesRecursive(projectPath);
2066
2658
  for (const file of files) {
2067
2659
  if (file.endsWith(this.extension)) {
2068
- const relativePath = path3__default.default.relative(projectPath, file);
2660
+ const relativePath = path3__namespace.default.relative(projectPath, file);
2069
2661
  const filePath = relativePath.slice(0, -this.extension.length);
2070
2662
  uris.push(`codex://${org}/${project}/${filePath}`);
2071
2663
  }
@@ -2085,9 +2677,9 @@ var CachePersistence = class {
2085
2677
  */
2086
2678
  async listFilesRecursive(dir) {
2087
2679
  const files = [];
2088
- const entries = await fs2__default.default.readdir(dir, { withFileTypes: true });
2680
+ const entries = await fs3__namespace.default.readdir(dir, { withFileTypes: true });
2089
2681
  for (const entry of entries) {
2090
- const fullPath = path3__default.default.join(dir, entry.name);
2682
+ const fullPath = path3__namespace.default.join(dir, entry.name);
2091
2683
  if (entry.isDirectory()) {
2092
2684
  files.push(...await this.listFilesRecursive(fullPath));
2093
2685
  } else if (entry.isFile()) {
@@ -2102,12 +2694,12 @@ var CachePersistence = class {
2102
2694
  async clear() {
2103
2695
  let count = 0;
2104
2696
  try {
2105
- const orgs = await fs2__default.default.readdir(this.cacheDir);
2697
+ const orgs = await fs3__namespace.default.readdir(this.cacheDir);
2106
2698
  for (const org of orgs) {
2107
- const orgPath = path3__default.default.join(this.cacheDir, org);
2108
- const stat = await fs2__default.default.stat(orgPath);
2699
+ const orgPath = path3__namespace.default.join(this.cacheDir, org);
2700
+ const stat = await fs3__namespace.default.stat(orgPath);
2109
2701
  if (stat.isDirectory()) {
2110
- await fs2__default.default.rm(orgPath, { recursive: true });
2702
+ await fs3__namespace.default.rm(orgPath, { recursive: true });
2111
2703
  count++;
2112
2704
  }
2113
2705
  }
@@ -2168,7 +2760,7 @@ var CachePersistence = class {
2168
2760
  * Ensure cache directory exists
2169
2761
  */
2170
2762
  async ensureDir() {
2171
- await fs2__default.default.mkdir(this.cacheDir, { recursive: true });
2763
+ await fs3__namespace.default.mkdir(this.cacheDir, { recursive: true });
2172
2764
  }
2173
2765
  /**
2174
2766
  * Get cache directory path
@@ -2216,8 +2808,17 @@ var CacheManager = class {
2216
2808
  * Get content for a reference
2217
2809
  *
2218
2810
  * Implements cache-first strategy with stale-while-revalidate.
2811
+ *
2812
+ * EXCEPTION: File plugin sources (current project files) bypass cache entirely.
2813
+ * They are always read fresh from disk for optimal development experience.
2219
2814
  */
2220
2815
  async get(reference, options) {
2816
+ if (reference.isCurrentProject && reference.sourceType === "file-plugin") {
2817
+ if (!this.storage) {
2818
+ throw new Error("Storage manager not set");
2819
+ }
2820
+ return await this.storage.fetch(reference, options);
2821
+ }
2221
2822
  const ttl = options?.ttl ?? this.config.defaultTtl;
2222
2823
  let entry = this.memoryCache.get(reference.uri);
2223
2824
  if (!entry && this.persistence) {
@@ -2423,13 +3024,18 @@ var CacheManager = class {
2423
3024
  }
2424
3025
  /**
2425
3026
  * Fetch content and store in cache
3027
+ *
3028
+ * EXCEPTION: File plugin sources are not cached (should not reach here,
3029
+ * but added as safety check).
2426
3030
  */
2427
3031
  async fetchAndCache(reference, ttl, options) {
2428
3032
  if (!this.storage) {
2429
3033
  throw new Error("Storage manager not set");
2430
3034
  }
2431
3035
  const result = await this.storage.fetch(reference, options);
2432
- await this.set(reference.uri, result, ttl);
3036
+ if (!(reference.isCurrentProject && reference.sourceType === "file-plugin")) {
3037
+ await this.set(reference.uri, result, ttl);
3038
+ }
2433
3039
  return result;
2434
3040
  }
2435
3041
  /**
@@ -2548,11 +3154,11 @@ var DEFAULT_SYNC_CONFIG = {
2548
3154
  deleteOrphans: false,
2549
3155
  conflictStrategy: "newest"
2550
3156
  };
2551
- function evaluatePath(path6, rules, direction, defaultExcludes = []) {
3157
+ function evaluatePath(path7, rules, direction, defaultExcludes = []) {
2552
3158
  for (const pattern of defaultExcludes) {
2553
- if (micromatch3__default.default.isMatch(path6, pattern)) {
3159
+ if (micromatch3__default.default.isMatch(path7, pattern)) {
2554
3160
  return {
2555
- path: path6,
3161
+ path: path7,
2556
3162
  shouldSync: false,
2557
3163
  reason: `Excluded by default pattern: ${pattern}`
2558
3164
  };
@@ -2563,9 +3169,9 @@ function evaluatePath(path6, rules, direction, defaultExcludes = []) {
2563
3169
  if (rule.direction && rule.direction !== direction) {
2564
3170
  continue;
2565
3171
  }
2566
- if (micromatch3__default.default.isMatch(path6, rule.pattern)) {
3172
+ if (micromatch3__default.default.isMatch(path7, rule.pattern)) {
2567
3173
  return {
2568
- path: path6,
3174
+ path: path7,
2569
3175
  shouldSync: rule.include,
2570
3176
  matchedRule: rule,
2571
3177
  reason: rule.include ? `Included by rule: ${rule.pattern}` : `Excluded by rule: ${rule.pattern}`
@@ -2573,21 +3179,21 @@ function evaluatePath(path6, rules, direction, defaultExcludes = []) {
2573
3179
  }
2574
3180
  }
2575
3181
  return {
2576
- path: path6,
3182
+ path: path7,
2577
3183
  shouldSync: true,
2578
3184
  reason: "No matching rule, included by default"
2579
3185
  };
2580
3186
  }
2581
3187
  function evaluatePaths(paths, rules, direction, defaultExcludes = []) {
2582
3188
  const results = /* @__PURE__ */ new Map();
2583
- for (const path6 of paths) {
2584
- results.set(path6, evaluatePath(path6, rules, direction, defaultExcludes));
3189
+ for (const path7 of paths) {
3190
+ results.set(path7, evaluatePath(path7, rules, direction, defaultExcludes));
2585
3191
  }
2586
3192
  return results;
2587
3193
  }
2588
3194
  function filterSyncablePaths(paths, rules, direction, defaultExcludes = []) {
2589
3195
  return paths.filter(
2590
- (path6) => evaluatePath(path6, rules, direction, defaultExcludes).shouldSync
3196
+ (path7) => evaluatePath(path7, rules, direction, defaultExcludes).shouldSync
2591
3197
  );
2592
3198
  }
2593
3199
  function createRulesFromPatterns(include = [], exclude = []) {
@@ -2853,8 +3459,8 @@ async function scanCodexWithRouting(options) {
2853
3459
  for (const filePath of allFiles) {
2854
3460
  totalScanned++;
2855
3461
  try {
2856
- const fullPath = path3__default.default.join(codexDir, filePath);
2857
- const stats = await fs2__default.default.stat(fullPath);
3462
+ const fullPath = path3__namespace.default.join(codexDir, filePath);
3463
+ const stats = await fs3__namespace.default.stat(fullPath);
2858
3464
  if (stats.size > maxFileSize) {
2859
3465
  totalSkipped++;
2860
3466
  errors.push({
@@ -2950,10 +3556,10 @@ async function listAllFilesRecursive(dirPath) {
2950
3556
  const files = [];
2951
3557
  async function scanDirectory(currentPath, relativePath = "") {
2952
3558
  try {
2953
- const entries = await fs2__default.default.readdir(currentPath, { withFileTypes: true });
3559
+ const entries = await fs3__namespace.default.readdir(currentPath, { withFileTypes: true });
2954
3560
  for (const entry of entries) {
2955
- const entryPath = path3__default.default.join(currentPath, entry.name);
2956
- const entryRelativePath = relativePath ? path3__default.default.join(relativePath, entry.name) : entry.name;
3561
+ const entryPath = path3__namespace.default.join(currentPath, entry.name);
3562
+ const entryRelativePath = relativePath ? path3__namespace.default.join(relativePath, entry.name) : entry.name;
2957
3563
  if (entry.isDirectory()) {
2958
3564
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
2959
3565
  continue;
@@ -3192,17 +3798,17 @@ var SyncManager = class {
3192
3798
  if (file.operation === "create" || file.operation === "update") {
3193
3799
  const sourcePath = `${plan.source}/${file.path}`;
3194
3800
  const targetPath = `${plan.target}/${file.path}`;
3195
- const fs4 = await import('fs/promises');
3196
- const path6 = await import('path');
3197
- const targetDir = path6.dirname(targetPath);
3198
- await fs4.mkdir(targetDir, { recursive: true });
3199
- await fs4.copyFile(sourcePath, targetPath);
3801
+ const fs5 = await import('fs/promises');
3802
+ const path7 = await import('path');
3803
+ const targetDir = path7.dirname(targetPath);
3804
+ await fs5.mkdir(targetDir, { recursive: true });
3805
+ await fs5.copyFile(sourcePath, targetPath);
3200
3806
  synced++;
3201
3807
  } else if (file.operation === "delete") {
3202
3808
  const targetPath = `${plan.target}/${file.path}`;
3203
- const fs4 = await import('fs/promises');
3809
+ const fs5 = await import('fs/promises');
3204
3810
  try {
3205
- await fs4.unlink(targetPath);
3811
+ await fs5.unlink(targetPath);
3206
3812
  synced++;
3207
3813
  } catch (error) {
3208
3814
  if (error.code !== "ENOENT") {
@@ -3257,15 +3863,15 @@ var SyncManager = class {
3257
3863
  /**
3258
3864
  * Get sync status for a file
3259
3865
  */
3260
- async getFileStatus(path6) {
3866
+ async getFileStatus(path7) {
3261
3867
  const manifest = await this.loadManifest();
3262
- return manifest?.entries[path6] ?? null;
3868
+ return manifest?.entries[path7] ?? null;
3263
3869
  }
3264
3870
  /**
3265
3871
  * Check if a file is synced
3266
3872
  */
3267
- async isFileSynced(path6) {
3268
- const status = await this.getFileStatus(path6);
3873
+ async isFileSynced(path7) {
3874
+ const status = await this.getFileStatus(path7);
3269
3875
  return status !== null;
3270
3876
  }
3271
3877
  /**
@@ -3349,19 +3955,19 @@ function ruleMatchesContext(rule, context) {
3349
3955
  }
3350
3956
  return true;
3351
3957
  }
3352
- function ruleMatchesPath(rule, path6) {
3353
- return micromatch3__default.default.isMatch(path6, rule.pattern);
3958
+ function ruleMatchesPath(rule, path7) {
3959
+ return micromatch3__default.default.isMatch(path7, rule.pattern);
3354
3960
  }
3355
3961
  function ruleMatchesAction(rule, action) {
3356
3962
  return rule.actions.includes(action);
3357
3963
  }
3358
- function evaluatePermission(path6, action, context, config) {
3964
+ function evaluatePermission(path7, action, context, config) {
3359
3965
  const sortedRules = [...config.rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
3360
3966
  for (const rule of sortedRules) {
3361
3967
  if (!ruleMatchesContext(rule, context)) {
3362
3968
  continue;
3363
3969
  }
3364
- if (!ruleMatchesPath(rule, path6)) {
3970
+ if (!ruleMatchesPath(rule, path7)) {
3365
3971
  continue;
3366
3972
  }
3367
3973
  if (!ruleMatchesAction(rule, action)) {
@@ -3380,24 +3986,24 @@ function evaluatePermission(path6, action, context, config) {
3380
3986
  reason: config.defaultAllow ? "Allowed by default" : "Denied by default"
3381
3987
  };
3382
3988
  }
3383
- function isAllowed(path6, action, context, config) {
3384
- const result = evaluatePermission(path6, action, context, config);
3989
+ function isAllowed(path7, action, context, config) {
3990
+ const result = evaluatePermission(path7, action, context, config);
3385
3991
  return result.allowed;
3386
3992
  }
3387
- function hasPermission(path6, action, requiredLevel, context, config) {
3388
- const result = evaluatePermission(path6, action, context, config);
3993
+ function hasPermission(path7, action, requiredLevel, context, config) {
3994
+ const result = evaluatePermission(path7, action, context, config);
3389
3995
  return levelGrants(result.level, requiredLevel);
3390
3996
  }
3391
3997
  function evaluatePermissions(paths, action, context, config) {
3392
3998
  const results = /* @__PURE__ */ new Map();
3393
- for (const path6 of paths) {
3394
- results.set(path6, evaluatePermission(path6, action, context, config));
3999
+ for (const path7 of paths) {
4000
+ results.set(path7, evaluatePermission(path7, action, context, config));
3395
4001
  }
3396
4002
  return results;
3397
4003
  }
3398
4004
  function filterByPermission(paths, action, context, config, requiredLevel = "read") {
3399
- return paths.filter((path6) => {
3400
- const result = evaluatePermission(path6, action, context, config);
4005
+ return paths.filter((path7) => {
4006
+ const result = evaluatePermission(path7, action, context, config);
3401
4007
  return levelGrants(result.level, requiredLevel);
3402
4008
  });
3403
4009
  }
@@ -3488,21 +4094,21 @@ var PermissionManager = class {
3488
4094
  /**
3489
4095
  * Check if an action is allowed for a path
3490
4096
  */
3491
- isAllowed(path6, action, context) {
3492
- const result = this.evaluate(path6, action, context);
4097
+ isAllowed(path7, action, context) {
4098
+ const result = this.evaluate(path7, action, context);
3493
4099
  return result.allowed;
3494
4100
  }
3495
4101
  /**
3496
4102
  * Check if a permission level is granted
3497
4103
  */
3498
- hasPermission(path6, action, requiredLevel, context) {
3499
- const result = this.evaluate(path6, action, context);
4104
+ hasPermission(path7, action, requiredLevel, context) {
4105
+ const result = this.evaluate(path7, action, context);
3500
4106
  return levelGrants(result.level, requiredLevel);
3501
4107
  }
3502
4108
  /**
3503
4109
  * Evaluate permission for a path and action
3504
4110
  */
3505
- evaluate(path6, action, context) {
4111
+ evaluate(path7, action, context) {
3506
4112
  const mergedContext = { ...this.defaultContext, ...context };
3507
4113
  if (!this.config.enforced) {
3508
4114
  return {
@@ -3511,7 +4117,7 @@ var PermissionManager = class {
3511
4117
  reason: "Permissions not enforced"
3512
4118
  };
3513
4119
  }
3514
- return evaluatePermission(path6, action, mergedContext, this.config);
4120
+ return evaluatePermission(path7, action, mergedContext, this.config);
3515
4121
  }
3516
4122
  /**
3517
4123
  * Filter paths by permission
@@ -3614,12 +4220,12 @@ var PermissionManager = class {
3614
4220
  /**
3615
4221
  * Assert permission (throws if denied)
3616
4222
  */
3617
- assertPermission(path6, action, requiredLevel = "read", context) {
3618
- const result = this.evaluate(path6, action, context);
4223
+ assertPermission(path7, action, requiredLevel = "read", context) {
4224
+ const result = this.evaluate(path7, action, context);
3619
4225
  if (!levelGrants(result.level, requiredLevel)) {
3620
4226
  throw new PermissionDeniedError(
3621
- `Permission denied for ${action} on ${path6}: ${result.reason}`,
3622
- path6,
4227
+ `Permission denied for ${action} on ${path7}: ${result.reason}`,
4228
+ path7,
3623
4229
  action,
3624
4230
  result
3625
4231
  );
@@ -3627,9 +4233,9 @@ var PermissionManager = class {
3627
4233
  }
3628
4234
  };
3629
4235
  var PermissionDeniedError = class extends Error {
3630
- constructor(message, path6, action, result) {
4236
+ constructor(message, path7, action, result) {
3631
4237
  super(message);
3632
- this.path = path6;
4238
+ this.path = path7;
3633
4239
  this.action = action;
3634
4240
  this.result = result;
3635
4241
  this.name = "PermissionDeniedError";
@@ -4126,8 +4732,8 @@ function convertToUri(reference, options = {}) {
4126
4732
  const parts = parseReference2(trimmed);
4127
4733
  const org = parts.org || options.defaultOrg || "_";
4128
4734
  const project = parts.project || options.defaultProject || "_";
4129
- const path6 = parts.path;
4130
- return `codex://${org}/${project}/${path6}`;
4735
+ const path7 = parts.path;
4736
+ return `codex://${org}/${project}/${path7}`;
4131
4737
  }
4132
4738
  function parseReference2(reference) {
4133
4739
  const trimmed = reference.trim();
@@ -4223,6 +4829,8 @@ exports.DEFAULT_MIGRATION_OPTIONS = DEFAULT_MIGRATION_OPTIONS;
4223
4829
  exports.DEFAULT_PERMISSION_CONFIG = DEFAULT_PERMISSION_CONFIG;
4224
4830
  exports.DEFAULT_SYNC_CONFIG = DEFAULT_SYNC_CONFIG;
4225
4831
  exports.DEFAULT_TYPE = DEFAULT_TYPE;
4832
+ exports.FilePluginFileNotFoundError = FilePluginFileNotFoundError;
4833
+ exports.FilePluginStorage = FilePluginStorage;
4226
4834
  exports.GitHubStorage = GitHubStorage;
4227
4835
  exports.HttpStorage = HttpStorage;
4228
4836
  exports.LEGACY_PATTERNS = LEGACY_PATTERNS;