agentloom 0.1.0 → 0.1.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.
Files changed (53) hide show
  1. package/README.md +91 -72
  2. package/bin/cli.mjs +3 -2
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +149 -17
  5. package/dist/commands/add.d.ts +7 -0
  6. package/dist/commands/add.js +122 -31
  7. package/dist/commands/agent.d.ts +2 -0
  8. package/dist/commands/agent.js +85 -0
  9. package/dist/commands/command.d.ts +2 -0
  10. package/dist/commands/command.js +98 -0
  11. package/dist/commands/delete.d.ts +9 -0
  12. package/dist/commands/delete.js +444 -0
  13. package/dist/commands/entity-utils.d.ts +13 -0
  14. package/dist/commands/entity-utils.js +58 -0
  15. package/dist/commands/find.d.ts +21 -0
  16. package/dist/commands/find.js +944 -0
  17. package/dist/commands/mcp.js +133 -55
  18. package/dist/commands/skills.d.ts +2 -1
  19. package/dist/commands/skills.js +105 -9
  20. package/dist/commands/sync.d.ts +6 -0
  21. package/dist/commands/sync.js +12 -10
  22. package/dist/commands/update.d.ts +7 -0
  23. package/dist/commands/update.js +286 -21
  24. package/dist/core/argv.d.ts +2 -1
  25. package/dist/core/argv.js +42 -2
  26. package/dist/core/commands.d.ts +13 -0
  27. package/dist/core/commands.js +65 -0
  28. package/dist/core/copy.d.ts +6 -0
  29. package/dist/core/copy.js +126 -65
  30. package/dist/core/importer.d.ts +28 -1
  31. package/dist/core/importer.js +1104 -41
  32. package/dist/core/lockfile.js +86 -3
  33. package/dist/core/manage-agents-bootstrap.d.ts +10 -0
  34. package/dist/core/manage-agents-bootstrap.js +40 -0
  35. package/dist/core/manifest.js +7 -1
  36. package/dist/core/router.d.ts +16 -0
  37. package/dist/core/router.js +66 -0
  38. package/dist/core/scope.d.ts +1 -1
  39. package/dist/core/scope.js +12 -8
  40. package/dist/core/settings.d.ts +4 -3
  41. package/dist/core/settings.js +10 -8
  42. package/dist/core/skills.d.ts +23 -0
  43. package/dist/core/skills.js +328 -0
  44. package/dist/core/sources.d.ts +3 -1
  45. package/dist/core/sources.js +31 -1
  46. package/dist/core/telemetry.d.ts +26 -0
  47. package/dist/core/telemetry.js +124 -0
  48. package/dist/core/version-notifier.d.ts +1 -0
  49. package/dist/core/version-notifier.js +22 -4
  50. package/dist/sync/index.d.ts +7 -1
  51. package/dist/sync/index.js +395 -131
  52. package/dist/types.d.ts +16 -1
  53. package/package.json +5 -4
