@fractary/codex-cli 0.3.1 → 0.4.1

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
@@ -42,7 +42,7 @@ Options:
42
42
  **Example:**
43
43
  ```bash
44
44
  fractary-codex init
45
- # Creates .fractary/codex.yaml and .codex-cache/
45
+ # Creates .fractary/codex/config.yaml and .fractary/codex/cache/
46
46
  ```
47
47
 
48
48
  ### `fetch` - Fetch Documents
@@ -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.
@@ -234,11 +269,11 @@ Options:
234
269
 
235
270
  ## Configuration
236
271
 
237
- Codex uses `.fractary/codex.yaml` for configuration:
272
+ Codex uses `.fractary/codex/config.yaml` for configuration:
238
273
 
239
274
  ```yaml
240
275
  organization: myorg
241
- cacheDir: .codex-cache
276
+ cacheDir: .fractary/codex/cache
242
277
 
243
278
  storage:
244
279
  - type: github
@@ -328,16 +363,6 @@ All commands use lazy-loading to avoid SDK initialization overhead for simple op
328
363
 
329
364
  MIT
330
365
 
331
- ## Documentation
332
-
333
- - [Command Reference](../docs/guides/command-reference.md) - Complete command reference for all interfaces
334
- - [API Reference](../docs/guides/api-reference.md) - Complete API documentation
335
- - [CLI Integration Guide](../docs/guides/cli-integration.md) - How to integrate into CLI applications
336
- - [Configuration Guide](../docs/guides/configuration.md) - Configuration reference
337
- - [Naming Conventions](../docs/guides/naming-conventions.md) - Naming standards across all interfaces
338
- - [MCP Migration Guide](../docs/guides/mcp-migration-guide.md) - Migrating to new MCP tool names
339
- - [Troubleshooting](../docs/guides/troubleshooting.md) - Common issues and solutions
340
-
341
366
  ## Links
342
367
 
343
368
  - [GitHub Repository](https://github.com/fractary/codex)
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
 
@@ -90,7 +93,7 @@ async function migrateConfig(legacyConfigPath, options) {
90
93
  organization: legacy.organization || legacy.organizationSlug || "default"
91
94
  };
92
95
  if (legacy.cache) {
93
- yamlConfig.cacheDir = legacy.cache.directory || ".codex-cache";
96
+ yamlConfig.cacheDir = legacy.cache.directory || ".fractary/codex/cache";
94
97
  }
95
98
  if (legacy.storage?.providers) {
96
99
  yamlConfig.storage = [];
@@ -199,7 +202,7 @@ async function writeYamlConfig(config, outputPath) {
199
202
  function getDefaultYamlConfig(organization) {
200
203
  return {
201
204
  organization,
202
- cacheDir: ".codex-cache",
205
+ cacheDir: ".fractary/codex/cache",
203
206
  storage: [
204
207
  {
205
208
  type: "local",
@@ -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
 
@@ -767,24 +942,18 @@ function initCommand() {
767
942
  console.log(chalk8__default.default.dim(`Organization: ${chalk8__default.default.cyan(org)}
