agentloom 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/ThirdPartyNoticeText.txt +3 -0
  4. package/bin/cli.mjs +8 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +61 -0
  7. package/dist/commands/add.d.ts +2 -0
  8. package/dist/commands/add.js +62 -0
  9. package/dist/commands/mcp.d.ts +2 -0
  10. package/dist/commands/mcp.js +188 -0
  11. package/dist/commands/skills.d.ts +1 -0
  12. package/dist/commands/skills.js +11 -0
  13. package/dist/commands/sync.d.ts +2 -0
  14. package/dist/commands/sync.js +25 -0
  15. package/dist/commands/update.d.ts +2 -0
  16. package/dist/commands/update.js +71 -0
  17. package/dist/core/agents.d.ts +7 -0
  18. package/dist/core/agents.js +67 -0
  19. package/dist/core/argv.d.ts +5 -0
  20. package/dist/core/argv.js +52 -0
  21. package/dist/core/copy.d.ts +16 -0
  22. package/dist/core/copy.js +167 -0
  23. package/dist/core/fs.d.ts +13 -0
  24. package/dist/core/fs.js +70 -0
  25. package/dist/core/importer.d.ts +21 -0
  26. package/dist/core/importer.js +201 -0
  27. package/dist/core/lockfile.d.ts +4 -0
  28. package/dist/core/lockfile.js +25 -0
  29. package/dist/core/manifest.d.ts +3 -0
  30. package/dist/core/manifest.js +17 -0
  31. package/dist/core/mcp.d.ts +4 -0
  32. package/dist/core/mcp.js +73 -0
  33. package/dist/core/scope.d.ts +9 -0
  34. package/dist/core/scope.js +64 -0
  35. package/dist/core/settings.d.ts +6 -0
  36. package/dist/core/settings.js +54 -0
  37. package/dist/core/sources.d.ts +20 -0
  38. package/dist/core/sources.js +162 -0
  39. package/dist/core/version-notifier.d.ts +8 -0
  40. package/dist/core/version-notifier.js +142 -0
  41. package/dist/core/version.d.ts +1 -0
  42. package/dist/core/version.js +25 -0
  43. package/dist/sync/index.d.ts +15 -0
  44. package/dist/sync/index.js +482 -0
  45. package/dist/types.d.ts +73 -0
  46. package/dist/types.js +8 -0
  47. package/package.json +60 -0