@@ -0,0 +1,328 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ import { ensureDir, slugify } from "./fs.js";
5
+ export const ROOT_SKILL_ARTIFACT_DIRS = [
6
+ "references",
7
+ "assets",
8
+ "scripts",
9
+ "templates",
10
+ "examples",
11
+ ];
12
+ export function parseSkillsDir(skillsDir) {
13
+ if (!fs.existsSync(skillsDir))
14
+ return [];
15
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
16
+ const skills = [];
17
+ for (const entry of entries) {
18
+ if (!entry.isDirectory())
19
+ continue;
20
+ const skillDir = path.join(skillsDir, entry.name);
21
+ const skillFile = path.join(skillDir, "SKILL.md");
22
+ if (!fs.existsSync(skillFile))
23
+ continue;
24
+ skills.push({
25
+ name: entry.name,
26
+ sourcePath: skillDir,
27
+ skillPath: skillFile,
28
+ layout: "nested",
29
+ });
30
+ }
31
+ if (skills.length > 0) {
32
+ return skills.sort((left, right) => left.name.localeCompare(right.name));
33
+ }
34
+ const rootSkillFile = path.join(skillsDir, "SKILL.md");
35
+ if (!fs.existsSync(rootSkillFile) || !fs.statSync(rootSkillFile).isFile()) {
36
+ return [];
37
+ }
38
+ const raw = fs.readFileSync(rootSkillFile, "utf8");
39
+ return [
40
+ {
41
+ name: extractSkillName(raw) || path.basename(skillsDir),
42
+ sourcePath: skillsDir,
43
+ skillPath: rootSkillFile,
44
+ layout: "root",
45
+ },
46
+ ];
47
+ }
48
+ function extractSkillName(raw) {
49
+ try {
50
+ const parsed = matter(raw);
51
+ if (typeof parsed.data.name !== "string")
52
+ return undefined;
53
+ const name = parsed.data.name.trim();
54
+ return name.length > 0 ? name : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ export function normalizeSkillSelector(value) {
61
+ return slugify(value.trim().replace(/\/+$/, "")).toLowerCase();
62
+ }
63
+ export function resolveSkillSelections(skills, selectors) {
64
+ const normalizedSelectors = selectors
65
+ .map((item) => item.trim())
66
+ .filter(Boolean)
67
+ .map(normalizeSkillSelector)
68
+ .filter(Boolean);
69
+ const selectedMap = new Map();
70
+ const unmatched = [];
71
+ for (const selector of normalizedSelectors) {
72
+ const matches = skills.filter((skill) => normalizeSkillSelector(skill.name) === selector);
73
+ if (matches.length === 0) {
74
+ unmatched.push(selector);
75
+ continue;
76
+ }
77
+ for (const match of matches) {
78
+ selectedMap.set(match.name, match);
79
+ }
80
+ }
81
+ return {
82
+ selected: [...selectedMap.values()],
83
+ unmatched,
84
+ };
85
+ }
86
+ export function copyRootSkillArtifacts(sourceRoot, targetDir) {
87
+ ensureDir(targetDir);
88
+ const sourceSkillPath = path.join(sourceRoot, "SKILL.md");
89
+ if (!fs.existsSync(sourceSkillPath) ||
90
+ !fs.statSync(sourceSkillPath).isFile()) {
91
+ throw new Error(`Root skill source is missing SKILL.md at ${sourceSkillPath}.`);
92
+ }
93
+ fs.copyFileSync(sourceSkillPath, path.join(targetDir, "SKILL.md"));
94
+ for (const artifactDirName of ROOT_SKILL_ARTIFACT_DIRS) {
95
+ const sourceArtifactDir = path.join(sourceRoot, artifactDirName);
96
+ if (!fs.existsSync(sourceArtifactDir) ||
97
+ !fs.statSync(sourceArtifactDir).isDirectory()) {
98
+ continue;
99
+ }
100
+ fs.cpSync(sourceArtifactDir, path.join(targetDir, artifactDirName), {
101
+ recursive: true,
102
+ force: true,
103
+ });
104
+ }
105
+ }
106
+ export function copySkillArtifacts(skill, targetDir) {
107
+ if (skill.layout === "nested") {
108
+ fs.cpSync(skill.sourcePath, targetDir, {
109
+ recursive: true,
110
+ force: true,
111
+ });
112
+ return;
113
+ }
114
+ copyRootSkillArtifacts(skill.sourcePath, targetDir);
115
+ }
116
+ export function skillContentMatchesTarget(skill, targetDir) {
117
+ if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
118
+ return false;
119
+ }
120
+ if (skill.layout === "nested") {
121
+ return directoriesAreEqual(skill.sourcePath, targetDir);
122
+ }
123
+ return rootSkillArtifactsEqual(skill.sourcePath, targetDir);
124
+ }
125
+ export function applySkillProviderSideEffects(options) {
126
+ const pathsToSymlink = resolveProviderSkillsPaths(options.paths, options.providers);
127
+ if (pathsToSymlink.length === 0)
128
+ return;
129
+ const canonicalSkillsDir = options.paths.skillsDir;
130
+ if (!options.dryRun) {
131
+ ensureDir(canonicalSkillsDir);
132
+ }
133
+ for (const targetSkillsDir of pathsToSymlink) {
134
+ enforceProviderSkillsSymlink({
135
+ targetSkillsDir,
136
+ canonicalSkillsDir,
137
+ dryRun: Boolean(options.dryRun),
138
+ warn: options.warn,
139
+ });
140
+ }
141
+ }
142
+ function resolveProviderSkillsPaths(paths, providers) {
143
+ const targets = new Set();
144
+ const hasClaudeStyleProvider = providers.includes("claude") || providers.includes("copilot");
145
+ if (hasClaudeStyleProvider) {
146
+ targets.add(paths.scope === "local"
147
+ ? path.join(paths.workspaceRoot, ".claude", "skills")
148
+ : path.join(paths.homeDir, ".claude", "skills"));
149
+ }
150
+ if (providers.includes("cursor")) {
151
+ targets.add(paths.scope === "local"
152
+ ? path.join(paths.workspaceRoot, ".cursor", "skills")
153
+ : path.join(paths.homeDir, ".cursor", "skills"));
154
+ }
155
+ return [...targets];
156
+ }
157
+ function enforceProviderSkillsSymlink(options) {
158
+ const resolvedCanonical = realPathOrResolved(options.canonicalSkillsDir);
159
+ const targetDir = options.targetSkillsDir;
160
+ if (!fs.existsSync(targetDir)) {
161
+ if (!options.dryRun) {
162
+ ensureDir(path.dirname(targetDir));
163
+ fs.symlinkSync(options.canonicalSkillsDir, targetDir, "dir");
164
+ }
165
+ return;
166
+ }
167
+ const targetStat = fs.lstatSync(targetDir);
168
+ if (targetStat.isSymbolicLink()) {
169
+ const resolvedTarget = realPathOrResolved(targetDir);
170
+ if (resolvedTarget === resolvedCanonical) {
171
+ return;
172
+ }
173
+ throw new Error(`Expected ${targetDir} to symlink to ${options.canonicalSkillsDir}, but it points to ${resolvedTarget}.`);
174
+ }
175
+ if (!targetStat.isDirectory()) {
176
+ throw new Error(`Cannot manage skills side effects because ${targetDir} exists and is not a directory.`);
177
+ }
178
+ migrateProviderSkillsIntoCanonical({
179
+ providerSkillsDir: targetDir,
180
+ canonicalSkillsDir: options.canonicalSkillsDir,
181
+ dryRun: options.dryRun,
182
+ warn: options.warn,
183
+ });
184
+ if (!options.dryRun) {
185
+ fs.rmSync(targetDir, { recursive: true, force: true });
186
+ ensureDir(path.dirname(targetDir));
187
+ fs.symlinkSync(options.canonicalSkillsDir, targetDir, "dir");
188
+ }
189
+ }
190
+ function migrateProviderSkillsIntoCanonical(options) {
191
+ const providerSkills = parseSkillsDir(options.providerSkillsDir);
192
+ for (const skill of providerSkills) {
193
+ const targetSkillDirName = skill.layout === "nested"
194
+ ? path.basename(skill.sourcePath)
195
+ : slugify(skill.name) || "skill";
196
+ const targetSkillDir = path.join(options.canonicalSkillsDir, targetSkillDirName);
197
+ if (fs.existsSync(targetSkillDir)) {
198
+ const sameContent = skill.layout === "nested"
199
+ ? directoriesAreEqual(skill.sourcePath, targetSkillDir)
200
+ : rootSkillArtifactsEqual(skill.sourcePath, targetSkillDir);
201
+ if (!sameContent) {
202
+ options.warn?.(`Skipped migrating provider skill "${targetSkillDirName}" because canonical skill content already exists.`);
203
+ }
204
+ continue;
205
+ }
206
+ if (options.dryRun) {
207
+ continue;
208
+ }
209
+ if (skill.layout === "nested") {
210
+ moveDirectory(skill.sourcePath, targetSkillDir);
211
+ continue;
212
+ }
213
+ copyRootSkillArtifacts(skill.sourcePath, targetSkillDir);
214
+ }
215
+ }
216
+ function moveDirectory(sourceDir, targetDir) {
217
+ ensureDir(path.dirname(targetDir));
218
+ try {
219
+ fs.renameSync(sourceDir, targetDir);
220
+ return;
221
+ }
222
+ catch (error) {
223
+ const code = error?.code;
224
+ if (code !== "EXDEV") {
225
+ throw error;
226
+ }
227
+ }
228
+ fs.cpSync(sourceDir, targetDir, { recursive: true, force: true });
229
+ fs.rmSync(sourceDir, { recursive: true, force: true });
230
+ }
231
+ function directoriesAreEqual(leftDir, rightDir) {
232
+ if (!fs.existsSync(leftDir) || !fs.existsSync(rightDir))
233
+ return false;
234
+ const leftEntries = collectFileEntries(leftDir);
235
+ const rightEntries = collectFileEntries(rightDir);
236
+ if (leftEntries.length !== rightEntries.length) {
237
+ return false;
238
+ }
239
+ for (let index = 0; index < leftEntries.length; index += 1) {
240
+ const left = leftEntries[index];
241
+ const right = rightEntries[index];
242
+ if (!left || !right || left.relativePath !== right.relativePath) {
243
+ return false;
244
+ }
245
+ if (!left.content.equals(right.content)) {
246
+ return false;
247
+ }
248
+ }
249
+ return true;
250
+ }
251
+ function rootSkillArtifactsEqual(sourceRoot, targetDir) {
252
+ const sourceArtifacts = collectRootSkillArtifacts(sourceRoot);
253
+ const targetArtifacts = collectRootSkillArtifacts(targetDir);
254
+ if (sourceArtifacts.length !== targetArtifacts.length) {
255
+ return false;
256
+ }
257
+ for (let index = 0; index < sourceArtifacts.length; index += 1) {
258
+ const left = sourceArtifacts[index];
259
+ const right = targetArtifacts[index];
260
+ if (!left || !right || left.relativePath !== right.relativePath) {
261
+ return false;
262
+ }
263
+ if (!left.content.equals(right.content)) {
264
+ return false;
265
+ }
266
+ }
267
+ return true;
268
+ }
269
+ function collectRootSkillArtifacts(rootDir) {
270
+ const collected = [];
271
+ const rootSkillPath = path.join(rootDir, "SKILL.md");
272
+ if (!fs.existsSync(rootSkillPath) || !fs.statSync(rootSkillPath).isFile()) {
273
+ return collected;
274
+ }
275
+ collected.push({
276
+ relativePath: "SKILL.md",
277
+ content: fs.readFileSync(rootSkillPath),
278
+ });
279
+ for (const artifactDirName of ROOT_SKILL_ARTIFACT_DIRS) {
280
+ const artifactDir = path.join(rootDir, artifactDirName);
281
+ if (!fs.existsSync(artifactDir) ||
282
+ !fs.statSync(artifactDir).isDirectory()) {
283
+ continue;
284
+ }
285
+ collected.push(...collectFileEntries(artifactDir, artifactDirName));
286
+ }
287
+ return collected.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
288
+ }
289
+ function collectFileEntries(rootDir, prefix = "") {
290
+ const entries = [];
291
+ const stack = [
292
+ {
293
+ absolute: rootDir,
294
+ relative: prefix,
295
+ },
296
+ ];
297
+ while (stack.length > 0) {
298
+ const current = stack.pop();
299
+ if (!current)
300
+ continue;
301
+ const children = fs.readdirSync(current.absolute, { withFileTypes: true });
302
+ for (const child of children) {
303
+ const absolutePath = path.join(current.absolute, child.name);
304
+ const relativePath = current.relative
305
+ ? path.posix.join(current.relative, child.name)
306
+ : child.name;
307
+ if (child.isDirectory()) {
308
+ stack.push({ absolute: absolutePath, relative: relativePath });
309
+ continue;
310
+ }
311
+ if (!child.isFile())
312
+ continue;
313
+ entries.push({
314
+ relativePath,
315
+ content: fs.readFileSync(absolutePath),
316
+ });
317
+ }
318
+ }
319
+ return entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
320
+ }
321
+ function realPathOrResolved(filePath) {
322
+ try {
323
+ return fs.realpathSync(filePath);
324
+ }
325
+ catch {
326
+ return path.resolve(filePath);
327
+ }
328
+ }
@@ -16,5 +16,7 @@ export declare function prepareSource(options: {
16
16
  ref?: string;
17
17
  subdir?: string;
18
18
  }): PreparedSource;
19
- export declare function discoverSourceAgentsDir(importRoot: string): string;
19
+ export declare function discoverSourceAgentsDir(importRoot: string): string | null;
20
20
  export declare function discoverSourceMcpPath(importRoot: string): string | null;
21
+ export declare function discoverSourceCommandsDir(importRoot: string): string | null;
22
+ export declare function discoverSourceSkillsDir(importRoot: string): string | null;
@@ -65,7 +65,7 @@ export function discoverSourceAgentsDir(importRoot) {
65
65
  if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
66
66
  return nested;
67
67
  }
68
- throw new Error(`No source agents directory found under ${importRoot} (expected agents/ or .agents/agents/).`);
68
+ return null;
69
69
  }