768
943
  `));
769
944
  }
770
- const configDir = path3__namespace.join(process.cwd(), ".fractary");
771
- const configPath = path3__namespace.join(configDir, "codex.yaml");
945
+ const configDir = path3__namespace.join(process.cwd(), ".fractary", "codex");
946
+ const configPath = path3__namespace.join(configDir, "config.yaml");
772
947
  const configExists = await fileExists(configPath);
773
- const legacyConfigPath = path3__namespace.join(process.cwd(), ".fractary", "plugins", "codex", "config.json");
774
- const legacyExists = await fileExists(legacyConfigPath);
775
948
  if (configExists && !options.force) {
776
- console.log(chalk8__default.default.yellow("\u26A0 Configuration already exists at .fractary/codex.yaml"));
949
+ console.log(chalk8__default.default.yellow("\u26A0 Configuration already exists at .fractary/codex/config.yaml"));
777
950
  console.log(chalk8__default.default.dim("Use --force to overwrite"));
778
951
  process.exit(1);
779
952
  }
780
- if (legacyExists && !configExists) {
781
- console.log(chalk8__default.default.yellow("\u26A0 Legacy configuration detected at .fractary/plugins/codex/config.json"));
782
- console.log(chalk8__default.default.dim('Run "fractary codex migrate" to upgrade to YAML format\n'));
783
- }
784
953
  console.log("Creating directory structure...");
785
954
  const dirs = [
786
- ".fractary",
787
- ".codex-cache"
955
+ ".fractary/codex",
956
+ ".fractary/codex/cache"
788
957
  ];
789
958
  for (const dir of dirs) {
790
959
  await fs__namespace.mkdir(path3__namespace.join(process.cwd(), dir), { recursive: true });
@@ -796,12 +965,12 @@ function initCommand() {
796
965
  config.mcp.enabled = true;
797
966
  }
798
967
  await writeYamlConfig(config, configPath);
799
- console.log(chalk8__default.default.green("\u2713"), chalk8__default.default.dim(".fractary/codex.yaml"));
800
- console.log(chalk8__default.default.green("\n\u2713 Codex v3.0 initialized successfully!\n"));
968
+ console.log(chalk8__default.default.green("\u2713"), chalk8__default.default.dim(".fractary/codex/config.yaml"));
969
+ console.log(chalk8__default.default.green("\n\u2713 Codex v4.0 initialized successfully!\n"));
801
970
  console.log(chalk8__default.default.bold("Configuration:"));
802
971
  console.log(chalk8__default.default.dim(` Organization: ${org}`));
803
- console.log(chalk8__default.default.dim(` Cache: .codex-cache/`));
804
- console.log(chalk8__default.default.dim(` Config: .fractary/codex.yaml`));
972
+ console.log(chalk8__default.default.dim(` Cache: .fractary/codex/cache/`));
973
+ console.log(chalk8__default.default.dim(` Config: .fractary/codex/config.yaml`));
805
974
  if (options.mcp) {
806
975
  console.log(chalk8__default.default.dim(` MCP Server: Enabled (port 3000)`));
807
976
  }
@@ -811,13 +980,9 @@ function initCommand() {
811
980
  console.log(chalk8__default.default.dim(" - HTTP endpoint"));
812
981
  console.log(chalk8__default.default.bold("\nNext steps:"));
813
982
  console.log(chalk8__default.default.dim(' 1. Set your GitHub token: export GITHUB_TOKEN="your_token"'));
814
- console.log(chalk8__default.default.dim(" 2. Edit .fractary/codex.yaml to configure storage providers"));
983
+ console.log(chalk8__default.default.dim(" 2. Edit .fractary/codex/config.yaml to configure storage providers"));
815
984
  console.log(chalk8__default.default.dim(" 3. Fetch a document: fractary codex fetch codex://org/project/path"));
816
985
  console.log(chalk8__default.default.dim(" 4. Check cache: fractary codex cache list"));
817
- if (legacyExists) {
818
- console.log(chalk8__default.default.yellow("\n\u26A0 Legacy config detected:"));
819
- console.log(chalk8__default.default.dim(' Run "fractary codex migrate" to convert your existing config'));
820
- }
821
986
  } catch (error) {
822
987
  console.error(chalk8__default.default.red("Error:"), error.message);
823
988
  process.exit(1);
@@ -1450,7 +1615,7 @@ function syncCommand() {
1450
1615
  let projectName = name;
1451
1616
  if (!projectName) {
1452
1617
  const detected = detectCurrentProject();
1453
- projectName = detected.project || null;
1618
+ projectName = detected.project || void 0;
1454
1619
  }
1455
1620
  if (!projectName) {
1456
1621
  console.error(chalk8__default.default.red("Error:"), "Could not determine project name.");
@@ -1473,7 +1638,7 @@ function syncCommand() {
1473
1638
  config: config.sync,
1474
1639
  manifestPath: path3__namespace.join(process.cwd(), ".fractary", ".codex-sync-manifest.json")
1475
1640
  });
1476
- const defaultPatterns = config.sync?.include || [
1641
+ const defaultPatterns = [
1477
1642
  "docs/**/*.md",
1478
1643
  "specs/**/*.md",
1479
1644
  ".fractary/standards/**",
@@ -1508,13 +1673,71 @@ function syncCommand() {
1508
1673
  include: includePatterns,
1509
1674
  exclude: excludePatterns
1510
1675
  };
1511
- const plan = await syncManager.createPlan(
1512
- config.organization,
1513
- projectName,
1514
- sourceDir,
1515
- targetFiles,
1516
- syncOptions
1517
- );
1676
+ let plan;
1677
+ let routingScan;
1678
+ if (direction === "from-codex") {
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);
1723
+ }
1724
+ const planWithRouting = await syncManager.createRoutingAwarePlan(
1725
+ config.organization,
1726
+ projectName,
1727
+ codexRepoPath,
1728
+ syncOptions
1729
+ );
1730
+ plan = planWithRouting;
1731
+ routingScan = planWithRouting.routingScan;
1732
+ } else {
1733
+ plan = await syncManager.createPlan(
1734
+ config.organization,
1735
+ projectName,
1736
+ sourceDir,
1737
+ targetFiles,
1738
+ syncOptions
1739
+ );
1740
+ }
1518
1741
  if (plan.totalFiles === 0) {
1519
1742
  if (options.json) {
1520
1743
  console.log(JSON.stringify({
@@ -1577,6 +1800,20 @@ function syncCommand() {
1577
1800
  if (plan.estimatedTime) {
1578
1801
  console.log(` Est. time: ${chalk8__default.default.dim(formatDuration(plan.estimatedTime))}`);
1579
1802
  }
1803
+ if (routingScan) {
1804
+ console.log("");
1805
+ console.log(chalk8__default.default.bold("Routing Statistics\n"));
1806
+ console.log(` Scanned: ${chalk8__default.default.cyan(routingScan.stats.totalScanned.toString())} files`);
1807
+ console.log(` Matched: ${chalk8__default.default.cyan(routingScan.stats.totalMatched.toString())} files`);
1808
+ console.log(` Source projects: ${chalk8__default.default.cyan(routingScan.stats.sourceProjects.length.toString())}`);
1809
+ if (routingScan.stats.sourceProjects.length > 0) {
1810
+ console.log(chalk8__default.default.dim(` ${routingScan.stats.sourceProjects.slice(0, 5).join(", ")}`));
1811
+ if (routingScan.stats.sourceProjects.length > 5) {
1812
+ console.log(chalk8__default.default.dim(` ... and ${routingScan.stats.sourceProjects.length - 5} more`));
1813
+ }
1814
+ }
1815
+ console.log(` Scan time: ${chalk8__default.default.dim(formatDuration(routingScan.stats.durationMs))}`);
1816
+ }
1580
1817
  console.log("");
1581
1818
  if (plan.conflicts.length > 0) {
1582
1819
  console.log(chalk8__default.default.yellow(`\u26A0 ${plan.conflicts.length} conflicts detected:`));