@fractary/codex-cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -153,6 +153,41 @@ Options:
153
153
  --parallel <n> Number of parallel sync operations (default: 3)
154
154
  ```
155
155
 
156
+ **Routing-Aware Sync (v4.1+):**
157
+
158
+ When using `--from-codex` direction, the sync command uses **routing-aware file discovery** to find all files across the entire codex that should sync to your project based on `codex_sync_include` frontmatter patterns.
159
+
160
+ How it works:
161
+ 1. Clones the entire codex repository to a temporary directory (`/tmp/fractary-codex-clone/`)
162
+ 2. Scans ALL markdown files in the codex recursively
163
+ 3. Evaluates `codex_sync_include` patterns in each file's frontmatter
164
+ 4. Returns only files that match your project name or pattern
165
+
166
+ **Example frontmatter in source files:**
167
+ ```yaml
168
+ ---
169
+ codex_sync_include: ['*'] # Syncs to ALL projects
170
+ codex_sync_include: ['lake-*', 'api-*'] # Syncs to lake-* and api-* projects
171
+ codex_sync_exclude: ['*-test'] # Except *-test projects
172
+ ---
173
+ ```
174
+
175
+ **Trade-offs:**
176
+ - **Efficient for discovery**: Finds all relevant files across hundreds of projects
177
+ - **Inefficient for execution**: Clones entire codex repository (uses shallow clone for speed)
178
+ - **Best practice**: Use `codex://` URI references + cache purging for most workflows; use sync occasionally when needed
179
+
180
+ **Recommended workflow:**
181
+ ```bash
182
+ # Primary: Reference files via codex:// URIs (no sync needed)
183
+ fractary-codex fetch codex://org/project/path/file.md
184
+
185
+ # When you need latest versions: Purge cache
186
+ fractary-codex cache clear --pattern "codex://org/project/*"
187
+
188
+ # Then MCP will re-fetch fresh content automatically
189
+ ```
190
+
156
191
  ### `types` - Type Registry Management
157
192
 
158
193
  Manage custom artifact types for classification and caching.
package/dist/cli.cjs CHANGED
@@ -5,6 +5,8 @@ var fs = require('fs/promises');
5
5
  var path3 = require('path');
6
6
  var yaml = require('js-yaml');
7
7
  var codex = require('@fractary/codex');
8
+ var os = require('os');
9
+ var child_process = require('child_process');
8
10
  var commander = require('commander');
9
11
  var chalk8 = require('chalk');
10
12
  var crypto = require('crypto');
@@ -32,6 +34,7 @@ function _interopNamespace(e) {
32
34
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
33
35
  var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
34
36
  var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
37
+ var os__namespace = /*#__PURE__*/_interopNamespace(os);
35
38
  var chalk8__default = /*#__PURE__*/_interopDefault(chalk8);
36
39
  var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
37
40
 
@@ -619,6 +622,178 @@ var init_codex_client = __esm({
619
622
  }
620
623
  });
621
624
 
