@dezkareid/ai-context-sync 0.0.1 → 1.1.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
@@ -1,45 +1,63 @@
1
1
  # @dezkareid/ai-context-sync
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ A CLI utility to synchronize AI agent context files across different providers, using `AGENTS.md` as the source of truth.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## Features
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ - **Source of Truth**: Uses `AGENTS.md` to define project context, rules, and workflows.
8
+ - **Multi-provider support**:
9
+ - **Claude**: Generates/updates `CLAUDE.md`.
10
+ - **Gemini**: Configures `.gemini/settings.json` to use `AGENTS.md`.
11
+ - **Gemini Markdown**: Generates/updates `GEMINI.md`.
12
+ - **Plugin Architecture**: Easily extendable to support other AI agents.
8
13
 
9
- ## Purpose
14
+ ## Installation
10
15
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@dezkareid/ai-context-sync`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
16
+ ```bash
17
+ pnpm install @dezkareid/ai-context-sync
18
+ ```
15
19
 
16
- ## What is OIDC Trusted Publishing?
20
+ ## Usage
17
21
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
22
+ ### Synchronize context
19
23
 
20
- ## Setup Instructions
24
+ Run the sync command in your project root (where `AGENTS.md` is located):
21
25
 
22
- To properly configure OIDC trusted publishing for this package:
26
+ ```bash
27
+ npx @dezkareid/ai-context-sync sync
28
+ ```
23
29
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
30
+ You can select the strategy using the `--strategy` (or `-s`) option:
28
31
 
29
- ## DO NOT USE THIS PACKAGE
32
+ ```bash
33
+ npx @dezkareid/ai-context-sync sync --strategy claude
34
+ npx @dezkareid/ai-context-sync sync --strategy gemini
35
+ npx @dezkareid/ai-context-sync sync --strategy all
36
+ npx @dezkareid/ai-context-sync sync --strategy "claude, gemini"
37
+ ```
30
38
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
39
+ If no strategy is provided, an interactive checkbox menu will appear to let you toggle which strategies to run.
36
40
 
37
- ## More Information
41
+ ### Configuration
38
42
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
43
+ The tool can save your selected strategies in a `.ai-context-configrc` file in the project root. This avoids being prompted every time you run the command.
42
44
 
43
- ---
45
+ To bypass reading or creating this configuration file, use the `--skip-config` flag:
44
46
 
45
- **Maintained for OIDC setup purposes only**
47
+ ```bash
48
+ npx @dezkareid/ai-context-sync sync --skip-config
49
+ ```
50
+
51
+ ### Directory option
52
+
53
+ ## How it works
54
+
55
+ 1. The tool looks for an `AGENTS.md` file in the target directory.
56
+ 2. It reads the content of `AGENTS.md`.
57
+ 3. It applies different strategies to update provider-specific files:
58
+ - **Claude**: Creates a symbolic link `CLAUDE.md` pointing to `AGENTS.md`.
59
+ - **Gemini**: Updates `.gemini/settings.json` to include `AGENTS.md` in the `context.fileName` list.
60
+
61
+ ## License
62
+
63
+ ISC
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIG_FILENAME = exports.AGENTS_FILENAME = void 0;
4
+ exports.AGENTS_FILENAME = 'AGENTS.md';
5
+ exports.CONFIG_FILENAME = '.ai-context-configrc';
package/dist/engine.js ADDED
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SyncEngine = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const constants_js_1 = require("./constants.js");
10
+ const claude_js_1 = require("./strategies/claude.js");
11
+ const gemini_js_1 = require("./strategies/gemini.js");
12
+ const gemini_md_js_1 = require("./strategies/gemini-md.js");
13
+ class SyncEngine {
14
+ allStrategies = [
15
+ new claude_js_1.ClaudeStrategy(),
16
+ new gemini_js_1.GeminiStrategy(),
17
+ new gemini_md_js_1.GeminiMdStrategy()
18
+ ];
19
+ async sync(projectRoot, selectedStrategies) {
20
+ const agentsPath = path_1.default.join(projectRoot, constants_js_1.AGENTS_FILENAME);
21
+ if (!(await fs_extra_1.default.pathExists(agentsPath))) {
22
+ throw new Error(`${constants_js_1.AGENTS_FILENAME} not found in ${projectRoot}`);
23
+ }
24
+ const context = await fs_extra_1.default.readFile(agentsPath, 'utf-8');
25
+ let strategiesToRun;
26
+ if (!selectedStrategies || (Array.isArray(selectedStrategies) && selectedStrategies.length === 0)) {
27
+ strategiesToRun = this.allStrategies;
28
+ }
29
+ else {
30
+ const selectedList = Array.isArray(selectedStrategies) ? selectedStrategies : [selectedStrategies];
31
+ const normalizedList = selectedList.map(s => s.toLowerCase());
32
+ if (normalizedList.includes('all') || normalizedList.includes('both')) {
33
+ strategiesToRun = this.allStrategies;
34
+ }
35
+ else {
36
+ strategiesToRun = this.allStrategies.filter(s => normalizedList.includes(s.name.toLowerCase()));
37
+ }
38
+ }
39
+ const availableNames = this.allStrategies.map(s => s.name).join(', ');
40
+ if (strategiesToRun.length === 0 && selectedStrategies) {
41
+ throw new Error(`No valid strategies found for: ${selectedStrategies}. Available strategies: ${availableNames}`);
42
+ }
43
+ for (const strategy of strategiesToRun) {
44
+ console.log(`Syncing for ${strategy.name}...`);
45
+ await strategy.sync(context, projectRoot);
46
+ }
47
+ }
48
+ }
49
+ exports.SyncEngine = SyncEngine;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const engine_js_1 = require("./engine.js");
8
+ const constants_js_1 = require("./constants.js");
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ (0, vitest_1.describe)('SyncEngine', () => {
13
+ let tempDir;
14
+ (0, vitest_1.beforeEach)(async () => {
15
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'sync-engine-test-'));
16
+ });
17
+ (0, vitest_1.afterEach)(async () => {
18
+ await fs_extra_1.default.remove(tempDir);
19
+ });
20
+ (0, vitest_1.it)('should throw error if AGENTS.md is missing', async () => {
21
+ const engine = new engine_js_1.SyncEngine();
22
+ await (0, vitest_1.expect)(engine.sync(tempDir)).rejects.toThrow(`${constants_js_1.AGENTS_FILENAME} not found`);
23
+ });
24
+ (0, vitest_1.it)('should run all strategies when AGENTS.md exists', async () => {
25
+ const engine = new engine_js_1.SyncEngine();
26
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
27
+ const context = '# Agent Context';
28
+ await fs_extra_1.default.writeFile(agentsPath, context);
29
+ await engine.sync(tempDir);
30
+ // Verify Claude
31
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
32
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(true);
33
+ (0, vitest_1.expect)(await fs_extra_1.default.readFile(claudePath, 'utf-8')).toBe(context);
34
+ // Verify Gemini
35
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
36
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsPath)).toBe(true);
37
+ const settings = await fs_extra_1.default.readJson(settingsPath);
38
+ (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
39
+ });
40
+ (0, vitest_1.it)('should run only Claude strategy when selected', async () => {
41
+ const engine = new engine_js_1.SyncEngine();
42
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
43
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
44
+ await engine.sync(tempDir, 'claude');
45
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
46
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(true);
47
+ const geminiDir = path_1.default.join(tempDir, '.gemini');
48
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(geminiDir)).toBe(false);
49
+ });
50
+ (0, vitest_1.it)('should run only Gemini strategy when selected', async () => {
51
+ const engine = new engine_js_1.SyncEngine();
52
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
53
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
54
+ await engine.sync(tempDir, 'gemini');
55
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
56
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(false);
57
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
58
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsPath)).toBe(true);
59
+ });
60
+ (0, vitest_1.it)('should run Gemini Markdown strategy when selected', async () => {
61
+ const engine = new engine_js_1.SyncEngine();
62
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
63
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
64
+ await engine.sync(tempDir, 'gemini-md');
65
+ const geminiMdPath = path_1.default.join(tempDir, 'GEMINI.md');
66
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(geminiMdPath)).toBe(true);
67
+ const stats = await fs_extra_1.default.lstat(geminiMdPath);
68
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
69
+ });
70
+ (0, vitest_1.it)('should run multiple selected strategies from array', async () => {
71
+ const engine = new engine_js_1.SyncEngine();
72
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
73
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
74
+ await engine.sync(tempDir, ['claude', 'gemini']);
75
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, 'CLAUDE.md'))).toBe(true);
76
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, '.gemini', 'settings.json'))).toBe(true);
77
+ });
78
+ (0, vitest_1.it)('should run all strategies when "all" is selected', async () => {
79
+ const engine = new engine_js_1.SyncEngine();
80
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
81
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
82
+ await engine.sync(tempDir, 'all');
83
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, 'CLAUDE.md'))).toBe(true);
84
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, '.gemini', 'settings.json'))).toBe(true);
85
+ });
86
+ (0, vitest_1.it)('should throw error for invalid strategy', async () => {
87
+ const engine = new engine_js_1.SyncEngine();
88
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
89
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
90
+ await (0, vitest_1.expect)(engine.sync(tempDir, 'invalid')).rejects.toThrow('No valid strategies found for: invalid. Available strategies: claude, gemini, gemini-md');
91
+ });
92
+ });
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const engine_js_1 = require("./engine.js");
9
+ const path_1 = __importDefault(require("path"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const constants_js_1 = require("./constants.js");
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name('ai-context-sync')
16
+ .description('Sync AI context files across different providers')
17
+ .version('1.0.0');
18
+ program
19
+ .command('sync')
20
+ .description('Synchronize context files from AGENTS.md')
21
+ .option('-d, --dir <path>', 'Project directory', process.cwd())
22
+ .option('-s, --strategy <strategy>', 'Sync strategy (claude, gemini, all, or comma-separated list)')
23
+ .option('--skip-config', 'Avoid reading/creating the config file', false)
24
+ .action(async (options) => {
25
+ try {
26
+ const projectRoot = path_1.default.resolve(options.dir);
27
+ const configPath = path_1.default.join(projectRoot, constants_js_1.CONFIG_FILENAME);
28
+ let strategy = options.strategy;
29
+ // 1. If no strategy provided, try to read from config
30
+ if (!strategy && !options.skipConfig) {
31
+ if (await fs_extra_1.default.pathExists(configPath)) {
32
+ try {
33
+ const config = await fs_extra_1.default.readJson(configPath);
34
+ if (config.strategies) {
35
+ strategy = config.strategies;
36
+ }
37
+ }
38
+ catch (e) {
39
+ // Ignore corrupted config and proceed to prompt
40
+ }
41
+ }
42
+ }
43
+ // 2. If still no strategy, prompt user
44
+ if (!strategy) {
45
+ const answers = await inquirer_1.default.prompt([
46
+ {
47
+ type: 'checkbox',
48
+ name: 'strategies',
49
+ message: 'Select the AI context files to sync:',
50
+ choices: [
51
+ { name: 'Claude (CLAUDE.md)', value: 'claude', checked: true },
52
+ { name: 'Gemini (.gemini/settings.json)', value: 'gemini', checked: true },
53
+ { name: 'Gemini Markdown (GEMINI.md)', value: 'gemini-md', checked: true }
54
+ ],
55
+ validate: (answer) => {
56
+ if (answer.length < 1) {
57
+ return 'You must choose at least one strategy.';
58
+ }
59
+ return true;
60
+ }
61
+ }
62
+ ]);
63
+ strategy = answers.strategies;
64
+ }
65
+ else if (typeof strategy === 'string') {
66
+ strategy = strategy.split(',').map(s => s.trim());
67
+ }
68
+ // 3. Save strategy to config if not skipping
69
+ if (!options.skipConfig) {
70
+ await fs_extra_1.default.writeJson(configPath, { strategies: strategy }, { spaces: 2 });
71
+ }
72
+ const engine = new engine_js_1.SyncEngine();
73
+ await engine.sync(projectRoot, strategy);
74
+ const strategyMsg = Array.isArray(strategy) ? strategy.join(', ') : strategy;
75
+ console.log(`Successfully synchronized context files using "${strategyMsg}"!`);
76
+ }
77
+ catch (error) {
78
+ console.error(`Error: ${error.message}`);
79
+ process.exit(1);
80
+ }
81
+ });
82
+ program.parse();
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClaudeStrategy = void 0;
4
+ const symlink_js_1 = require("./symlink.js");
5
+ class ClaudeStrategy extends symlink_js_1.SymlinkStrategy {
6
+ name = 'claude';
7
+ targetFilename = 'CLAUDE.md';
8
+ }
9
+ exports.ClaudeStrategy = ClaudeStrategy;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const claude_js_1 = require("./claude.js");
8
+ const constants_js_1 = require("../constants.js");
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ (0, vitest_1.describe)('ClaudeStrategy', () => {
13
+ let tempDir;
14
+ (0, vitest_1.beforeEach)(async () => {
15
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'claude-strategy-test-'));
16
+ });
17
+ (0, vitest_1.afterEach)(async () => {
18
+ await fs_extra_1.default.remove(tempDir);
19
+ });
20
+ (0, vitest_1.it)('should create a symbolic link CLAUDE.md pointing to AGENTS.md', async () => {
21
+ const strategy = new claude_js_1.ClaudeStrategy();
22
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
23
+ await fs_extra_1.default.writeFile(agentsPath, '# Agents');
24
+ await strategy.sync('# Agents', tempDir);
25
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
26
+ const stats = await fs_extra_1.default.lstat(claudePath);
27
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
28
+ const target = await fs_extra_1.default.readlink(claudePath);
29
+ (0, vitest_1.expect)(target).toBe(constants_js_1.AGENTS_FILENAME);
30
+ });
31
+ });
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GeminiMdStrategy = void 0;
4
+ const symlink_js_1 = require("./symlink.js");
5
+ class GeminiMdStrategy extends symlink_js_1.SymlinkStrategy {
6
+ name = 'gemini-md';
7
+ targetFilename = 'GEMINI.md';
8
+ }
9
+ exports.GeminiMdStrategy = GeminiMdStrategy;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const gemini_md_js_1 = require("./gemini-md.js");
8
+ const index_js_1 = require("./index.js");
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ (0, vitest_1.describe)('GeminiMdStrategy', () => {
13
+ let tempDir;
14
+ let strategy;
15
+ (0, vitest_1.beforeEach)(async () => {
16
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'gemini-md-strategy-test-'));
17
+ await fs_extra_1.default.writeFile(path_1.default.join(tempDir, index_js_1.AGENTS_FILE), '# Agents');
18
+ strategy = new gemini_md_js_1.GeminiMdStrategy();
19
+ });
20
+ (0, vitest_1.afterEach)(async () => {
21
+ await fs_extra_1.default.remove(tempDir);
22
+ });
23
+ (0, vitest_1.it)('should create a symbolic link GEMINI.md pointing to AGENTS.md', async () => {
24
+ await strategy.sync('', tempDir);
25
+ const targetPath = path_1.default.join(tempDir, 'GEMINI.md');
26
+ const stats = await fs_extra_1.default.lstat(targetPath);
27
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
28
+ const target = await fs_extra_1.default.readlink(targetPath);
29
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
30
+ });
31
+ });
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GeminiStrategy = void 0;
7
+ const index_js_1 = require("./index.js");
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ class GeminiStrategy {
11
+ name = 'gemini';
12
+ async sync(_context, projectRoot) {
13
+ const geminiDir = path_1.default.join(projectRoot, '.gemini');
14
+ const settingsPath = path_1.default.join(geminiDir, 'settings.json');
15
+ await fs_extra_1.default.ensureDir(geminiDir);
16
+ let settings = {};
17
+ let exists = false;
18
+ if (await fs_extra_1.default.pathExists(settingsPath)) {
19
+ exists = true;
20
+ try {
21
+ settings = await fs_extra_1.default.readJson(settingsPath);
22
+ }
23
+ catch (e) {
24
+ settings = {};
25
+ }
26
+ }
27
+ if (!settings.context) {
28
+ settings.context = {};
29
+ }
30
+ const currentFiles = settings.context.fileName;
31
+ let modified = false;
32
+ if (Array.isArray(currentFiles)) {
33
+ if (!currentFiles.includes(index_js_1.AGENTS_FILE)) {
34
+ settings.context.fileName = [...currentFiles, index_js_1.AGENTS_FILE];
35
+ modified = true;
36
+ }
37
+ }
38
+ else if (typeof currentFiles === 'string') {
39
+ if (currentFiles !== index_js_1.AGENTS_FILE) {
40
+ settings.context.fileName = [currentFiles, index_js_1.AGENTS_FILE];
41
+ modified = true;
42
+ }
43
+ }
44
+ else {
45
+ settings.context.fileName = [index_js_1.AGENTS_FILE];
46
+ modified = true;
47
+ }
48
+ if (modified || !exists) {
49
+ await fs_extra_1.default.writeJson(settingsPath, settings, { spaces: 2 });
50
+ }
51
+ }
52
+ }
53
+ exports.GeminiStrategy = GeminiStrategy;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const gemini_js_1 = require("./gemini.js");
8
+ const constants_js_1 = require("../constants.js");
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ (0, vitest_1.describe)('GeminiStrategy', () => {
13
+ let tempDir;
14
+ (0, vitest_1.beforeEach)(async () => {
15
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'gemini-strategy-test-'));
16
+ });
17
+ (0, vitest_1.afterEach)(async () => {
18
+ await fs_extra_1.default.remove(tempDir);
19
+ });
20
+ (0, vitest_1.it)('should create .gemini/settings.json if it does not exist', async () => {
21
+ const strategy = new gemini_js_1.GeminiStrategy();
22
+ await strategy.sync('context', tempDir);
23
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
24
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsPath)).toBe(true);
25
+ const settings = await fs_extra_1.default.readJson(settingsPath);
26
+ (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
27
+ });
28
+ (0, vitest_1.it)('should update existing context.fileName array', async () => {
29
+ const strategy = new gemini_js_1.GeminiStrategy();
30
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
31
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
32
+ await fs_extra_1.default.writeJson(settingsPath, {
33
+ context: {
34
+ fileName: ['OTHER.md']
35
+ }
36
+ });
37
+ await strategy.sync('context', tempDir);
38
+ const settings = await fs_extra_1.default.readJson(settingsPath);
39
+ (0, vitest_1.expect)(settings.context.fileName).toContain('OTHER.md');
40
+ (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
41
+ });
42
+ (0, vitest_1.it)('should convert string fileName to array and add AGENTS.md', async () => {
43
+ const strategy = new gemini_js_1.GeminiStrategy();
44
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
45
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
46
+ await fs_extra_1.default.writeJson(settingsPath, {
47
+ context: {
48
+ fileName: 'OTHER.md'
49
+ }
50
+ });
51
+ await strategy.sync('context', tempDir);
52
+ const settings = await fs_extra_1.default.readJson(settingsPath);
53
+ (0, vitest_1.expect)(Array.isArray(settings.context.fileName)).toBe(true);
54
+ (0, vitest_1.expect)(settings.context.fileName).toContain('OTHER.md');
55
+ (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
56
+ });
57
+ (0, vitest_1.it)('should not add AGENTS.md twice', async () => {
58
+ const strategy = new gemini_js_1.GeminiStrategy();
59
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
60
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
61
+ await fs_extra_1.default.writeJson(settingsPath, {
62
+ context: {
63
+ fileName: [constants_js_1.AGENTS_FILENAME]
64
+ }
65
+ });
66
+ await strategy.sync('context', tempDir);
67
+ const settings = await fs_extra_1.default.readJson(settingsPath);
68
+ (0, vitest_1.expect)(settings.context.fileName.filter((f) => f === constants_js_1.AGENTS_FILENAME).length).toBe(1);
69
+ });
70
+ (0, vitest_1.it)('should not write to settings.json if AGENTS.md is already present', async () => {
71
+ const strategy = new gemini_js_1.GeminiStrategy();
72
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
73
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
74
+ await fs_extra_1.default.writeJson(settingsPath, {
75
+ context: {
76
+ fileName: [constants_js_1.AGENTS_FILENAME]
77
+ }
78
+ });
79
+ const mtimeBefore = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
80
+ // Wait a bit to ensure mtime would change if written
81
+ await new Promise(resolve => setTimeout(resolve, 10));
82
+ await strategy.sync('context', tempDir);
83
+ const mtimeAfter = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
84
+ (0, vitest_1.expect)(mtimeAfter).toBe(mtimeBefore);
85
+ });
86
+ (0, vitest_1.it)('should not write to settings.json if fileName is already AGENTS.md as a string', async () => {
87
+ const strategy = new gemini_js_1.GeminiStrategy();
88
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
89
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
90
+ await fs_extra_1.default.writeJson(settingsPath, {
91
+ context: {
92
+ fileName: constants_js_1.AGENTS_FILENAME
93
+ }
94
+ });
95
+ const mtimeBefore = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
96
+ await new Promise(resolve => setTimeout(resolve, 10));
97
+ await strategy.sync('context', tempDir);
98
+ const mtimeAfter = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
99
+ (0, vitest_1.expect)(mtimeAfter).toBe(mtimeBefore);
100
+ });
101
+ (0, vitest_1.it)('should handle invalid JSON in settings.json by starting fresh', async () => {
102
+ const strategy = new gemini_js_1.GeminiStrategy();
103
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
104
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
105
+ await fs_extra_1.default.writeFile(settingsPath, 'not a json');
106
+ await strategy.sync('context', tempDir);
107
+ const settings = await fs_extra_1.default.readJson(settingsPath);
108
+ (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
109
+ });
110
+ });
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENTS_FILE = void 0;
4
+ const constants_js_1 = require("../constants.js");
5
+ exports.AGENTS_FILE = constants_js_1.AGENTS_FILENAME;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SymlinkStrategy = void 0;
7
+ const index_js_1 = require("./index.js");
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ class SymlinkStrategy {
11
+ async sync(_context, projectRoot) {
12
+ const targetPath = path_1.default.join(projectRoot, this.targetFilename);
13
+ // Check if it exists and what type it is
14
+ if (await fs_extra_1.default.pathExists(targetPath)) {
15
+ const stats = await fs_extra_1.default.lstat(targetPath);
16
+ if (stats.isSymbolicLink()) {
17
+ const target = await fs_extra_1.default.readlink(targetPath);
18
+ if (target === index_js_1.AGENTS_FILE) {
19
+ // Already points to AGENTS.md, nothing to do
20
+ return;
21
+ }
22
+ }
23
+ // If it's a file or a link to somewhere else, remove it
24
+ await fs_extra_1.default.remove(targetPath);
25
+ }
26
+ // Create relative symlink
27
+ await fs_extra_1.default.ensureSymlink(index_js_1.AGENTS_FILE, targetPath);
28
+ }
29
+ }
30
+ exports.SymlinkStrategy = SymlinkStrategy;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const symlink_js_1 = require("./symlink.js");
8
+ const index_js_1 = require("./index.js");
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ class TestSymlinkStrategy extends symlink_js_1.SymlinkStrategy {
13
+ name = 'test';
14
+ targetFilename = 'TEST.md';
15
+ }
16
+ (0, vitest_1.describe)('SymlinkStrategy', () => {
17
+ let tempDir;
18
+ let strategy;
19
+ (0, vitest_1.beforeEach)(async () => {
20
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'symlink-strategy-test-'));
21
+ await fs_extra_1.default.writeFile(path_1.default.join(tempDir, index_js_1.AGENTS_FILE), '# Agents');
22
+ strategy = new TestSymlinkStrategy();
23
+ });
24
+ (0, vitest_1.afterEach)(async () => {
25
+ await fs_extra_1.default.remove(tempDir);
26
+ });
27
+ (0, vitest_1.it)('should create a symlink to AGENTS.md', async () => {
28
+ await strategy.sync('', tempDir);
29
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
30
+ const stats = await fs_extra_1.default.lstat(targetPath);
31
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
32
+ const target = await fs_extra_1.default.readlink(targetPath);
33
+ // It might be absolute or relative depending on environment/implementation
34
+ (0, vitest_1.expect)(target).toContain(index_js_1.AGENTS_FILE);
35
+ });
36
+ (0, vitest_1.it)('should not recreate symlink if it already points to AGENTS.md', async () => {
37
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
38
+ await fs_extra_1.default.ensureSymlink(index_js_1.AGENTS_FILE, targetPath);
39
+ // Get initial mtime
40
+ const initialStats = await fs_extra_1.default.lstat(targetPath);
41
+ await strategy.sync('', tempDir);
42
+ const finalStats = await fs_extra_1.default.lstat(targetPath);
43
+ (0, vitest_1.expect)(finalStats.mtimeMs).toBe(initialStats.mtimeMs);
44
+ const target = await fs_extra_1.default.readlink(targetPath);
45
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
46
+ });
47
+ (0, vitest_1.it)('should recreate symlink if it points elsewhere', async () => {
48
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
49
+ const otherFile = path_1.default.join(tempDir, 'OTHER.md');
50
+ await fs_extra_1.default.writeFile(otherFile, 'other content');
51
+ await fs_extra_1.default.ensureSymlink('OTHER.md', targetPath);
52
+ await strategy.sync('', tempDir);
53
+ const target = await fs_extra_1.default.readlink(targetPath);
54
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
55
+ });
56
+ (0, vitest_1.it)('should replace existing file with a symlink', async () => {
57
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
58
+ await fs_extra_1.default.writeFile(targetPath, 'existing file');
59
+ await strategy.sync('', tempDir);
60
+ const stats = await fs_extra_1.default.lstat(targetPath);
61
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
62
+ const target = await fs_extra_1.default.readlink(targetPath);
63
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
64
+ });
65
+ });
package/package.json CHANGED
@@ -1,10 +1,56 @@
1
1
  {
2
2
  "name": "@dezkareid/ai-context-sync",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @dezkareid/ai-context-sync",
3
+ "version": "1.1.0",
4
+ "description": "CLI utility to synchronize AI agent context files from AGENTS.md",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "ai-context-sync": "./dist/index.js"
8
+ },
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "provenance": true
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/dezkareid/dezkareid.git",
20
+ "directory": "packages/ai-context-sync"
21
+ },
5
22
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
23
+ "ai",
24
+ "context",
25
+ "sync",
26
+ "claude",
27
+ "gemini",
28
+ "agents"
29
+ ],
30
+ "author": "Joel Humberto Gomez Paredes <elmaildeldezkareid@gmail.com>",
31
+ "license": "ISC",
32
+ "bugs": {
33
+ "url": "https://github.com/dezkareid/dezkareid/issues"
34
+ },
35
+ "homepage": "https://github.com/dezkareid/dezkareid/tree/main/projects/dezkareid/packages/ai-context-sync#readme",
36
+ "dependencies": {
37
+ "commander": "12.0.0",
38
+ "fs-extra": "11.2.0",
39
+ "globby": "14.0.1",
40
+ "inquirer": "9.2.12"
41
+ },
42
+ "devDependencies": {
43
+ "@types/fs-extra": "11.0.4",
44
+ "@types/inquirer": "9.0.7",
45
+ "@types/node": "25.0.10",
46
+ "@vitest/coverage-v8": "4.0.18",
47
+ "typescript": "5.9.3",
48
+ "vitest": "4.0.18"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "start": "node dist/index.js",
53
+ "dev": "tsc -w",
54
+ "test": "vitest run --coverage"
55
+ }
56
+ }