@moda-ai/cli 1.55.96 → 1.55.97

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
@@ -489,3 +489,26 @@ in order; changed old literals cannot qualify through unchanged sibling text.
489
489
  Keep call inputs and redirected output outside the repository: they can contain customer content.
490
490
  Python, unresolved dynamic dispatch and new files without old context remain unmodelled. This
491
491
  local result does not establish deployment attribution or production precision/recall.
492
+
493
+ ### Updating agent installations
494
+
495
+ The CLI checks npm at most daily. When a newer version is cached, `--json`
496
+ commands also print an update notice on stderr without requiring a terminal,
497
+ including on validation and authentication failures. JSON stdout is unchanged.
498
+ With a writable home, notices are limited to once an hour for each running/latest version pair;
499
+ `--quiet`, `--no-update-check`, `MODA_CLI_UPDATE_CHECK=0`, `DO_NOT_TRACK=1`,
500
+ and CI suppress them. Read-only homes still receive cached guidance without a
501
+ persisted cooldown. Agent envelopes continue to carry `meta.update`.
502
+
503
+ After updating, run `moda skills install moda-cli` from the project directory.
504
+ It refreshes the installed guide and the init-managed Moda block in `AGENTS.md`,
505
+ replacing legacy `moda fixes` instructions. Text outside that block stays intact;
506
+ locally edited skill files are kept. Malformed or duplicated block markers and
507
+ symlinked `AGENTS.md` files are left unchanged with a warning and recovery steps.
508
+ Repeated installation is idempotent.
509
+
510
+ Retired `fix`, `fixes`, `prompts ab`, and `prompts propose` calls fail locally
511
+ with migration guidance. Read problem evidence with `moda problems get <id>
512
+ --evidence`, make changes in your project, and use `moda prompts status`,
513
+ `moda prompts diff`, and `moda prompts sync --dry-run` to review prompt versions.
514
+ The retired names stay out of help and the manifest.
@@ -41,7 +41,7 @@ import { execFileSync } from "node:child_process";
41
41
  import { createHash } from "node:crypto";
