@songsid/agend 2.1.1-beta.2 → 2.1.1-beta.21

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 (156) hide show
  1. package/README.md +1 -0
  2. package/README.zh-TW.md +1 -0
  3. package/dist/access-path.js +15 -6
  4. package/dist/access-path.js.map +1 -1
  5. package/dist/agent-cli-instructions.md +9 -0
  6. package/dist/backend/antigravity.d.ts +4 -1
  7. package/dist/backend/antigravity.js +24 -13
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +37 -0
  10. package/dist/backend/claude-code.js +47 -8
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +17 -0
  13. package/dist/backend/codex.js +279 -62
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +4 -7
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.d.ts +24 -0
  18. package/dist/backend/grok.js +57 -8
  19. package/dist/backend/grok.js.map +1 -1
  20. package/dist/backend/kiro.d.ts +27 -0
  21. package/dist/backend/kiro.js +96 -15
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/opencode.js +4 -7
  24. package/dist/backend/opencode.js.map +1 -1
  25. package/dist/backend/types.d.ts +44 -0
  26. package/dist/backend/types.js +3 -3
  27. package/dist/backend/types.js.map +1 -1
  28. package/dist/channel/adapters/discord.d.ts +15 -0
  29. package/dist/channel/adapters/discord.js +94 -6
  30. package/dist/channel/adapters/discord.js.map +1 -1
  31. package/dist/channel/adapters/telegram.d.ts +21 -0
  32. package/dist/channel/adapters/telegram.js +132 -15
  33. package/dist/channel/adapters/telegram.js.map +1 -1
  34. package/dist/channel/ipc-bridge.d.ts +17 -1
  35. package/dist/channel/ipc-bridge.js +53 -17
  36. package/dist/channel/ipc-bridge.js.map +1 -1
  37. package/dist/channel/ipc-timeouts.d.ts +40 -0
  38. package/dist/channel/ipc-timeouts.js +58 -0
  39. package/dist/channel/ipc-timeouts.js.map +1 -0
  40. package/dist/channel/mcp-server.js +30 -14
  41. package/dist/channel/mcp-server.js.map +1 -1
  42. package/dist/channel/mcp-tools.js +28 -1
  43. package/dist/channel/mcp-tools.js.map +1 -1
  44. package/dist/channel/message-queue.d.ts +1 -0
  45. package/dist/channel/message-queue.js +49 -17
  46. package/dist/channel/message-queue.js.map +1 -1
  47. package/dist/channel/reconnect-backoff.d.ts +17 -0
  48. package/dist/channel/reconnect-backoff.js +21 -0
  49. package/dist/channel/reconnect-backoff.js.map +1 -0
  50. package/dist/channel/types.d.ts +29 -0
  51. package/dist/classic-channel-manager.js +1 -5
  52. package/dist/classic-channel-manager.js.map +1 -1
  53. package/dist/cli.js +148 -60
  54. package/dist/cli.js.map +1 -1
  55. package/dist/completion.d.ts +27 -0
  56. package/dist/completion.js +121 -0
  57. package/dist/completion.js.map +1 -0
  58. package/dist/config-validator.js +27 -0
  59. package/dist/config-validator.js.map +1 -1
  60. package/dist/config.js +6 -0
  61. package/dist/config.js.map +1 -1
  62. package/dist/cost-guard.d.ts +3 -1
  63. package/dist/cost-guard.js +3 -1
  64. package/dist/cost-guard.js.map +1 -1
  65. package/dist/daemon.d.ts +230 -13
  66. package/dist/daemon.js +975 -335
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/event-log.d.ts +31 -0
  69. package/dist/event-log.js +96 -0
  70. package/dist/event-log.js.map +1 -1
  71. package/dist/fleet-context.d.ts +2 -0
  72. package/dist/fleet-manager.d.ts +209 -4
  73. package/dist/fleet-manager.js +813 -84
  74. package/dist/fleet-manager.js.map +1 -1
  75. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +22 -0
  76. package/dist/general-knowledge/skills/model-discovery/SKILL.md +14 -23
  77. package/dist/general-knowledge/skills/session-management/SKILL.md +15 -34
  78. package/dist/hang-detector.d.ts +19 -13
  79. package/dist/hang-detector.js +19 -49
  80. package/dist/hang-detector.js.map +1 -1
  81. package/dist/instance-lifecycle.d.ts +21 -1
  82. package/dist/instance-lifecycle.js +141 -7
  83. package/dist/instance-lifecycle.js.map +1 -1
  84. package/dist/instructions.d.ts +5 -0
  85. package/dist/instructions.js +10 -5
  86. package/dist/instructions.js.map +1 -1
  87. package/dist/locale.js +3 -0
  88. package/dist/locale.js.map +1 -1
  89. package/dist/logger.js +14 -0
  90. package/dist/logger.js.map +1 -1
  91. package/dist/mcp-liveness.d.ts +21 -0
  92. package/dist/mcp-liveness.js +27 -0
  93. package/dist/mcp-liveness.js.map +1 -0
  94. package/dist/outbound-handlers.d.ts +16 -0
  95. package/dist/outbound-handlers.js +121 -27
  96. package/dist/outbound-handlers.js.map +1 -1
  97. package/dist/outbound-schemas.d.ts +7 -2
  98. package/dist/outbound-schemas.js +13 -2
  99. package/dist/outbound-schemas.js.map +1 -1
  100. package/dist/pane-write-lock.d.ts +48 -0
  101. package/dist/pane-write-lock.js +73 -0
  102. package/dist/pane-write-lock.js.map +1 -0
  103. package/dist/process-memory.d.ts +31 -0
  104. package/dist/process-memory.js +79 -0
  105. package/dist/process-memory.js.map +1 -0
  106. package/dist/quickstart.js +17 -16
  107. package/dist/quickstart.js.map +1 -1
  108. package/dist/scheduler/db.js +3 -0
  109. package/dist/scheduler/db.js.map +1 -1
  110. package/dist/sd-notify.d.ts +45 -0
  111. package/dist/sd-notify.js +74 -3
  112. package/dist/sd-notify.js.map +1 -1
  113. package/dist/secret-file.d.ts +33 -0
  114. package/dist/secret-file.js +36 -0
  115. package/dist/secret-file.js.map +1 -0
  116. package/dist/setup-wizard.js +9 -7
  117. package/dist/setup-wizard.js.map +1 -1
  118. package/dist/tmux-control.d.ts +58 -5
  119. package/dist/tmux-control.js +107 -13
  120. package/dist/tmux-control.js.map +1 -1
  121. package/dist/tmux-manager.d.ts +47 -2
  122. package/dist/tmux-manager.js +103 -8
  123. package/dist/tmux-manager.js.map +1 -1
  124. package/dist/topic-commands.d.ts +43 -1
  125. package/dist/topic-commands.js +196 -66
  126. package/dist/topic-commands.js.map +1 -1
  127. package/dist/tui-glyphs.d.ts +25 -0
  128. package/dist/tui-glyphs.js +26 -0
  129. package/dist/tui-glyphs.js.map +1 -0
  130. package/dist/types.d.ts +19 -0
  131. package/dist/ui/view.html +291 -33
  132. package/dist/update-check.d.ts +22 -0
  133. package/dist/update-check.js +44 -0
  134. package/dist/update-check.js.map +1 -0
  135. package/dist/usage/providers.d.ts +50 -0
  136. package/dist/usage/providers.js +611 -0
  137. package/dist/usage/providers.js.map +1 -0
  138. package/dist/usage/usage-api.d.ts +50 -0
  139. package/dist/usage/usage-api.js +111 -0
  140. package/dist/usage/usage-api.js.map +1 -0
  141. package/dist/view-api.d.ts +10 -0
  142. package/dist/view-api.js +67 -14
  143. package/dist/view-api.js.map +1 -1
  144. package/dist/web-api.js +5 -2
  145. package/dist/web-api.js.map +1 -1
  146. package/package.json +4 -1
  147. package/templates/systemd.service.ejs +9 -1
  148. package/dist/channel/tool-tracker.d.ts +0 -13
  149. package/dist/channel/tool-tracker.js +0 -58
  150. package/dist/channel/tool-tracker.js.map +0 -1
  151. package/dist/daemon-entry.d.ts +0 -1
  152. package/dist/daemon-entry.js +0 -30
  153. package/dist/daemon-entry.js.map +0 -1
  154. package/dist/fleet-system-prompt.d.ts +0 -11
  155. package/dist/fleet-system-prompt.js +0 -61
  156. package/dist/fleet-system-prompt.js.map +0 -1
