agents-sync 0.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/SCHEMA.MD +156 -0
- package/agents.config.json +22 -0
- package/dist/cli.js +2 -0
- package/dist/config.js +21 -0
- package/dist/index.js +132 -0
- package/dist/schema.js +83 -0
- package/dist/translation/claude.js +43 -0
- package/dist/translation/codex.js +47 -0
- package/dist/translation/gemini.js +50 -0
- package/dist/translation/index.js +13 -0
- package/package.json +28 -0
- package/readme.md +46 -0
package/SCHEMA.MD
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Canonical Agents Schema (Markdown)
|
|
2
|
+
|
|
3
|
+
This repo treats `.agents` as the source of truth. Each file is Markdown with an optional metadata block at the top. The watcher parses these files and generates tool-specific outputs for Claude, Gemini, and Codex.
|
|
4
|
+
|
|
5
|
+
## Folder Layout
|
|
6
|
+
|
|
7
|
+
`.agents/commands/` holds command definitions.
|
|
8
|
+
`.agents/hooks/` holds hook definitions.
|
|
9
|
+
`.agents/skills/` holds skill definitions.
|
|
10
|
+
`AGENTS.MD` is the global config document.
|
|
11
|
+
|
|
12
|
+
## Canonical File Format
|
|
13
|
+
|
|
14
|
+
Each file is Markdown. At the top you can include a metadata fence:
|
|
15
|
+
|
|
16
|
+
```agents
|
|
17
|
+
id: hi
|
|
18
|
+
type: command
|
|
19
|
+
title: Say Hello
|
|
20
|
+
description: Greet the user politely.
|
|
21
|
+
aliases: hello, greet
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Everything after the `agents` fence is the body. The body becomes the tool-specific prompt or instructions.
|
|
25
|
+
|
|
26
|
+
### Required Fields
|
|
27
|
+
|
|
28
|
+
`id` or `name` must be present, or it will be inferred from the filename.
|
|
29
|
+
`type` can be omitted if the folder implies it (`commands`, `hooks`, `skills`).
|
|
30
|
+
|
|
31
|
+
### Optional Fields
|
|
32
|
+
|
|
33
|
+
`title` short label for humans.
|
|
34
|
+
`description` short summary.
|
|
35
|
+
`aliases` comma-separated list of alternate names.
|
|
36
|
+
`event` for hooks (example: `event: pre-commit`).
|
|
37
|
+
`tags` and `tools` are allowed but currently not emitted to targets.
|
|
38
|
+
|
|
39
|
+
### List Fields
|
|
40
|
+
|
|
41
|
+
For list fields (`aliases`, `tags`, `tools`) you can use:
|
|
42
|
+
|
|
43
|
+
`aliases: hello, greet` or `aliases: ["hello", "greet"]`.
|
|
44
|
+
|
|
45
|
+
## AGENTS.MD Schema
|
|
46
|
+
|
|
47
|
+
`AGENTS.MD` is the canonical global configuration document. It uses the same metadata fence and body rules.
|
|
48
|
+
|
|
49
|
+
Recommended template:
|
|
50
|
+
|
|
51
|
+
```agents
|
|
52
|
+
id: workspace
|
|
53
|
+
type: config
|
|
54
|
+
title: Workspace Defaults
|
|
55
|
+
description: Global guidance applied across tools.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use the body for shared rules, style, and constraints that should apply everywhere.
|
|
59
|
+
|
|
60
|
+
## Canonical Examples
|
|
61
|
+
|
|
62
|
+
### Command
|
|
63
|
+
|
|
64
|
+
```agents
|
|
65
|
+
id: hi
|
|
66
|
+
type: command
|
|
67
|
+
title: Say Hello
|
|
68
|
+
description: Greet the user politely.
|
|
69
|
+
aliases: hello, greet
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Write a friendly hello and ask how the user wants to proceed.
|
|
73
|
+
|
|
74
|
+
### Hook
|
|
75
|
+
|
|
76
|
+
```agents
|
|
77
|
+
id: before_commit
|
|
78
|
+
type: hook
|
|
79
|
+
title: Pre-Commit Guard
|
|
80
|
+
event: pre-commit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Check for unstaged files and remind the user to run tests.
|
|
84
|
+
|
|
85
|
+
### Skill
|
|
86
|
+
|
|
87
|
+
```agents
|
|
88
|
+
id: bug_triage
|
|
89
|
+
type: skill
|
|
90
|
+
title: Bug Triage
|
|
91
|
+
description: Quick triage checklist.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Classify severity, confirm reproduction steps, and identify owners.
|
|
95
|
+
|
|
96
|
+
### Config (Root)
|
|
97
|
+
|
|
98
|
+
`AGENTS.MD` is the shared config document.
|
|
99
|
+
|
|
100
|
+
```agents
|
|
101
|
+
type: config
|
|
102
|
+
title: Workspace Defaults
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This is the global guidance for the workspace, shared across tools.
|
|
106
|
+
|
|
107
|
+
## Translation Rules
|
|
108
|
+
|
|
109
|
+
The translation layer renders tool-specific files using the canonical metadata and body.
|
|
110
|
+
|
|
111
|
+
### Claude
|
|
112
|
+
|
|
113
|
+
Output format: Markdown.
|
|
114
|
+
Output paths:
|
|
115
|
+
`.claude/commands/<id>.md`
|
|
116
|
+
`.claude/hooks/<id>.md`
|
|
117
|
+
`.claude/skills/<id>.md`
|
|
118
|
+
`CLAUDE.MD` from `AGENTS.MD`.
|
|
119
|
+
|
|
120
|
+
Mapping:
|
|
121
|
+
`id` becomes the command name.
|
|
122
|
+
`description`, `title`, `aliases`, and `event` are emitted as YAML frontmatter.
|
|
123
|
+
Body is emitted as the command or hook prompt.
|
|
124
|
+
|
|
125
|
+
### Gemini
|
|
126
|
+
|
|
127
|
+
Output format: TOML.
|
|
128
|
+
Output paths:
|
|
129
|
+
`.gemini/commands/<id>.toml`
|
|
130
|
+
`.gemini/hooks/<id>.toml`
|
|
131
|
+
`.gemini/skills/<id>.toml`
|
|
132
|
+
`GEMINI.MD` from `AGENTS.MD`.
|
|
133
|
+
|
|
134
|
+
Mapping:
|
|
135
|
+
`type`, `name`, `title`, `description`, `aliases`, and `event` become TOML keys.
|
|
136
|
+
Body becomes `prompt = """..."""`.
|
|
137
|
+
|
|
138
|
+
### Codex
|
|
139
|
+
|
|
140
|
+
Output format: Markdown.
|
|
141
|
+
Output paths:
|
|
142
|
+
`.codex/commands/<id>.md`
|
|
143
|
+
`.codex/hooks/<id>.md`
|
|
144
|
+
`.codex/skills/<id>.md`
|
|
145
|
+
`CODEX.MD` from `AGENTS.MD`.
|
|
146
|
+
|
|
147
|
+
Mapping:
|
|
148
|
+
Top heading `# <type>: <id>`.
|
|
149
|
+
`title`, `description`, `aliases`, and `event` emitted as labeled lines.
|
|
150
|
+
Body emitted as the prompt.
|
|
151
|
+
|
|
152
|
+
## Notes
|
|
153
|
+
|
|
154
|
+
The metadata fence must be the first block in the file to be parsed.
|
|
155
|
+
Everything after the metadata fence is treated as the body.
|
|
156
|
+
If metadata is missing, `type` is inferred from the folder and `id` from the filename.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceDir": ".agents",
|
|
3
|
+
"agentsSubdir": "",
|
|
4
|
+
"targets": {
|
|
5
|
+
"claude": {
|
|
6
|
+
"outputDir": ".claude"
|
|
7
|
+
},
|
|
8
|
+
"codex": {
|
|
9
|
+
"outputDir": ".codex"
|
|
10
|
+
},
|
|
11
|
+
"gemini": {
|
|
12
|
+
"outputDir": ".gemini",
|
|
13
|
+
"agentFileExtension": ".toml"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"rootDoc": "AGENTS.MD",
|
|
17
|
+
"rootTargets": {
|
|
18
|
+
"claude": "CLAUDE.MD",
|
|
19
|
+
"gemini": "GEMINI.MD",
|
|
20
|
+
"codex": "CODEX.MD"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/cli.js
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
sourceDir: ".agents",
|
|
6
|
+
agentsSubdir: "",
|
|
7
|
+
targets: {},
|
|
8
|
+
rootDoc: "AGENTS.MD",
|
|
9
|
+
rootTargets: {}
|
|
10
|
+
};
|
|
11
|
+
export const loadConfig = async (configPath) => {
|
|
12
|
+
if (!existsSync(configPath))
|
|
13
|
+
return DEFAULT_CONFIG;
|
|
14
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
17
|
+
};
|
|
18
|
+
export const resolveTargetAgentsDir = (cwd, target, agentsSubdir) => {
|
|
19
|
+
const base = target.agentsOutputDir ?? path.join(target.outputDir, agentsSubdir);
|
|
20
|
+
return path.resolve(cwd, base);
|
|
21
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import chokidar from "chokidar";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { loadConfig, resolveTargetAgentsDir } from "./config.js";
|
|
6
|
+
import { parseAgentsMarkdown, inferKindFromPath } from "./schema.js";
|
|
7
|
+
import { translators } from "./translation/index.js";
|
|
8
|
+
const CWD = process.cwd();
|
|
9
|
+
const CONFIG_PATH = path.join(CWD, "agents.config.json");
|
|
10
|
+
const ensureDir = async (dirPath) => {
|
|
11
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
12
|
+
};
|
|
13
|
+
const writeIfChanged = async (filePath, content) => {
|
|
14
|
+
let existing = null;
|
|
15
|
+
try {
|
|
16
|
+
existing = await fs.readFile(filePath, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
existing = null;
|
|
20
|
+
}
|
|
21
|
+
if (existing === content)
|
|
22
|
+
return;
|
|
23
|
+
await ensureDir(path.dirname(filePath));
|
|
24
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
25
|
+
};
|
|
26
|
+
const walkFiles = async (dirPath) => {
|
|
27
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
28
|
+
const files = [];
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const full = path.join(dirPath, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
files.push(...(await walkFiles(full)));
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile()) {
|
|
35
|
+
files.push(full);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return files;
|
|
39
|
+
};
|
|
40
|
+
const main = async () => {
|
|
41
|
+
const config = await loadConfig(CONFIG_PATH);
|
|
42
|
+
const sourceDir = path.resolve(CWD, config.sourceDir);
|
|
43
|
+
const rootDoc = path.resolve(CWD, config.rootDoc);
|
|
44
|
+
const targetEntries = Object.entries(config.targets);
|
|
45
|
+
for (const [, target] of targetEntries) {
|
|
46
|
+
await ensureDir(resolveTargetAgentsDir(CWD, target, config.agentsSubdir));
|
|
47
|
+
}
|
|
48
|
+
const syncSourceFile = async (filePath) => {
|
|
49
|
+
const rel = path.relative(sourceDir, filePath);
|
|
50
|
+
const kind = inferKindFromPath(rel);
|
|
51
|
+
if (!kind)
|
|
52
|
+
return;
|
|
53
|
+
const id = path.basename(rel, path.extname(rel));
|
|
54
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
55
|
+
const doc = parseAgentsMarkdown(content, { kind, id });
|
|
56
|
+
await Promise.all(targetEntries.map(async ([targetKey, target]) => {
|
|
57
|
+
const translator = translators[targetKey];
|
|
58
|
+
if (!translator)
|
|
59
|
+
return;
|
|
60
|
+
const baseDir = resolveTargetAgentsDir(CWD, target, config.agentsSubdir);
|
|
61
|
+
const subdir = translator.subdir(doc, target);
|
|
62
|
+
const ext = translator.fileExtension(doc, target);
|
|
63
|
+
const outPath = path.join(baseDir, subdir, `${doc.id}${ext}`);
|
|
64
|
+
const translated = translator.renderDoc(doc, target);
|
|
65
|
+
await writeIfChanged(outPath, translated);
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
const removeSourceFile = async (filePath) => {
|
|
69
|
+
const rel = path.relative(sourceDir, filePath);
|
|
70
|
+
const kind = inferKindFromPath(rel);
|
|
71
|
+
if (!kind)
|
|
72
|
+
return;
|
|
73
|
+
const id = path.basename(rel, path.extname(rel));
|
|
74
|
+
const doc = parseAgentsMarkdown("", { kind, id });
|
|
75
|
+
await Promise.all(targetEntries.map(async ([targetKey, target]) => {
|
|
76
|
+
const translator = translators[targetKey];
|
|
77
|
+
if (!translator)
|
|
78
|
+
return;
|
|
79
|
+
const baseDir = resolveTargetAgentsDir(CWD, target, config.agentsSubdir);
|
|
80
|
+
const subdir = translator.subdir(doc, target);
|
|
81
|
+
const ext = translator.fileExtension(doc, target);
|
|
82
|
+
const outPath = path.join(baseDir, subdir, `${id}${ext}`);
|
|
83
|
+
await fs.rm(outPath, { force: true });
|
|
84
|
+
}));
|
|
85
|
+
};
|
|
86
|
+
const syncRootDoc = async () => {
|
|
87
|
+
if (!existsSync(rootDoc))
|
|
88
|
+
return;
|
|
89
|
+
const content = await fs.readFile(rootDoc, "utf8");
|
|
90
|
+
const doc = parseAgentsMarkdown(content, { kind: "config", id: "root" });
|
|
91
|
+
const rootTargets = config.rootTargets;
|
|
92
|
+
await Promise.all(Object.entries(rootTargets).map(async ([targetKey, outName]) => {
|
|
93
|
+
const translator = translators[targetKey];
|
|
94
|
+
if (!translator)
|
|
95
|
+
return;
|
|
96
|
+
const outPath = path.resolve(CWD, outName);
|
|
97
|
+
const translated = translator.renderDoc(doc, config.targets[targetKey]);
|
|
98
|
+
await writeIfChanged(outPath, translated);
|
|
99
|
+
}));
|
|
100
|
+
};
|
|
101
|
+
if (existsSync(sourceDir)) {
|
|
102
|
+
const files = await walkFiles(sourceDir);
|
|
103
|
+
for (const filePath of files) {
|
|
104
|
+
await syncSourceFile(filePath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await syncRootDoc();
|
|
108
|
+
const agentsWatcher = chokidar.watch(sourceDir, {
|
|
109
|
+
ignoreInitial: true,
|
|
110
|
+
awaitWriteFinish: {
|
|
111
|
+
stabilityThreshold: 150,
|
|
112
|
+
pollInterval: 50
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
agentsWatcher.on("add", syncSourceFile);
|
|
116
|
+
agentsWatcher.on("change", syncSourceFile);
|
|
117
|
+
agentsWatcher.on("unlink", removeSourceFile);
|
|
118
|
+
const rootWatcher = chokidar.watch(rootDoc, {
|
|
119
|
+
ignoreInitial: true,
|
|
120
|
+
awaitWriteFinish: {
|
|
121
|
+
stabilityThreshold: 150,
|
|
122
|
+
pollInterval: 50
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
rootWatcher.on("add", syncRootDoc);
|
|
126
|
+
rootWatcher.on("change", syncRootDoc);
|
|
127
|
+
process.stdout.write("agents-sync watching .agents and AGENTS.MD\n");
|
|
128
|
+
};
|
|
129
|
+
main().catch((err) => {
|
|
130
|
+
console.error(err);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const LIST_FIELDS = new Set(["aliases", "tags", "tools"]);
|
|
3
|
+
const parseList = (raw) => {
|
|
4
|
+
const trimmed = raw.trim();
|
|
5
|
+
if (!trimmed)
|
|
6
|
+
return [];
|
|
7
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(trimmed);
|
|
10
|
+
if (Array.isArray(parsed))
|
|
11
|
+
return parsed.map(String);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// fall through to comma parsing
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return trimmed
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((v) => v.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
};
|
|
22
|
+
const parseMetaBlock = (raw) => {
|
|
23
|
+
const meta = {};
|
|
24
|
+
const lines = raw.split(/\r?\n/);
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
28
|
+
continue;
|
|
29
|
+
const idx = trimmed.indexOf(":");
|
|
30
|
+
if (idx === -1)
|
|
31
|
+
continue;
|
|
32
|
+
const key = trimmed.slice(0, idx).trim();
|
|
33
|
+
const value = trimmed.slice(idx + 1).trim();
|
|
34
|
+
if (!key)
|
|
35
|
+
continue;
|
|
36
|
+
if (LIST_FIELDS.has(key)) {
|
|
37
|
+
meta[key] = parseList(value);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
meta[key] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return meta;
|
|
44
|
+
};
|
|
45
|
+
export const parseAgentsMarkdown = (content, fallback) => {
|
|
46
|
+
const fenceMatch = content.match(/^```agents\s*\n([\s\S]*?)\n```\s*\n?/i);
|
|
47
|
+
const meta = fenceMatch ? parseMetaBlock(fenceMatch[1]) : {};
|
|
48
|
+
const body = fenceMatch
|
|
49
|
+
? content.slice(fenceMatch[0].length).trim()
|
|
50
|
+
: content.trim();
|
|
51
|
+
const kind = meta.type ||
|
|
52
|
+
meta.kind ||
|
|
53
|
+
fallback.kind;
|
|
54
|
+
const id = meta.id ||
|
|
55
|
+
meta.name ||
|
|
56
|
+
fallback.id;
|
|
57
|
+
const title = meta.title || undefined;
|
|
58
|
+
const description = meta.description || undefined;
|
|
59
|
+
const aliases = meta.aliases || undefined;
|
|
60
|
+
const event = meta.event || undefined;
|
|
61
|
+
return {
|
|
62
|
+
kind,
|
|
63
|
+
id,
|
|
64
|
+
title,
|
|
65
|
+
description,
|
|
66
|
+
aliases,
|
|
67
|
+
event,
|
|
68
|
+
body,
|
|
69
|
+
meta
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export const inferKindFromPath = (relPath) => {
|
|
73
|
+
const parts = relPath.split(path.sep).map((p) => p.toLowerCase());
|
|
74
|
+
if (parts.includes("commands"))
|
|
75
|
+
return "command";
|
|
76
|
+
if (parts.includes("hooks"))
|
|
77
|
+
return "hook";
|
|
78
|
+
if (parts.includes("skills"))
|
|
79
|
+
return "skill";
|
|
80
|
+
if (parts.includes("config") || parts.includes("configs"))
|
|
81
|
+
return "config";
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Docs
|
|
2
|
+
* @see https://platform.claude.com/docs/en/home
|
|
3
|
+
*/
|
|
4
|
+
const yamlList = (label, items) => {
|
|
5
|
+
if (!items.length)
|
|
6
|
+
return [];
|
|
7
|
+
const lines = [label + ":"];
|
|
8
|
+
for (const item of items)
|
|
9
|
+
lines.push(` - ${item}`);
|
|
10
|
+
return lines;
|
|
11
|
+
};
|
|
12
|
+
const renderFrontmatter = (doc) => {
|
|
13
|
+
const lines = ["---", `name: ${doc.id}`];
|
|
14
|
+
if (doc.title)
|
|
15
|
+
lines.push(`title: ${doc.title}`);
|
|
16
|
+
if (doc.description)
|
|
17
|
+
lines.push(`description: ${doc.description}`);
|
|
18
|
+
if (doc.kind === "hook" && doc.event)
|
|
19
|
+
lines.push(`event: ${doc.event}`);
|
|
20
|
+
if (doc.aliases?.length)
|
|
21
|
+
lines.push(...yamlList("aliases", doc.aliases));
|
|
22
|
+
lines.push("---");
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
};
|
|
25
|
+
export const renderDoc = (doc) => {
|
|
26
|
+
const parts = [renderFrontmatter(doc)];
|
|
27
|
+
if (doc.body)
|
|
28
|
+
parts.push(doc.body);
|
|
29
|
+
return parts.join("\n\n");
|
|
30
|
+
};
|
|
31
|
+
export const fileExtension = (_doc) => ".md";
|
|
32
|
+
export const subdir = (doc) => {
|
|
33
|
+
switch (doc.kind) {
|
|
34
|
+
case "command":
|
|
35
|
+
return "commands";
|
|
36
|
+
case "hook":
|
|
37
|
+
return "hooks";
|
|
38
|
+
case "skill":
|
|
39
|
+
return "skills";
|
|
40
|
+
default:
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Docs
|
|
2
|
+
* @see https://developers.openai.com/codex/
|
|
3
|
+
*/
|
|
4
|
+
const sectionLine = (label, value) => {
|
|
5
|
+
if (!value)
|
|
6
|
+
return null;
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
if (!value.length)
|
|
9
|
+
return null;
|
|
10
|
+
return `${label}: ${value.join(", ")}`;
|
|
11
|
+
}
|
|
12
|
+
return `${label}: ${value}`;
|
|
13
|
+
};
|
|
14
|
+
export const renderDoc = (doc) => {
|
|
15
|
+
const lines = [];
|
|
16
|
+
lines.push(`# ${doc.kind}: ${doc.id}`);
|
|
17
|
+
const titleLine = sectionLine("Title", doc.title);
|
|
18
|
+
if (titleLine)
|
|
19
|
+
lines.push(titleLine);
|
|
20
|
+
const descLine = sectionLine("Description", doc.description);
|
|
21
|
+
if (descLine)
|
|
22
|
+
lines.push(descLine);
|
|
23
|
+
const aliasLine = sectionLine("Aliases", doc.aliases);
|
|
24
|
+
if (aliasLine)
|
|
25
|
+
lines.push(aliasLine);
|
|
26
|
+
if (doc.kind === "hook" && doc.event) {
|
|
27
|
+
lines.push(`Event: ${doc.event}`);
|
|
28
|
+
}
|
|
29
|
+
if (doc.body) {
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push(doc.body);
|
|
32
|
+
}
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
};
|
|
35
|
+
export const fileExtension = (_doc) => ".md";
|
|
36
|
+
export const subdir = (doc) => {
|
|
37
|
+
switch (doc.kind) {
|
|
38
|
+
case "command":
|
|
39
|
+
return "commands";
|
|
40
|
+
case "hook":
|
|
41
|
+
return "hooks";
|
|
42
|
+
case "skill":
|
|
43
|
+
return "skills";
|
|
44
|
+
default:
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Docs
|
|
2
|
+
* @see https://geminicli.com/docs/
|
|
3
|
+
*/
|
|
4
|
+
const tomlString = (value) => {
|
|
5
|
+
const escaped = value.replace(/"""/g, "\\\"\\\"\\\"");
|
|
6
|
+
return `"""${escaped}"""`;
|
|
7
|
+
};
|
|
8
|
+
const tomlLine = (key, value) => {
|
|
9
|
+
if (!value)
|
|
10
|
+
return null;
|
|
11
|
+
return `${key} = ${JSON.stringify(value)}`;
|
|
12
|
+
};
|
|
13
|
+
const tomlArray = (key, items) => {
|
|
14
|
+
if (!items?.length)
|
|
15
|
+
return null;
|
|
16
|
+
return `${key} = [${items.map((i) => JSON.stringify(i)).join(", ")}]`;
|
|
17
|
+
};
|
|
18
|
+
export const renderDoc = (doc) => {
|
|
19
|
+
const lines = [];
|
|
20
|
+
lines.push(`type = ${JSON.stringify(doc.kind)}`);
|
|
21
|
+
lines.push(`name = ${JSON.stringify(doc.id)}`);
|
|
22
|
+
const titleLine = tomlLine("title", doc.title);
|
|
23
|
+
if (titleLine)
|
|
24
|
+
lines.push(titleLine);
|
|
25
|
+
const descLine = tomlLine("description", doc.description);
|
|
26
|
+
if (descLine)
|
|
27
|
+
lines.push(descLine);
|
|
28
|
+
const aliasLine = tomlArray("aliases", doc.aliases);
|
|
29
|
+
if (aliasLine)
|
|
30
|
+
lines.push(aliasLine);
|
|
31
|
+
if (doc.kind === "hook" && doc.event) {
|
|
32
|
+
lines.push(`event = ${JSON.stringify(doc.event)}`);
|
|
33
|
+
}
|
|
34
|
+
const body = doc.body ?? "";
|
|
35
|
+
lines.push(`prompt = ${tomlString(body)}`);
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
};
|
|
38
|
+
export const fileExtension = (_doc) => ".toml";
|
|
39
|
+
export const subdir = (doc) => {
|
|
40
|
+
switch (doc.kind) {
|
|
41
|
+
case "command":
|
|
42
|
+
return "commands";
|
|
43
|
+
case "hook":
|
|
44
|
+
return "hooks";
|
|
45
|
+
case "skill":
|
|
46
|
+
return "skills";
|
|
47
|
+
default:
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as claude from "./claude.js";
|
|
2
|
+
import * as gemini from "./gemini.js";
|
|
3
|
+
import * as codex from "./codex.js";
|
|
4
|
+
const withOverrides = (base) => ({
|
|
5
|
+
renderDoc: (doc, _target) => base.renderDoc(doc),
|
|
6
|
+
fileExtension: (doc, target) => target?.agentFileExtension ?? base.fileExtension(doc),
|
|
7
|
+
subdir: (doc, target) => base.subdir(doc, target)
|
|
8
|
+
});
|
|
9
|
+
export const translators = {
|
|
10
|
+
claude: withOverrides(claude),
|
|
11
|
+
gemini: withOverrides(gemini),
|
|
12
|
+
codex: withOverrides(codex)
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agents-sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agents-sync": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"SCHEMA.MD",
|
|
12
|
+
"agents.config.json"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"postbuild": "node scripts/add-shebang.mjs",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"prepare": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chokidar": "^3.6.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsx": "^4.7.0",
|
|
26
|
+
"typescript": "^5.4.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# agents-sync
|
|
2
|
+
|
|
3
|
+
A small TypeScript watcher that keeps `.agents` as the shared source of truth and mirrors translated files into `.claude`, `.codex`, and `.gemini`. It also mirrors `AGENTS.MD` into `CLAUDE.MD`, `GEMINI.MD`, and `CODEX.MD` using the same translation rules.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Using As A CLI
|
|
13
|
+
|
|
14
|
+
After `npm install` you can run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run build
|
|
18
|
+
npm link
|
|
19
|
+
agents-sync
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
To use in another project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i -D agents-sync
|
|
26
|
+
npx agents-sync
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Canonical Schema
|
|
30
|
+
|
|
31
|
+
The canonical format is Markdown with an optional `agents` metadata fence. See `SCHEMA.MD` for the full schema and translation rules.
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Edit `agents.config.json` to change output folders or extensions.
|
|
36
|
+
|
|
37
|
+
- `sourceDir`: shared canonical folder.
|
|
38
|
+
- `agentsSubdir`: optional subdir within each target output (empty by default).
|
|
39
|
+
- `targets`: output directories and file extensions per tool.
|
|
40
|
+
- `rootDoc`: shared canonical root doc.
|
|
41
|
+
- `rootTargets`: which target docs to emit.
|
|
42
|
+
|
|
43
|
+
## Notes
|
|
44
|
+
|
|
45
|
+
- Output files are only written when content changes.
|
|
46
|
+
- Unknown folders under `.agents` are ignored unless they are `commands`, `hooks`, or `skills`.
|