@moskala/oneagent-core 0.2.0 → 0.2.2
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/apply-template.ts +123 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
|
|
4
|
+
export interface TemplateDefinition {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
skills: string[];
|
|
8
|
+
instructions: string;
|
|
9
|
+
rules: Array<{ name: string; content: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Phase 1: writes instructions.md and rules/*.md.
|
|
13
|
+
// Call this BEFORE generate() so symlinks to rules are created.
|
|
14
|
+
export async function applyTemplateFiles(root: string, template: TemplateDefinition): Promise<void> {
|
|
15
|
+
const oneagentDir = path.join(root, ".oneagent");
|
|
16
|
+
|
|
17
|
+
await fs.mkdir(path.join(oneagentDir, "rules"), { recursive: true });
|
|
18
|
+
await fs.mkdir(path.join(oneagentDir, "skills"), { recursive: true });
|
|
19
|
+
|
|
20
|
+
await Bun.write(path.join(oneagentDir, "instructions.md"), template.instructions);
|
|
21
|
+
|
|
22
|
+
for (const rule of template.rules) {
|
|
23
|
+
await Bun.write(path.join(oneagentDir, "rules", `${rule.name}.md`), rule.content);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Phase 2: installs skills via `bunx skills add <identifier> --yes`.
|
|
28
|
+
// Call this AFTER generate() so agent directories (symlinks) already exist.
|
|
29
|
+
export async function installTemplateSkills(
|
|
30
|
+
root: string,
|
|
31
|
+
template: TemplateDefinition,
|
|
32
|
+
onSkillInstalled?: (identifier: string) => void,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
for (const identifier of template.skills) {
|
|
35
|
+
try {
|
|
36
|
+
await Bun.$`bunx skills add ${identifier} --agent universal --yes`.cwd(root).quiet();
|
|
37
|
+
onSkillInstalled?.(identifier);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
throw new Error(`Failed to install skill "${identifier}": ${message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fetches a template from a GitHub URL.
|
|
46
|
+
// Expects the repository to contain: template.yml, instructions.md, and optionally rules/*.md
|
|
47
|
+
export async function fetchTemplateFromGitHub(url: string): Promise<TemplateDefinition> {
|
|
48
|
+
// Convert GitHub URL to raw content base URL
|
|
49
|
+
// e.g. https://github.com/owner/repo → https://raw.githubusercontent.com/owner/repo/main
|
|
50
|
+
const rawBase = githubUrlToRawBase(url);
|
|
51
|
+
|
|
52
|
+
const [yamlText, instructions] = await Promise.all([
|
|
53
|
+
fetchText(`${rawBase}/template.yml`),
|
|
54
|
+
fetchText(`${rawBase}/instructions.md`),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const descMatch = yamlText.match(/^description:\s*(.+)$/m);
|
|
58
|
+
const description = descMatch?.[1]?.trim() ?? "";
|
|
59
|
+
|
|
60
|
+
const nameMatch = yamlText.match(/^name:\s*(.+)$/m);
|
|
61
|
+
const name = nameMatch?.[1]?.trim() ?? "custom";
|
|
62
|
+
|
|
63
|
+
const skills: string[] = [];
|
|
64
|
+
const skillsBlockMatch = yamlText.match(/^skills:\s*\n((?: - .+\n?)*)/m);
|
|
65
|
+
if (skillsBlockMatch) {
|
|
66
|
+
const lines = skillsBlockMatch[1]!.split("\n").filter(Boolean);
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const skill = line.replace(/^\s*-\s*/, "").trim();
|
|
69
|
+
if (skill) skills.push(skill);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Try to list rules via GitHub API
|
|
74
|
+
const rules = await fetchGitHubRules(url);
|
|
75
|
+
|
|
76
|
+
return { name, description, skills, instructions, rules };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchText(url: string): Promise<string> {
|
|
80
|
+
const response = await fetch(url);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
return response.text();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function githubUrlToRawBase(url: string): string {
|
|
88
|
+
// Handle https://github.com/owner/repo/tree/branch or https://github.com/owner/repo
|
|
89
|
+
const match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\/tree\/([^/]+))?(?:\/.*)?$/);
|
|
90
|
+
if (!match) {
|
|
91
|
+
throw new Error(`Invalid GitHub URL: "${url}". Expected format: https://github.com/owner/repo`);
|
|
92
|
+
}
|
|
93
|
+
const [, owner, repo, branch = "main"] = match;
|
|
94
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function fetchGitHubRules(repoUrl: string): Promise<Array<{ name: string; content: string }>> {
|
|
98
|
+
// Parse owner/repo from URL
|
|
99
|
+
const match = repoUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\/tree\/([^/]+))?(?:\/.*)?$/);
|
|
100
|
+
if (!match) return [];
|
|
101
|
+
const [, owner, repo, branch = "main"] = match;
|
|
102
|
+
|
|
103
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/rules?ref=${branch}`;
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(apiUrl, {
|
|
106
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) return [];
|
|
109
|
+
|
|
110
|
+
const files = (await response.json()) as Array<{ name: string; download_url: string | null }>;
|
|
111
|
+
const mdFiles = files.filter((f) => f.name.endsWith(".md") && f.download_url);
|
|
112
|
+
|
|
113
|
+
const rules = await Promise.all(
|
|
114
|
+
mdFiles.map(async (f) => {
|
|
115
|
+
const content = await fetchText(f.download_url!);
|
|
116
|
+
return { name: path.basename(f.name, ".md"), content };
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
return rules;
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/index.ts
CHANGED