625
+ // src/utils/codex-repository.ts
626
+ var codex_repository_exports = {};
627
+ __export(codex_repository_exports, {
628
+ ensureCodexCloned: () => ensureCodexCloned,
629
+ getCodexRepoUrl: () => getCodexRepoUrl,
630
+ getTempCodexPath: () => getTempCodexPath,
631
+ isValidGitRepo: () => isValidGitRepo
632
+ });
633
+ function spawnAsync(command, args, options) {
634
+ return new Promise((resolve, reject) => {
635
+ const child = child_process.spawn(command, args, {
636
+ ...options,
637
+ env: process.env
638
+ });
639
+ let stdout = "";
640
+ let stderr = "";
641
+ child.stdout.on("data", (data) => {
642
+ stdout += data.toString();
643
+ });
644
+ child.stderr.on("data", (data) => {
645
+ stderr += data.toString();
646
+ });
647
+ child.on("error", (error) => {
648
+ reject(error);
649
+ });
650
+ child.on("close", (code) => {
651
+ if (code !== 0) {
652
+ const error = new Error(stderr || `Command exited with code ${code}`);
653
+ error.code = code;
654
+ reject(error);
655
+ } else {
656
+ resolve(stdout);
657
+ }
658
+ });
659
+ });
660
+ }
661
+ function sanitizePathComponent(component) {
662
+ if (!component || typeof component !== "string") {
663
+ throw new Error("Path component must be a non-empty string");
664
+ }
665
+ const sanitized = component.replace(/\.\./g, "").replace(/[/\\]/g, "").trim();
666
+ if (!sanitized) {
667
+ throw new Error(`Invalid path component: ${component}`);
668
+ }
669
+ return sanitized;
670
+ }
671
+ function validateGitHubName(name, type) {
672
+ if (!name || typeof name !== "string") {
673
+ throw new Error(`GitHub ${type} name must be a non-empty string`);
674
+ }
675
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
676
+ throw new Error(`Invalid GitHub ${type} name: ${name}. Must contain only alphanumeric characters, hyphens, underscores, and dots.`);
677
+ }
678
+ if (name.startsWith(".") || name.startsWith("-")) {
679
+ throw new Error(`GitHub ${type} name cannot start with a dot or hyphen: ${name}`);
680
+ }
681
+ }
682
+ function getTempCodexPath(config) {
683
+ const codexRepo = config.codex_repository || "codex";
684
+ const sanitizedOrg = sanitizePathComponent(config.organization);
685
+ const sanitizedRepo = sanitizePathComponent(codexRepo);
686
+ return path3__namespace.join(
687
+ os__namespace.tmpdir(),
688
+ "fractary-codex-clone",
689
+ `${sanitizedOrg}-${sanitizedRepo}-${process.pid}`
690
+ );
691
+ }
692
+ async function isValidGitRepo(repoPath) {
693
+ try {
694
+ const gitDir = path3__namespace.join(repoPath, ".git");
695
+ const stats = await fs__namespace.stat(gitDir);
696
+ return stats.isDirectory();
697
+ } catch {
698
+ return false;
699
+ }
700
+ }
701
+ function getCodexRepoUrl(config) {
702
+ const codexRepo = config.codex_repository || "codex";
703
+ validateGitHubName(config.organization, "organization");
704
+ validateGitHubName(codexRepo, "repository");
705
+ return `https://github.com/${config.organization}/${codexRepo}.git`;
706
+ }
707
+ async function execGit(repoPath, args) {
708
+ try {
709
+ const stdout = await spawnAsync("git", args, {
710
+ cwd: repoPath
711
+ });
712
+ return stdout;
713
+ } catch (error) {
714
+ if (error.code === "ENOENT") {
715
+ throw new Error(`Git command not found. Ensure git is installed and in PATH.`);
716
+ }
717
+ if (error.code === "EACCES") {
718
+ throw new Error(`Permission denied accessing repository at ${repoPath}`);
719
+ }
720
+ if (error.code === 128) {
721
+ throw new Error(`Git authentication failed. Check your credentials and repository access.`);
722
+ }
723
+ throw error;
724
+ }
725
+ }
726
+ async function gitClone(url, targetPath, options) {
727
+ const parentDir = path3__namespace.dirname(targetPath);
728
+ await fs__namespace.mkdir(parentDir, { recursive: true });
729
+ const args = ["clone"];
730
+ if (options?.depth) {
731
+ if (!Number.isInteger(options.depth) || options.depth <= 0) {
732
+ throw new Error(`Invalid depth parameter: ${options.depth}. Must be a positive integer.`);
733
+ }
734
+ args.push("--depth", String(options.depth));
735
+ }
736
+ if (options?.branch) {
737
+ if (!/^[\w\-./]+$/.test(options.branch)) {
738
+ throw new Error(`Invalid branch name: ${options.branch}`);
739
+ }
740
+ args.push("--branch", options.branch);
741
+ }
742
+ args.push("--single-branch");
743
+ args.push(url, targetPath);
744
+ await spawnAsync("git", args, { cwd: parentDir });
745
+ }
746
+ async function gitFetch(repoPath, branch) {
747
+ {
748
+ if (!/^[\w\-./]+$/.test(branch)) {
749
+ throw new Error(`Invalid branch name: ${branch}`);
750
+ }
751
+ await execGit(repoPath, ["fetch", "origin", `${branch}:${branch}`]);
752
+ }
753
+ }
754
+ async function gitCheckout(repoPath, branch) {
755
+ if (!/^[\w\-./]+$/.test(branch)) {
756
+ throw new Error(`Invalid branch name: ${branch}`);
757
+ }
758
+ await execGit(repoPath, ["checkout", branch]);
759
+ }
760
+ async function gitPull(repoPath) {
761
+ await execGit(repoPath, ["pull"]);
762
+ }
763
+ async function ensureCodexCloned(config, options) {
764
+ const tempPath = getTempCodexPath(config);
765
+ const branch = options?.branch || "main";
766
+ if (await isValidGitRepo(tempPath) && !options?.force) {
767
+ try {
768
+ await gitFetch(tempPath, branch);
769
+ await gitCheckout(tempPath, branch);
770
+ await gitPull(tempPath);
771
+ return tempPath;
772
+ } catch (error) {
773
+ console.warn(`Failed to update existing clone: ${error.message}`);
774
+ console.warn(`Removing and cloning fresh...`);
775
+ await fs__namespace.rm(tempPath, { recursive: true, force: true });
776
+ }
777
+ }
778
+ const repoUrl = getCodexRepoUrl(config);
779
+ try {
780
+ await fs__namespace.rm(tempPath, { recursive: true, force: true });
781
+ } catch (error) {
782
+ console.warn(`Could not remove existing directory ${tempPath}: ${error.message}`);
783
+ }
784
+ await gitClone(repoUrl, tempPath, {
785
+ branch,
786
+ depth: 1
787
+ // Shallow clone for efficiency
788
+ });
789
+ return tempPath;
790
+ }
791
+ var init_codex_repository = __esm({
792
+ "src/utils/codex-repository.ts"() {
793
+ init_cjs_shims();
794
+ }
795
+ });
796
+
622
797
  // src/cli.ts
