awesome-agents 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/AGENTS.md +60 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/bin/awesome-agents.js +9 -0
- package/docs/cli.md +34 -0
- package/docs/product/README.md +12 -0
- package/docs/product/awesome-agents-flow-notes.md +45 -0
- package/package.json +44 -0
- package/src/cli.js +208 -0
- package/src/constants.js +19 -0
- package/src/frontmatter.js +33 -0
- package/src/installer.js +315 -0
- package/src/registry.js +58 -0
- package/src/renderers.js +256 -0
- package/src/source.js +179 -0
package/src/source.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { DEFAULT_SOURCE } from "./constants.js";
|
|
8
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
9
|
+
|
|
10
|
+
const GITHUB_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
11
|
+
|
|
12
|
+
export function splitSourceSpec(spec) {
|
|
13
|
+
if (!spec) {
|
|
14
|
+
return { source: DEFAULT_SOURCE, profile: undefined };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const atIndex = spec.lastIndexOf("@");
|
|
18
|
+
if (!spec.startsWith("git@") && atIndex > 0 && atIndex > spec.lastIndexOf("/")) {
|
|
19
|
+
return {
|
|
20
|
+
source: spec.slice(0, atIndex),
|
|
21
|
+
profile: spec.slice(atIndex + 1)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { source: spec, profile: undefined };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function expandHome(input, home = os.homedir()) {
|
|
29
|
+
if (input === "~") {
|
|
30
|
+
return home;
|
|
31
|
+
}
|
|
32
|
+
if (input.startsWith("~/")) {
|
|
33
|
+
return path.join(home, input.slice(2));
|
|
34
|
+
}
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isLocalSource(source, home = os.homedir()) {
|
|
39
|
+
const expanded = expandHome(source, home);
|
|
40
|
+
return path.isAbsolute(expanded) || source.startsWith(".") || source.startsWith("~") || existsSync(expanded);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeGithubUrl(source) {
|
|
44
|
+
if (GITHUB_SHORTHAND.test(source)) {
|
|
45
|
+
return `https://github.com/${source}.git`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const github = source.match(/^https:\/\/github\.com\/([^/]+\/[^/.]+)(?:\.git)?\/?$/);
|
|
49
|
+
if (github) {
|
|
50
|
+
return `https://github.com/${github[1]}.git`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (source.startsWith("git@github.com:") || source.endsWith(".git")) {
|
|
54
|
+
return source;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function materializeSource(sourceInput = DEFAULT_SOURCE, options = {}) {
|
|
61
|
+
const home = options.home ?? os.homedir();
|
|
62
|
+
const source = sourceInput || DEFAULT_SOURCE;
|
|
63
|
+
|
|
64
|
+
if (isLocalSource(source, home)) {
|
|
65
|
+
const sourcePath = path.resolve(expandHome(source, home));
|
|
66
|
+
await assertTouchGrassLayout(sourcePath);
|
|
67
|
+
return {
|
|
68
|
+
kind: "local",
|
|
69
|
+
source: sourcePath,
|
|
70
|
+
path: sourcePath,
|
|
71
|
+
cleanup: async () => {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const gitUrl = normalizeGithubUrl(source);
|
|
76
|
+
if (!gitUrl) {
|
|
77
|
+
throw new Error(`Unsupported source "${source}". Use a local path, owner/repo, or GitHub URL.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const tmpRoot = options.tmpRoot ?? os.tmpdir();
|
|
81
|
+
const cloneDir = await fs.mkdtemp(path.join(tmpRoot, "awesome-agents-source-"));
|
|
82
|
+
const clone = spawnSync("git", ["clone", "--depth=1", gitUrl, cloneDir], {
|
|
83
|
+
encoding: "utf8"
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (clone.status !== 0) {
|
|
87
|
+
await fs.rm(cloneDir, { recursive: true, force: true });
|
|
88
|
+
const detail = clone.stderr?.trim() || clone.stdout?.trim() || `git exited with ${clone.status}`;
|
|
89
|
+
throw new Error(`Could not clone ${source}: ${detail}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await assertTouchGrassLayout(cloneDir);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
kind: "git",
|
|
96
|
+
source,
|
|
97
|
+
gitUrl,
|
|
98
|
+
path: cloneDir,
|
|
99
|
+
cleanup: async () => {
|
|
100
|
+
await fs.rm(cloneDir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function assertTouchGrassLayout(sourcePath) {
|
|
106
|
+
const profilesDir = path.join(sourcePath, "agents", "profiles");
|
|
107
|
+
if (!existsSync(profilesDir)) {
|
|
108
|
+
throw new Error(`No agents/profiles directory found in ${sourcePath}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function loadCatalog(sourcePath) {
|
|
113
|
+
const profilesDir = path.join(sourcePath, "agents", "profiles");
|
|
114
|
+
const adapterRoot = path.join(sourcePath, "agents", "adapters");
|
|
115
|
+
const files = await fs.readdir(profilesDir, { withFileTypes: true });
|
|
116
|
+
const profiles = [];
|
|
117
|
+
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
if (!file.isFile() || !file.name.endsWith(".md")) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const filePath = path.join(profilesDir, file.name);
|
|
124
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
125
|
+
const parsed = parseFrontmatter(raw, filePath);
|
|
126
|
+
const slug = parsed.attributes.slug ?? path.basename(file.name, ".md");
|
|
127
|
+
|
|
128
|
+
profiles.push({
|
|
129
|
+
slug,
|
|
130
|
+
name: parsed.attributes.name ?? slug,
|
|
131
|
+
summary: parsed.attributes.summary ?? "",
|
|
132
|
+
attributes: parsed.attributes,
|
|
133
|
+
body: parsed.body,
|
|
134
|
+
raw,
|
|
135
|
+
filePath,
|
|
136
|
+
relativePath: path.relative(sourcePath, filePath),
|
|
137
|
+
adapters: await loadAdapters(adapterRoot, slug, sourcePath)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
profiles.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
142
|
+
return { sourcePath, profiles };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function loadAdapters(adapterRoot, slug, sourcePath) {
|
|
146
|
+
if (!existsSync(adapterRoot)) {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const adapters = {};
|
|
151
|
+
const harnessDirs = await fs.readdir(adapterRoot, { withFileTypes: true });
|
|
152
|
+
for (const harnessDir of harnessDirs) {
|
|
153
|
+
if (!harnessDir.isDirectory()) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const adapterPath = path.join(adapterRoot, harnessDir.name, `${slug}.md`);
|
|
158
|
+
if (!existsSync(adapterPath)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const raw = await fs.readFile(adapterPath, "utf8");
|
|
163
|
+
const parsed = parseFrontmatter(raw, adapterPath);
|
|
164
|
+
adapters[harnessDir.name] = {
|
|
165
|
+
harness: harnessDir.name,
|
|
166
|
+
attributes: parsed.attributes,
|
|
167
|
+
body: parsed.body,
|
|
168
|
+
raw,
|
|
169
|
+
filePath: adapterPath,
|
|
170
|
+
relativePath: path.relative(sourcePath, adapterPath)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return adapters;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function resolvePackageRoot() {
|
|
178
|
+
return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
179
|
+
}
|