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,201 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { cancel, isCancel, select, text as promptText } from "@clack/prompts";
4
+ import { buildAgentMarkdown, parseAgentsDir, targetFileNameForAgent, } from "./agents.js";
5
+ import { ensureDir, hashContent, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
6
+ import { readLockfile, upsertLockEntry, writeLockfile } from "./lockfile.js";
7
+ import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
8
+ import { discoverSourceAgentsDir, discoverSourceMcpPath, prepareSource, } from "./sources.js";
9
+ export class NonInteractiveConflictError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "NonInteractiveConflictError";
13
+ }
14
+ }
15
+ export async function importSource(options) {
16
+ const prepared = prepareSource({
17
+ source: options.source,
18
+ ref: options.ref,
19
+ subdir: options.subdir,
20
+ });
21
+ try {
22
+ const sourceAgentsDir = discoverSourceAgentsDir(prepared.importRoot);
23
+ const sourceMcpPath = discoverSourceMcpPath(prepared.importRoot);
24
+ const sourceAgents = parseAgentsDir(sourceAgentsDir);
25
+ if (sourceAgents.length === 0) {
26
+ throw new Error(`No agent files found in ${sourceAgentsDir}.`);
27
+ }
28
+ ensureDir(options.paths.agentsDir);
29
+ const importedAgents = [];
30
+ const importedAgentHashes = [];
31
+ for (const [index, agent] of sourceAgents.entries()) {
32
+ let targetFileName = targetFileNameForAgent(agent);
33
+ if (options.rename && sourceAgents.length === 1) {
34
+ targetFileName = `${slugify(options.rename) || "agent"}.md`;
35
+ }
36
+ const resolvedFileName = await resolveAgentConflict({
37
+ targetFileName,
38
+ agentContent: buildAgentMarkdown(agent.frontmatter, agent.body),
39
+ paths: options.paths,
40
+ yes: !!options.yes,
41
+ nonInteractive: !!options.nonInteractive,
42
+ promptLabel: `${agent.name} (${index + 1}/${sourceAgents.length})`,
43
+ });
44
+ if (!resolvedFileName)
45
+ continue;
46
+ const targetPath = path.join(options.paths.agentsDir, resolvedFileName);
47
+ const content = buildAgentMarkdown(agent.frontmatter, agent.body);
48
+ writeTextAtomic(targetPath, content);
49
+ importedAgents.push(relativePosix(options.paths.agentsRoot, targetPath));
50
+ importedAgentHashes.push(hashContent(content));
51
+ }
52
+ const importedMcpServers = [];
53
+ if (sourceMcpPath) {
54
+ const sourceMcp = normalizeMcp(readJsonIfExists(sourceMcpPath));
55
+ const targetMcp = readCanonicalMcp(options.paths);
56
+ const merged = await resolveMcpConflict({
57
+ sourceMcp,
58
+ targetMcp,
59
+ yes: !!options.yes,
60
+ nonInteractive: !!options.nonInteractive,
61
+ });
62
+ writeCanonicalMcp(options.paths, merged);
63
+ importedMcpServers.push(...Object.keys(sourceMcp.mcpServers));
64
+ }
65
+ const lockfile = readLockfile(options.paths);
66
+ const contentHash = hashContent(JSON.stringify({
67
+ agents: importedAgentHashes,
68
+ mcp: importedMcpServers,
69
+ }));
70
+ const lockEntry = {
71
+ source: prepared.spec.source,
72
+ sourceType: prepared.spec.type,
73
+ requestedRef: options.ref,
74
+ resolvedCommit: prepared.resolvedCommit,
75
+ subdir: options.subdir,
76
+ importedAt: new Date().toISOString(),
77
+ importedAgents,
78
+ importedMcpServers,
79
+ contentHash,
80
+ };
81
+ upsertLockEntry(lockfile, lockEntry);
82
+ writeLockfile(options.paths, lockfile);
83
+ return {
84
+ source: prepared.spec.source,
85
+ sourceType: prepared.spec.type,
86
+ importedAgents,
87
+ importedMcpServers,
88
+ resolvedCommit: prepared.resolvedCommit,
89
+ };
90
+ }
91
+ finally {
92
+ prepared.cleanup();
93
+ }
94
+ }
95
+ async function resolveAgentConflict(options) {
96
+ const targetPath = path.join(options.paths.agentsDir, options.targetFileName);
97
+ if (!fs.existsSync(targetPath))
98
+ return options.targetFileName;
99
+ const existing = fs.readFileSync(targetPath, "utf8");
100
+ if (existing === options.agentContent)
101
+ return options.targetFileName;
102
+ if (options.yes) {
103
+ return options.targetFileName;
104
+ }
105
+ if (options.nonInteractive) {
106
+ throw new NonInteractiveConflictError(`Conflict for ${options.targetFileName}. Use --yes or run interactively.`);
107
+ }
108
+ const choice = await select({
109
+ message: `Agent conflict for ${options.promptLabel}`,
110
+ options: [
111
+ { value: "overwrite", label: `Overwrite ${options.targetFileName}` },
112
+ { value: "skip", label: "Skip this agent" },
113
+ { value: "rename", label: "Rename imported agent" },
114
+ ],
115
+ });
116
+ if (isCancel(choice)) {
117
+ cancel("Operation cancelled.");
118
+ process.exit(1);
119
+ }
120
+ if (choice === "skip")
121
+ return null;
122
+ if (choice === "rename") {
123
+ const entered = await promptText({
124
+ message: `New filename (without extension) for ${options.promptLabel}`,
125
+ placeholder: options.targetFileName.replace(/\.md$/, ""),
126
+ validate(value) {
127
+ if (!value.trim())
128
+ return "Name is required.";
129
+ if (/[\\/]/.test(value))
130
+ return "Use a simple filename.";
131
+ return undefined;
132
+ },
133
+ });
134
+ if (isCancel(entered)) {
135
+ cancel("Operation cancelled.");
136
+ process.exit(1);
137
+ }
138
+ const renamedFileName = `${slugify(String(entered)) || "agent"}.md`;
139
+ return resolveAgentConflict({
140
+ ...options,
141
+ targetFileName: renamedFileName,
142
+ });
143
+ }
144
+ return options.targetFileName;
145
+ }
146
+ function normalizeMcp(raw) {
147
+ if (!raw) {
148
+ return {
149
+ version: 1,
150
+ mcpServers: {},
151
+ };
152
+ }
153
+ if (typeof raw.mcpServers === "object" && raw.mcpServers !== null) {
154
+ return {
155
+ version: 1,
156
+ mcpServers: raw.mcpServers,
157
+ };
158
+ }
159
+ return {
160
+ version: 1,
161
+ mcpServers: {},
162
+ };
163
+ }
164
+ async function resolveMcpConflict(options) {
165
+ const sourceNames = Object.keys(options.sourceMcp.mcpServers);
166
+ const targetNames = new Set(Object.keys(options.targetMcp.mcpServers));
167
+ const overlap = sourceNames.filter((name) => targetNames.has(name));
168
+ if (overlap.length === 0 || options.yes) {
169
+ return mergeMcp(options.targetMcp, options.sourceMcp);
170
+ }
171
+ if (options.nonInteractive) {
172
+ throw new NonInteractiveConflictError("MCP server conflicts found. Use --yes or run interactively.");
173
+ }
174
+ const choice = await select({
175
+ message: `MCP conflicts detected for: ${overlap.join(", ")}`,
176
+ options: [
177
+ { value: "merge", label: "Merge (source servers override overlaps)" },
178
+ { value: "replace", label: "Replace destination MCP with source MCP" },
179
+ ],
180
+ });
181
+ if (isCancel(choice)) {
182
+ cancel("Operation cancelled.");
183
+ process.exit(1);
184
+ }
185
+ if (choice === "replace") {
186
+ return {
187
+ version: 1,
188
+ mcpServers: { ...options.sourceMcp.mcpServers },
189
+ };
190
+ }
191
+ return mergeMcp(options.targetMcp, options.sourceMcp);
192
+ }
193
+ function mergeMcp(target, source) {
194
+ return {
195
+ version: 1,
196
+ mcpServers: {
197
+ ...target.mcpServers,
198
+ ...source.mcpServers,
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentsLockFile, LockEntry, ScopePaths } from "../types.js";
2
+ export declare function readLockfile(paths: ScopePaths): AgentsLockFile;
3
+ export declare function writeLockfile(paths: ScopePaths, lockfile: AgentsLockFile): void;
4
+ export declare function upsertLockEntry(lockfile: AgentsLockFile, entry: LockEntry): void;
@@ -0,0 +1,25 @@
1
+ import { readJsonIfExists, writeJsonAtomic } from "./fs.js";
2
+ const EMPTY_LOCK = {
3
+ version: 1,
4
+ entries: [],
5
+ };
6
+ export function readLockfile(paths) {
7
+ const lock = readJsonIfExists(paths.lockPath);
8
+ if (!lock || lock.version !== 1 || !Array.isArray(lock.entries)) {
9
+ return { ...EMPTY_LOCK };
10
+ }
11
+ return lock;
12
+ }
13
+ export function writeLockfile(paths, lockfile) {
14
+ writeJsonAtomic(paths.lockPath, lockfile);
15
+ }
16
+ export function upsertLockEntry(lockfile, entry) {
17
+ const index = lockfile.entries.findIndex((item) => item.source === entry.source &&
18
+ item.sourceType === entry.sourceType &&
19
+ item.subdir === entry.subdir);
20
+ if (index >= 0) {
21
+ lockfile.entries[index] = entry;
22
+ return;
23
+ }
24
+ lockfile.entries.push(entry);
25
+ }
@@ -0,0 +1,3 @@
1
+ import type { ScopePaths, SyncManifest } from "../types.js";
2
+ export declare function readManifest(paths: ScopePaths): SyncManifest;
3
+ export declare function writeManifest(paths: ScopePaths, manifest: SyncManifest): void;
@@ -0,0 +1,17 @@
1
+ import { readJsonIfExists, writeJsonAtomic } from "./fs.js";
2
+ const EMPTY_MANIFEST = {
3
+ version: 1,
4
+ generatedFiles: [],
5
+ };
6
+ export function readManifest(paths) {
7
+ const manifest = readJsonIfExists(paths.manifestPath);
8
+ if (!manifest ||
9
+ manifest.version !== 1 ||
10
+ !Array.isArray(manifest.generatedFiles)) {
11
+ return { ...EMPTY_MANIFEST };
12
+ }
13
+ return manifest;
14
+ }
15
+ export function writeManifest(paths, manifest) {
16
+ writeJsonAtomic(paths.manifestPath, manifest);
17
+ }
@@ -0,0 +1,4 @@
1
+ import type { CanonicalMcpFile, Provider, ScopePaths } from "../types.js";
2
+ export declare function readCanonicalMcp(paths: Pick<ScopePaths, "mcpPath">): CanonicalMcpFile;
3
+ export declare function writeCanonicalMcp(paths: Pick<ScopePaths, "mcpPath">, value: CanonicalMcpFile): void;
4
+ export declare function resolveMcpForProvider(mcp: CanonicalMcpFile, provider: Provider): Record<string, Record<string, unknown>>;
@@ -0,0 +1,73 @@
1
+ import { isObject, readJsonIfExists, writeJsonAtomic } from "./fs.js";
2
+ const EMPTY_MCP = {
3
+ version: 1,
4
+ mcpServers: {},
5
+ };
6
+ export function readCanonicalMcp(paths) {
7
+ const parsed = readJsonIfExists(paths.mcpPath);
8
+ if (!parsed)
9
+ return { ...EMPTY_MCP };
10
+ if (isObject(parsed) && isObject(parsed.mcpServers)) {
11
+ return {
12
+ version: 1,
13
+ mcpServers: parsed.mcpServers,
14
+ };
15
+ }
16
+ return {
17
+ version: 1,
18
+ mcpServers: {},
19
+ };
20
+ }
21
+ export function writeCanonicalMcp(paths, value) {
22
+ writeJsonAtomic(paths.mcpPath, {
23
+ version: 1,
24
+ mcpServers: value.mcpServers,
25
+ });
26
+ }
27
+ function deepMerge(base, override) {
28
+ const result = { ...base };
29
+ for (const [key, value] of Object.entries(override)) {
30
+ const prev = result[key];
31
+ if (isObject(prev) && isObject(value)) {
32
+ result[key] = deepMerge(prev, value);
33
+ }
34
+ else {
35
+ result[key] = value;
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ function normalizeServer(server) {
41
+ const normalizedBase = {};
42
+ const providers = isObject(server.providers)
43
+ ? server.providers
44
+ : {};
45
+ if (isObject(server.base)) {
46
+ Object.assign(normalizedBase, server.base);
47
+ }
48
+ for (const [key, value] of Object.entries(server)) {
49
+ if (key === "base" || key === "providers")
50
+ continue;
51
+ normalizedBase[key] = value;
52
+ }
53
+ return {
54
+ base: normalizedBase,
55
+ providers,
56
+ };
57
+ }
58
+ export function resolveMcpForProvider(mcp, provider) {
59
+ const resolved = {};
60
+ for (const [serverName, rawServer] of Object.entries(mcp.mcpServers)) {
61
+ const server = normalizeServer(rawServer);
62
+ const providerOverride = server.providers[provider];
63
+ if (providerOverride === false)
64
+ continue;
65
+ if (isObject(providerOverride)) {
66
+ resolved[serverName] = deepMerge(server.base, providerOverride);
67
+ }
68
+ else {
69
+ resolved[serverName] = { ...server.base };
70
+ }
71
+ }
72
+ return resolved;
73
+ }
@@ -0,0 +1,9 @@
1
+ import type { Scope, ScopePaths } from "../types.js";
2
+ export interface ScopeResolutionOptions {
3
+ cwd: string;
4
+ global?: boolean;
5
+ local?: boolean;
6
+ interactive?: boolean;
7
+ }
8
+ export declare function buildScopePaths(cwd: string, scope: Scope): ScopePaths;
9
+ export declare function resolveScope(options: ScopeResolutionOptions): Promise<ScopePaths>;
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { cancel, isCancel, select } from "@clack/prompts";
5
+ import { getGlobalSettingsPath, readSettings } from "./settings.js";
6
+ export function buildScopePaths(cwd, scope) {
7
+ const workspaceRoot = cwd;
8
+ const homeDir = os.homedir();
9
+ const agentsRoot = scope === "local"
10
+ ? path.join(workspaceRoot, ".agents")
11
+ : path.join(homeDir, ".agents");
12
+ return {
13
+ scope,
14
+ workspaceRoot,
15
+ homeDir,
16
+ agentsRoot,
17
+ agentsDir: path.join(agentsRoot, "agents"),
18
+ mcpPath: path.join(agentsRoot, "mcp.json"),
19
+ lockPath: path.join(agentsRoot, "agents.lock.json"),
20
+ settingsPath: path.join(agentsRoot, "settings.local.json"),
21
+ manifestPath: path.join(agentsRoot, ".sync-manifest.json"),
22
+ };
23
+ }
24
+ export async function resolveScope(options) {
25
+ const { cwd } = options;
26
+ if (options.global && options.local) {
27
+ throw new Error("Use either --global or --local, not both.");
28
+ }
29
+ if (options.global)
30
+ return buildScopePaths(cwd, "global");
31
+ if (options.local)
32
+ return buildScopePaths(cwd, "local");
33
+ const hasLocalAgents = fs.existsSync(path.join(cwd, ".agents"));
34
+ if (!hasLocalAgents) {
35
+ return buildScopePaths(cwd, "global");
36
+ }
37
+ const interactive = options.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
38
+ if (!interactive) {
39
+ return buildScopePaths(cwd, "local");
40
+ }
41
+ const globalSettings = readSettings(getGlobalSettingsPath());
42
+ const defaultScope = globalSettings.lastScope === "global" ? "global" : "local";
43
+ const selected = await select({
44
+ message: "Choose scope for this command",
45
+ options: [
46
+ {
47
+ value: "local",
48
+ label: ".agents in this repository",
49
+ hint: defaultScope === "local" ? "default" : undefined,
50
+ },
51
+ {
52
+ value: "global",
53
+ label: "~/.agents shared config",
54
+ hint: defaultScope === "global" ? "default" : undefined,
55
+ },
56
+ ],
57
+ initialValue: defaultScope,
58
+ });
59
+ if (isCancel(selected)) {
60
+ cancel("Operation cancelled.");
61
+ process.exit(1);
62
+ }
63
+ return buildScopePaths(cwd, selected);
64
+ }
@@ -0,0 +1,6 @@
1
+ import type { DotagentsSettings, Provider, Scope, ScopePaths } from "../types.js";
2
+ export declare function getGlobalSettingsPath(homeDir?: string): string;
3
+ export declare function readSettings(settingsPath: string): DotagentsSettings;
4
+ export declare function writeSettings(settingsPath: string, settings: DotagentsSettings): void;
5
+ export declare function updateLastScope(settingsPath: string, scope: Scope, providers?: Provider[]): void;
6
+ export declare function settingsPathForScope(paths: ScopePaths): string;
@@ -0,0 +1,54 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { readJsonIfExists, writeJsonAtomic } from "./fs.js";
4
+ const DEFAULT_SETTINGS = {
5
+ version: 1,
6
+ defaultProviders: [
7
+ "cursor",
8
+ "claude",
9
+ "codex",
10
+ "opencode",
11
+ "gemini",
12
+ "copilot",
13
+ ],
14
+ telemetry: {
15
+ enabled: true,
16
+ },
17
+ };
18
+ export function getGlobalSettingsPath(homeDir = os.homedir()) {
19
+ return path.join(homeDir, ".agents", "settings.local.json");
20
+ }
21
+ export function readSettings(settingsPath) {
22
+ const settings = readJsonIfExists(settingsPath);
23
+ if (!settings)
24
+ return { ...DEFAULT_SETTINGS };
25
+ return {
26
+ ...DEFAULT_SETTINGS,
27
+ ...settings,
28
+ telemetry: {
29
+ ...DEFAULT_SETTINGS.telemetry,
30
+ ...settings.telemetry,
31
+ },
32
+ defaultProviders: settings.defaultProviders && settings.defaultProviders.length > 0
33
+ ? settings.defaultProviders
34
+ : DEFAULT_SETTINGS.defaultProviders,
35
+ };
36
+ }
37
+ export function writeSettings(settingsPath, settings) {
38
+ writeJsonAtomic(settingsPath, settings);
39
+ }
40
+ export function updateLastScope(settingsPath, scope, providers) {
41
+ const current = readSettings(settingsPath);
42
+ const next = {
43
+ ...current,
44
+ version: 1,
45
+ lastScope: scope,
46
+ };
47
+ if (providers && providers.length > 0) {
48
+ next.defaultProviders = [...providers];
49
+ }
50
+ writeSettings(settingsPath, next);
51
+ }
52
+ export function settingsPathForScope(paths) {
53
+ return paths.settingsPath;
54
+ }
@@ -0,0 +1,20 @@
1
+ export type SourceType = "local" | "github" | "git";
2
+ export interface SourceSpec {
3
+ source: string;
4
+ type: SourceType;
5
+ }
6
+ export interface PreparedSource {
7
+ spec: SourceSpec;
8
+ rootPath: string;
9
+ importRoot: string;
10
+ resolvedCommit: string;
11
+ cleanup: () => void;
12
+ }
13
+ export declare function parseSourceSpec(source: string): SourceSpec;
14
+ export declare function prepareSource(options: {
15
+ source: string;
16
+ ref?: string;
17
+ subdir?: string;
18
+ }): PreparedSource;
19
+ export declare function discoverSourceAgentsDir(importRoot: string): string;
20
+ export declare function discoverSourceMcpPath(importRoot: string): string | null;
@@ -0,0 +1,162 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ export function parseSourceSpec(source) {
7
+ const trimmed = source.trim();
8
+ if (!trimmed) {
9
+ throw new Error("Source cannot be empty.");
10
+ }
11
+ const resolvedLocalPath = path.resolve(trimmed);
12
+ if (isExplicitLocalPath(trimmed) || fs.existsSync(resolvedLocalPath)) {
13
+ return { source: resolvedLocalPath, type: "local" };
14
+ }
15
+ if (isGitUrl(trimmed)) {
16
+ return { source: trimmed, type: "git" };
17
+ }
18
+ if (isGitHubSlug(trimmed)) {
19
+ return { source: trimmed, type: "github" };
20
+ }
21
+ return { source: path.resolve(trimmed), type: "local" };
22
+ }
23
+ export function prepareSource(options) {
24
+ const spec = parseSourceSpec(options.source);
25
+ if (spec.type === "local") {
26
+ if (!fs.existsSync(spec.source)) {
27
+ throw new Error(`Local source not found: ${spec.source}`);
28
+ }
29
+ const importRoot = resolveImportRoot(spec.source, options.subdir);
30
+ const resolvedCommit = resolveLocalCommitOrHash(spec.source);
31
+ return {
32
+ spec,
33
+ rootPath: spec.source,
34
+ importRoot,
35
+ resolvedCommit,
36
+ cleanup: () => undefined,
37
+ };
38
+ }
39
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agentloom-"));
40
+ const cloneUrl = spec.type === "github"
41
+ ? `https://github.com/${spec.source}.git`
42
+ : spec.source;
43
+ runGit(["clone", cloneUrl, tmpRoot]);
44
+ if (options.ref) {
45
+ runGit(["-C", tmpRoot, "checkout", options.ref]);
46
+ }
47
+ const resolvedCommit = runGit(["-C", tmpRoot, "rev-parse", "HEAD"]).trim();
48
+ const importRoot = resolveImportRoot(tmpRoot, options.subdir);
49
+ return {
50
+ spec,
51
+ rootPath: tmpRoot,
52
+ importRoot,
53
+ resolvedCommit,
54
+ cleanup: () => {
55
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
56
+ },
57
+ };
58
+ }
59
+ export function discoverSourceAgentsDir(importRoot) {
60
+ const direct = path.join(importRoot, "agents");
61
+ if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
62
+ return direct;
63
+ }
64
+ const nested = path.join(importRoot, ".agents", "agents");
65
+ if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
66
+ return nested;
67
+ }
68
+ throw new Error(`No source agents directory found under ${importRoot} (expected agents/ or .agents/agents/).`);
69
+ }
70
+ export function discoverSourceMcpPath(importRoot) {
71
+ const nested = path.join(importRoot, ".agents", "mcp.json");
72
+ if (fs.existsSync(nested))
73
+ return nested;
74
+ const direct = path.join(importRoot, "mcp.json");
75
+ if (fs.existsSync(direct))
76
+ return direct;
77
+ return null;
78
+ }
79
+ function resolveImportRoot(rootPath, subdir) {
80
+ if (!subdir)
81
+ return rootPath;
82
+ const importRoot = path.resolve(rootPath, subdir);
83
+ if (!fs.existsSync(importRoot)) {
84
+ throw new Error(`Subdir does not exist in source: ${subdir}`);
85
+ }
86
+ return importRoot;
87
+ }
88
+ function isGitHubSlug(source) {
89
+ return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(source);
90
+ }
91
+ function isExplicitLocalPath(source) {
92
+ return (path.isAbsolute(source) ||
93
+ source.startsWith("./") ||
94
+ source.startsWith("../") ||
95
+ source.startsWith(".\\") ||
96
+ source.startsWith("..\\"));
97
+ }
98
+ function isGitUrl(source) {
99
+ return (source.startsWith("http://") ||
100
+ source.startsWith("https://") ||
101
+ source.startsWith("git@") ||
102
+ source.endsWith(".git"));
103
+ }
104
+ function resolveLocalCommitOrHash(localPath) {
105
+ try {
106
+ return runGit(["-C", localPath, "rev-parse", "HEAD"]).trim();
107
+ }
108
+ catch {
109
+ return `local-${hashLocalPathContents(localPath)}`;
110
+ }
111
+ }
112
+ function hashLocalPathContents(localPath) {
113
+ const stat = fs.statSync(localPath);
114
+ const hasher = createHash("sha256");
115
+ if (stat.isFile()) {
116
+ hasher.update("file");
117
+ hasher.update("\0");
118
+ hasher.update(fs.readFileSync(localPath));
119
+ return hasher.digest("hex");
120
+ }
121
+ const files = collectFiles(localPath);
122
+ if (files.length === 0) {
123
+ hasher.update("empty");
124
+ }
125
+ for (const filePath of files) {
126
+ const relativeFilePath = path
127
+ .relative(localPath, filePath)
128
+ .split(path.sep)
129
+ .join("/");
130
+ hasher.update(relativeFilePath);
131
+ hasher.update("\0");
132
+ hasher.update(fs.readFileSync(filePath));
133
+ hasher.update("\0");
134
+ }
135
+ return hasher.digest("hex");
136
+ }
137
+ function collectFiles(rootPath) {
138
+ const entries = [];
139
+ const stack = [rootPath];
140
+ while (stack.length > 0) {
141
+ const currentPath = stack.pop();
142
+ if (!currentPath)
143
+ continue;
144
+ const children = fs.readdirSync(currentPath, { withFileTypes: true });
145
+ for (const child of children) {
146
+ const childPath = path.join(currentPath, child.name);
147
+ if (child.isDirectory()) {
148
+ stack.push(childPath);
149
+ }
150
+ else if (child.isFile()) {
151
+ entries.push(childPath);
152
+ }
153
+ }
154
+ }
155
+ return entries.sort();
156
+ }
157
+ function runGit(args) {
158
+ return execFileSync("git", args, {
159
+ stdio: ["ignore", "pipe", "pipe"],
160
+ encoding: "utf8",
161
+ });
162
+ }
@@ -0,0 +1,8 @@
1
+ type MaybeNotifyOptions = {
2
+ command: string;
3
+ packageName?: string;
4
+ currentVersion: string;
5
+ };
6
+ export declare function maybeNotifyVersionUpdate(options: MaybeNotifyOptions): Promise<void>;
7
+ export declare function isNewerVersion(candidate: string, current: string): boolean;
8
+ export {};