@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/README.md +266 -161
- package/dist/index.cjs +3956 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +996 -17
- package/dist/index.d.ts +996 -17
- package/dist/index.js +3821 -2
- package/dist/index.js.map +1 -1
- package/package.json +11 -5
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
|
|
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
|
-
|
|
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
|