623
798
  init_cjs_shims();
624
799
 
@@ -1501,15 +1676,50 @@ function syncCommand() {
1501
1676
  let plan;
1502
1677
  let routingScan;
1503
1678
  if (direction === "from-codex") {
1504
- const codexRepoPath = config.cacheDir || path3__namespace.join(process.cwd(), ".fractary", "codex-cache");
1505
- if (options.json) {
1506
- console.log(JSON.stringify({
1507
- info: "Routing-aware sync: scanning entire codex repository for files targeting this project",
1508
- codexPath: codexRepoPath
1509
- }, null, 2));
1510
- } else {
1511
- console.log(chalk8__default.default.blue("\u2139 Using routing-aware sync"));
1512
- console.log(chalk8__default.default.dim(" Scanning entire codex for files routing to this project...\n"));
1679
+ let codexRepoPath;
1680
+ try {
1681
+ const { ensureCodexCloned: ensureCodexCloned2 } = await Promise.resolve().then(() => (init_codex_repository(), codex_repository_exports));
1682
+ if (!options.json) {
1683
+ console.log(chalk8__default.default.blue("\u2139 Cloning/updating codex repository..."));
1684
+ }
1685
+ codexRepoPath = await ensureCodexCloned2(config, {
1686
+ branch: targetBranch
1687
+ });
1688
+ if (!options.json) {
1689
+ console.log(chalk8__default.default.dim(` Codex cloned to: ${codexRepoPath}`));
1690
+ console.log(chalk8__default.default.dim(" Scanning for files routing to this project...\n"));
1691
+ } else {
1692
+ console.log(JSON.stringify({
1693
+ info: "Routing-aware sync: cloned codex repository and scanning for files targeting this project",
1694
+ codexPath: codexRepoPath
1695
+ }, null, 2));
1696
+ }
1697
+ } catch (error) {
1698
+ console.error(chalk8__default.default.red("Error:"), "Failed to clone codex repository");
1699
+ console.error(chalk8__default.default.dim(` ${error.message}`));
1700
+ console.log(chalk8__default.default.yellow("\nTroubleshooting:"));
1701
+ if (error.message.includes("Git command not found")) {
1702
+ console.log(chalk8__default.default.dim(" Git is not installed or not in PATH."));
1703
+ console.log(chalk8__default.default.dim(" Install git: https://git-scm.com/downloads"));
1704
+ } else if (error.message.includes("authentication failed") || error.message.includes("Authentication failed")) {
1705
+ console.log(chalk8__default.default.dim(" GitHub authentication is required for private repositories."));
1706
+ console.log(chalk8__default.default.dim(" 1. Check auth status: gh auth status"));
1707
+ console.log(chalk8__default.default.dim(" 2. Login if needed: gh auth login"));
1708
+ console.log(chalk8__default.default.dim(" 3. Or set GITHUB_TOKEN environment variable"));
1709
+ } else if (error.message.includes("Permission denied")) {
1710
+ console.log(chalk8__default.default.dim(" Permission denied accessing repository files."));
1711
+ console.log(chalk8__default.default.dim(" 1. Check file/directory permissions"));
1712
+ console.log(chalk8__default.default.dim(" 2. Ensure you have access to the repository"));
1713
+ } else if (error.message.includes("not found") || error.message.includes("does not exist")) {
1714
+ console.log(chalk8__default.default.dim(` Repository not found: ${config.organization}/${config.codex_repository || "codex"}`));
1715
+ console.log(chalk8__default.default.dim(" 1. Verify the repository exists on GitHub"));
1716
+ console.log(chalk8__default.default.dim(" 2. Check organization and repository names in config"));
1717
+ } else {
1718
+ console.log(chalk8__default.default.dim(" 1. Ensure git is installed: git --version"));
1719
+ console.log(chalk8__default.default.dim(" 2. Check GitHub auth: gh auth status"));
1720
+ console.log(chalk8__default.default.dim(` 3. Verify repo exists: ${config.organization}/${config.codex_repository || "codex"}`));
1721
+ }
1722
+ process.exit(1);
1513
1723
  }
1514
1724
  const planWithRouting = await syncManager.createRoutingAwarePlan(
1515
1725
  config.organization,