agentloom 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -55,6 +55,7 @@ Global scope uses `~/.agents` with the same file layout.
55
55
  - `agentloom add <source>`
56
56
  - `agentloom find <query>`
57
57
  - `agentloom update [source]`
58
+ - `agentloom upgrade`
58
59
  - `agentloom sync`
59
60
  - `agentloom delete <source|name>`
60
61
 
@@ -118,6 +119,7 @@ agentloom --help
118
119
  agentloom find --help
119
120
  agentloom add --help
120
121
  agentloom update --help
122
+ agentloom upgrade --help
121
123
  agentloom sync --help
122
124
  agentloom delete --help
123
125
  agentloom agent --help
@@ -131,12 +133,16 @@ agentloom mcp server --help
131
133
 
132
134
  ### Version update notice
133
135
 
134
- `agentloom` now performs a best-effort npm version check and shows an update hint when a newer release is available.
136
+ `agentloom` performs best-effort npm version checks and upgrades when a newer release is available.
135
137
 
136
- - check is cached (`~/.agents/.agentloom-version-cache.json`)
137
- - check runs at most once every 12 hours
138
- - check is skipped in non-interactive sessions
139
- - disable via:
138
+ - checks are cached (`~/.agents/.agentloom-version-cache.json`)
139
+ - check/upgrade attempts run at most once every 2 hours per detected version
140
+ - in interactive TTY sessions, agentloom asks before upgrading
141
+ - in non-interactive sessions, upgrades run without prompts
142
+ - after an approved upgrade, the original command is re-run automatically
143
+ - if running from `npx`, auto-upgrade re-runs with `npx agentloom@<latest>`
144
+ - manual upgrade command: `agentloom upgrade`
145
+ - disable auto checks via:
140
146
 