package/dist/cli.js CHANGED
@@ -13,6 +13,9 @@ import { spawn, execSync, execFileSync } from "node:child_process";
13
13
  import { getAgendHome, getTmuxSocketName } from "./paths.js";
14
14
  import { readClassicLastActivityAt } from "./classic-channel-manager.js";
15
15
  import { hasPausedMarker } from "./pause-marker.js";
16
+ import { PIE_PERCENT_RE } from "./tui-glyphs.js";
17
+ import { COMPLETION_SHELLS, completionScript } from "./completion.js";
18
+ import { getUpdateSelector, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, } from "./update-check.js";
16
19
  /** Prefix tmux args with -L when socket isolation is active. */
17
20
  function tmuxArgs(args) {
18
21
  const socket = getTmuxSocketName();
@@ -66,8 +69,14 @@ fleet
66
69
  const fleet = loadFleetConfig(FLEET_CONFIG_PATH);
67
70
  const port = fleet.health_port ?? 19280;
68
71
  try {
69
- const healthResp = await fetch(`http://127.0.0.1:${port}/health`);
70
- if (healthResp.ok) {
72
+ // Any HTTP answer means a fleet daemon is listening — including a 503,
73
+ // which /health now returns when the fleet is degraded (no adapter
74
+ // connected, an instance crashed). Checking `.ok` here would treat a
75
+ // degraded fleet as "not running" and start a SECOND fleet on the same
76
+ // data dir, whose startup cleanup kills the first one's tmux windows.
77
+ // Only a connection error means nothing is there (the catch below).
78
+ await fetch(`http://127.0.0.1:${port}/health`);
79
+ {
71
80
  try {
72
81
  const resp = await fetch(`http://127.0.0.1:${port}/api/instance/${encodeURIComponent(instance)}/start`, { method: "POST" });
73
82
  const body = await resp.json();
@@ -90,6 +99,59 @@ fleet
90
99
  }
91
100
  const { FleetManager } = await import("./fleet-manager.js");
92
101
  const fm = new FleetManager(DATA_DIR);
102
+ // Register crash/signal handlers BEFORE startAll(). Startup routinely takes
103
+ // minutes (sequential general spawn, staggered instances, warmup waits), and
104
+ // without these a Ctrl-C or `systemctl stop` in that window got Node's default
105
+ // behaviour: instant death with fleet.pid already written, databases unclosed,
106
+ // and every tmux window orphaned.
107
+ let stopping = false;
108
+ const shutdown = async () => {
109
+ if (stopping)
110
+ return; // SIGINT and SIGTERM share this, and crash paths call stopAll too
111
+ stopping = true;
112
+ console.log("\nStopping fleet...");
113
+ await fm.stopAll().catch(err => console.error("Shutdown error:", err));
114
+ process.exit(0);
115
+ };
116
+ process.on("SIGINT", shutdown);
117
+ process.on("SIGTERM", shutdown);
118
+ process.on("uncaughtException", (err) => {
119
+ // An uncaught exception leaves unknown state — still the one case worth
120
+ // tearing down for.
121
+ console.error("Uncaught exception:", err);
122
+ if (stopping)
123
+ return;
124
+ stopping = true;
125
+ fm.stopAll().catch(() => { }).finally(() => process.exit(1));
126
+ });
127
+ process.on("unhandledRejection", (err) => {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ // 409 = another bot poller exists — the adapter retries, don't crash.
130
+ if (msg.includes("409") && msg.includes("getUpdates")) {
131
+ console.error("Bot polling conflict (409) — retrying...");
132
+ return;
133
+ }
134
+ // Do NOT take the fleet down. A rejected promise in one instance's tmux
135
+ // call, health tick, or model switch used to stop every other working
136
+ // instance and exit(1) — the widest blast radius in the process. The
137
+ // per-instance health check and crash recovery handle a genuinely broken
138
+ // instance on their own.
139
+ //
140
+ // Surviving quietly would be its own failure mode, so this reports to the
141
+ // General topic as well as the log: the operator must not have to read
142
+ // daemon.log to learn the fleet swallowed a fault. notifyFleetError()
143
+ // throttles repeats, since these usually arrive from a loop.
144
+ console.error("Unhandled rejection (fleet continues):", err);
145
+ if (err instanceof Error && err.stack)
146
+ console.error(err.stack);
147
+ try {
148
+ fm.notifyFleetError(`⚠️ Fleet caught an unhandled error and kept running:\n\`${msg}\`\nSee daemon.log for the stack trace.`);
149
+ }
150
+ catch (notifyErr) {
151
+ // Never let the reporting path itself become an unhandled rejection.
152
+ console.error("Failed to report unhandled rejection:", notifyErr);
153
+ }
154
+ });
93
155
  if (instance) {
94
156
  const config = fm.loadConfig(FLEET_CONFIG_PATH);
95
157
  const inst = config.instances[instance];
@@ -107,30 +169,6 @@ fleet
107
169
  await fm.startAll(FLEET_CONFIG_PATH);
108
170
  }
109
171
  console.log("Fleet started");
110
- // Keep process alive + clean shutdown on Ctrl+C
111
- const shutdown = async () => {
112
- console.log("\nStopping fleet...");
113
- await fm.stopAll();
114
- process.exit(0);
115
- };
116
- process.on("SIGINT", shutdown);
117
- process.on("SIGTERM", shutdown);
118
- process.on("uncaughtException", async (err) => {
119
- console.error("Uncaught exception:", err);
120
- await fm.stopAll().catch(() => { });
121
- process.exit(1);
122
- });
123
- process.on("unhandledRejection", async (err) => {
124
- const msg = err instanceof Error ? err.message : String(err);
125
- // 409 = another bot poller exists — adapter handles retry, don't crash
126
- if (msg.includes("409") && msg.includes("getUpdates")) {
127
- console.error("Bot polling conflict (409) — retrying...");
128
- return;
129
- }
130
- console.error("Unhandled rejection:", err);
131
- await fm.stopAll().catch(() => { });
132
- process.exit(1);
133
- });
134
172
  });
135
173
  fleet
136
174
  .command("stop")
@@ -1088,11 +1126,22 @@ program
1088
1126
  .description("Update AgEnD to latest version and restart service")
1089
1127
  .option("--version <ver>", "Specific version to install")
1090
1128
  .option("--beta", "Install beta version")
1129
+ .option("--force", "Force reinstall and restart even when already up to date")
1091
1130
  .action(async (opts) => {
1092
1131
  const { spawnSync } = await import("node:child_process");
1093
- const tag = opts.version ? opts.version : (opts.beta ? "beta" : "latest");
1132
+ const tag = getUpdateSelector(opts);
1094
1133
  const pkg = `@songsid/agend@${tag}`;
1095
- console.log(`\n Updating AgEnD to ${tag}...\n`);
1134
+ const targetVersion = lookupTargetVersion(tag);
1135
+ if (shouldSkipUpdate(pkgVersion, targetVersion, opts.force)) {
1136
+ console.log(`\n ✓ Already up to date (v${pkgVersion})\n`);
1137
+ return;
1138
+ }
1139
+ if (targetVersion) {
1140
+ console.log(`\n Updating AgEnD v${pkgVersion} → v${targetVersion}...\n`);
1141
+ }
1142
+ else {
1143
+ console.log(`\n Updating AgEnD to ${tag}...\n`);
1144
+ }
1096
1145
  // Detect and remove stale npm link (local build that shadows global install)
1097
1146
  try {
1098
1147
  const agendPath = execSync("which agend", { encoding: "utf-8", stdio: "pipe" }).trim();
@@ -1204,12 +1253,8 @@ program
1204
1253
  // systemd → user systemd → launchd → detached pid).
1205
1254
  console.log(" Restarting fleet...");
1206
1255
  const restartResult = spawnSync(agendPath, ["restart"], { encoding: "utf-8", timeout: 30000, stdio: "inherit" });
1207
- if (restartResult.status === 0) {
1208
- console.log(" ✓ Service restarted");
1209
- }
1210
- else {
1211
- console.log(" ⚠ Auto-restart failed. Run: agend start");
1212
- }
1256
+ if (!reportUpdateRestart(restartResult.status))
1257
+ process.exitCode = 1;
1213
1258
  });
1214
1259
  program
1215
1260
  .command("reload")
@@ -1826,28 +1871,7 @@ async function fuzzyMatch(query, names) {
1826
1871
  return null;
1827
1872
  }
1828
1873
  async function resolveInstance(query, config) {
1829
- const names = Object.keys(config.instances);
1830
- // Include classic instances from classicBot.yaml
1831
- try {
1832
- const classicPath = join(DATA_DIR, "classicBot.yaml");
1833
- if (existsSync(classicPath)) {
1834
- const yamlMod = (await import("js-yaml")).default;
1835
- const classic = yamlMod.load(readFileSync(classicPath, "utf-8"));
1836
- if (classic?.channels) {
1837
- for (const [key, val] of Object.entries(classic.channels)) {
1838
- // New format persists instanceName; old format keyed by channelId.
1839
- if (val.instanceName) {
1840
- names.push(val.instanceName);
1841
- continue;
1842
- }
1843
- const channelId = val.channelId ?? key;
1844
- const chName = (val.name ?? channelId).toLowerCase().replace(/[^\p{L}\d-]/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "project";
1845
- names.push(`classic-${chName}-${channelId.slice(-4)}`);
1846
- }
1847
- }
1848
- }
1849
- }
1850
- catch { /* ignore */ }
1874
+ const names = await listInstanceNames(config);
1851
1875
  const match = await fuzzyMatch(query, names);
1852
1876
  if (!match) {
1853
1877
  console.error(`No instance matching "${query}". Available: ${names.join(", ")}`);
@@ -1942,7 +1966,7 @@ function formatTimeSince(isoStr) {
1942
1966
  * Universal context-% parser for `agend ls` (all backends). Kept in sync with
1943
1967
  * parseContextPercent() in topic-commands.ts — same formats, bottom-up so the
1944
1968
  * most recent prompt wins:
1945
- * kiro classic "8% !>" / TUI " 1%" / bracket "[8%]" / prompt "8% ❯"
1969
+ * kiro classic "8% !>" / TUI " 1%" (any pie glyph) / bracket "[8%]" / prompt "8% ❯"
1946
1970
  * codex "Context N% left" (remaining → 100-N) or "Context N% used"
1947
1971
  * opencode "1.2K (6%)"
1948
1972
  * grok "12K / 500K" (used tokens / context window)
@@ -1969,7 +1993,7 @@ const defaultParser = (output) => {
1969
1993
  return used / total * 100;
1970
1994
  }
1971
1995
  const m = line.match(/(\d+)%.*[!❯>]/)
1972
- || line.match(/◔\s*(\d+)%/)
1996
+ || line.match(PIE_PERCENT_RE)
1973
1997
  || line.match(/\[(\d+)%\]/)
1974
1998
  || line.match(/Context\s+(\d+)%\s+used/i)
1975
1999
  || line.match(/\d+(?:\.\d+)?[KM]?\s*\((\d+)%\)/);
@@ -1980,10 +2004,48 @@ const defaultParser = (output) => {
1980
2004
  };
1981
2005
  /** Optional backend-specific overrides; none needed — defaultParser covers all. */
1982
2006
  const contextParsers = {};
2007
+ /**
2008
+ * Every instance name the CLI accepts: fleet.yaml instances plus the
2009
+ * dynamically-created ClassicBot ones. Shared by `resolveInstance` (fuzzy
2010
+ * matching for `attach`) and `ls --names-only` (shell completion), so the
2011
+ * candidates offered by tab-completion are exactly the ones `attach` resolves.
2012
+ */
2013
+ async function listInstanceNames(config) {
2014
+ const names = Object.keys(config.instances);
2015
+ try {
2016
+ const classicPath = join(DATA_DIR, "classicBot.yaml");
2017
+ if (existsSync(classicPath)) {
2018
+ const yamlMod = (await import("js-yaml")).default;
2019
+ const classic = yamlMod.load(readFileSync(classicPath, "utf-8"));
2020
+ if (classic?.channels) {
2021
+ for (const [key, val] of Object.entries(classic.channels)) {
2022
+ // New format persists instanceName; old format keyed by channelId.
2023
+ if (val.instanceName) {
2024
+ names.push(val.instanceName);
2025
+ continue;
2026
+ }
2027
+ const channelId = val.channelId ?? key;
2028
+ const chName = (val.name ?? channelId).toLowerCase().replace(/[^\p{L}\d-]/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "project";
2029
+ names.push(`classic-${chName}-${channelId.slice(-4)}`);
2030
+ }
2031
+ }
2032
+ }
2033
+ }
2034
+ catch { /* ignore */ }
2035
+ return names;
2036
+ }
1983
2037
  async function lsAction(opts) {
1984
2038
  const yaml = (await import("js-yaml")).default;
1985
2039
  const config = yaml.load(readFileSync(FLEET_CONFIG_PATH, "utf-8"));
1986
2040
  const names = Object.keys(config.instances);
2041
+ // --names-only feeds shell completion, so it must return before any tmux or
2042
+ // status work (a tab press should not wait on the fleet) and must print
2043
+ // nothing but names — a "no instances" notice would become a candidate.
2044
+ if (opts.namesOnly) {
2045
+ for (const n of await listInstanceNames(config))
2046
+ console.log(n);
2047
+ return;
2048
+ }
1987
2049
  // Load classic channels from classicBot.yaml (keyed by channelId)
1988
2050
  const classicPath = join(DATA_DIR, "classicBot.yaml");
1989
2051
  let classicConfig = null;
@@ -2269,12 +2331,38 @@ program
2269
2331
  .command("ls")
2270
2332
  .description("List all instances with status, backend, team, and last activity")
2271
2333
  .option("--json", "Output as JSON")
2334
+ .option("--names-only", "Print instance names one per line (for shell completion)")
2272
2335
  .action(async (opts) => {
2273
2336
  await lsAction(opts);
2274
2337
  });
2275
2338
  program
2276
- .command("health")
2277
- .description("Fleet health check shows problems and diagnostics")
2339
+ .command("completion")
2340
+ .description("Print a shell completion script (bash or zsh)")
2341
+ .argument("<shell>", `Shell to generate for (${COMPLETION_SHELLS.join(", ")})`)
2342
+ .addHelpText("after", `
2343
+ Install:
2344
+ bash echo 'eval "$(agend completion bash)"' >> ~/.bashrc
2345
+ zsh echo 'eval "$(agend completion zsh)"' >> ~/.zshrc
2346
+
2347
+ Then reload the shell. Tab-completes instance names for \`agend attach\` and
2348
+ \`agend fleet start|stop|restart\`, and subcommands elsewhere.`)
2349
+ .action((shell) => {
2350
+ if (!COMPLETION_SHELLS.includes(shell)) {
2351
+ console.error(`Unsupported shell: ${shell}. Supported: ${COMPLETION_SHELLS.join(", ")}`);
2352
+ process.exit(2);
2353
+ }
2354
+ // Subcommand names come from commander itself, so the script can't drift
2355
+ // out of sync with the CLI the way a hardcoded list would.
2356
+ const fleetCmd = program.commands.find(c => c.name() === "fleet");
2357
+ console.log(completionScript(shell, {
2358
+ topLevel: program.commands.map(c => c.name()),
2359
+ fleetSub: fleetCmd?.commands.map(c => c.name()) ?? [],
2360
+ instanceCommands: ["attach"],
2361
+ fleetInstanceCommands: ["start", "stop", "restart"],
2362
+ }));
2363
+ });
2364
+ program
2365
+ .command("health").description("Fleet health check — shows problems and diagnostics")
2278
2366
  .option("--json", "Output as JSON")
2279
2367
  .option("-q, --quiet", "One-line summary only")
2280
2368
  .action(async (opts) => {