@@ -0,0 +1,71 @@
1
+ import { importSource, NonInteractiveConflictError } from "../core/importer.js";
2
+ import { readLockfile } from "../core/lockfile.js";
3
+ import { resolveScope } from "../core/scope.js";
4
+ import { prepareSource } from "../core/sources.js";
5
+ import { parseProvidersFlag } from "../core/argv.js";
6
+ import { getUpdateHelpText } from "../core/copy.js";
7
+ import { formatSyncSummary, syncFromCanonical } from "../sync/index.js";
8
+ export async function runUpdateCommand(argv, cwd) {
9
+ if (argv.help) {
10
+ console.log(getUpdateHelpText());
11
+ return;
12
+ }
13
+ const nonInteractive = !(process.stdin.isTTY && process.stdout.isTTY);
14
+ const paths = await resolveScope({
15
+ cwd,
16
+ global: Boolean(argv.global),
17
+ local: Boolean(argv.local),
18
+ interactive: !nonInteractive,
19
+ });
20
+ const lockfile = readLockfile(paths);
21
+ if (lockfile.entries.length === 0) {
22
+ console.log(`No lock entries found in ${paths.lockPath}.`);
23
+ return;
24
+ }
25
+ let updated = 0;
26
+ let skipped = 0;
27
+ for (const entry of lockfile.entries) {
28
+ const probe = prepareSource({
29
+ source: entry.source,
30
+ ref: entry.requestedRef,
31
+ subdir: entry.subdir,
32
+ });
33
+ const hasNewCommit = probe.resolvedCommit !== entry.resolvedCommit;
34
+ probe.cleanup();
35
+ if (!hasNewCommit) {
36
+ skipped += 1;
37
+ continue;
38
+ }
39
+ try {
40
+ await importSource({
41
+ source: entry.source,
42
+ ref: entry.requestedRef,
43
+ subdir: entry.subdir,
44
+ yes: Boolean(argv.yes),
45
+ nonInteractive,
46
+ paths,
47
+ });
48
+ updated += 1;
49
+ }
50
+ catch (err) {
51
+ if (err instanceof NonInteractiveConflictError) {
52
+ console.error(err.message);
53
+ process.exit(2);
54
+ }
55
+ throw err;
56
+ }
57
+ }
58
+ console.log(`Updated entries: ${updated}`);
59
+ console.log(`Unchanged entries: ${skipped}`);
60
+ if (updated > 0 && !argv["no-sync"]) {
61
+ const syncSummary = await syncFromCanonical({
62
+ paths,
63
+ providers: parseProvidersFlag(argv.providers),
64
+ yes: Boolean(argv.yes),
65
+ nonInteractive,
66
+ dryRun: Boolean(argv["dry-run"]),
67
+ });
68
+ console.log("");
69
+ console.log(formatSyncSummary(syncSummary, paths.agentsRoot));
70
+ }
71
+ }
@@ -0,0 +1,7 @@
1
+ import type { AgentFrontmatter, CanonicalAgent, Provider } from "../types.js";
2
+ export declare function parseAgentsDir(agentsDir: string): CanonicalAgent[];
3
+ export declare function parseAgentMarkdown(raw: string, sourcePath: string, fileName?: string): CanonicalAgent;
4
+ export declare function buildAgentMarkdown(frontmatter: AgentFrontmatter, body: string): string;
5
+ export declare function targetFileNameForAgent(agent: CanonicalAgent): string;
6
+ export declare function getProviderConfig(frontmatter: AgentFrontmatter, provider: Provider): Record<string, unknown> | null;
7
+ export declare function isProviderEnabled(frontmatter: AgentFrontmatter, provider: Provider): boolean;
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ import YAML from "yaml";
5
+ import { isObject, slugify } from "./fs.js";
6
+ export function parseAgentsDir(agentsDir) {
7
+ if (!fs.existsSync(agentsDir))
8
+ return [];
9
+ const files = fs
10
+ .readdirSync(agentsDir)
11
+ .filter((entry) => entry.endsWith(".md"))
12
+ .sort();
13
+ return files.map((entry) => {
14
+ const filePath = path.join(agentsDir, entry);
15
+ const raw = fs.readFileSync(filePath, "utf8");
16
+ return parseAgentMarkdown(raw, filePath, entry);
17
+ });
18
+ }
19
+ export function parseAgentMarkdown(raw, sourcePath, fileName = path.basename(sourcePath)) {
20
+ const parsed = matter(raw);
21
+ if (!isObject(parsed.data)) {
22
+ throw new Error(`Invalid frontmatter in ${sourcePath}: expected object.`);
23
+ }
24
+ const frontmatter = parsed.data;
25
+ if (typeof frontmatter.name !== "string" || frontmatter.name.trim() === "") {
26
+ throw new Error(`Invalid frontmatter in ${sourcePath}: missing \`name\`.`);
27
+ }
28
+ if (typeof frontmatter.description !== "string" ||
29
+ frontmatter.description.trim() === "") {
30
+ throw new Error(`Invalid frontmatter in ${sourcePath}: missing \`description\`.`);
31
+ }
32
+ const normalizedName = frontmatter.name.trim();
33
+ const normalizedDescription = frontmatter.description.trim();
34
+ const body = parsed.content.trimStart();
35
+ return {
36
+ name: normalizedName,
37
+ description: normalizedDescription,
38
+ body,
39
+ frontmatter: {
40
+ ...frontmatter,
41
+ name: normalizedName,
42
+ description: normalizedDescription,
43
+ },
44
+ sourcePath,
45
+ fileName,
46
+ };
47
+ }
48
+ export function buildAgentMarkdown(frontmatter, body) {
49
+ const fm = YAML.stringify(frontmatter).trimEnd();
50
+ const normalizedBody = body.trimStart();
51
+ return `---\n${fm}\n---\n\n${normalizedBody}${normalizedBody.endsWith("\n") ? "" : "\n"}`;
52
+ }
53
+ export function targetFileNameForAgent(agent) {
54
+ const slug = slugify(agent.name);
55
+ return `${slug || "agent"}.md`;
56
+ }
57
+ export function getProviderConfig(frontmatter, provider) {
58
+ const value = frontmatter[provider];
59
+ if (value === false)
60
+ return null;
61
+ if (isObject(value))
62
+ return value;
63
+ return {};
64
+ }
65
+ export function isProviderEnabled(frontmatter, provider) {
66
+ return frontmatter[provider] !== false;
67
+ }
@@ -0,0 +1,5 @@
1
+ import type { ParsedArgs } from "minimist";
2
+ import type { Provider } from "../types.js";
3
+ export declare function parseArgs(argv: string[]): ParsedArgs;
4
+ export declare function parseProvidersFlag(input: unknown): Provider[] | undefined;
5
+ export declare function getStringArrayFlag(value: unknown, fallback?: string[]): string[];
@@ -0,0 +1,52 @@
1
+ import minimist from "minimist";
2
+ export function parseArgs(argv) {
3
+ return minimist(argv, {
4
+ boolean: ["global", "local", "yes", "no-sync", "dry-run", "json", "help"],
5
+ string: ["ref", "subdir", "providers", "rename", "command", "url"],
6
+ alias: {
7
+ g: "global",
8
+ l: "local",
9
+ y: "yes",
10
+ h: "help",
11
+ },
12
+ "--": true,
13
+ });
14
+ }
15
+ export function parseProvidersFlag(input) {
16
+ if (typeof input !== "string" || input.trim() === "")
17
+ return undefined;
18
+ const parsed = input
19
+ .split(",")
20
+ .map((item) => item.trim())
21
+ .filter(Boolean)
22
+ .map((item) => item.toLowerCase());
23
+ const validProviders = [];
24
+ for (const provider of parsed) {
25
+ if (provider === "cursor" ||
26
+ provider === "claude" ||
27
+ provider === "codex" ||
28
+ provider === "opencode" ||
29
+ provider === "gemini" ||
30
+ provider === "copilot") {
31
+ validProviders.push(provider);
32
+ continue;
33
+ }
34
+ throw new Error(`Unknown provider: ${provider}. Expected one of: cursor, claude, codex, opencode, gemini, copilot.`);
35
+ }
36
+ return [...new Set(validProviders)];
37
+ }
38
+ export function getStringArrayFlag(value, fallback = []) {
39
+ if (Array.isArray(value)) {
40
+ return value
41
+ .flatMap((item) => String(item).split(","))
42
+ .map((item) => item.trim())
43
+ .filter(Boolean);
44
+ }
45
+ if (typeof value === "string") {
46
+ return value
47
+ .split(",")
48
+ .map((item) => item.trim())
49
+ .filter(Boolean);
50
+ }
51
+ return fallback;
52
+ }
@@ -0,0 +1,16 @@
1
+ type UsageErrorInput = {
2
+ issue: string;
3
+ usage: string;
4
+ example?: string;
5
+ };
6
+ export declare function getRootHelpText(): string;
7
+ export declare function getAddHelpText(): string;
8
+ export declare function getUpdateHelpText(): string;
9
+ export declare function getSyncHelpText(): string;
10
+ export declare function getMcpHelpText(): string;
11
+ export declare function getMcpAddHelpText(): string;
12
+ export declare function getMcpListHelpText(): string;
13
+ export declare function getMcpDeleteHelpText(): string;
14
+ export declare function formatUsageError(input: UsageErrorInput): string;
15
+ export declare function formatUnknownCommandError(command: string): string;
16
+ export {};
@@ -0,0 +1,167 @@
1
+ const PROVIDERS_CSV = "cursor,claude,codex,opencode,gemini,copilot";
2
+ export function getRootHelpText() {
3
+ return `agentloom - unified agent and MCP sync CLI
4
+
5
+ Usage:
6
+ agentloom <command> [options]
7
+
8
+ Commands:
9
+ skills ... Pass through to "npx skills ..." (vercel-labs/skills)
10
+ add <source> Import agents and MCP from a repo source
11
+ update Refresh lockfile-managed imports
12
+ sync Generate provider-specific outputs
13
+ mcp <add|list|delete> Manage canonical MCP servers
14
+ help Show this help text
15
+
16
+ Common options:
17
+ --local Use .agents from current workspace
18
+ --global Use ~/.agents
19
+ --yes Skip interactive confirmations
20
+ --no-sync Skip post-change sync (mutating commands)
21
+ --providers <csv> Limit sync providers (${PROVIDERS_CSV})
22
+ --dry-run Print planned sync changes without writing files
23
+
24
+ Examples:
25
+ agentloom add vercel-labs/skills
26
+ agentloom add /repo --subdir packages/agents
27
+ agentloom update --local
28
+ agentloom sync --providers codex,claude,cursor
29
+ agentloom mcp add browser-tools --command npx --arg browser-tools-mcp
30
+ agentloom skills add vercel-labs/skills
31
+ `;
32
+ }
33
+ export function getAddHelpText() {
34
+ return `Import canonical agents and MCP from a source repository.
35
+
36
+ Usage:
37
+ agentloom add <source> [options]
38
+
39
+ Options:
40
+ --ref <ref> Git ref (branch/tag/commit) for remote sources
41
+ --subdir <path> Subdirectory inside source repo
42
+ --rename <name> Rename imported agent (single-agent import only)
43
+ --local | --global Choose destination scope
44
+ --yes Skip conflict prompts (overwrite/merge defaults)
45
+ --no-sync Do not run sync after import
46
+ --providers <csv> Providers for post-import sync (${PROVIDERS_CSV})
47
+ --dry-run Show sync plan without writing provider files
48
+
49
+ Example:
50
+ agentloom add vercel-labs/skills --subdir skills --providers codex,claude
51
+ `;
52
+ }
53
+ export function getUpdateHelpText() {
54
+ return `Refresh lockfile-managed sources and re-import updated revisions.
55
+
56
+ Usage:
57
+ agentloom update [options]
58
+
59
+ Options:
60
+ --local | --global Choose lockfile scope
61
+ --yes Skip conflict prompts during re-import
62
+ --no-sync Do not run sync after updates
63
+ --providers <csv> Providers for post-update sync (${PROVIDERS_CSV})
64
+ --dry-run Show sync plan without writing provider files
65
+
66
+ Example:
67
+ agentloom update --local --providers codex,cursor
68
+ `;
69
+ }
70
+ export function getSyncHelpText() {
71
+ return `Generate provider-specific agent and MCP files from canonical .agents data.
72
+
73
+ Usage:
74
+ agentloom sync [options]
75
+
76
+ Options:
77
+ --local | --global Choose canonical scope
78
+ --providers <csv> Limit providers (${PROVIDERS_CSV})
79
+ --yes Auto-delete stale generated files
80
+ --dry-run Show file changes without writing
81
+
82
+ Example:
83
+ agentloom sync --local --providers codex,claude,cursor --dry-run
84
+ `;
85
+ }
86
+ export function getMcpHelpText() {
87
+ return `Manage canonical MCP servers in .agents/mcp.json.
88
+
89
+ Usage:
90
+ agentloom mcp <command> [options]
91
+
92
+ Commands:
93
+ add <name> Add or update an MCP server
94
+ list List configured MCP servers
95
+ delete <name> Remove an MCP server
96
+
97
+ Shared options:
98
+ --local | --global Choose canonical scope
99
+ --no-sync Skip post-change sync (add/delete only)
100
+ --providers <csv> Providers for post-change sync (${PROVIDERS_CSV})
101
+
102
+ Examples:
103
+ agentloom mcp add browser --command npx --arg browser-tools-mcp
104
+ agentloom mcp list --json
105
+ agentloom mcp delete browser
106
+ `;
107
+ }
108
+ export function getMcpAddHelpText() {
109
+ return `Add or update an MCP server in canonical .agents/mcp.json.
110
+
111
+ Usage:
112
+ agentloom mcp add <name> (--url <url> | --command <cmd>) [options]
113
+
114
+ Options:
115
+ --arg <value> Repeatable command argument
116
+ --env KEY=VALUE Repeatable environment variable
117
+ --providers <csv> Provider-specific server assignment (${PROVIDERS_CSV})
118
+ --local | --global Choose canonical scope
119
+ --no-sync Skip post-change sync
120
+
121
+ Examples:
122
+ agentloom mcp add browser --command npx --arg browser-tools-mcp
123
+ agentloom mcp add docs --url https://example.com/mcp --providers codex,claude
124
+ `;
125
+ }
126
+ export function getMcpListHelpText() {
127
+ return `List canonical MCP servers.
128
+
129
+ Usage:
130
+ agentloom mcp list [options]
131
+
132
+ Options:
133
+ --json Print raw JSON
134
+ --local | --global Choose canonical scope
135
+
136
+ Example:
137
+ agentloom mcp list --json
138
+ `;
139
+ }
140
+ export function getMcpDeleteHelpText() {
141
+ return `Delete an MCP server from canonical .agents/mcp.json.
142
+
143
+ Usage:
144
+ agentloom mcp delete <name> [options]
145
+
146
+ Options:
147
+ --local | --global Choose canonical scope
148
+ --no-sync Skip post-change sync
149
+
150
+ Example:
151
+ agentloom mcp delete browser
152
+ `;
153
+ }
154
+ export function formatUsageError(input) {
155
+ const lines = [`Issue: ${input.issue}`, `Usage: ${input.usage}`];
156
+ if (input.example) {
157
+ lines.push(`Example: ${input.example}`);
158
+ }
159
+ return lines.join("\n");
160
+ }
161
+ export function formatUnknownCommandError(command) {
162
+ return formatUsageError({
163
+ issue: `Unknown command "${command}".`,
164
+ usage: "agentloom --help",
165
+ example: "agentloom sync --local",
166
+ });
167
+ }
@@ -0,0 +1,13 @@
1
+ export declare function ensureDir(dirPath: string): void;
2
+ export declare function readTextIfExists(filePath: string): string | null;
3
+ export declare function readJsonIfExists<T>(filePath: string): T | null;
4
+ export declare function writeJsonAtomic(filePath: string, value: unknown): void;
5
+ export declare function writeTextAtomic(filePath: string, content: string): void;
6
+ export declare function listMarkdownFiles(dirPath: string): string[];
7
+ export declare function hashContent(input: string): string;
8
+ export declare function hashFiles(filePaths: string[]): string;
9
+ export declare function slugify(input: string): string;
10
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
11
+ export declare function toPosixPath(filePath: string): string;
12
+ export declare function relativePosix(fromPath: string, toPath: string): string;
13
+ export declare function removeFileIfExists(filePath: string): void;
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export function ensureDir(dirPath) {
5
+ fs.mkdirSync(dirPath, { recursive: true });
6
+ }
7
+ export function readTextIfExists(filePath) {
8
+ if (!fs.existsSync(filePath))
9
+ return null;
10
+ return fs.readFileSync(filePath, "utf8");
11
+ }
12
+ export function readJsonIfExists(filePath) {
13
+ const text = readTextIfExists(filePath);
14
+ if (text === null)
15
+ return null;
16
+ return JSON.parse(text);
17
+ }
18
+ export function writeJsonAtomic(filePath, value) {
19
+ writeTextAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`);
20
+ }
21
+ export function writeTextAtomic(filePath, content) {
22
+ ensureDir(path.dirname(filePath));
23
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
24
+ fs.writeFileSync(tempPath, content, "utf8");
25
+ fs.renameSync(tempPath, filePath);
26
+ }
27
+ export function listMarkdownFiles(dirPath) {
28
+ if (!fs.existsSync(dirPath))
29
+ return [];
30
+ return fs
31
+ .readdirSync(dirPath)
32
+ .filter((entry) => entry.endsWith(".md") || entry.endsWith(".mdc"))
33
+ .map((entry) => path.join(dirPath, entry));
34
+ }
35
+ export function hashContent(input) {
36
+ return createHash("sha256").update(input).digest("hex");
37
+ }
38
+ export function hashFiles(filePaths) {
39
+ const hasher = createHash("sha256");
40
+ for (const filePath of [...filePaths].sort()) {
41
+ hasher.update(filePath);
42
+ hasher.update("\0");
43
+ hasher.update(fs.readFileSync(filePath));
44
+ hasher.update("\0");
45
+ }
46
+ return hasher.digest("hex");
47
+ }
48
+ export function slugify(input) {
49
+ return input
50
+ .trim()
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9_-]+/g, "-")
53
+ .replace(/-{2,}/g, "-")
54
+ .replace(/^-+|-+$/g, "")
55
+ .slice(0, 80);
56
+ }
57
+ export function isObject(value) {
58
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59
+ }
60
+ export function toPosixPath(filePath) {
61
+ return filePath.split(path.sep).join("/");
62
+ }
63
+ export function relativePosix(fromPath, toPath) {
64
+ return toPosixPath(path.relative(fromPath, toPath));
65
+ }
66
+ export function removeFileIfExists(filePath) {
67
+ if (fs.existsSync(filePath)) {
68
+ fs.unlinkSync(filePath);
69
+ }
70
+ }
@@ -0,0 +1,21 @@
1
+ import type { ScopePaths } from "../types.js";
2
+ export declare class NonInteractiveConflictError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export interface ImportOptions {
6
+ source: string;
7
+ ref?: string;
8
+ subdir?: string;
9
+ rename?: string;
10
+ yes?: boolean;
11
+ nonInteractive?: boolean;
12
+ paths: ScopePaths;
13
+ }
14
+ export interface ImportSummary {
15
+ source: string;
16
+ sourceType: "local" | "github" | "git";
17
+ importedAgents: string[];
18
+ importedMcpServers: string[];
19
+ resolvedCommit: string;
20
+ }
21
+ export declare function importSource(options: ImportOptions): Promise<ImportSummary>;