70
70
  export function discoverSourceMcpPath(importRoot) {
71
71
  const nested = path.join(importRoot, ".agents", "mcp.json");
@@ -76,6 +76,36 @@ export function discoverSourceMcpPath(importRoot) {
76
76
  return direct;
77
77
  return null;
78
78
  }
79
+ export function discoverSourceCommandsDir(importRoot) {
80
+ const nested = path.join(importRoot, ".agents", "commands");
81
+ if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
82
+ return nested;
83
+ }
84
+ const direct = path.join(importRoot, "commands");
85
+ if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
86
+ return direct;
87
+ }
88
+ const prompts = path.join(importRoot, "prompts");
89
+ if (fs.existsSync(prompts) && fs.statSync(prompts).isDirectory()) {
90
+ return prompts;
91
+ }
92
+ return null;
93
+ }
94
+ export function discoverSourceSkillsDir(importRoot) {
95
+ const nested = path.join(importRoot, ".agents", "skills");
96
+ if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
97
+ return nested;
98
+ }
99
+ const direct = path.join(importRoot, "skills");
100
+ if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
101
+ return direct;
102
+ }
103
+ const rootSkill = path.join(importRoot, "SKILL.md");
104
+ if (fs.existsSync(rootSkill) && fs.statSync(rootSkill).isFile()) {
105
+ return importRoot;
106
+ }
107
+ return null;
108
+ }
79
109
  function resolveImportRoot(rootPath, subdir) {
80
110
  if (!subdir)
81
111
  return rootPath;
@@ -0,0 +1,26 @@
1
+ import type { ImportSummary } from "./importer.js";
2
+ export type TelemetrySource = {
3
+ owner: string;
4
+ repo: string;
5
+ };
6
+ export type TelemetryItem = {
7
+ entityType: "agent" | "skill" | "command" | "mcp";
8
+ name: string;
9
+ filePath: string;
10
+ };
11
+ export declare function parseGitHubSource(input: string): TelemetrySource | null;
12
+ export declare function buildTelemetryItems(summary: ImportSummary): TelemetryItem[];
13
+ export declare function buildInstallTelemetryPayload(input: {
14
+ source: TelemetrySource;
15
+ summary: ImportSummary;
16
+ }): {
17
+ eventId: `${string}-${string}-${string}-${string}-${string}`;
18
+ occurredAt: string;
19
+ cliVersion: string;
20
+ source: TelemetrySource;
21
+ items: TelemetryItem[];
22
+ };
23
+ export declare function sendAddTelemetryEvent(input: {
24
+ rawSource: string;
25
+ summary: ImportSummary;
26
+ }): Promise<void>;
@@ -0,0 +1,124 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import { getCliVersion } from "./version.js";
4
+ import { parseSourceSpec } from "./sources.js";
5
+ const DEFAULT_TELEMETRY_ENDPOINT = "https://agentloom.sh/api/v1/installs";
6
+ const TELEMETRY_TIMEOUT_MS = 1800;
7
+ export function parseGitHubSource(input) {
8
+ const spec = parseSourceSpec(input);
9
+ if (spec.type === "github") {
10
+ const [owner, repo] = spec.source.split("/");
11
+ if (!owner || !repo) {
12
+ return null;
13
+ }
14
+ return { owner, repo };
15
+ }
16
+ if (spec.type !== "git") {
17
+ return null;
18
+ }
19
+ const gitSource = spec.source.trim();
20
+ const sshMatch = gitSource.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
21
+ if (sshMatch) {
22
+ return {
23
+ owner: sshMatch[1],
24
+ repo: sshMatch[2],
25
+ };
26
+ }
27
+ let parsedUrl;
28
+ try {
29
+ parsedUrl = new URL(gitSource);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ if (parsedUrl.hostname.toLowerCase() !== "github.com") {
35
+ return null;
36
+ }
37
+ const segments = parsedUrl.pathname.split("/").filter(Boolean);
38
+ if (segments.length < 2) {
39
+ return null;
40
+ }
41
+ const owner = segments[0];
42
+ const repo = segments[1].replace(/\.git$/i, "");
43
+ if (!owner || !repo) {
44
+ return null;
45
+ }
46
+ return { owner, repo };
47
+ }
48
+ export function buildTelemetryItems(summary) {
49
+ const items = [];
50
+ for (const filePath of summary.importedAgents) {
51
+ const name = path.basename(filePath, path.extname(filePath));
52
+ items.push({ entityType: "agent", name, filePath });
53
+ }
54
+ for (const filePath of summary.importedCommands) {
55
+ const name = path.basename(filePath, path.extname(filePath));
56
+ items.push({ entityType: "command", name, filePath });
57
+ }
58
+ for (const serverName of summary.importedMcpServers) {
59
+ items.push({ entityType: "mcp", name: serverName, filePath: "mcp.json" });
60
+ }
61
+ if (summary.telemetrySkills && summary.telemetrySkills.length > 0) {
62
+ for (const skill of summary.telemetrySkills) {
63
+ items.push({
64
+ entityType: "skill",
65
+ name: skill.name,
66
+ filePath: skill.filePath.replace(/^\/+/, ""),
67
+ });
68
+ }
69
+ }
70
+ else {
71
+ for (const skillName of summary.importedSkills) {
72
+ items.push({
73
+ entityType: "skill",
74
+ name: skillName,
75
+ filePath: `skills/${skillName}/SKILL.md`,
76
+ });
77
+ }
78
+ }
79
+ return items;
80
+ }
81
+ export function buildInstallTelemetryPayload(input) {
82
+ return {
83
+ eventId: randomUUID(),
84
+ occurredAt: new Date().toISOString(),
85
+ cliVersion: getCliVersion(),
86
+ source: input.source,
87
+ items: buildTelemetryItems(input.summary),
88
+ };
89
+ }
90
+ export async function sendAddTelemetryEvent(input) {
91
+ if (process.env.AGENTLOOM_DISABLE_TELEMETRY === "1") {
92
+ return;
93
+ }
94
+ const source = parseGitHubSource(input.rawSource);
95
+ if (!source) {
96
+ return;
97
+ }
98
+ const payload = buildInstallTelemetryPayload({
99
+ source,
100
+ summary: input.summary,
101
+ });
102
+ if (payload.items.length === 0) {
103
+ return;
104
+ }
105
+ const endpoint = process.env.AGENTLOOM_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_ENDPOINT;
106
+ const controller = new AbortController();
107
+ const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
108
+ try {
109
+ await fetch(endpoint, {
110
+ method: "POST",
111
+ headers: {
112
+ "Content-Type": "application/json",
113
+ },
114
+ body: JSON.stringify(payload),
115
+ signal: controller.signal,
116
+ });
117
+ }
118
+ catch {
119
+ // Telemetry is fail-open by design.
120
+ }
121
+ finally {
122
+ clearTimeout(timeout);
123
+ }
124
+ }
@@ -5,4 +5,5 @@ type MaybeNotifyOptions = {
5
5
  };
6
6
  export declare function maybeNotifyVersionUpdate(options: MaybeNotifyOptions): Promise<void>;
7
7
  export declare function isNewerVersion(candidate: string, current: string): boolean;
8
+ export declare function promptAndUpdate(current: string, latest: string): Promise<"updated" | "declined" | "failed">;
8
9
  export {};
@@ -1,7 +1,9 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import https from "node:https";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
6
+ import { confirm, isCancel } from "@clack/prompts";
5
7
  import { ensureDir, writeJsonAtomic } from "./fs.js";
6
8
  const UPDATE_CACHE_PATH = path.join(os.homedir(), ".agents", ".agentloom-version-cache.json");
7
9
  const CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
@@ -25,7 +27,7 @@ export async function maybeNotifyVersionUpdate(options) {
25
27
  if (cache.latestVersion &&
26
28
  isNewerVersion(cache.latestVersion, options.currentVersion) &&
27
29
  cache.lastNotifiedVersion !== cache.latestVersion) {
28
- printNotice(options.currentVersion, cache.latestVersion);
30
+ await promptAndUpdate(options.currentVersion, cache.latestVersion);
29
31
  cache.lastNotifiedVersion = cache.latestVersion;
30
32
  writeVersionCache(cache);
31
33
  }
@@ -44,7 +46,7 @@ export async function maybeNotifyVersionUpdate(options) {
44
46
  cache.latestVersion = latest;
45
47
  if (isNewerVersion(latest, options.currentVersion) &&
46
48
  cache.lastNotifiedVersion !== latest) {
47
- printNotice(options.currentVersion, latest);
49
+ await promptAndUpdate(options.currentVersion, latest);
48
50
  cache.lastNotifiedVersion = latest;
49
51
  }
50
52
  writeVersionCache(cache);
@@ -109,8 +111,24 @@ function fetchLatestVersion(packageName) {
109
111
  req.on("error", () => resolve(null));
110
112
  });
111
113
  }
112
- function printNotice(current, latest) {
113
- console.error(`\nUpdate available for agentloom: ${current} -> ${latest}\nRun: npm i -g agentloom\n`);
114
+ export async function promptAndUpdate(current, latest) {
115
+ const accepted = await confirm({
116
+ message: `Update available: ${current} → ${latest}. Update now?`,
117
+ initialValue: true,
118
+ });
119
+ if (isCancel(accepted) || !accepted)
120
+ return "declined";
121
+ console.log(`\nUpdating agentloom to ${latest}...\n`);
122
+ const result = spawnSync("npm", ["i", "-g", "agentloom"], {
123
+ stdio: "inherit",
124
+ });
125
+ if (result.status === 0) {
126
+ console.log(`\nUpdated to ${latest}. Please re-run your command.\n`);
127
+ process.exit(0);
128
+ return "updated"; // unreachable, but satisfies type checker
129
+ }
130
+ console.error(`\nUpdate failed. You can update manually: npm i -g agentloom\n`);
131
+ return "failed";
114
132
  }
115
133
  function readVersionCache() {
116
134
  try {
@@ -1,15 +1,21 @@
1
- import type { Provider, ScopePaths } from "../types.js";
1
+ import type { EntityType, Provider, ScopePaths } from "../types.js";
2
2
  export interface SyncOptions {
3
3
  paths: ScopePaths;
4
4
  providers?: Provider[];
5
5
  yes?: boolean;
6
6
  nonInteractive?: boolean;
7
7
  dryRun?: boolean;
8
+ target?: EntityType | "all";
8
9
  }
9
10
  export interface SyncSummary {
10
11
  providers: Provider[];
11
12
  generatedFiles: string[];
12
13
  removedFiles: string[];
13
14
  }
15
+ export declare function resolveProvidersForSync(options: {
16
+ paths: ScopePaths;
17
+ explicitProviders?: Provider[];
18
+ nonInteractive?: boolean;
19
+ }): Promise<Provider[]>;
14
20
  export declare function syncFromCanonical(options: SyncOptions): Promise<SyncSummary>;
15
21
  export declare function formatSyncSummary(summary: SyncSummary, agentsRoot: string): string;