@moskala/oneagent-core 0.2.4 → 0.2.6
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 +1 -1
- package/src/agents.ts +71 -0
- package/src/apply-template.ts +90 -1
- package/src/detect.ts +2 -1
- package/src/generate.ts +2 -2
- package/src/index.ts +1 -0
- package/src/opencode.ts +14 -0
- package/src/status.ts +1 -1
- package/src/symlinks.ts +25 -84
package/package.json
CHANGED
package/src/agents.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AgentTarget } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface AgentDefinition {
|
|
4
|
+
target: AgentTarget;
|
|
5
|
+
displayName: string;
|
|
6
|
+
hint: string;
|
|
7
|
+
/** Paths (relative to root) checked to detect agent presence. Any match = present. */
|
|
8
|
+
detectIndicators: string[];
|
|
9
|
+
/** Symlink path for main instructions file (relative to root). */
|
|
10
|
+
mainFile: string;
|
|
11
|
+
/** Whole-dir symlink for rules (relative to root). Omit if not applicable. */
|
|
12
|
+
rulesDir?: string;
|
|
13
|
+
/** Whole-dir symlink for skills (relative to root). Omit if not applicable. */
|
|
14
|
+
skillsDir?: string;
|
|
15
|
+
/** Legacy files to remove during init (superseded by current format). */
|
|
16
|
+
deprecatedFiles?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const AGENT_DEFINITIONS: AgentDefinition[] = [
|
|
20
|
+
{
|
|
21
|
+
target: "claude",
|
|
22
|
+
displayName: "Claude Code",
|
|
23
|
+
hint: "CLAUDE.md + .claude/rules/",
|
|
24
|
+
detectIndicators: ["CLAUDE.md", ".claude"],
|
|
25
|
+
mainFile: "CLAUDE.md",
|
|
26
|
+
rulesDir: ".claude/rules",
|
|
27
|
+
skillsDir: ".claude/skills",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
target: "cursor",
|
|
31
|
+
displayName: "Cursor",
|
|
32
|
+
hint: "AGENTS.md + .cursor/rules/",
|
|
33
|
+
detectIndicators: [".cursor", ".cursorrules"],
|
|
34
|
+
mainFile: "AGENTS.md",
|
|
35
|
+
rulesDir: ".cursor/rules",
|
|
36
|
+
skillsDir: ".cursor/skills",
|
|
37
|
+
deprecatedFiles: [".cursorrules"],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
target: "windsurf",
|
|
41
|
+
displayName: "Windsurf",
|
|
42
|
+
hint: "AGENTS.md + .windsurf/rules/",
|
|
43
|
+
detectIndicators: [".windsurf", ".windsurfrules"],
|
|
44
|
+
mainFile: "AGENTS.md",
|
|
45
|
+
rulesDir: ".windsurf/rules",
|
|
46
|
+
skillsDir: ".windsurf/skills",
|
|
47
|
+
deprecatedFiles: [".windsurfrules"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
target: "opencode",
|
|
51
|
+
displayName: "OpenCode",
|
|
52
|
+
hint: "AGENTS.md + .opencode/",
|
|
53
|
+
detectIndicators: ["opencode.json", ".opencode"],
|
|
54
|
+
mainFile: "AGENTS.md",
|
|
55
|
+
rulesDir: ".opencode/rules",
|
|
56
|
+
skillsDir: ".opencode/skills",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
target: "copilot",
|
|
60
|
+
displayName: "GitHub Copilot",
|
|
61
|
+
hint: ".github/instructions/*.instructions.md",
|
|
62
|
+
detectIndicators: [".github/copilot-instructions.md", ".github"],
|
|
63
|
+
mainFile: ".github/copilot-instructions.md",
|
|
64
|
+
skillsDir: ".github/skills",
|
|
65
|
+
// rules: generated as .instructions.md files, not symlinks
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export function getAgentDef(target: AgentTarget): AgentDefinition {
|
|
70
|
+
return AGENT_DEFINITIONS.find((d) => d.target === target)!;
|
|
71
|
+
}
|
package/src/apply-template.ts
CHANGED
|
@@ -2,17 +2,49 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import { execFile } from "child_process";
|
|
4
4
|
import { promisify } from "util";
|
|
5
|
+
import type { AgentTarget } from "./types.ts";
|
|
6
|
+
import { addOpenCodePlugin } from "./opencode.ts";
|
|
5
7
|
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
7
9
|
|
|
10
|
+
export interface TemplatePlugin {
|
|
11
|
+
target: AgentTarget;
|
|
12
|
+
id: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export interface TemplateDefinition {
|
|
9
16
|
name: string;
|
|
10
17
|
description: string;
|
|
11
18
|
skills: string[];
|
|
19
|
+
plugins: TemplatePlugin[];
|
|
12
20
|
instructions: string;
|
|
13
21
|
rules: Array<{ name: string; content: string }>;
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
// Parses the `plugins:` block from a template.yml string.
|
|
25
|
+
// Expects entries in the format:
|
|
26
|
+
// plugins:
|
|
27
|
+
// - target: claude
|
|
28
|
+
// id: typescript-lsp@claude-plugins-official
|
|
29
|
+
export function parsePluginsFromYaml(yamlText: string): TemplatePlugin[] {
|
|
30
|
+
const plugins: TemplatePlugin[] = [];
|
|
31
|
+
const section = yamlText.match(/^plugins:\s*\n((?:(?: -.+|\s{4}.+)\n?)*)/m);
|
|
32
|
+
if (!section) return plugins;
|
|
33
|
+
const block = section[1]!;
|
|
34
|
+
const entries = block.split(/\n(?= -)/);
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const targetMatch = entry.match(/target:\s*(\S+)/);
|
|
37
|
+
const idMatch = entry.match(/id:\s*(.+)/);
|
|
38
|
+
if (targetMatch && idMatch) {
|
|
39
|
+
plugins.push({
|
|
40
|
+
target: targetMatch[1]!.trim() as AgentTarget,
|
|
41
|
+
id: idMatch[1]!.trim(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return plugins;
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
// Phase 1: writes instructions.md and rules/*.md.
|
|
17
49
|
// Call this BEFORE generate() so symlinks to rules are created.
|
|
18
50
|
export async function applyTemplateFiles(root: string, template: TemplateDefinition): Promise<void> {
|
|
@@ -46,6 +78,61 @@ export async function installTemplateSkills(
|
|
|
46
78
|
}
|
|
47
79
|
}
|
|
48
80
|
|
|
81
|
+
export interface PluginInstallResult {
|
|
82
|
+
installed: TemplatePlugin[];
|
|
83
|
+
manual: TemplatePlugin[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Phase 3: installs plugins for active targets. Call AFTER generate().
|
|
87
|
+
// - claude → `claude plugin install <id>`
|
|
88
|
+
// - copilot → `copilot plugin install <id>`
|
|
89
|
+
// - opencode → adds id to plugin[] in opencode.json
|
|
90
|
+
// - cursor → added to manual list (no CLI yet — user runs /add-plugin in chat)
|
|
91
|
+
// - windsurf → skipped (no marketplace)
|
|
92
|
+
export async function installTemplatePlugins(
|
|
93
|
+
root: string,
|
|
94
|
+
template: TemplateDefinition,
|
|
95
|
+
activeTargets: AgentTarget[],
|
|
96
|
+
onPluginInstalled?: (plugin: TemplatePlugin) => void,
|
|
97
|
+
): Promise<PluginInstallResult> {
|
|
98
|
+
const installed: TemplatePlugin[] = [];
|
|
99
|
+
const manual: TemplatePlugin[] = [];
|
|
100
|
+
|
|
101
|
+
for (const plugin of template.plugins) {
|
|
102
|
+
if (!activeTargets.includes(plugin.target)) continue;
|
|
103
|
+
|
|
104
|
+
switch (plugin.target) {
|
|
105
|
+
case "claude":
|
|
106
|
+
await execFileAsync("claude", ["plugin", "install", plugin.id], { cwd: root });
|
|
107
|
+
installed.push(plugin);
|
|
108
|
+
onPluginInstalled?.(plugin);
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case "copilot":
|
|
112
|
+
await execFileAsync("copilot", ["plugin", "install", plugin.id], { cwd: root });
|
|
113
|
+
installed.push(plugin);
|
|
114
|
+
onPluginInstalled?.(plugin);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case "opencode":
|
|
118
|
+
await addOpenCodePlugin(root, plugin.id);
|
|
119
|
+
installed.push(plugin);
|
|
120
|
+
onPluginInstalled?.(plugin);
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case "cursor":
|
|
124
|
+
manual.push(plugin);
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case "windsurf":
|
|
128
|
+
// No marketplace yet — skip silently
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { installed, manual };
|
|
134
|
+
}
|
|
135
|
+
|
|
49
136
|
// Fetches a template from a GitHub URL.
|
|
50
137
|
// Expects the repository to contain: template.yml, instructions.md, and optionally rules/*.md
|
|
51
138
|
export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefinition> {
|
|
@@ -74,10 +161,12 @@ export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefi
|
|
|
74
161
|
}
|
|
75
162
|
}
|
|
76
163
|
|
|
164
|
+
const plugins = parsePluginsFromYaml(yamlText);
|
|
165
|
+
|
|
77
166
|
// Try to list rules via GitHub API
|
|
78
167
|
const rules = await fetchGitHubRules(url);
|
|
79
168
|
|
|
80
|
-
return { name, description, skills, instructions, rules };
|
|
169
|
+
return { name, description, skills, plugins, instructions, rules };
|
|
81
170
|
}
|
|
82
171
|
|
|
83
172
|
async function fetchText(url: string): Promise<string> {
|
package/src/detect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import type { DetectedFile } from "./types.ts";
|
|
4
|
+
import { AGENT_DEFINITIONS } from "./agents.ts";
|
|
4
5
|
|
|
5
6
|
export const AGENT_FILES = [
|
|
6
7
|
"CLAUDE.md",
|
|
@@ -46,7 +47,7 @@ export function filesHaveSameContent(files: DetectedFile[]): boolean {
|
|
|
46
47
|
return files.every((f) => f.content === first);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
const DEPRECATED_FILES = [
|
|
50
|
+
const DEPRECATED_FILES = AGENT_DEFINITIONS.flatMap((d) => d.deprecatedFiles ?? []);
|
|
50
51
|
|
|
51
52
|
export async function removeDeprecatedFiles(root: string): Promise<void> {
|
|
52
53
|
for (const rel of DEPRECATED_FILES) {
|
package/src/generate.ts
CHANGED
|
@@ -22,7 +22,7 @@ export async function detectGenerateCollisions(root: string, config: Config): Pr
|
|
|
22
22
|
const mainEntries = buildMainSymlinks(root, targets);
|
|
23
23
|
// 2. Rule/skill symlink paths
|
|
24
24
|
const ruleSkillEntries = [
|
|
25
|
-
...buildRulesSymlinks(root, targets
|
|
25
|
+
...buildRulesSymlinks(root, targets),
|
|
26
26
|
...buildSkillSymlinks(root, targets),
|
|
27
27
|
// .agents/skills skipped — handled by migrateAgentsSkillsDir
|
|
28
28
|
];
|
|
@@ -73,7 +73,7 @@ export async function generate(root: string, config: Config): Promise<void> {
|
|
|
73
73
|
await migrateRuleAndSkillFiles(root);
|
|
74
74
|
|
|
75
75
|
const mainSymlinks = buildMainSymlinks(root, targets);
|
|
76
|
-
const rulesSymlinks = buildRulesSymlinks(root, targets
|
|
76
|
+
const rulesSymlinks = buildRulesSymlinks(root, targets);
|
|
77
77
|
const skillSymlinks = await buildSkillSymlinks(root, targets);
|
|
78
78
|
await createAllSymlinks([...mainSymlinks, ...rulesSymlinks, ...skillSymlinks, ...buildAgentsDirSymlinks(root)]);
|
|
79
79
|
|
package/src/index.ts
CHANGED
package/src/opencode.ts
CHANGED
|
@@ -18,6 +18,20 @@ export function buildOpencodeConfig(existing: Record<string, unknown> | null): o
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export async function addOpenCodePlugin(root: string, id: string): Promise<void> {
|
|
22
|
+
const filePath = path.join(root, "opencode.json");
|
|
23
|
+
let existing: Record<string, unknown>;
|
|
24
|
+
try {
|
|
25
|
+
existing = JSON.parse(await fs.readFile(filePath, "utf-8")) as Record<string, unknown>;
|
|
26
|
+
} catch {
|
|
27
|
+
return; // no opencode.json — no-op
|
|
28
|
+
}
|
|
29
|
+
const current = Array.isArray(existing.plugin) ? (existing.plugin as string[]) : [];
|
|
30
|
+
if (current.includes(id)) return;
|
|
31
|
+
existing.plugin = [...current, id];
|
|
32
|
+
await fs.writeFile(filePath, JSON.stringify(existing, null, 2) + "\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export async function writeOpencode(root: string, _rules: RuleFile[]): Promise<void> {
|
|
22
36
|
const existing = await readOpencode(root);
|
|
23
37
|
const config = buildOpencodeConfig(existing);
|
package/src/status.ts
CHANGED
|
@@ -44,7 +44,7 @@ export async function checkStatus(root: string, config: Config): Promise<StatusR
|
|
|
44
44
|
|
|
45
45
|
const allEntries = [
|
|
46
46
|
...buildMainSymlinks(root, targets),
|
|
47
|
-
...buildRulesSymlinks(root, targets
|
|
47
|
+
...buildRulesSymlinks(root, targets),
|
|
48
48
|
...buildSkillSymlinks(root, targets),
|
|
49
49
|
...buildAgentsDirSymlinks(root),
|
|
50
50
|
];
|
package/src/symlinks.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
-
import type { AgentTarget,
|
|
3
|
+
import type { AgentTarget, SymlinkCheck, SymlinkEntry } from "./types.ts";
|
|
4
|
+
import { AGENT_DEFINITIONS } from "./agents.ts";
|
|
4
5
|
|
|
5
6
|
export async function ensureDir(dirPath: string): Promise<void> {
|
|
6
7
|
await fs.mkdir(dirPath, { recursive: true });
|
|
@@ -9,9 +10,9 @@ export async function ensureDir(dirPath: string): Promise<void> {
|
|
|
9
10
|
export async function createSymlink(symlinkPath: string, target: string): Promise<void> {
|
|
10
11
|
await ensureDir(path.dirname(symlinkPath));
|
|
11
12
|
try {
|
|
12
|
-
await fs.
|
|
13
|
+
await fs.rm(symlinkPath, { recursive: true });
|
|
13
14
|
} catch {
|
|
14
|
-
//
|
|
15
|
+
// doesn't exist — that's fine
|
|
15
16
|
}
|
|
16
17
|
await fs.symlink(target, symlinkPath);
|
|
17
18
|
}
|
|
@@ -25,26 +26,8 @@ export function buildMainSymlinks(root: string, targets: AgentTarget[]): Symlink
|
|
|
25
26
|
const seen = new Map<string, SymlinkEntry>();
|
|
26
27
|
|
|
27
28
|
for (const target of targets) {
|
|
28
|
-
|
|
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
|
-
|
|
29
|
+
const def = AGENT_DEFINITIONS.find((d) => d.target === target)!;
|
|
30
|
+
const symlinkPath = path.join(root, def.mainFile);
|
|
48
31
|
if (!seen.has(symlinkPath)) {
|
|
49
32
|
seen.set(symlinkPath, {
|
|
50
33
|
symlinkPath,
|
|
@@ -57,64 +40,24 @@ export function buildMainSymlinks(root: string, targets: AgentTarget[]): Symlink
|
|
|
57
40
|
return Array.from(seen.values());
|
|
58
41
|
}
|
|
59
42
|
|
|
60
|
-
export function buildRulesSymlinks(
|
|
61
|
-
root
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
43
|
+
export function buildRulesSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
44
|
+
const targetAbs = path.join(root, ".oneagent/rules");
|
|
45
|
+
return AGENT_DEFINITIONS
|
|
46
|
+
.filter((d) => targets.includes(d.target) && d.rulesDir)
|
|
47
|
+
.map((d) => {
|
|
48
|
+
const symlinkPath = path.join(root, d.rulesDir!);
|
|
49
|
+
return { symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: d.rulesDir! };
|
|
50
|
+
});
|
|
99
51
|
}
|
|
100
52
|
|
|
101
|
-
// Creates whole-directory symlinks: .claude/skills → .oneagent/skills, .cursor/skills → .oneagent/skills
|
|
102
53
|
export function buildSkillSymlinks(root: string, targets: AgentTarget[]): SymlinkEntry[] {
|
|
103
54
|
const targetAbs = path.join(root, ".oneagent/skills");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return (Object.entries(agentDirs) as [AgentTarget, string][])
|
|
112
|
-
.filter(([target]) => targets.includes(target))
|
|
113
|
-
.map(([, dir]) => ({
|
|
114
|
-
symlinkPath: dir,
|
|
115
|
-
target: relativeTarget(dir, targetAbs),
|
|
116
|
-
label: path.relative(root, dir),
|
|
117
|
-
}));
|
|
55
|
+
return AGENT_DEFINITIONS
|
|
56
|
+
.filter((d) => targets.includes(d.target) && d.skillsDir)
|
|
57
|
+
.map((d) => {
|
|
58
|
+
const symlinkPath = path.join(root, d.skillsDir!);
|
|
59
|
+
return { symlinkPath, target: relativeTarget(symlinkPath, targetAbs), label: d.skillsDir! };
|
|
60
|
+
});
|
|
118
61
|
}
|
|
119
62
|
|
|
120
63
|
export function buildAgentsDirSymlinks(root: string): SymlinkEntry[] {
|
|
@@ -179,14 +122,12 @@ async function migrateAndRemoveDir(src: string, dest: string, root: string): Pro
|
|
|
179
122
|
export async function migrateRuleAndSkillFiles(root: string): Promise<void> {
|
|
180
123
|
const destRules = path.join(root, ".oneagent/rules");
|
|
181
124
|
const destSkills = path.join(root, ".oneagent/skills");
|
|
182
|
-
// Rules dirs:
|
|
183
|
-
// The directories themselves stay — generate() recreates per-file symlinks inside them.
|
|
125
|
+
// Rules dirs: the entire directory becomes a symlink, so move files out and remove the dir.
|
|
184
126
|
// Sequential to avoid same-name conflicts across dirs.
|
|
185
|
-
await
|
|
186
|
-
await
|
|
187
|
-
await
|
|
188
|
-
|
|
189
|
-
// so the real directory must be removed first to make room for the symlink.
|
|
127
|
+
await migrateAndRemoveDir(path.join(root, ".cursor/rules"), destRules, root);
|
|
128
|
+
await migrateAndRemoveDir(path.join(root, ".claude/rules"), destRules, root);
|
|
129
|
+
await migrateAndRemoveDir(path.join(root, ".windsurf/rules"), destRules, root);
|
|
130
|
+
await migrateAndRemoveDir(path.join(root, ".opencode/rules"), destRules, root);
|
|
190
131
|
await migrateAndRemoveDir(path.join(root, ".agents/skills"), destSkills, root);
|
|
191
132
|
}
|
|
192
133
|
|