@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moskala/oneagent-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Core library for oneagent — one source of truth for AI agent rules",
6
6
  "license": "MIT",
@@ -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
@@ -8,3 +8,4 @@ export * from "./copilot.ts";
8
8
  export * from "./opencode.ts";
9
9
  export * from "./generate.ts";
10
10
  export * from "./status.ts";
11
+ export * from "./apply-template.ts";