141
147
  ```bash
142
148
  AGENTLOOM_DISABLE_UPDATE_NOTIFIER=1
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { runInitCommand } from "./commands/init.js";
9
9
  import { runMcpCommand } from "./commands/mcp.js";
10
10
  import { runSkillCommand } from "./commands/skills.js";
11
11
  import { runSyncCommand } from "./commands/sync.js";
12
+ import { runUpgradeCommand } from "./commands/upgrade.js";
12
13
  import { runUpdateCommand } from "./commands/update.js";
13
14
  import { formatUnknownCommandError, getRootHelpText } from "./core/copy.js";
14
15
  import { maybePromptManageAgentsBootstrap } from "./core/manage-agents-bootstrap.js";
@@ -36,23 +37,24 @@ export async function runCli(argv) {
36
37
  }
37
38
  const parsed = parseArgs(argv);
38
39
  const cwd = process.cwd();
39
- const shouldBootstrapManageAgents = await maybePromptManageAgentsBootstrap({
40
- command,
41
- help: Boolean(parsed.help),
42
- yes: Boolean(parsed.yes),
43
- cwd,
44
- });
45
40
  const route = parseCommandRoute(argv);
46
41
  if (!parsed.help) {
47
42
  await maybeNotifyVersionUpdate({
48
43
  command,
44
+ argv,
49
45
  currentVersion: version,
50
46
  });
51
47
  }
52
48
  if (!route) {
53
49
  throw new Error(formatUnknownCommandError(command));
54
50
  }
55
- await runRoutedCommand(route, parsed, cwd, command);
51
+ const shouldBootstrapManageAgents = await maybePromptManageAgentsBootstrap({
52
+ command,
53
+ help: Boolean(parsed.help),
54
+ yes: Boolean(parsed.yes),
55
+ cwd,
56
+ });
57
+ await runRoutedCommand(route, parsed, cwd, command, version);
56
58
  if (shouldBootstrapManageAgents) {
57
59
  const bootstrapArgs = buildManageAgentsBootstrapArgs(parsed, cwd);
58
60
  await runSkillCommand(parseArgs(bootstrapArgs), cwd);
@@ -61,7 +63,7 @@ export async function runCli(argv) {
61
63
  function printHelp() {
62
64
  console.log(getRootHelpText());
63
65
  }
64
- async function runRoutedCommand(route, parsed, cwd, command) {
66
+ async function runRoutedCommand(route, parsed, cwd, command, version) {
65
67
  if (!route) {
66
68
  throw new Error(formatUnknownCommandError(command));
67
69
  }
@@ -76,6 +78,9 @@ async function runRoutedCommand(route, parsed, cwd, command) {
76
78
  case "update":
77
79
  await runUpdateCommand(parsed, cwd);
78
80
  return;
81
+ case "upgrade":
82
+ await runUpgradeCommand(parsed, version);
83
+ return;
79
84
  case "sync":
80
85
  await runSyncCommand(parsed, cwd);
81
86
  return;
@@ -0,0 +1,2 @@
1
+ import type { ParsedArgs } from "minimist";
2
+ export declare function runUpgradeCommand(argv: ParsedArgs, currentVersion: string): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { getUpgradeHelpText } from "../core/copy.js";
2
+ import { upgradeToLatest } from "../core/version-notifier.js";
3
+ export async function runUpgradeCommand(argv, currentVersion) {
4
+ if (argv.help) {
5
+ console.log(getUpgradeHelpText());
6
+ return;
7
+ }
8
+ const result = await upgradeToLatest({ currentVersion });
9
+ if (result === "updated")
10
+ return;
11
+ if (result === "already-latest") {
12
+ console.log(`agentloom ${currentVersion} is already up to date.`);
13
+ return;
14
+ }
15
+ if (result === "unavailable") {
16
+ throw new Error("Unable to reach npm to check the latest agentloom release.");
17
+ }
18
+ throw new Error("Automatic upgrade failed.");
19
+ }
@@ -46,7 +46,7 @@ export function parseAgentMarkdown(raw, sourcePath, fileName = path.basename(sou
46
46
  };
47
47
  }
48
48
  export function buildAgentMarkdown(frontmatter, body) {
49
- const fm = YAML.stringify(frontmatter).trimEnd();
49
+ const fm = YAML.stringify(frontmatter, { lineWidth: 0 }).trimEnd();
50
50
  const normalizedBody = body.trimStart();
51
51
  return `---\n${fm}\n---\n\n${normalizedBody}${normalizedBody.endsWith("\n") ? "" : "\n"}`;
52
52
  }
@@ -7,6 +7,7 @@ export declare function getRootHelpText(): string;
7
7
  export declare function getFindHelpText(): string;
8
8
  export declare function getAddHelpText(): string;
9
9
  export declare function getUpdateHelpText(): string;
10
+ export declare function getUpgradeHelpText(): string;
10
11
  export declare function getSyncHelpText(): string;
11
12
  export declare function getInitHelpText(): string;
12
13
  export declare function getCommandHelpText(): string;
package/dist/core/copy.js CHANGED
@@ -12,6 +12,7 @@ Aggregate commands:
12
12
  init Bootstrap canonical files, migrate providers, then sync
13
13
  find <query> Search remote + local entities
14
14
  update [source] Refresh lockfile-managed imports
15
+ upgrade Install the latest CLI release
15
16
  sync Migrate provider configs then generate provider outputs
16
17
  delete <source|name> Delete imported entities by source or name
17
18
 
@@ -113,6 +114,18 @@ Example:
113
114
  agentloom update farnoodma/agents --providers codex,cursor
114
115
  `;
115
116
  }
