@moskala/oneagent-core 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/package.json +31 -0
- package/src/config.ts +19 -0
- package/src/copilot.ts +21 -0
- package/src/detect.ts +47 -0
- package/src/generate.ts +21 -0
- package/src/index.ts +9 -0
- package/src/opencode.ts +24 -0
- package/src/rules.ts +34 -0
- package/src/status.ts +44 -0
- package/src/symlinks.ts +116 -0
- package/src/types.ts +49 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moskala/oneagent-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Core library for oneagent — one source of truth for AI agent rules",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/moskalakamil/oneagent"
|
|
10
|
+
},
|
|
11
|
+
"module": "./src/index.ts",
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"!src/__tests__"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "bun test"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"yaml": "^2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/bun": "latest",
|
|
29
|
+
"typescript": "^5"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parse, stringify } from "yaml";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { Config } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const CONFIG_REL = ".oneagent/config.yml";
|
|
6
|
+
|
|
7
|
+
export async function configExists(root: string): Promise<boolean> {
|
|
8
|
+
return Bun.file(path.join(root, CONFIG_REL)).exists();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function readConfig(root: string): Promise<Config> {
|
|
12
|
+
const content = await Bun.file(path.join(root, CONFIG_REL)).text();
|
|
13
|
+
return parse(content) as Config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function writeConfig(root: string, config: Config): Promise<void> {
|
|
17
|
+
const filePath = path.join(root, CONFIG_REL);
|
|
18
|
+
await Bun.write(filePath, stringify(config));
|
|
19
|
+
}
|
package/src/copilot.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import type { RuleFile } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export function buildCopilotContent(rule: RuleFile): string {
|
|
6
|
+
return `---\napplyTo: "${rule.applyTo}"\n---\n${rule.content}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function copilotFilePath(root: string, ruleName: string): string {
|
|
10
|
+
return path.join(root, ".github/instructions", `${ruleName}.instructions.md`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function generateCopilotRule(root: string, rule: RuleFile): Promise<void> {
|
|
14
|
+
const filePath = copilotFilePath(root, rule.name);
|
|
15
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
+
await Bun.write(filePath, buildCopilotContent(rule));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function generateCopilotRules(root: string, rules: RuleFile[]): Promise<void> {
|
|
20
|
+
await Promise.all(rules.map((rule) => generateCopilotRule(root, rule)));
|
|
21
|
+
}
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import type { DetectedFile } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export const AGENT_FILES = [
|
|
6
|
+
"CLAUDE.md",
|
|
7
|
+
"AGENTS.md",
|
|
8
|
+
".cursorrules",
|
|
9
|
+
".windsurfrules",
|
|
10
|
+
".github/copilot-instructions.md",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export async function readDetectedFile(root: string, rel: string): Promise<DetectedFile | null> {
|
|
14
|
+
const absolutePath = path.join(root, rel);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const stat = await fs.lstat(absolutePath);
|
|
18
|
+
|
|
19
|
+
if (stat.isSymbolicLink()) {
|
|
20
|
+
const linkTarget = await fs.readlink(absolutePath);
|
|
21
|
+
const resolved = path.resolve(path.dirname(absolutePath), linkTarget);
|
|
22
|
+
if (resolved.startsWith(path.join(root, ".one"))) return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = await Bun.file(absolutePath).text();
|
|
26
|
+
return {
|
|
27
|
+
relativePath: rel,
|
|
28
|
+
absolutePath,
|
|
29
|
+
sizeBytes: stat.size,
|
|
30
|
+
modifiedAt: stat.mtime,
|
|
31
|
+
content,
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function detectExistingFiles(root: string): Promise<DetectedFile[]> {
|
|
39
|
+
const results = await Promise.all(AGENT_FILES.map((rel) => readDetectedFile(root, rel)));
|
|
40
|
+
return results.filter((f): f is DetectedFile => f !== null);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function filesHaveSameContent(files: DetectedFile[]): boolean {
|
|
44
|
+
if (files.length <= 1) return true;
|
|
45
|
+
const first = files[0]!.content;
|
|
46
|
+
return files.every((f) => f.content === first);
|
|
47
|
+
}
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Config } from "./types.ts";
|
|
2
|
+
import { readRules } from "./rules.ts";
|
|
3
|
+
import { buildMainSymlinks, buildRulesSymlinks, createAllSymlinks } from "./symlinks.ts";
|
|
4
|
+
import { generateCopilotRules } from "./copilot.ts";
|
|
5
|
+
import { writeOpencode } from "./opencode.ts";
|
|
6
|
+
|
|
7
|
+
export async function generate(root: string, config: Config): Promise<void> {
|
|
8
|
+
const rules = await readRules(root);
|
|
9
|
+
|
|
10
|
+
const mainSymlinks = buildMainSymlinks(root, config.targets);
|
|
11
|
+
const rulesSymlinks = buildRulesSymlinks(root, config.targets, rules);
|
|
12
|
+
await createAllSymlinks([...mainSymlinks, ...rulesSymlinks]);
|
|
13
|
+
|
|
14
|
+
if (config.targets.includes("copilot")) {
|
|
15
|
+
await generateCopilotRules(root, rules);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (config.targets.includes("opencode")) {
|
|
19
|
+
await writeOpencode(root, rules);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./types.ts";
|
|
2
|
+
export * from "./config.ts";
|
|
3
|
+
export * from "./detect.ts";
|
|
4
|
+
export * from "./rules.ts";
|
|
5
|
+
export * from "./symlinks.ts";
|
|
6
|
+
export * from "./copilot.ts";
|
|
7
|
+
export * from "./opencode.ts";
|
|
8
|
+
export * from "./generate.ts";
|
|
9
|
+
export * from "./status.ts";
|
package/src/opencode.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import type { RuleFile } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function readOpencode(root: string): Promise<Record<string, unknown> | null> {
|
|
5
|
+
try {
|
|
6
|
+
const content = await Bun.file(path.join(root, "opencode.json")).text();
|
|
7
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildOpencodeConfig(existing: Record<string, unknown> | null): object {
|
|
14
|
+
return {
|
|
15
|
+
...existing,
|
|
16
|
+
instructions: ".oneagent/instructions.md",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function writeOpencode(root: string, _rules: RuleFile[]): Promise<void> {
|
|
21
|
+
const existing = await readOpencode(root);
|
|
22
|
+
const config = buildOpencodeConfig(existing);
|
|
23
|
+
await Bun.write(path.join(root, "opencode.json"), JSON.stringify(config, null, 2) + "\n");
|
|
24
|
+
}
|
package/src/rules.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import type { RuleFile } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export function parseFrontmatter(raw: string): { applyTo: string; content: string } {
|
|
6
|
+
const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
7
|
+
if (!match) return { applyTo: "**", content: raw };
|
|
8
|
+
|
|
9
|
+
const frontmatter = match[1] ?? "";
|
|
10
|
+
const content = match[2] ?? "";
|
|
11
|
+
|
|
12
|
+
const applyToMatch = frontmatter.match(/applyTo:\s*["']?([^"'\n]+)["']?/);
|
|
13
|
+
const applyTo = applyToMatch?.[1]?.trim() ?? "**";
|
|
14
|
+
|
|
15
|
+
return { applyTo, content };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readRuleFile(filePath: string): Promise<RuleFile> {
|
|
19
|
+
const raw = await Bun.file(filePath).text();
|
|
20
|
+
const { applyTo, content } = parseFrontmatter(raw);
|
|
21
|
+
return { name: path.basename(filePath, ".md"), path: filePath, applyTo, content };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readRules(root: string): Promise<RuleFile[]> {
|
|
25
|
+
const rulesDir = path.join(root, ".oneagent/rules");
|
|
26
|
+
try {
|
|
27
|
+
const files = await fs.readdir(rulesDir);
|
|
28
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
29
|
+
const rules = await Promise.all(mdFiles.map((f) => readRuleFile(path.join(rulesDir, f))));
|
|
30
|
+
return rules.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Config, GeneratedFileCheck, OpenCodeCheck, RuleFile, StatusResult } from "./types.ts";
|
|
2
|
+
import { readRules } from "./rules.ts";
|
|
3
|
+
import { buildMainSymlinks, buildRulesSymlinks, checkSymlink } from "./symlinks.ts";
|
|
4
|
+
import { buildCopilotContent, copilotFilePath } from "./copilot.ts";
|
|
5
|
+
import { readOpencode } from "./opencode.ts";
|
|
6
|
+
|
|
7
|
+
export async function checkGeneratedFile(root: string, rule: RuleFile): Promise<GeneratedFileCheck> {
|
|
8
|
+
const filePath = copilotFilePath(root, rule.name);
|
|
9
|
+
const expected = buildCopilotContent(rule);
|
|
10
|
+
try {
|
|
11
|
+
const content = await Bun.file(filePath).text();
|
|
12
|
+
return { path: filePath, exists: true, upToDate: content === expected };
|
|
13
|
+
} catch {
|
|
14
|
+
return { path: filePath, exists: false, upToDate: false };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function checkOpencodeStatus(
|
|
19
|
+
root: string,
|
|
20
|
+
_rules: RuleFile[],
|
|
21
|
+
): Promise<OpenCodeCheck> {
|
|
22
|
+
const existing = await readOpencode(root);
|
|
23
|
+
if (!existing) return { exists: false, valid: false };
|
|
24
|
+
return { exists: true, valid: existing["instructions"] === ".oneagent/instructions.md" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function checkStatus(root: string, config: Config): Promise<StatusResult> {
|
|
28
|
+
const rules = await readRules(root);
|
|
29
|
+
|
|
30
|
+
const allEntries = [
|
|
31
|
+
...buildMainSymlinks(root, config.targets),
|
|
32
|
+
...buildRulesSymlinks(root, config.targets, rules),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const symlinks = await Promise.all(allEntries.map(checkSymlink));
|
|
36
|
+
|
|
37
|
+
const generatedFiles = config.targets.includes("copilot")
|
|
38
|
+
? await Promise.all(rules.map((rule) => checkGeneratedFile(root, rule)))
|
|
39
|
+
: [];
|
|
40
|
+
|
|
41
|
+
const opencode = await checkOpencodeStatus(root, rules);
|
|
42
|
+
|
|
43
|
+
return { symlinks, generatedFiles, opencode };
|
|
44
|
+
}
|
package/src/symlinks.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import type { AgentTarget, RuleFile, SymlinkCheck, SymlinkEntry } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export async function ensureDir(dirPath: string): Promise<void> {
|
|
6
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function createSymlink(symlinkPath: string, target: string): Promise<void> {
|
|
10
|
+
await ensureDir(path.dirname(symlinkPath));
|
|
11
|
+
try {
|
|
12
|
+
await fs.unlink(symlinkPath);
|
|
13
|
+
} catch {
|
|
14
|
+
// file doesn't exist — that's fine
|
|
15
|
+
}
|
|
16
|
+
await fs.symlink(target, symlinkPath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function relativeTarget(symlinkPath: string, targetAbsPath: string): string {
|
|
20
|
+
return path.relative(path.dirname(symlinkPath), targetAbsPath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildMainSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
24
|
+
const instructionsAbs = path.join(root, ".oneagent/instructions.md");
|
|
25
|
+
const seen = new Map<string, SymlinkEntry>();
|
|
26
|
+
|
|
27
|
+
for (const target of targets) {
|
|
28
|
+
let symlinkPath: string;
|
|
29
|
+
|
|
30
|
+
switch (target) {
|
|
31
|
+
case "claude":
|
|
32
|
+
symlinkPath = path.join(root, "CLAUDE.md");
|
|
33
|
+
break;
|
|
34
|
+
case "cursor":
|
|
35
|
+
symlinkPath = path.join(root, "AGENTS.md");
|
|
36
|
+
break;
|
|
37
|
+
case "windsurf":
|
|
38
|
+
symlinkPath = path.join(root, ".windsurfrules");
|
|
39
|
+
break;
|
|
40
|
+
case "opencode":
|
|
41
|
+
symlinkPath = path.join(root, "AGENTS.md");
|
|
42
|
+
break;
|
|
43
|
+
case "copilot":
|
|
44
|
+
symlinkPath = path.join(root, ".github/copilot-instructions.md");
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!seen.has(symlinkPath)) {
|
|
49
|
+
seen.set(symlinkPath, {
|
|
50
|
+
symlinkPath,
|
|
51
|
+
target: relativeTarget(symlinkPath, instructionsAbs),
|
|
52
|
+
label: path.relative(root, symlinkPath),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Array.from(seen.values());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildRulesSymlinks(
|
|
61
|
+
root: string,
|
|
62
|
+
targets: AgentTarget[],
|
|
63
|
+
rules: RuleFile[],
|
|
64
|
+
): SymlinkEntry[] {
|
|
65
|
+
const entries: SymlinkEntry[] = [];
|
|
66
|
+
|
|
67
|
+
for (const target of targets) {
|
|
68
|
+
let rulesDir: string | null = null;
|
|
69
|
+
|
|
70
|
+
switch (target) {
|
|
71
|
+
case "claude":
|
|
72
|
+
rulesDir = path.join(root, ".claude/rules");
|
|
73
|
+
break;
|
|
74
|
+
case "cursor":
|
|
75
|
+
rulesDir = path.join(root, ".cursor/rules");
|
|
76
|
+
break;
|
|
77
|
+
case "windsurf":
|
|
78
|
+
rulesDir = path.join(root, ".windsurf/rules");
|
|
79
|
+
break;
|
|
80
|
+
case "opencode":
|
|
81
|
+
case "copilot":
|
|
82
|
+
rulesDir = null;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!rulesDir) continue;
|
|
87
|
+
|
|
88
|
+
for (const rule of rules) {
|
|
89
|
+
const symlinkPath = path.join(rulesDir, `${rule.name}.md`);
|
|
90
|
+
entries.push({
|
|
91
|
+
symlinkPath,
|
|
92
|
+
target: relativeTarget(symlinkPath, rule.path),
|
|
93
|
+
label: path.relative(root, symlinkPath),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return entries;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function createAllSymlinks(entries: SymlinkEntry[]): Promise<void> {
|
|
102
|
+
await Promise.all(entries.map((e) => createSymlink(e.symlinkPath, e.target)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function checkSymlink(entry: SymlinkEntry): Promise<SymlinkCheck> {
|
|
106
|
+
try {
|
|
107
|
+
const stat = await fs.lstat(entry.symlinkPath);
|
|
108
|
+
if (!stat.isSymbolicLink()) {
|
|
109
|
+
return { ...entry, exists: true, valid: false };
|
|
110
|
+
}
|
|
111
|
+
const linkTarget = await fs.readlink(entry.symlinkPath);
|
|
112
|
+
return { ...entry, exists: true, valid: linkTarget === entry.target };
|
|
113
|
+
} catch {
|
|
114
|
+
return { ...entry, exists: false, valid: false };
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type AgentTarget = "claude" | "cursor" | "windsurf" | "opencode" | "copilot";
|
|
2
|
+
|
|
3
|
+
export interface Config {
|
|
4
|
+
version: 1;
|
|
5
|
+
targets: AgentTarget[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DetectedFile {
|
|
9
|
+
relativePath: string;
|
|
10
|
+
absolutePath: string;
|
|
11
|
+
sizeBytes: number;
|
|
12
|
+
modifiedAt: Date;
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RuleFile {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
applyTo: string;
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SymlinkEntry {
|
|
24
|
+
symlinkPath: string;
|
|
25
|
+
target: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SymlinkCheck extends SymlinkEntry {
|
|
30
|
+
exists: boolean;
|
|
31
|
+
valid: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StatusResult {
|
|
35
|
+
symlinks: SymlinkCheck[];
|
|
36
|
+
generatedFiles: GeneratedFileCheck[];
|
|
37
|
+
opencode: OpenCodeCheck;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GeneratedFileCheck {
|
|
41
|
+
path: string;
|
|
42
|
+
exists: boolean;
|
|
43
|
+
upToDate: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OpenCodeCheck {
|
|
47
|
+
exists: boolean;
|
|
48
|
+
valid: boolean;
|
|
49
|
+
}
|