42
42
  import {
43
43
  existsSync as existsSync2,
44
- lstatSync,
44
+ lstatSync as lstatSync2,
45
45
  mkdirSync as mkdirSync2,
46
46
  readdirSync,
47
47
  readFileSync as readFileSync2,
@@ -50,7 +50,7 @@ import {
50
50
  import { basename, dirname as dirname2, join as join2, relative } from "node:path";
51
51
 
52
52
  // src/agents-md.ts
53
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
53
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
54
54
  import { dirname, join } from "node:path";
55
55
  var AGENTS_MD_FILENAME = "AGENTS.md";
56
56
  var FIXES_BLOCK_BEGIN = "<!-- moda:fixes:begin -->";
@@ -119,7 +119,7 @@ function upsertManagedBlock(existing, block) {
119
119
  const problem = begins === 1 && ends === 1 ? `its ${FIXES_BLOCK_END} marker comes before its ${FIXES_BLOCK_BEGIN} marker` : `it has ${begins} ${FIXES_BLOCK_BEGIN} and ${ends} ${FIXES_BLOCK_END} marker(s) instead of one of each`;
120
120
  return {
121
121
  ok: false,
122
- reason: `The Moda block in ${AGENTS_MD_FILENAME} was not updated because ${problem}. ` + "The file was left unchanged so no text is lost. Keep exactly one begin marker followed by one end marker " + "(or delete the stray markers), then rerun `moda skills push`."
122
+ reason: `The Moda block in ${AGENTS_MD_FILENAME} was not updated because ${problem}. ` + "The file was left unchanged so no text is lost. Keep exactly one begin marker followed by one end marker " + "(or delete the stray markers), then rerun `moda skills install moda-cli` (or `moda skills push`)."
123
123
  };
124
124
  }
125
125
  function writeFileIfChanged(filePath, content) {
@@ -141,6 +141,15 @@ function writeFileIfChanged(filePath, content) {
141
141
  function upsertFixesAgentsBlock(cwd) {
142
142
  const filePath = join(cwd, AGENTS_MD_FILENAME);
143
143
  let existing = null;
144
+ try {
145
+ if (lstatSync(filePath).isSymbolicLink()) {
146
+ return { path: AGENTS_MD_FILENAME, status: "skipped", reason: "AGENTS.md is a symlink and was left unchanged. Update the linked file manually, or replace the link with a project file and rerun `moda skills install moda-cli`." };
147
+ }
148
+ } catch (err) {
149
+ if (err.code !== "ENOENT") {
150
+ return { path: AGENTS_MD_FILENAME, status: "failed", reason: `Could not inspect ${AGENTS_MD_FILENAME}: ${err instanceof Error ? err.message : String(err)}` };
151
+ }
152
+ }
144
153
  if (existsSync(filePath)) {
145
154
  try {
146
155
  existing = readFileSync(filePath, "utf8");
@@ -485,7 +494,7 @@ function walkSkillFiles(dir, depth) {
485
494
  const fullPath = join2(dir, entry);
486
495
  let stat;
487
496
  try {
488
- stat = lstatSync(fullPath);
497
+ stat = lstatSync2(fullPath);
489
498
  } catch {
490
499
  continue;
491
500
  }
@@ -899,9 +908,15 @@ async function installModaCliSkill(context) {
899
908
  if (result.status === "skipped") {
900
909
  throw new CliInputError("No supported coding agent was detected here, so the moda-cli skill was not installed.", "Run this from a project with a .claude or .cursor directory, or with Claude Code on PATH. Only Claude Code and Cursor are supported.");
901
910
  }
911
+ const agentsMd = result.status === "failed" ? undefined : upsertFixesAgentsBlock(context.cwd);
912
+ const blockWarning = agentsMdProblem(agentsMd);
913
+ if (blockWarning) {
914
+ result.warnings.push(blockWarning);
915
+ result.status = "warning";
916
+ }
902
917
  const summary = summarizeInstall(result);
903
918
  if (context.outputMode !== "human") {
904
- context.output.writeData({ skill: MODA_CLI_SKILL_ID, status: result.status, agents: result.agents, counts: result.counts, summary, outcomes: result.outcomes, notes: result.notes }, { ...result.status === "failed" ? { status: "error" } : {}, warnings: result.warnings });
919
+ context.output.writeData({ skill: MODA_CLI_SKILL_ID, status: result.status, agents: result.agents, counts: result.counts, summary, outcomes: result.outcomes, notes: result.notes, agentsMd }, { ...result.status === "failed" ? { status: "error" } : {}, warnings: result.warnings });
905
920
  } else {
906
921
  console.log(`moda-cli skill for ${result.agents.join(", ")}: ${summary}`);
907
922
  for (const note of result.notes)
package/dist/cli.js CHANGED
@@ -142,7 +142,7 @@ import {
142
142
  } from "./cli-0v6na3yp.js";
143
143
 
144
144
  // src/cli.ts
145
- import { randomUUID as randomUUID3 } from "node:crypto";
145
+ import { randomUUID as randomUUID4 } from "node:crypto";
146
146
  import { z } from "zod";
147
147
 
148
148
  // src/trace-findings.ts
@@ -314,6 +314,17 @@ function batchNotRunWarning(results) {
314
314
  return [`Families not run (detector off or trace not processed yet), so no detections there does not mean clean: ${notRun.join("; ")}.`];
315
315
  }
316
316
 
317
+ // src/retired-commands.ts
318
+ function retiredCommandError(command, positionals) {
319
+ const fix = command === "fix" || command === "fixes";
320
+ const prompt = command === "prompts" && ["ab", "propose"].includes(positionals[0]);
321
+ if (!fix && !prompt)
322
+ return;
323
+ const name = fix ? command : `prompts ${positionals[0]}`;
324
+ const commands = fix ? ["moda problems", "moda problems get <problem_id> --evidence", "moda skills install moda-cli"] : ["moda prompts status", "moda prompts diff", "moda prompts sync --dry-run", "moda skills install moda-cli"];
325
+ return new CliInputError(`The 'moda ${name}' command was retired. Nothing was run. ` + (fix ? "Moda supplies production evidence; engineers make changes in their own code, prompts, tools or skills." : "The old automatic prompt proposal and A/B workflow was retired. Review and edit prompt changes in your project."), `Use ${commands.map((value) => `\`${value}\``).join(", ")}. ` + "Refresh the installed guide and the Moda-managed AGENTS.md block with `moda skills install moda-cli`. " + "Review any user-authored instructions that still reference the retired workflow.", "retired_command", { field: "<command>", suggestedCommands: commands });
326
+ }
327
+
317
328
  // src/telemetry.ts
318
329
  import { spawn as spawn2 } from "node:child_process";
319
330
 
@@ -444,7 +455,7 @@ function appendLines(path, text) {
444
455
  function sleepSync(ms) {
445
456
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
446
457
  }
447
- function createLockFile(path, staleMs, now) {
458
+ function createLockFile(path, staleMs, now, onError) {
448
459
  for (let attempt = 0;attempt < 2; attempt += 1) {
449
460
  try {
450
461
  const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW, 384);
@@ -455,13 +466,18 @@ function createLockFile(path, staleMs, now) {
455
466
  }
456
467
  return true;
457
468
  } catch (error) {
458
- if (error.code !== "EEXIST")
469
+ if (error.code !== "EEXIST") {
470
+ onError?.(error);
459
471
  return false;
472
+ }
460
473
  try {
461
474
  if (now - statSync(path).mtimeMs < staleMs)
462
475
  return false;
463
476
  unlinkSync(path);
464
- } catch {}
477
+ } catch (error2) {
478
+ if (error2.code !== "ENOENT")
479
+ onError?.(error2);
480
+ }
465
481
  }
466
482
  }
467
483
  return false;
@@ -542,10 +558,10 @@ function flushDue(entries, group, now) {
542
558
  }
543
559
  return false;
544
560
  }
545
- function acquireFlushLock(dir = telemetrySpoolDir(), now = Date.now()) {
561
+ function acquireFlushLock(dir = telemetrySpoolDir(), now = Date.now(), onError) {
546
562
  ensureDir(dir);
547
563
  const path = join(dir, LOCK_FILE);
548
- return createLockFile(path, FLUSH_LOCK_STALE_MS, now) ? path : undefined;
564
+ return createLockFile(path, FLUSH_LOCK_STALE_MS, now, onError) ? path : undefined;
549
565
  }
550
566
  function releaseFlushLock(path) {
551
567
  if (!path)
@@ -976,7 +992,8 @@ async function sendCliUsageTelemetry(input) {
976
992
  }
977
993
 
978
994
  // src/update-check.ts
979
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
995
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync } from "node:fs";
996
+ import { randomUUID as randomUUID3 } from "node:crypto";
980
997
  import { join as join2 } from "node:path";
981
998
  var UPDATE_CHECK_FLAG = "no-update-check";
982
999
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -1091,12 +1108,59 @@ function getUpdateNotice(options = {}) {
1091
1108
  ].join(`
1092
1109
  `);
1093
1110
  }
1094
- function printUpdateNotice(options = {}) {
1095
- if (!process.stderr.isTTY)
1111
+ function printUpdateNotice(options = {}, mode = {}, now) {
1112
+ if (mode.quiet || !process.stderr.isTTY && mode.outputMode !== "json")
1096
1113
  return;
1097
- const notice = getUpdateNotice(options);
1098
- if (notice)
1114
+ let lock;
1115
+ try {
1116
+ const notice = getUpdateNotice(options);
1117
+ if (!notice)
1118
+ return;
1119
+ if (mode.outputMode === "json") {
1120
+ let lockFailed = false;
1121
+ try {
1122
+ lock = acquireFlushLock(join2(getModaDir(options), "update-notice-lock"), now ?? Date.now(), () => {
1123
+ lockFailed = true;
1124
+ });
1125
+ } catch {
1126
+ lockFailed = true;
1127
+ }
1128
+ if (!lock) {
1129
+ if (lockFailed)
1130
+ console.error(notice);
1131
+ return;
1132
+ }
1133
+ const noticeNow = now ?? Date.now();
1134
+ const path = join2(getModaDir(options), "update-notice.json");
1135
+ const version = `${VERSION}:${getAvailableUpdate(options)}`;
1136
+ const notices = {};
1137
+ try {
1138
+ const previous = JSON.parse(readFileSync2(path, "utf8"));
1139
+ if (previous?.notices && typeof previous.notices === "object") {
1140
+ for (const [pair, timestamp] of Object.entries(previous.notices)) {
1141
+ if (typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp <= noticeNow && noticeNow - timestamp < 60 * 60 * 1000)
1142
+ notices[pair] = timestamp;
1143
+ }
1144
+ }
1145
+ if (notices[version] !== undefined)
1146
+ return;
1147
+ } catch {}
1148
+ const temporary = `${path}.${randomUUID3()}.tmp`;
1149
+ try {
1150
+ mkdirSync2(getModaDir(options), { recursive: true });
1151
+ writeFileSync(temporary, JSON.stringify({ notices: { ...notices, [version]: noticeNow } }) + `
1152
+ `, { flag: "wx", mode: 384 });
1153
+ renameSync2(temporary, path);
1154
+ } catch {} finally {
1155
+ try {
1156
+ rmSync2(temporary, { force: true });
1157
+ } catch {}
1158
+ }
1159
+ }
1099
1160
  console.error(notice);
1161
+ } catch {} finally {
1162
+ releaseFlushLock(lock);
1163
+ }
1100
1164
  }
1101
1165
  // src/command-tree.ts
1102
1166
  var CANONICAL_LEAVES = [
@@ -3005,13 +3069,13 @@ async function runPromptsCommand(...args) {
3005
3069
  return (await import("./prompts-x3vdeax6.js")).runPromptsCommand(...args);
3006
3070
  }
3007
3071
  async function runSkillsCommand(...args) {
3008
- return (await import("./skills-1pv2mp9v.js")).runSkillsCommand(...args);
3072
+ return (await import("./skills-9c1xvp2s.js")).runSkillsCommand(...args);
3009
3073
  }
3010
3074
  async function runHarnessCommand(...args) {
3011
3075
  return (await import("./harness-jka07f99.js")).runHarnessCommand(...args);
3012
3076
  }
3013
3077
  async function runSyncCommand(...args) {
3014
- return (await import("./sync-4zeyb408.js")).runSyncCommand(...args);
3078
+ return (await import("./sync-skzdw9fb.js")).runSyncCommand(...args);
3015
3079
  }
3016
3080
  async function runRegistryCommand(...args) {
3017
3081
  return (await import("./registry-4mhr7xsd.js")).runRegistryCommand(...args);
@@ -6368,7 +6432,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6368
6432
  }
6369
6433
  const data = await callDataAPI(reviewPath, {
6370
6434
  method: "POST",
6371
- body: JSON.stringify({ ...reviewBody, request_nonce: randomUUID3() }),
6435
+ body: JSON.stringify({ ...reviewBody, request_nonce: randomUUID4() }),
6372
6436
  headers: { "Content-Type": "application/json" }
6373
6437
  });
6374
6438
  context.output.writeData(data);
@@ -6461,7 +6525,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6461
6525
  });
6462
6526
  break;
6463
6527
  }
6464
- const sentBody = request.body && request.nonce ? { ...request.body, request_nonce: randomUUID3() } : request.body;
6528
+ const sentBody = request.body && request.nonce ? { ...request.body, request_nonce: randomUUID4() } : request.body;
6465
6529
  const data = await callDataAPI(request.path, {
6466
6530
  method: request.method,
6467
6531
  ...sentBody ? { body: JSON.stringify(sentBody), headers: { "Content-Type": "application/json" } } : {}
@@ -6796,7 +6860,7 @@ var commandRegistry = createCommandRegistry([
6796
6860
  resetApiRequestCountBeforeRun: true,
6797
6861
  telemetry: "result-and-error",
6798
6862
  handler: async (context) => {
6799
- const { runInit } = await import("./index-w2hs49tb.js");
6863
+ const { runInit } = await import("./index-wdttmhsg.js");
6800
6864
  if (context.outputMode === "agent-stream") {
6801
6865
  context.output.writeEvent({
6802
6866
  event: "started",
@@ -7945,6 +8009,12 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
7945
8009
  try {
7946
8010
  return await dispatchParsedCommandInner(parsed, startedAt);
7947
8011
  } finally {
8012
+ if (!isUpdateCheckDisabled(parsed.flags) && parsed.command && !["--version", "-V", "--help", "-h"].includes(parsed.command) && !optOutFlagSet(parsed.flags, "help") && !optOutFlagSet(parsed.flags, "version") && !optOutFlagSet(parsed.flags, "json-schemas")) {
8013
+ printUpdateNotice({}, {
8014
+ outputMode: resolveOutputMode(parsed.flags, { isTTY: process.stdout.isTTY, env: process.env }),
8015
+ quiet: optOutFlagSet(parsed.flags, "quiet")
8016
+ });
8017
+ }
7948
8018
  if (previousProfileEnv === undefined)
7949
8019
  delete process.env.MODA_PROFILE;
7950
8020
  else
@@ -8100,6 +8170,15 @@ async function dispatchParsedCommandInner(typedParsed, startedAt) {
8100
8170
  console.log(`moda-cli ${VERSION}`);
8101
8171
  return 0;
8102
8172
  }
8173
+ const retired = retiredCommandError(command, typedParsed.positionals);
8174
+ if (retired) {
8175
+ const mode = resolveOutputMode(typedFlags, { isTTY: process.stdout.isTTY, env: process.env });
8176
+ if (mode === "agent" || mode === "agent-stream" || mode === "json")
8177
+ printAgentCliError(command, retired, mode);
8178
+ else
8179
+ printCliError(retired);
8180
+ return 1;
8181
+ }
8103
8182
  if (command === "--help" || command === "-h" || flags.help === "true") {
8104
8183
  if (command && printTreeHelp(typedParsed))
8105
8184
  return 0;
@@ -8330,9 +8409,6 @@ async function dispatchParsedCommandInner(typedParsed, startedAt) {
8330
8409
  recorded: context.getTelemetryMeta()
8331
8410
  });
8332
8411
  }
8333
- if (!updateCheckDisabled) {
8334
- printUpdateNotice();
8335
- }
8336
8412
  printFeedbackTip(definition.name, context);
8337
8413
  return finishRun(context, { kind: "completed", exitCode: result.exitCode });
8338
8414
  } catch (thrown) {
@@ -8354,9 +8430,6 @@ async function dispatchParsedCommandInner(typedParsed, startedAt) {
8354
8430
  } else {
8355
8431
  printCliError(error);
8356
8432
  }
8357
- if (!updateCheckDisabled) {
8358
- printUpdateNotice();
8359
- }
8360
8433
  context.output.close();
8361
8434
  return exitCodeForError(error);
8362
8435
  }
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  agentsMdProblem,
6
6
  runSkillSync
7
- } from "./cli-f0z97a7t.js";
7
+ } from "./cli-4t3zznhh.js";
8
8
  import"./cli-998n4kz9.js";
9
9
  import {
10
10
  buildDoctorReport
@@ -3,7 +3,7 @@ import {
3
3
  pullSkills,
4
4
  runSkillSync,
5
5
  runSkillsCommand
6
- } from "./cli-f0z97a7t.js";
6
+ } from "./cli-4t3zznhh.js";
7
7
  import"./cli-qm5b1vck.js";
8
8
  import"./cli-rjav0vj1.js";
9
9
  import"./cli-hcwdz9sd.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runSkillsCommand
3
- } from "./cli-f0z97a7t.js";
3
+ } from "./cli-4t3zznhh.js";
4
4
  import {
5
5
  runPromptsCommand
6
6
  } from "./cli-92zfpz9f.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.55.96",
3
+ "version": "1.55.97",
4
4
  "description": "CLI for Moda: production monitoring and evals for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-10-23T22:44:22.701Z",
4
- "cli_version": "1.55.96",
3
+ "bundled_at": "2026-10-24T02:25:39.492Z",
4
+ "cli_version": "1.55.97",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-cloudflare-think",