117
+ export function getUpgradeHelpText() {
118
+ return `Install the latest agentloom release.
119
+
120
+ Usage:
121
+ agentloom upgrade
122
+
123
+ Behavior:
124
+ - Checks npm for the newest published version
125
+ - Upgrades immediately without interactive prompts
126
+ - Reports when the current version is already up to date
127
+ `;
128
+ }
116
129
  export function getSyncHelpText() {
117
130
  return `Migrate provider configs into canonical .agents data, then generate provider-specific outputs.
118
131
 
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { readJsonIfExists, writeJsonAtomic } from "./fs.js";
2
3
  const EMPTY_LOCK = {
3
4
  version: 1,
@@ -10,7 +11,7 @@ export function readLockfile(paths) {
10
11
  }
11
12
  return {
12
13
  version: 1,
13
- entries: lock.entries.map((entry) => ({
14
+ entries: lock.entries.map((entry) => normalizeLockEntryForRuntime(paths, {
14
15
  ...entry,
15
16
  importedAgents: Array.isArray(entry.importedAgents)
16
17
  ? entry.importedAgents
@@ -66,7 +67,10 @@ function normalizeRenameMap(value) {
66
67
  return Object.keys(normalized).length > 0 ? normalized : undefined;
67
68
  }
68
69
  export function writeLockfile(paths, lockfile) {
69
- writeJsonAtomic(paths.lockPath, lockfile);
70
+ writeJsonAtomic(paths.lockPath, {
71
+ version: 1,
72
+ entries: lockfile.entries.map((entry) => normalizeLockEntryForDisk(paths, entry)),
73
+ });
70
74
  }
71
75
  export function upsertLockEntry(lockfile, entry) {
72
76
  const index = lockfile.entries.findIndex((item) => item.source === entry.source &&
@@ -106,3 +110,31 @@ function normalizeSelectionForKey(value) {
106
110
  ...new Set(value.map((item) => item.trim().toLowerCase()).filter(Boolean)),
107
111
  ].sort();
108
112
  }
113
+ function normalizeLockEntryForRuntime(paths, entry) {
114
+ if (paths.scope !== "local" || entry.sourceType !== "local") {
115
+ return entry;
116
+ }
117
+ if (path.isAbsolute(entry.source)) {
118
+ return entry;
119
+ }
120
+ return {
121
+ ...entry,
122
+ source: path.resolve(paths.workspaceRoot, entry.source),
123
+ };
124
+ }
125
+ function normalizeLockEntryForDisk(paths, entry) {
126
+ if (paths.scope !== "local" || entry.sourceType !== "local") {
127
+ return entry;
128
+ }
129
+ if (!path.isAbsolute(entry.source)) {
130
+ return {
131
+ ...entry,
132
+ source: entry.source || ".",
133
+ };
134
+ }
135
+ const relativeSource = path.relative(paths.workspaceRoot, entry.source);
136
+ return {
137
+ ...entry,
138
+ source: relativeSource || ".",
139
+ };
140
+ }
@@ -9,6 +9,7 @@ const SKIP_COMMANDS = new Set([
9
9
  "version",
10
10
  "--version",
11
11
  "-v",
12
+ "upgrade",
12
13
  ]);
13
14
  export function getGlobalManageAgentsSkillPath(homeDir = os.homedir()) {
14
15
  return path.join(homeDir, ".agents", "skills", "manage-agents", "SKILL.md");
@@ -1,8 +1,10 @@
1
- import { readJsonIfExists, writeJsonAtomic } from "./fs.js";
1
+ import path from "node:path";
2
+ import { readJsonIfExists, toPosixPath, writeJsonAtomic } from "./fs.js";
2
3
  const EMPTY_MANIFEST = {
3
4
  version: 1,
4
5
  generatedFiles: [],
5
6
  };
7
+ const ENTITY_TYPES = ["agent", "command", "mcp", "skill"];
6
8
  export function readManifest(paths) {
7
9
  const manifest = readJsonIfExists(paths.manifestPath);
8
10
  if (!manifest ||
@@ -10,14 +12,129 @@ export function readManifest(paths) {
10
12
  !Array.isArray(manifest.generatedFiles)) {
11
13
  return { ...EMPTY_MANIFEST };
12
14
  }
13
- const generatedByEntity = manifest.generatedByEntity && typeof manifest.generatedByEntity === "object"
14
- ? manifest.generatedByEntity
15
- : undefined;
15
+ const generatedByEntity = normalizeGeneratedByEntityForRuntime(paths, manifest.generatedByEntity);
16
+ const codex = normalizeCodexMetadata(manifest.codex);
16
17
  return {
17
- ...manifest,
18
+ version: 1,
19
+ generatedFiles: normalizePathListForRuntime(paths, manifest.generatedFiles),
18
20
  generatedByEntity,
21
+ codex,
19
22
  };
20
23
  }
21
24
  export function writeManifest(paths, manifest) {
22
- writeJsonAtomic(paths.manifestPath, manifest);
25
+ const generatedByEntity = normalizeGeneratedByEntityForDisk(paths, manifest.generatedByEntity);
26
+ const codex = normalizeCodexMetadata(manifest.codex);
27
+ writeJsonAtomic(paths.manifestPath, {
28
+ version: 1,
29
+ generatedFiles: normalizePathListForDisk(paths, manifest.generatedFiles),
30
+ generatedByEntity,
31
+ codex,
32
+ });
33
+ }
34
+ function normalizeGeneratedByEntityForRuntime(paths, generatedByEntity) {
35
+ if (!generatedByEntity || typeof generatedByEntity !== "object") {
36
+ return undefined;
37
+ }
38
+ const normalized = {};
39
+ for (const entity of ENTITY_TYPES) {
40
+ const values = normalizePathListForRuntime(paths, generatedByEntity[entity]);
41
+ if (values.length === 0)
42
+ continue;
43
+ normalized[entity] = values;
44
+ }
45
+ return Object.keys(normalized).length > 0 ? normalized : {};
46
+ }
47
+ function normalizeGeneratedByEntityForDisk(paths, generatedByEntity) {
48
+ if (!generatedByEntity || typeof generatedByEntity !== "object") {
49
+ return undefined;
50
+ }
51
+ const normalized = {};
52
+ for (const entity of ENTITY_TYPES) {
53
+ const values = normalizePathListForDisk(paths, generatedByEntity[entity]);
54
+ if (values.length === 0)
55
+ continue;
56
+ normalized[entity] = values;
57
+ }
58
+ return Object.keys(normalized).length > 0 ? normalized : {};
59
+ }
60
+ function normalizePathListForRuntime(paths, value) {
61
+ if (!Array.isArray(value))
62
+ return [];
63
+ const normalized = value
64
+ .filter((item) => typeof item === "string")
65
+ .map((item) => resolveManifestPathForRuntime(paths, item))
66
+ .filter((item) => item.length > 0);
67
+ return [...new Set(normalized)].sort();
68
+ }
69
+ function normalizePathListForDisk(paths, value) {
70
+ if (!Array.isArray(value))
71
+ return [];
72
+ const normalized = value
73
+ .filter((item) => typeof item === "string")
74
+ .map((item) => resolveManifestPathForDisk(paths, item))
75
+ .filter((item) => item.length > 0);
76
+ return [...new Set(normalized)].sort();
77
+ }
78
+ function resolveManifestPathForRuntime(paths, filePath) {
79
+ const normalized = filePath.trim();
80
+ if (!normalized)
81
+ return "";
82
+ if (normalized === "~") {
83
+ return paths.homeDir;
84
+ }
85
+ if (normalized.startsWith("~/")) {
86
+ return path.resolve(paths.homeDir, normalized.slice(2));
87
+ }
88
+ if (path.isAbsolute(normalized)) {
89
+ return path.normalize(normalized);
90
+ }
91
+ return path.resolve(paths.workspaceRoot, normalized);
92
+ }
93
+ function resolveManifestPathForDisk(paths, filePath) {
94
+ const normalized = filePath.trim();
95
+ if (!normalized)
96
+ return "";
97
+ if (!path.isAbsolute(normalized)) {
98
+ return toPosixPath(normalized);
99
+ }
100
+ const absolutePath = path.normalize(normalized);
101
+ if (paths.scope === "global") {
102
+ if (isSubpath(paths.homeDir, absolutePath)) {
103
+ const relativePath = path.relative(paths.homeDir, absolutePath);
104
+ return relativePath ? `~/${toPosixPath(relativePath)}` : "~";
105
+ }
106
+ return toPosixPath(absolutePath);
107
+ }
108
+ if (isSubpath(paths.workspaceRoot, absolutePath)) {
109
+ const relativePath = path.relative(paths.workspaceRoot, absolutePath);
110
+ return toPosixPath(relativePath || ".");
111
+ }
112
+ if (isSubpath(paths.homeDir, absolutePath)) {
113
+ const relativePath = path.relative(paths.homeDir, absolutePath);
114
+ return relativePath ? `~/${toPosixPath(relativePath)}` : "~";
115
+ }
116
+ return toPosixPath(absolutePath);
117
+ }
118
+ function isSubpath(rootPath, candidatePath) {
119
+ const relative = path.relative(rootPath, candidatePath);
120
+ return (relative === "" ||
121
+ (!relative.startsWith("..") && !path.isAbsolute(relative)));
122
+ }
123
+ function normalizeCodexMetadata(codex) {
124
+ if (!codex || typeof codex !== "object")
125
+ return undefined;
126
+ const roles = Array.isArray(codex.roles)
127
+ ? [...new Set(codex.roles.filter((item) => !!item))].sort()
128
+ : undefined;
129
+ const mcpServers = Array.isArray(codex.mcpServers)
130
+ ? [
131
+ ...new Set(codex.mcpServers.filter((item) => !!item)),
132
+ ].sort()
133
+ : undefined;
134
+ if (!roles && !mcpServers)
135
+ return undefined;
136
+ return {
137
+ roles,
138
+ mcpServers,
139
+ };
23
140
  }
@@ -420,6 +420,11 @@ async function migrateCommands(options, summary) {
420
420
  }
421
421
  }
422
422
  function readProviderCommands(paths, provider) {
423
+ // Codex prompts are home-scoped; importing them into local canonical state
424
+ // causes unrelated global prompts to appear in fresh repositories.
425
+ if (provider === "codex" && paths.scope === "local") {
426
+ return [];
427
+ }
423
428
  const commandsDir = getProviderCommandsDir(paths, provider);
424
429
  if (!fs.existsSync(commandsDir) || !fs.statSync(commandsDir).isDirectory()) {
425
430
  return [];
@@ -1,5 +1,5 @@
1
1
  import type { EntityType } from "../types.js";
2
- export type AggregateVerb = "add" | "find" | "update" | "sync" | "delete" | "init";
2
+ export type AggregateVerb = "add" | "find" | "update" | "upgrade" | "sync" | "delete" | "init";
3
3
  export type EntityVerb = "add" | "list" | "delete" | "find" | "update" | "sync";
4
4
  export type McpServerVerb = "add" | "list" | "delete";
5
5
  export type CommandRoute = {
@@ -2,6 +2,7 @@ const AGGREGATE_VERBS = new Set([
2
2
  "add",
3
3
  "find",
4
4
  "update",
5
+ "upgrade",
5
6
  "sync",
6
7
  "delete",
7
8
  "init",
@@ -1,9 +1,19 @@
1
1
  type MaybeNotifyOptions = {
2
2
  command: string;
3
+ argv: string[];
3
4
  packageName?: string;
4
5
  currentVersion: string;
5
6
  };
7
+ export type UpgradeResult = "updated" | "already-latest" | "failed" | "unavailable";
6
8
  export declare function maybeNotifyVersionUpdate(options: MaybeNotifyOptions): Promise<void>;
9
+ export declare function upgradeToLatest(options: {
10
+ currentVersion: string;
11
+ packageName?: string;
12
+ }): Promise<UpgradeResult>;
7
13
  export declare function isNewerVersion(candidate: string, current: string): boolean;
8
- export declare function promptAndUpdate(current: string, latest: string): Promise<"updated" | "declined" | "failed">;
14
+ export declare function promptAndUpdate(current: string, latest: string, options?: {
15
+ packageName?: string;
16
+ rerunArgs?: string[];
17
+ }): Promise<"updated" | "already-latest" | "failed">;
18
+ export declare function maybeConfirmAutoUpgrade(current: string, latest: string): Promise<boolean>;
9
19
  export {};
@@ -6,50 +6,73 @@ import path from "node:path";
6
6
  import { confirm, isCancel } from "@clack/prompts";
7
7
  import { ensureDir, writeJsonAtomic } from "./fs.js";
8
8
  const UPDATE_CACHE_PATH = path.join(os.homedir(), ".agents", ".agentloom-version-cache.json");
9
- const CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
9
+ const CHECK_INTERVAL_MS = 2 * 60 * 60 * 1000;
10
10
  const REQUEST_TIMEOUT_MS = 1800;
11
+ const DISABLE_UPDATE_ENV = "AGENTLOOM_DISABLE_UPDATE_NOTIFIER";
12
+ const SKIP_COMMANDS = new Set([
13
+ "help",
14
+ "--help",
15
+ "-h",
16
+ "version",
17
+ "--version",
18
+ "-v",
19
+ "upgrade",
20
+ ]);
11
21
  export async function maybeNotifyVersionUpdate(options) {
12
- if (process.env.AGENTLOOM_DISABLE_UPDATE_NOTIFIER === "1")
22
+ if (process.env[DISABLE_UPDATE_ENV] === "1")
13
23
  return;
14
- if (!process.stdout.isTTY || !process.stderr.isTTY)
24
+ const loweredCommand = options.command.trim().toLowerCase();
25
+ if (SKIP_COMMANDS.has(loweredCommand))
15
26
  return;
16
- const loweredCommand = options.command.toLowerCase();
17
- if (loweredCommand === "help" ||
18
- loweredCommand === "--help" ||
19
- loweredCommand === "-h" ||
20
- loweredCommand === "version" ||
21
- loweredCommand === "--version" ||
22
- loweredCommand === "-v") {
23
- return;
24
- }
25
27
  const packageName = options.packageName ?? "agentloom";
26
28
  const cache = readVersionCache();
27
- if (cache.latestVersion &&
28
- isNewerVersion(cache.latestVersion, options.currentVersion) &&
29
- cache.lastNotifiedVersion !== cache.latestVersion) {
30
- await promptAndUpdate(options.currentVersion, cache.latestVersion);
31
- cache.lastNotifiedVersion = cache.latestVersion;
32
- writeVersionCache(cache);
33
- }
29
+ const latest = await resolveLatestVersion({
30
+ packageName,
31
+ cache,
32
+ forceFetch: false,
33
+ });
34
+ if (!latest)
35
+ return;
36
+ if (!isNewerVersion(latest, options.currentVersion))
37
+ return;
34
38
  const now = Date.now();
35
- const lastChecked = parseTime(cache.lastCheckedAt);
36
- if (lastChecked && now - lastChecked < CHECK_INTERVAL_MS) {
39
+ if (!shouldAttemptAutoUpgrade(cache, latest, now)) {
37
40
  return;
38
41
  }
39
- const latest = await fetchLatestVersion(packageName);
40
- if (!latest) {
41
- cache.lastCheckedAt = new Date(now).toISOString();
42
+ const approved = await maybeConfirmAutoUpgrade(options.currentVersion, latest);
43
+ if (!approved) {
44
+ recordUpgradeAttempt(cache, latest, now);
42
45
  writeVersionCache(cache);
43
46
  return;
44
47
  }
45
- cache.lastCheckedAt = new Date(now).toISOString();
46
- cache.latestVersion = latest;
47
- if (isNewerVersion(latest, options.currentVersion) &&
48
- cache.lastNotifiedVersion !== latest) {
49
- await promptAndUpdate(options.currentVersion, latest);
50
- cache.lastNotifiedVersion = latest;
48
+ // Persist attempt before upgrade/rerun, because rerun success paths can exit the process.
49
+ recordUpgradeAttempt(cache, latest, now);
50
+ writeVersionCache(cache);
51
+ await promptAndUpdate(options.currentVersion, latest, {
52
+ packageName,
53
+ rerunArgs: options.argv,
54
+ });
55
+ }
56
+ export async function upgradeToLatest(options) {
57
+ const packageName = options.packageName ?? "agentloom";
58
+ const cache = readVersionCache();
59
+ const latest = await resolveLatestVersion({
60
+ packageName,
61
+ cache,
62
+ forceFetch: true,
63
+ });
64
+ if (!latest)
65
+ return "unavailable";
66
+ if (!isNewerVersion(latest, options.currentVersion)) {
67
+ return "already-latest";
51
68
  }
69
+ const result = await promptAndUpdate(options.currentVersion, latest, {
70
+ packageName,
71
+ });
72
+ const now = Date.now();
73
+ recordUpgradeAttempt(cache, latest, now);
52
74
  writeVersionCache(cache);
75
+ return result;
53
76
  }
54
77
  export function isNewerVersion(candidate, current) {
55
78
  const next = parseSemver(candidate);
@@ -111,24 +134,54 @@ function fetchLatestVersion(packageName) {
111
134
  req.on("error", () => resolve(null));
112
135
  });
113
136
  }
114
- export async function promptAndUpdate(current, latest) {
115
- const accepted = await confirm({
116
- message: `Update available: ${current} → ${latest}. Update now?`,
117
- initialValue: true,
137
+ export async function promptAndUpdate(current, latest, options = {}) {
138
+ if (!isNewerVersion(latest, current))
139
+ return "already-latest";
140
+ const packageName = options.packageName ?? "agentloom";
141
+ const rerunArgs = options.rerunArgs ?? [];
142
+ if (rerunArgs.length > 0 && isLikelyNpxExecution()) {
143
+ if (!canRunPackageViaNpx(packageName, latest)) {
144
+ console.error("\nAutomatic npx upgrade is currently unavailable; continuing with current version.\n");
145
+ return "failed";
146
+ }
147
+ console.log(`\nUpdate available: ${current} → ${latest}. Re-running with npx ${packageName}@${latest}...\n`);
148
+ const rerun = spawnSync("npx", ["--yes", `${packageName}@${latest}`, ...rerunArgs], {
149
+ stdio: "inherit",
150
+ env: buildChildEnv(),
151
+ });
152
+ if (rerun.error || rerun.status === null) {
153
+ console.error("\nAutomatic npx rerun failed after upgrade. Please run your command again.\n");
154
+ process.exit(1);
155
+ return "failed"; // unreachable, but satisfies type checker
156
+ }
157
+ process.exit(rerun.status);
158
+ return rerun.status === 0 ? "updated" : "failed"; // unreachable, but satisfies type checker
159
+ }
160
+ console.log(`\nUpdating ${packageName} ${current} → ${latest}...\n`);
161
+ const install = spawnSync("npm", ["i", "-g", `${packageName}@${latest}`], {
162
+ stdio: "inherit",
118
163
  });
119
- if (isCancel(accepted) || !accepted)
120
- return "declined";
121
- console.log(`\nUpdating agentloom to ${latest}...\n`);
122
- const result = spawnSync("npm", ["i", "-g", "agentloom"], {
164
+ if (install.status !== 0) {
165
+ console.error("\nAutomatic upgrade failed. Run `agentloom upgrade` to retry.\n");
166
+ return "failed";
167
+ }
168
+ if (rerunArgs.length === 0) {
169
+ console.log(`\nUpdated to ${latest}.\n`);
170
+ return "updated";
171
+ }
172
+ console.log(`\nUpdated to ${latest}. Re-running your command...\n`);
173
+ const invocation = buildRerunInvocation(rerunArgs);
174
+ const rerun = spawnSync(invocation.command, invocation.args, {
123
175
  stdio: "inherit",
176
+ env: buildChildEnv(),
124
177
  });
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
178
+ if (rerun.error || rerun.status === null) {
179
+ console.error("\nAutomatic rerun failed after upgrade. Please run your command again.\n");
180
+ process.exit(1);
181
+ return "failed"; // unreachable, but satisfies type checker
129
182
  }
130
- console.error(`\nUpdate failed. You can update manually: npm i -g agentloom\n`);
131
- return "failed";
183
+ process.exit(rerun.status);
184
+ return "updated"; // unreachable, but satisfies type checker
132
185
  }
133
186
  function readVersionCache() {
134
187
  try {
@@ -158,3 +211,85 @@ function parseTime(value) {
158
211
  return null;
159
212
  return parsed;
160
213
  }
214
+ async function resolveLatestVersion(options) {
215
+ const now = Date.now();
216
+ const lastChecked = parseTime(options.cache.lastCheckedAt);
217
+ const shouldFetch = options.forceFetch ||
218
+ !lastChecked ||
219
+ now - lastChecked >= CHECK_INTERVAL_MS;
220
+ if (!shouldFetch) {
221
+ return options.cache.latestVersion ?? null;
222
+ }
223
+ const fetched = await fetchLatestVersion(options.packageName);
224
+ options.cache.lastCheckedAt = new Date(now).toISOString();
225
+ if (fetched) {
226
+ options.cache.latestVersion = fetched;
227
+ }
228
+ writeVersionCache(options.cache);
229
+ if (options.forceFetch) {
230
+ return fetched ?? null;
231
+ }
232
+ return fetched ?? options.cache.latestVersion ?? null;
233
+ }
234
+ function shouldAttemptAutoUpgrade(cache, latest, now) {
235
+ const lastAttemptVersion = cache.lastUpgradeAttemptVersion ?? cache.lastAutoUpgradeVersion;
236
+ if (lastAttemptVersion !== latest)
237
+ return true;
238
+ const lastAttempt = parseTime(cache.lastUpgradeAttemptAt ?? cache.lastAutoUpgradeAt);
239
+ if (!lastAttempt)
240
+ return true;
241
+ return now - lastAttempt >= CHECK_INTERVAL_MS;
242
+ }
243
+ function isLikelyNpxExecution() {
244
+ const npmCommand = process.env.npm_command?.trim().toLowerCase();
245
+ if (npmCommand === "exec")
246
+ return true;
247
+ const argv1 = process.argv[1];
248
+ if (typeof argv1 !== "string" || argv1.length === 0)
249
+ return false;
250
+ return argv1.includes(`${path.sep}_npx${path.sep}`);
251
+ }
252
+ function buildChildEnv() {
253
+ return {
254
+ ...process.env,
255
+ [DISABLE_UPDATE_ENV]: "1",
256
+ };
257
+ }
258
+ export async function maybeConfirmAutoUpgrade(current, latest) {
259
+ if (!isInteractiveTty())
260
+ return true;
261
+ const approved = await confirm({
262
+ message: `Update available: ${current} → ${latest}. Upgrade now and re-run your command?`,
263
+ initialValue: true,
264
+ });
265
+ if (isCancel(approved) || approved !== true)
266
+ return false;
267
+ return true;
268
+ }
269
+ function isInteractiveTty() {
270
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY);
271
+ }
272
+ function recordUpgradeAttempt(cache, latest, now) {
273
+ cache.lastUpgradeAttemptVersion = latest;
274
+ cache.lastUpgradeAttemptAt = new Date(now).toISOString();
275
+ }
276
+ function canRunPackageViaNpx(packageName, version) {
277
+ const probe = spawnSync("npx", ["--yes", `${packageName}@${version}`, "--version"], {
278
+ stdio: "ignore",
279
+ env: buildChildEnv(),
280
+ });
281
+ return !probe.error && probe.status === 0;
282
+ }
283
+ function buildRerunInvocation(rerunArgs) {
284
+ const scriptPath = process.argv[1];
285
+ if (typeof scriptPath === "string" && scriptPath.trim().length > 0) {
286
+ return {
287
+ command: process.execPath,
288
+ args: [...process.execArgv, scriptPath, ...rerunArgs],
289
+ };
290
+ }
291
+ return {
292
+ command: "agentloom",
293
+ args: rerunArgs,
294
+ };
295
+ }
@@ -224,7 +224,7 @@ function buildProviderAgentContent(provider, agent, providerConfig) {
224
224
  description: agent.description,
225
225
  ...providerConfig,
226
226
  };
227
- const fm = YAML.stringify(frontmatter).trimEnd();
227
+ const fm = YAML.stringify(frontmatter, { lineWidth: 0 }).trimEnd();
228
228
  return `---\n${fm}\n---\n\n${agent.body.trimStart()}${agent.body.endsWith("\n") ? "" : "\n"}`;
229
229
  }
230
230
  function syncProviderCommands(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentloom",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
5
5
  "type": "module",
6
6
  "bin": {