@mono-agent/agent-app 0.3.0 → 0.4.1

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 (186) hide show
  1. package/README.md +24 -10
  2. package/dist/adapter-send-tools-main.d.ts +3 -0
  3. package/dist/adapter-send-tools-main.d.ts.map +1 -0
  4. package/dist/adapter-send-tools-main.js +20 -0
  5. package/dist/adapter-send-tools-main.js.map +1 -0
  6. package/dist/adapter-send-tools.d.ts +125 -0
  7. package/dist/adapter-send-tools.d.ts.map +1 -0
  8. package/dist/adapter-send-tools.js +629 -0
  9. package/dist/adapter-send-tools.js.map +1 -0
  10. package/dist/app-config.d.ts +86 -26
  11. package/dist/app-config.d.ts.map +1 -1
  12. package/dist/app-config.js +284 -90
  13. package/dist/app-config.js.map +1 -1
  14. package/dist/app.d.ts +51 -20
  15. package/dist/app.d.ts.map +1 -1
  16. package/dist/app.js +889 -119
  17. package/dist/app.js.map +1 -1
  18. package/dist/artifact-retention.d.ts +32 -0
  19. package/dist/artifact-retention.d.ts.map +1 -0
  20. package/dist/artifact-retention.js +111 -0
  21. package/dist/artifact-retention.js.map +1 -0
  22. package/dist/audit-runs.d.ts +12 -0
  23. package/dist/audit-runs.d.ts.map +1 -0
  24. package/dist/audit-runs.js +68 -0
  25. package/dist/audit-runs.js.map +1 -0
  26. package/dist/backfill.d.ts +75 -0
  27. package/dist/backfill.d.ts.map +1 -0
  28. package/dist/backfill.js +350 -0
  29. package/dist/backfill.js.map +1 -0
  30. package/dist/background.d.ts +8 -5
  31. package/dist/background.d.ts.map +1 -1
  32. package/dist/background.js +279 -53
  33. package/dist/background.js.map +1 -1
  34. package/dist/broadcast-recorder.d.ts +27 -0
  35. package/dist/broadcast-recorder.d.ts.map +1 -0
  36. package/dist/broadcast-recorder.js +82 -0
  37. package/dist/broadcast-recorder.js.map +1 -0
  38. package/dist/channel-config-view.d.ts +28 -0
  39. package/dist/channel-config-view.d.ts.map +1 -0
  40. package/dist/channel-config-view.js +79 -0
  41. package/dist/channel-config-view.js.map +1 -0
  42. package/dist/channel-gate.d.ts +22 -0
  43. package/dist/channel-gate.d.ts.map +1 -0
  44. package/dist/channel-gate.js +35 -0
  45. package/dist/channel-gate.js.map +1 -0
  46. package/dist/channel-plugins.d.ts +39 -0
  47. package/dist/channel-plugins.d.ts.map +1 -0
  48. package/dist/channel-plugins.js +346 -0
  49. package/dist/channel-plugins.js.map +1 -0
  50. package/dist/channels.d.ts +62 -66
  51. package/dist/channels.d.ts.map +1 -1
  52. package/dist/channels.js +850 -120
  53. package/dist/channels.js.map +1 -1
  54. package/dist/cli.d.ts +103 -4
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +984 -112
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config-reference.d.ts +24 -0
  59. package/dist/config-reference.d.ts.map +1 -0
  60. package/dist/config-reference.js +580 -0
  61. package/dist/config-reference.js.map +1 -0
  62. package/dist/configured-agent.d.ts +104 -0
  63. package/dist/configured-agent.d.ts.map +1 -0
  64. package/dist/configured-agent.js +578 -0
  65. package/dist/configured-agent.js.map +1 -0
  66. package/dist/consumer-contract.d.ts +36 -0
  67. package/dist/consumer-contract.d.ts.map +1 -0
  68. package/dist/consumer-contract.js +290 -0
  69. package/dist/consumer-contract.js.map +1 -0
  70. package/dist/doctor.d.ts +17 -0
  71. package/dist/doctor.d.ts.map +1 -1
  72. package/dist/doctor.js +392 -95
  73. package/dist/doctor.js.map +1 -1
  74. package/dist/index.d.ts +14 -6
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +7 -3
  77. package/dist/index.js.map +1 -1
  78. package/dist/init.d.ts +23 -7
  79. package/dist/init.d.ts.map +1 -1
  80. package/dist/init.js +91 -26
  81. package/dist/init.js.map +1 -1
  82. package/dist/install-skill.d.ts +1 -1
  83. package/dist/install-skill.js +2 -2
  84. package/dist/install-skill.js.map +1 -1
  85. package/dist/interaction-bridge.d.ts +72 -0
  86. package/dist/interaction-bridge.d.ts.map +1 -0
  87. package/dist/interaction-bridge.js +354 -0
  88. package/dist/interaction-bridge.js.map +1 -0
  89. package/dist/launchd.d.ts +0 -2
  90. package/dist/launchd.d.ts.map +1 -1
  91. package/dist/launchd.js +0 -2
  92. package/dist/launchd.js.map +1 -1
  93. package/dist/memory-command.d.ts +10 -0
  94. package/dist/memory-command.d.ts.map +1 -0
  95. package/dist/memory-command.js +649 -0
  96. package/dist/memory-command.js.map +1 -0
  97. package/dist/memory-recall-main.d.ts +3 -0
  98. package/dist/memory-recall-main.d.ts.map +1 -0
  99. package/dist/memory-recall-main.js +30 -0
  100. package/dist/memory-recall-main.js.map +1 -0
  101. package/dist/memory-recall.d.ts +147 -0
  102. package/dist/memory-recall.d.ts.map +1 -0
  103. package/dist/memory-recall.js +345 -0
  104. package/dist/memory-recall.js.map +1 -0
  105. package/dist/memory-rituals.d.ts +8 -11
  106. package/dist/memory-rituals.d.ts.map +1 -1
  107. package/dist/memory-rituals.js +26 -25
  108. package/dist/memory-rituals.js.map +1 -1
  109. package/dist/metrics.d.ts +13 -0
  110. package/dist/metrics.d.ts.map +1 -0
  111. package/dist/metrics.js +105 -0
  112. package/dist/metrics.js.map +1 -0
  113. package/dist/notify-destinations.d.ts +23 -0
  114. package/dist/notify-destinations.d.ts.map +1 -0
  115. package/dist/notify-destinations.js +79 -0
  116. package/dist/notify-destinations.js.map +1 -0
  117. package/dist/posted-message-index.d.ts +61 -0
  118. package/dist/posted-message-index.d.ts.map +1 -0
  119. package/dist/posted-message-index.js +178 -0
  120. package/dist/posted-message-index.js.map +1 -0
  121. package/dist/proactive-notify.d.ts +32 -0
  122. package/dist/proactive-notify.d.ts.map +1 -0
  123. package/dist/proactive-notify.js +62 -0
  124. package/dist/proactive-notify.js.map +1 -0
  125. package/dist/recipes/base.d.ts +15 -0
  126. package/dist/recipes/base.d.ts.map +1 -0
  127. package/dist/recipes/base.js +51 -0
  128. package/dist/recipes/base.js.map +1 -0
  129. package/dist/recipes/catalog.d.ts +4 -0
  130. package/dist/recipes/catalog.d.ts.map +1 -0
  131. package/dist/recipes/catalog.js +509 -0
  132. package/dist/recipes/catalog.js.map +1 -0
  133. package/dist/recipes/index.d.ts +11 -0
  134. package/dist/recipes/index.d.ts.map +1 -0
  135. package/dist/recipes/index.js +14 -0
  136. package/dist/recipes/index.js.map +1 -0
  137. package/dist/recipes/types.d.ts +70 -0
  138. package/dist/recipes/types.d.ts.map +1 -0
  139. package/dist/recipes/types.js +15 -0
  140. package/dist/recipes/types.js.map +1 -0
  141. package/dist/request-model-override.d.ts +67 -0
  142. package/dist/request-model-override.d.ts.map +1 -0
  143. package/dist/request-model-override.js +96 -0
  144. package/dist/request-model-override.js.map +1 -0
  145. package/dist/runs-health.d.ts +22 -0
  146. package/dist/runs-health.d.ts.map +1 -0
  147. package/dist/runs-health.js +144 -0
  148. package/dist/runs-health.js.map +1 -0
  149. package/dist/seen-conversations.d.ts +22 -0
  150. package/dist/seen-conversations.d.ts.map +1 -0
  151. package/dist/seen-conversations.js +120 -0
  152. package/dist/seen-conversations.js.map +1 -0
  153. package/dist/sessions.d.ts +21 -0
  154. package/dist/sessions.d.ts.map +1 -0
  155. package/dist/sessions.js +45 -0
  156. package/dist/sessions.js.map +1 -0
  157. package/dist/setup.d.ts +29 -0
  158. package/dist/setup.d.ts.map +1 -0
  159. package/dist/setup.js +97 -0
  160. package/dist/setup.js.map +1 -0
  161. package/dist/trigger-overrides.d.ts +17 -0
  162. package/dist/trigger-overrides.d.ts.map +1 -0
  163. package/dist/trigger-overrides.js +30 -0
  164. package/dist/trigger-overrides.js.map +1 -0
  165. package/dist/tui-command.d.ts +56 -0
  166. package/dist/tui-command.d.ts.map +1 -0
  167. package/dist/tui-command.js +160 -0
  168. package/dist/tui-command.js.map +1 -0
  169. package/dist/ui.d.ts +54 -0
  170. package/dist/ui.d.ts.map +1 -0
  171. package/dist/ui.js +157 -0
  172. package/dist/ui.js.map +1 -0
  173. package/dist/web-command.d.ts +39 -0
  174. package/dist/web-command.d.ts.map +1 -0
  175. package/dist/web-command.js +151 -0
  176. package/dist/web-command.js.map +1 -0
  177. package/package.json +22 -17
  178. package/schema/mono-agent.config.schema.json +1686 -0
  179. package/skills/mono-agent-composer/SKILL.md +38 -12
  180. package/skills/mono-agent-composer/agents/openai.yaml +4 -0
  181. package/skills/mono-agent-composer/references/config-blueprint.md +84 -31
  182. package/skills/mono-agent-composer/references/discovery-questions.md +49 -36
  183. package/skills/mono-agent-composer/references/feature-coverage.md +26 -19
  184. package/skills/mono-agent-composer/references/package-map.md +20 -22
  185. package/skills/mono-agent-composer/references/playbooks.md +209 -0
  186. package/skills/mono-agent-composer/references/validation.md +14 -5
package/dist/cli.js CHANGED
@@ -1,52 +1,165 @@
1
1
  #!/usr/bin/env node
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stat } from "node:fs/promises";
2
4
  import { basename, resolve } from "node:path";
3
5
  import process from "node:process";
4
6
  import { fileURLToPath } from "node:url";
7
+ import { buildMonoAgentConfigView, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, } from "@mono-agent/config";
8
+ import { listRecordedRuns } from "@mono-agent/observability";
9
+ import { describeSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
5
10
  import { startMonoAgentApp } from "./app.js";
11
+ import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, } from "./app-config.js";
12
+ import { runAuditRuns } from "./audit-runs.js";
13
+ import { runBackfill } from "./backfill.js";
6
14
  import { defaultBackgroundDeps, resolveInstanceTarget, restartBackground, startBackground, statusBackground, stopBackground, tailLogs, } from "./background.js";
15
+ import { collectChannelConfigViews } from "./channel-config-view.js";
16
+ import { resolveChannelDrivers } from "./channels.js";
17
+ import { findUnknownAppConfigWarnings } from "./config-reference.js";
7
18
  import { validateMonoAgentFolder } from "./doctor.js";
8
- import { initMonoAgentFolder } from "./init.js";
19
+ import { initMonoAgentFolder, isWithChannel } from "./init.js";
9
20
  import { installComposerSkill } from "./install-skill.js";
21
+ import { runMetrics } from "./metrics.js";
22
+ import { findRecipe, RECIPE_CATALOG, recipeIds, resolveRecipeInputs } from "./recipes/index.js";
23
+ import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
24
+ import { purgeSessions } from "./sessions.js";
25
+ import { collectSetupOptions } from "./setup.js";
26
+ import * as ui from "./ui.js";
10
27
  const DEFAULT_LOG_LINES = 200;
28
+ // Node's maximum setInterval/setTimeout delay (2^31 - 1 ms, ~24.8 days). A
29
+ // referenced timer at this delay keeps the foreground event loop alive without
30
+ // busy-waiting; larger values silently overflow to a 1ms delay.
31
+ const KEEP_ALIVE_INTERVAL_MS = 2_147_483_647;
11
32
  const BACKGROUND_COMMANDS = ["start", "restart", "stop", "status", "logs"];
12
- const KNOWN_COMMANDS = ["init", "validate", "start", "restart", "stop", "status", "logs", "install-skill"];
33
+ const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "config", "recipes", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory"];
13
34
  export function parseCliArgs(argv) {
14
35
  const [command, ...rest] = argv;
15
36
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
16
- return { command: "help", noConsole: false, force: false, foreground: false, follow: false };
37
+ return { command: "help", positionals: [], force: false, foreground: false, follow: false, all: false, dryRun: false, includeMemory: false };
17
38
  }
18
39
  if (!KNOWN_COMMANDS.includes(command)) {
19
40
  throw new Error(`Unknown command \`${command}\`. Expected ${KNOWN_COMMANDS.join(", ")}.`);
20
41
  }
21
- const cmd = command;
42
+ // `doctor` is an alias of `validate`; normalize here so every downstream
43
+ // validate path (env-file resolution, --consumer, routing) applies unchanged.
44
+ const cmd = (command === "doctor" ? "validate" : command);
22
45
  const isLogs = cmd === "logs";
23
46
  let configPath;
24
- let port;
25
47
  let model;
26
48
  let fallbackModels;
27
49
  let memory;
50
+ let recipe;
51
+ let withChannels;
52
+ const positionals = [];
28
53
  let envFile;
29
54
  let target;
30
- let noConsole = false;
31
55
  let force = false;
32
56
  let foreground = false;
33
57
  let follow = false;
34
58
  let lines;
59
+ let run;
60
+ let all = false;
61
+ let since;
62
+ let until;
63
+ let dryRun = false;
64
+ let includeMemory = false;
65
+ let artifactDir;
66
+ let groupBy;
67
+ let consumerPath;
68
+ let agent;
69
+ let conversation;
70
+ let staleAfterMs;
71
+ let json = false;
72
+ let limit;
73
+ let host;
74
+ let port;
75
+ let open;
76
+ let allowNonLoopback;
35
77
  for (let i = 0; i < rest.length; i += 1) {
36
78
  const flag = rest[i];
37
79
  switch (flag) {
38
80
  case "--config":
39
81
  configPath = requireValue(rest, ++i, flag);
40
82
  break;
83
+ case "--run":
84
+ run = requireValue(rest, ++i, flag);
85
+ break;
86
+ case "--all":
87
+ all = true;
88
+ break;
89
+ case "--since":
90
+ since = requireValue(rest, ++i, flag);
91
+ break;
92
+ case "--until":
93
+ until = requireValue(rest, ++i, flag);
94
+ break;
95
+ case "--dry-run":
96
+ dryRun = true;
97
+ break;
98
+ case "--include-memory":
99
+ includeMemory = true;
100
+ break;
101
+ case "--artifact-dir":
102
+ artifactDir = requireValue(rest, ++i, flag);
103
+ break;
104
+ case "--artifacts":
105
+ artifactDir = requireValue(rest, ++i, flag);
106
+ break;
107
+ case "--by": {
108
+ const raw = requireValue(rest, ++i, flag);
109
+ if (raw !== "model" && raw !== "channel" && raw !== "failureKind") {
110
+ throw new Error("--by must be model, channel, or failureKind.");
111
+ }
112
+ groupBy = raw;
113
+ break;
114
+ }
115
+ case "--consumer":
116
+ consumerPath = requireValue(rest, ++i, flag);
117
+ break;
118
+ case "--agent":
119
+ agent = requireValue(rest, ++i, flag);
120
+ break;
121
+ case "--conversation":
122
+ conversation = requireValue(rest, ++i, flag);
123
+ break;
124
+ case "--stale-after-ms": {
125
+ const raw = requireValue(rest, ++i, flag);
126
+ const parsed = Number(raw);
127
+ if (!Number.isInteger(parsed) || parsed < 1) {
128
+ throw new Error("--stale-after-ms must be a positive integer.");
129
+ }
130
+ staleAfterMs = parsed;
131
+ break;
132
+ }
133
+ case "--json":
134
+ json = true;
135
+ break;
136
+ case "--limit": {
137
+ const raw = requireValue(rest, ++i, flag);
138
+ const parsed = Number(raw);
139
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
140
+ throw new Error("--limit must be an integer between 1 and 100.");
141
+ }
142
+ limit = parsed;
143
+ break;
144
+ }
145
+ case "--host":
146
+ host = requireValue(rest, ++i, flag);
147
+ break;
41
148
  case "--port": {
42
149
  const raw = requireValue(rest, ++i, flag);
43
150
  const parsed = Number(raw);
44
- if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) {
151
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
45
152
  throw new Error("--port must be an integer between 0 and 65535.");
46
153
  }
47
154
  port = parsed;
48
155
  break;
49
156
  }
157
+ case "--no-open":
158
+ open = false;
159
+ break;
160
+ case "--allow-non-loopback":
161
+ allowNonLoopback = true;
162
+ break;
50
163
  case "--model":
51
164
  model = requireValue(rest, ++i, flag);
52
165
  break;
@@ -64,6 +177,15 @@ export function parseCliArgs(argv) {
64
177
  memory = raw;
65
178
  break;
66
179
  }
180
+ case "--recipe":
181
+ recipe = requireValue(rest, ++i, flag);
182
+ break;
183
+ case "--with":
184
+ withChannels = requireValue(rest, ++i, flag)
185
+ .split(",")
186
+ .map((entry) => entry.trim())
187
+ .filter((entry) => entry.length > 0);
188
+ break;
67
189
  case "--env-file":
68
190
  envFile = requireValue(rest, ++i, flag);
69
191
  break;
@@ -78,9 +200,6 @@ export function parseCliArgs(argv) {
78
200
  case "--force":
79
201
  force = true;
80
202
  break;
81
- case "--no-console":
82
- noConsole = true;
83
- break;
84
203
  case "--foreground":
85
204
  foreground = true;
86
205
  break;
@@ -106,23 +225,62 @@ export function parseCliArgs(argv) {
106
225
  break;
107
226
  }
108
227
  default:
109
- throw new Error(`Unknown flag \`${flag}\` for \`mono-agent ${command}\`.`);
228
+ if (flag === undefined) {
229
+ break;
230
+ }
231
+ if (flag.startsWith("--")) {
232
+ throw new Error(`Unknown flag \`${flag}\` for \`mono-agent ${command}\`.`);
233
+ }
234
+ // Non-flag tokens are positional arguments (e.g. `recipes show <id>`).
235
+ positionals.push(flag);
236
+ break;
110
237
  }
111
238
  }
239
+ if (consumerPath !== undefined && cmd !== "validate" && cmd !== "audit-runs") {
240
+ throw new Error("--consumer is only supported for `mono-agent validate` and `mono-agent audit-runs`.");
241
+ }
242
+ if ((host !== undefined || port !== undefined || open !== undefined || allowNonLoopback !== undefined) && cmd !== "web") {
243
+ throw new Error("--host, --port, --no-open, and --allow-non-loopback are only supported for `mono-agent web`.");
244
+ }
245
+ if (includeMemory && cmd !== "audit-runs" && cmd !== "metrics" && cmd !== "backfill" && cmd !== "web") {
246
+ throw new Error("--include-memory is only supported for `mono-agent audit-runs`, `mono-agent metrics`, `mono-agent backfill`, and `mono-agent web`.");
247
+ }
248
+ if (limit !== undefined && cmd !== "memory") {
249
+ throw new Error("--limit is only supported for `mono-agent memory`.");
250
+ }
112
251
  return {
113
252
  command: cmd,
114
253
  ...(configPath === undefined ? {} : { configPath }),
115
- ...(port === undefined ? {} : { port }),
116
254
  ...(model === undefined ? {} : { model }),
117
255
  ...(fallbackModels === undefined ? {} : { fallbackModels }),
118
256
  ...(memory === undefined ? {} : { memory }),
257
+ ...(recipe === undefined ? {} : { recipe }),
258
+ ...(withChannels === undefined ? {} : { withChannels }),
259
+ positionals,
119
260
  ...(envFile === undefined ? {} : { envFile }),
120
261
  ...(target === undefined ? {} : { target }),
121
- noConsole,
122
262
  force,
123
263
  foreground,
124
264
  follow,
125
265
  ...(lines === undefined ? {} : { lines }),
266
+ ...(run === undefined ? {} : { run }),
267
+ all,
268
+ ...(since === undefined ? {} : { since }),
269
+ ...(until === undefined ? {} : { until }),
270
+ dryRun,
271
+ includeMemory,
272
+ ...(artifactDir === undefined ? {} : { artifactDir }),
273
+ ...(groupBy === undefined ? {} : { groupBy }),
274
+ ...(consumerPath === undefined ? {} : { consumerPath }),
275
+ ...(staleAfterMs === undefined ? {} : { staleAfterMs }),
276
+ ...(json ? { json } : {}),
277
+ ...(limit === undefined ? {} : { limit }),
278
+ ...(agent === undefined ? {} : { agent }),
279
+ ...(conversation === undefined ? {} : { conversation }),
280
+ ...(host === undefined ? {} : { host }),
281
+ ...(port === undefined ? {} : { port }),
282
+ ...(open === undefined ? {} : { open }),
283
+ ...(allowNonLoopback === undefined ? {} : { allowNonLoopback }),
126
284
  };
127
285
  }
128
286
  /**
@@ -139,6 +297,15 @@ export function loadCliEnvFile(path) {
139
297
  return false;
140
298
  }
141
299
  }
300
+ function resolveValidateContext(args, invocationCwd) {
301
+ const cwd = resolve(invocationCwd, args.consumerPath ?? ".");
302
+ return {
303
+ cwd,
304
+ configPath: resolve(cwd, args.configPath ?? "mono-agent.config.json"),
305
+ envFilePath: resolve(cwd, args.envFile ?? ".env"),
306
+ allowFilesystemWrites: args.consumerPath === undefined,
307
+ };
308
+ }
142
309
  function requireValue(args, index, flag) {
143
310
  const value = args[index];
144
311
  if (value === undefined || value.startsWith("--")) {
@@ -146,38 +313,143 @@ function requireValue(args, index, flag) {
146
313
  }
147
314
  return value;
148
315
  }
149
- const HELP_TEXT = `mono-agent — config-first agent host
150
-
151
- Usage:
152
- mono-agent init [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]
153
- Scaffold mono-agent.config.json, IDENTITY.md, and .mono-agent/ in the
154
- current folder. Existing files are never overwritten.
155
-
156
- mono-agent validate [--config <path>] [--env-file <path>]
157
- Load every config section and report what would run, wait, or fail.
158
-
159
- mono-agent start [--config <path>] [--port <n>] [--no-console] [--env-file <path>] [--foreground|-f]
160
- Start the agent as a background macOS service (launchd), print its
161
- instance info, and return. Re-running restarts the running instance.
162
- Use --foreground (-f) to run in the blocking foreground instead.
163
-
164
- mono-agent restart [--config <path>] [--port <n>] [--no-console]
165
- Restart the background instance for this config (starts it if stopped).
166
-
167
- mono-agent stop [--config <path>]
168
- Stop the background instance and remove its LaunchAgent.
169
-
170
- mono-agent status [--config <path>]
171
- Show this config's instance plus any other running instances.
172
-
173
- mono-agent logs [--config <path>] [--follow|-f] [--lines <n>]
174
- Print (and optionally follow) the background instance's log files.
175
-
176
- mono-agent install-skill [--target claude|codex|both] [--force]
177
- Copy the bundled mono-agent-composer skill into ~/.claude/skills and
178
- ~/.codex/skills (default: both). Refuses to overwrite without --force.
179
-
180
- Background mode runs the agent under launchd, keeping it alive across logins
316
+ const HELP_COMMANDS = [
317
+ {
318
+ signature: "mono-agent init [--recipe <id>] [--with <csv>] [--dry-run]\n" +
319
+ " [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]",
320
+ lines: [
321
+ "Scaffold mono-agent.config.json, IDENTITY.md, and .mono-agent/ in the",
322
+ "current folder. With --recipe, build from a blueprint (+ .env.example);",
323
+ "--with adds channels, --dry-run previews. Existing files are never overwritten.",
324
+ ],
325
+ },
326
+ {
327
+ signature: "mono-agent setup [--recipe <id>] [--with <csv>] [--dry-run]\n" +
328
+ " [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]",
329
+ lines: [
330
+ "Guided setup when attached to a TTY: choose a recipe, answer non-secret",
331
+ "inputs, select channel add-ons, scaffold, validate, and print the secrets",
332
+ "checklist. In non-TTY contexts it falls back to init-style flags.",
333
+ ],
334
+ },
335
+ {
336
+ signature: "mono-agent recipes list | show <id>",
337
+ lines: [
338
+ "List the executable config blueprints, or show one's generated config,",
339
+ ".env.example, and follow-up checklist.",
340
+ ],
341
+ },
342
+ {
343
+ signature: "mono-agent validate [--recipe <id>] [--consumer <path>] [--config <path>] [--env-file <path>]",
344
+ lines: [
345
+ "Load every config section and report what would run, wait, or fail.",
346
+ "--consumer validates another agent folder read-only, including its .env.",
347
+ "With --recipe, also report whether the recipe's capabilities are live.",
348
+ "`mono-agent doctor` is an alias for this command.",
349
+ ],
350
+ },
351
+ {
352
+ signature: "mono-agent config [--config <path>] [--env-file <path>]",
353
+ lines: [
354
+ "Print the resolved config field-by-field, tagging each value with where",
355
+ "it came from (env / json / default), plus the channel summary. Read-only.",
356
+ ],
357
+ },
358
+ {
359
+ signature: "mono-agent start [--config <path>] [--env-file <path>] [--foreground|-f]",
360
+ lines: [
361
+ "Start the agent as a background macOS service (launchd), print its",
362
+ "instance info, and return. Re-running restarts the running instance.",
363
+ "Refuses to start without a valid mono-agent.config.json in the folder.",
364
+ "Use --foreground (-f) to run in the blocking foreground instead.",
365
+ ],
366
+ },
367
+ {
368
+ signature: "mono-agent restart [--config <path>] [--force]",
369
+ lines: [
370
+ "Restart the background instance for this config (starts it if stopped).",
371
+ "--force also clears the persisted pi sessions so it starts with fresh",
372
+ "conversations instead of resuming saved ones (durable memory is untouched).",
373
+ ],
374
+ },
375
+ {
376
+ signature: "mono-agent stop [--config <path>]",
377
+ lines: ["Stop the background instance and remove its LaunchAgent."],
378
+ },
379
+ {
380
+ signature: "mono-agent status [--config <path>]",
381
+ lines: ["Show this config's instance plus any other running instances."],
382
+ },
383
+ {
384
+ signature: "mono-agent logs [--config <path>] [--follow|-f] [--lines <n>]",
385
+ lines: ["Print (and optionally follow) the background instance's log files."],
386
+ },
387
+ {
388
+ signature: "mono-agent tui [--agent <label|sourceId>] [--conversation <id>]",
389
+ lines: [
390
+ "Open the operator console from any directory: live chat with full",
391
+ "thinking/tool/telemetry insight, recorded-run replay, and config view.",
392
+ "Discovers running agents via the trace-source registry; one running",
393
+ "agent connects directly, several open a picker.",
394
+ ],
395
+ },
396
+ {
397
+ signature: "mono-agent web [--host <addr>] [--port <n>] [--no-open] [--allow-non-loopback] [--include-memory]",
398
+ lines: [
399
+ "Serve the read-only Session Recorder web PWA from any directory: a live",
400
+ "flight-recorder over every agent's runs (prompt, reasoning, tools, cost).",
401
+ "Discovers running agents via the trace-source registry — the same",
402
+ "mechanism as `tui` — and streams new/updated runs in real time.",
403
+ "--include-memory also shows memory-maintenance runs.",
404
+ ],
405
+ },
406
+ {
407
+ signature: "mono-agent install-skill [--target claude|codex|both] [--force]",
408
+ lines: [
409
+ "Copy the bundled mono-agent-composer skill into ~/.claude/skills and",
410
+ "~/.agents/skills (default: both). Refuses to overwrite without --force.",
411
+ ],
412
+ },
413
+ {
414
+ signature: "mono-agent backfill (--run <id> | --all) [--since <iso>] [--until <iso>]\n" +
415
+ " [--include-memory] [--dry-run] [--config <path>] [--env-file <path>]",
416
+ lines: [
417
+ "Export already-recorded agent-run artifacts to the configured Phoenix exporter",
418
+ "with their historical timestamps. Trace ids are deterministic per run, so",
419
+ "re-running overwrites rather than duplicating. --dry-run maps and",
420
+ "serializes without sending. --include-memory adds memory-run artifacts",
421
+ "for --all; explicit --run can target a memory run directly.",
422
+ ],
423
+ },
424
+ {
425
+ signature: "mono-agent audit-runs [--artifact-dir <path> | --consumer <path>]\n" +
426
+ " [--include-memory] [--config <path>] [--env-file <path>] [--stale-after-ms <n>] [--json]",
427
+ lines: [
428
+ "Read local agent-run summary artifacts without rewriting them. Reports parse",
429
+ "failures, status and failure-kind histograms, stale running summaries,",
430
+ "and per-failure-kind rates. --include-memory includes memory-run artifacts.",
431
+ ],
432
+ },
433
+ {
434
+ signature: "mono-agent metrics [--artifacts <path>] [--since <iso>] [--until <iso>]\n" +
435
+ " [--include-memory] [--by model|channel|failureKind] [--json] [--config <path>] [--env-file <path>]",
436
+ lines: [
437
+ "Aggregate local agent-run summaries into status rates, failure-kind rates,",
438
+ "duration percentiles, and total/per-run cost. Read-only and offline.",
439
+ "--include-memory includes memory-run artifacts.",
440
+ ],
441
+ },
442
+ {
443
+ signature: "mono-agent memory [stats|today|show <date>|search <query>|top]\n" +
444
+ " [--limit <n>] [--json] [--config <path>] [--env-file <path>]",
445
+ lines: [
446
+ "Preview the configured memory store from an agent folder. Reads the",
447
+ "memory block from mono-agent.config.json, not the standalone memory-bujo",
448
+ "env workflow. Human-first output by default; --json is for scripts.",
449
+ ],
450
+ },
451
+ ];
452
+ const HELP_NOTES = `Background mode runs the agent under launchd, keeping it alive across logins
181
453
  (auto-restarting only on crash) until you run stop. Secrets are read from the
182
454
  .env file in the working directory, the same as foreground mode. The background
183
455
  commands require macOS; elsewhere use start --foreground.
@@ -188,24 +460,52 @@ pi:<provider>:<model> (e.g. pi:ollama:gemma4:31b).
188
460
  A .env file in the current folder is loaded automatically when present;
189
461
  already-exported shell variables take precedence.
190
462
  `;
463
+ /** Build the colorized help screen (plain text when color is disabled). */
464
+ export function renderHelp() {
465
+ let out = ui.banner("mono-agent", "config-first agent host") + "\n";
466
+ out += ui.heading("Usage");
467
+ for (const entry of HELP_COMMANDS) {
468
+ const [first, ...rest] = entry.signature.split("\n");
469
+ out += ` ${ui.style.bold(ui.style.cyan(first ?? ""))}\n`;
470
+ for (const cont of rest) {
471
+ out += ` ${ui.style.cyan(cont)}\n`;
472
+ }
473
+ for (const line of entry.lines) {
474
+ out += ` ${ui.style.dim(line)}\n`;
475
+ }
476
+ out += "\n";
477
+ }
478
+ out += ui.style.dim(HELP_NOTES);
479
+ return out;
480
+ }
191
481
  export async function runCli(argv) {
192
482
  let args;
193
483
  try {
194
484
  args = parseCliArgs(argv);
195
485
  }
196
486
  catch (error) {
197
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP_TEXT}`);
487
+ process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
488
+ process.stdout.write(`\n${renderHelp()}`);
198
489
  return 2;
199
490
  }
200
- loadCliEnvFile(resolve(process.cwd(), args.envFile ?? ".env"));
491
+ const invocationCwd = process.cwd();
492
+ loadCliEnvFile(args.command === "validate"
493
+ ? resolveValidateContext(args, invocationCwd).envFilePath
494
+ : resolve(invocationCwd, args.envFile ?? ".env"));
201
495
  switch (args.command) {
202
496
  case "help":
203
- process.stdout.write(HELP_TEXT);
497
+ process.stdout.write(renderHelp());
204
498
  return 0;
205
499
  case "init":
206
500
  return await runInit(args);
501
+ case "setup":
502
+ return await runSetup(args);
207
503
  case "validate":
208
504
  return await runValidate(args);
505
+ case "config":
506
+ return await runConfig(args);
507
+ case "recipes":
508
+ return runRecipes(args);
209
509
  case "start":
210
510
  return await runStart(args);
211
511
  case "restart":
@@ -213,31 +513,217 @@ export async function runCli(argv) {
213
513
  case "status":
214
514
  case "logs":
215
515
  return await runBackgroundCommand(args, args.command);
516
+ case "tui": {
517
+ // Lazy import: the operator console (and pi-tui) load only on demand.
518
+ const { runTui } = await import("./tui-command.js");
519
+ return await runTui({
520
+ configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
521
+ cwd: process.cwd(),
522
+ env: process.env,
523
+ ...(args.agent === undefined ? {} : { agent: args.agent }),
524
+ ...(args.conversation === undefined ? {} : { conversationId: args.conversation }),
525
+ });
526
+ }
527
+ case "web": {
528
+ // Lazy import: the web server (and express/session-web) load only on demand.
529
+ const { runWeb } = await import("./web-command.js");
530
+ return await runWeb({
531
+ configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
532
+ cwd: process.cwd(),
533
+ env: process.env,
534
+ ...(args.host === undefined ? {} : { host: args.host }),
535
+ ...(args.port === undefined ? {} : { port: args.port }),
536
+ ...(args.open === undefined ? {} : { open: args.open }),
537
+ ...(args.allowNonLoopback === undefined ? {} : { allowNonLoopback: args.allowNonLoopback }),
538
+ includeMemory: args.includeMemory,
539
+ });
540
+ }
216
541
  case "install-skill":
217
542
  return await runInstallSkill(args);
543
+ case "backfill":
544
+ return await runBackfill({
545
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
546
+ ...(args.run === undefined ? {} : { run: args.run }),
547
+ all: args.all,
548
+ ...(args.since === undefined ? {} : { since: args.since }),
549
+ ...(args.until === undefined ? {} : { until: args.until }),
550
+ dryRun: args.dryRun,
551
+ includeMemory: args.includeMemory,
552
+ });
553
+ case "audit-runs":
554
+ return await runAuditRuns({
555
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
556
+ ...(args.artifactDir === undefined ? {} : { artifactDir: args.artifactDir }),
557
+ ...(args.consumerPath === undefined ? {} : { consumerPath: args.consumerPath }),
558
+ ...(args.staleAfterMs === undefined ? {} : { staleAfterMs: args.staleAfterMs }),
559
+ json: args.json === true,
560
+ includeMemory: args.includeMemory,
561
+ });
562
+ case "metrics":
563
+ return await runMetrics({
564
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
565
+ ...(args.artifactDir === undefined ? {} : { artifactDir: args.artifactDir }),
566
+ ...(args.since === undefined ? {} : { since: args.since }),
567
+ ...(args.until === undefined ? {} : { until: args.until }),
568
+ ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
569
+ json: args.json === true,
570
+ includeMemory: args.includeMemory,
571
+ });
572
+ case "memory": {
573
+ // Lazy import: the memory preview path pulls SQLite/backend clients only on demand.
574
+ const { runMemoryCommand } = await import("./memory-command.js");
575
+ return await runMemoryCommand({
576
+ cwd: process.cwd(),
577
+ env: process.env,
578
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
579
+ positionals: args.positionals,
580
+ json: args.json === true,
581
+ ...(args.limit === undefined ? {} : { limit: args.limit }),
582
+ });
583
+ }
218
584
  }
219
585
  }
220
586
  async function runInit(args) {
587
+ const recipe = resolveRecipeArg(args);
588
+ if (recipe === "unknown") {
589
+ return 1;
590
+ }
591
+ const withChannels = resolveWithChannels(args);
592
+ if (withChannels === "invalid") {
593
+ return 1;
594
+ }
221
595
  const result = await initMonoAgentFolder({
222
596
  dir: process.cwd(),
223
597
  ...(args.model === undefined ? {} : { model: args.model }),
224
598
  ...(args.fallbackModels === undefined ? {} : { fallbackModels: args.fallbackModels }),
225
599
  ...(args.memory === undefined ? {} : { memory: args.memory }),
600
+ ...(recipe === undefined ? {} : { recipe }),
601
+ ...(withChannels === undefined ? {} : { withChannels }),
602
+ dryRun: args.dryRun,
226
603
  });
604
+ printInitResult(result, recipe);
605
+ printNextSteps(result.configPath, recipe);
606
+ return 0;
607
+ }
608
+ async function runSetup(args) {
609
+ if (process.stdin.isTTY !== true) {
610
+ return await runInit(args);
611
+ }
612
+ const recipeArg = resolveRecipeArg(args);
613
+ if (recipeArg === "unknown") {
614
+ return 1;
615
+ }
616
+ const withChannelsArg = resolveWithChannels(args);
617
+ if (withChannelsArg === "invalid") {
618
+ return 1;
619
+ }
620
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
621
+ let collected;
622
+ try {
623
+ collected = await collectSetupOptions({
624
+ prompt: { question: (prompt) => rl.question(prompt) },
625
+ ...(recipeArg === undefined ? {} : { recipe: recipeArg }),
626
+ ...(args.model === undefined ? {} : { model: args.model }),
627
+ ...(args.fallbackModels === undefined ? {} : { fallbackModels: args.fallbackModels }),
628
+ ...(withChannelsArg === undefined ? {} : { withChannels: withChannelsArg }),
629
+ });
630
+ }
631
+ finally {
632
+ rl.close();
633
+ }
634
+ const result = await initMonoAgentFolder({
635
+ dir: process.cwd(),
636
+ ...(collected.fallbackModels.length === 0 ? {} : { fallbackModels: collected.fallbackModels }),
637
+ ...(args.memory === undefined ? {} : { memory: args.memory }),
638
+ recipe: collected.recipe,
639
+ recipeInputs: collected.recipeInputs,
640
+ withChannels: collected.withChannels,
641
+ dryRun: args.dryRun,
642
+ });
643
+ printInitResult(result, collected.recipe);
644
+ if (!result.dryRun) {
645
+ const report = await validateMonoAgentFolder({
646
+ env: process.env,
647
+ cwd: process.cwd(),
648
+ configPath: result.configPath,
649
+ });
650
+ process.stdout.write("\n" + ui.heading("Validation"));
651
+ for (const section of report.sections) {
652
+ process.stdout.write(formatSection(section));
653
+ }
654
+ process.stdout.write(renderRecipeCompleteness(collected.recipe, report));
655
+ printSecretsChecklist(collected.secrets);
656
+ printNextSteps(result.configPath, collected.recipe);
657
+ return report.ok ? 0 : 1;
658
+ }
659
+ printSecretsChecklist(collected.secrets);
660
+ printNextSteps(result.configPath, collected.recipe);
661
+ return 0;
662
+ }
663
+ function resolveRecipeArg(args) {
664
+ if (args.recipe === undefined) {
665
+ return undefined;
666
+ }
667
+ const recipe = findRecipe(args.recipe);
668
+ if (recipe === undefined) {
669
+ process.stderr.write(ui.errorLine(`Unknown recipe \`${args.recipe}\`.`));
670
+ process.stderr.write(ui.hint("Run `mono-agent recipes list` to see available recipes."));
671
+ return "unknown";
672
+ }
673
+ return recipe;
674
+ }
675
+ function resolveWithChannels(args) {
676
+ if (args.withChannels === undefined) {
677
+ return undefined;
678
+ }
679
+ const invalid = args.withChannels.filter((channel) => !isWithChannel(channel));
680
+ if (invalid.length > 0) {
681
+ process.stderr.write(ui.errorLine(`Unknown --with channel(s): ${invalid.join(", ")}.`));
682
+ process.stderr.write(ui.hint("Valid channels: telegram, slack, webhook, openaiApi, cron."));
683
+ return "invalid";
684
+ }
685
+ return args.withChannels.filter(isWithChannel);
686
+ }
687
+ function printInitResult(result, recipe) {
688
+ if (result.dryRun) {
689
+ process.stdout.write(ui.style.dim("Dry run — nothing was written.\n"));
690
+ }
691
+ const verb = result.dryRun ? "would create" : "created";
227
692
  for (const path of result.created) {
228
- process.stdout.write(`created ${path}\n`);
693
+ process.stdout.write(`${ui.badge("ok")}${ui.style.green(verb.padEnd(12))} ${path}\n`);
229
694
  }
230
695
  for (const path of result.skipped) {
231
- process.stdout.write(`kept ${path}\n`);
696
+ process.stdout.write(ui.style.dim(` kept ${path}`) + "\n");
232
697
  }
233
698
  if (result.knowledgeFiles.length > 0) {
234
- process.stdout.write(`\nIdentity references existing knowledge: ${result.knowledgeFiles.join(", ")}\n`);
699
+ process.stdout.write(`\nIdentity references existing knowledge: ${ui.style.cyan(result.knowledgeFiles.join(", "))}\n`);
700
+ }
701
+ if (recipe !== undefined) {
702
+ process.stdout.write(`\n${ui.style.bold("Recipe:")} ${ui.style.cyan(recipe.id)} ${ui.style.dim(`(risk: ${recipe.riskLevel})`)}\n`);
703
+ if (recipe.envExample !== undefined) {
704
+ process.stdout.write(ui.style.dim("Fill the secret placeholders in .env.example, then copy it to .env.\n"));
705
+ }
706
+ }
707
+ }
708
+ function printSecretsChecklist(secrets) {
709
+ process.stdout.write("\n" + ui.heading("Secrets checklist"));
710
+ if (secrets.length === 0) {
711
+ process.stdout.write(ui.style.dim("No secret recipe inputs were prompted. Review .env.example if the recipe created one.\n"));
712
+ return;
713
+ }
714
+ process.stdout.write(ui.style.dim("Copy .env.example to .env, then fill these variables. Secret values were not prompted or written to JSON.\n"));
715
+ for (const secret of secrets) {
716
+ const key = secret.envVar ?? secret.id;
717
+ process.stdout.write(` ${ui.style.bold(key)} ${ui.style.dim(`- ${secret.label}: ${secret.description}`)}\n`);
235
718
  }
236
- process.stdout.write("\nNext steps:\n" +
237
- ` 1. Edit ${result.configPath} (model, channels, skills, memory, sandbox).\n` +
238
- " 2. mono-agent validate\n" +
239
- " 3. mono-agent start\n");
240
- return 0;
719
+ }
720
+ function printNextSteps(configPath, recipe) {
721
+ const validateCmd = recipe === undefined ? "mono-agent validate" : `mono-agent validate --recipe ${recipe.id}`;
722
+ process.stdout.write("\n" +
723
+ ui.heading("Next steps") +
724
+ ` ${ui.style.bold("1.")} Edit ${configPath} ${ui.style.dim("(model, channels, skills, memory, sandbox)")}\n` +
725
+ ` ${ui.style.bold("2.")} ${validateCmd}\n` +
726
+ ` ${ui.style.bold("3.")} mono-agent start\n`);
241
727
  }
242
728
  async function runInstallSkill(args) {
243
729
  let result;
@@ -248,40 +734,287 @@ async function runInstallSkill(args) {
248
734
  });
249
735
  }
250
736
  catch (error) {
251
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
737
+ process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
252
738
  return 1;
253
739
  }
254
740
  for (const path of result.installed) {
255
- process.stdout.write(`installed ${path}\n`);
741
+ process.stdout.write(`${ui.badge("ok")}${ui.style.green("installed")} ${path}\n`);
256
742
  }
257
743
  return 0;
258
744
  }
259
745
  async function runValidate(args) {
260
- const cwd = process.cwd();
746
+ const context = resolveValidateContext(args, process.cwd());
261
747
  const report = await validateMonoAgentFolder({
262
748
  env: process.env,
263
- cwd,
264
- configPath: resolve(cwd, args.configPath ?? "mono-agent.config.json"),
749
+ cwd: context.cwd,
750
+ configPath: context.configPath,
751
+ allowFilesystemWrites: context.allowFilesystemWrites,
265
752
  });
266
753
  for (const section of report.sections) {
267
- process.stdout.write(`${statusIcon(section.status)} ${section.label}\n`);
268
- for (const detail of section.details) {
269
- process.stdout.write(` ${detail}\n`);
754
+ process.stdout.write(formatSection(section));
755
+ }
756
+ if (args.recipe !== undefined) {
757
+ const recipe = findRecipe(args.recipe);
758
+ if (recipe === undefined) {
759
+ process.stderr.write(ui.errorLine(`Unknown recipe \`${args.recipe}\`.`));
760
+ process.stderr.write(ui.hint("Run `mono-agent recipes list` to see available recipes."));
761
+ return 1;
270
762
  }
763
+ process.stdout.write(renderRecipeCompleteness(recipe, report));
271
764
  }
272
- process.stdout.write(report.ok ? "\nConfig is ready to start.\n" : "\nFix the errors above, then re-run mono-agent validate.\n");
765
+ process.stdout.write(report.ok
766
+ ? `\n${ui.style.green("✓ Config is ready to start.")}\n${ui.style.dim("Run `mono-agent config` for the full field-by-field view.")}\n`
767
+ : `\n${ui.hint("Fix the errors above, then re-run mono-agent validate.")}`);
768
+ process.stdout.write(ui.style.dim("Core sections activate by presence; channels need `enabled: true` — see docs/config (How sections activate).\n"));
273
769
  return report.ok ? 0 : 1;
274
770
  }
275
- function statusIcon(status) {
276
- switch (status) {
277
- case "ok":
278
- return "[ok] ";
279
- case "waiting":
280
- return "[waiting] ";
281
- case "disabled":
282
- return "[off] ";
283
- case "error":
284
- return "[error] ";
771
+ /**
772
+ * Capability-aware recipe check: for each capability the recipe promises, report
773
+ * whether the matching doctor section has reached the expected status. `waiting`
774
+ * stays non-fatal (it never changes the validate exit code) but is surfaced as
775
+ * "selected recipe incomplete" so the operator knows what is left to wire up.
776
+ */
777
+ function renderRecipeCompleteness(recipe, report) {
778
+ let out = "\n" + ui.heading(`Recipe: ${recipe.id}`);
779
+ let incomplete = 0;
780
+ for (const expectation of recipe.validateExpectations) {
781
+ const section = report.sections.find((entry) => entry.id === expectation.sectionId);
782
+ const status = section?.status ?? "missing";
783
+ const met = status === expectation.mustBe;
784
+ if (!met) {
785
+ incomplete += 1;
786
+ }
787
+ const badge = met ? ui.badge("ok") : ui.badge(status === "error" ? "error" : "waiting");
788
+ out += `${badge}${ui.style.bold(expectation.sectionId)} ${ui.style.dim(`(${status}, expected ${expectation.mustBe})`)}\n`;
789
+ if (!met && expectation.note !== undefined) {
790
+ out += ` ${ui.style.dim(expectation.note)}\n`;
791
+ }
792
+ }
793
+ out += incomplete === 0
794
+ ? `${ui.style.green(`✓ Recipe ${recipe.id} is fully configured.`)}\n`
795
+ : ui.style.yellow(`⚠ Selected recipe incomplete: ${incomplete} capability(ies) not yet live.\n`);
796
+ return out;
797
+ }
798
+ function riskColor(risk) {
799
+ if (risk === "high") {
800
+ return ui.style.red(risk);
801
+ }
802
+ if (risk === "medium") {
803
+ return ui.style.yellow(risk);
804
+ }
805
+ return ui.style.green(risk);
806
+ }
807
+ /** `mono-agent recipes list` — one line per recipe. */
808
+ export function renderRecipeList() {
809
+ let out = ui.banner("mono-agent", "recipes") + "\n";
810
+ for (const recipe of RECIPE_CATALOG) {
811
+ out += `${ui.style.bold(ui.style.cyan(recipe.id))} ${ui.style.dim(`[${riskColor(recipe.riskLevel)}]`)}\n`;
812
+ out += ` ${recipe.title}\n`;
813
+ out += ` ${ui.style.dim(recipe.tags.join(", "))}\n`;
814
+ }
815
+ out += "\n" + ui.style.dim("Scaffold one with: mono-agent init --recipe <id> [--dry-run] [--with slack,cron]\n");
816
+ return out;
817
+ }
818
+ /** `mono-agent recipes show <id>` — description, generated JSON, env example, checklist. */
819
+ export function renderRecipeShow(recipe) {
820
+ const inputs = resolveRecipeInputs(recipe);
821
+ let out = ui.banner("mono-agent", `recipe: ${recipe.id}`) + "\n";
822
+ out += `${ui.style.bold(recipe.title)} ${ui.style.dim(`(risk: ${riskColor(recipe.riskLevel)})`)}\n`;
823
+ out += `${recipe.description}\n`;
824
+ if (recipe.playbook !== undefined) {
825
+ out += ui.style.dim(`Playbook: docs/playbooks/${recipe.playbook}\n`);
826
+ }
827
+ out += "\n" + ui.heading("Generated mono-agent.config.json");
828
+ out += JSON.stringify(recipe.config(inputs), null, 2) + "\n";
829
+ const envExample = recipe.envExample?.(inputs);
830
+ if (envExample !== undefined && envExample.trim().length > 0) {
831
+ out += "\n" + ui.heading(".env.example");
832
+ out += envExample.endsWith("\n") ? envExample : envExample + "\n";
833
+ }
834
+ const files = recipe.files?.(inputs) ?? [];
835
+ if (files.length > 0) {
836
+ out += "\n" + ui.heading("Scaffolded files");
837
+ for (const file of files) {
838
+ out += ` ${ui.style.cyan(file.path)}\n`;
839
+ }
840
+ }
841
+ if (recipe.validateExpectations.length > 0) {
842
+ out += "\n" + ui.heading("Follow-up checklist");
843
+ for (const expectation of recipe.validateExpectations) {
844
+ const note = expectation.note === undefined ? "" : ` — ${expectation.note}`;
845
+ out += ` ${ui.style.gray("•")} ${expectation.sectionId} ${ui.style.dim(`must be ${expectation.mustBe}`)}${ui.style.dim(note)}\n`;
846
+ }
847
+ }
848
+ return out;
849
+ }
850
+ /** Dispatch `mono-agent recipes list|show <id>`. */
851
+ function runRecipes(args) {
852
+ const [sub, id] = args.positionals;
853
+ if (sub === undefined || sub === "list") {
854
+ process.stdout.write(renderRecipeList());
855
+ return 0;
856
+ }
857
+ if (sub === "show") {
858
+ if (id === undefined) {
859
+ process.stderr.write(ui.errorLine("Usage: mono-agent recipes show <id>."));
860
+ process.stderr.write(ui.hint(`Available: ${recipeIds().join(", ")}.`));
861
+ return 2;
862
+ }
863
+ const recipe = findRecipe(id);
864
+ if (recipe === undefined) {
865
+ process.stderr.write(ui.errorLine(`Unknown recipe \`${id}\`.`));
866
+ process.stderr.write(ui.hint("Run `mono-agent recipes list` to see available recipes."));
867
+ return 1;
868
+ }
869
+ process.stdout.write(renderRecipeShow(recipe));
870
+ return 0;
871
+ }
872
+ process.stderr.write(ui.errorLine(`Unknown recipes subcommand \`${sub}\`. Expected list or show.`));
873
+ return 2;
874
+ }
875
+ const SOURCE_TAG = {
876
+ env: ui.style.green("[env]"),
877
+ json: ui.style.cyan("[json]"),
878
+ default: ui.style.dim("[default]"),
879
+ };
880
+ /**
881
+ * Render the complete, source-annotated config view: every core section and
882
+ * field with its resolved value and whether it came from an env var, the JSON
883
+ * file, or the built-in default. This is the single discovery surface that
884
+ * replaced the old partial config panes.
885
+ */
886
+ export function renderConfigView(sections) {
887
+ let out = "";
888
+ for (const section of sections) {
889
+ const badgeStatus = section.status === "active" ? "ok" : "disabled";
890
+ out += `${ui.badge(badgeStatus)}${ui.style.bold(section.label)}\n`;
891
+ const width = section.fields.reduce((max, field) => Math.max(max, field.label.length), 0);
892
+ for (const field of section.fields) {
893
+ const tag = SOURCE_TAG[field.source];
894
+ const lock = field.redacted === true ? ` ${ui.style.dim("(secret)")}` : "";
895
+ const defaultRestatement = field.restatesDefault === true ? ` ${ui.style.dim("(same as default)")}` : "";
896
+ out += ` ${ui.style.gray(field.label.padEnd(width))} ${field.value}${lock}${defaultRestatement} ${tag}\n`;
897
+ }
898
+ }
899
+ return out;
900
+ }
901
+ /**
902
+ * `mono-agent config`: print the resolved configuration field-by-field with the
903
+ * source (env / json / default) of every value, then the channel summary. Read
904
+ * only — edits go in mono-agent.config.json and take effect on the next restart.
905
+ */
906
+ async function runConfig(args) {
907
+ const cwd = process.cwd();
908
+ const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
909
+ const env = process.env;
910
+ const jsonResult = await readMonoAgentConfigJson(configPath);
911
+ let config;
912
+ try {
913
+ config = await loadAppCoreConfig({ env, cwd, configPath });
914
+ }
915
+ catch (error) {
916
+ if (isAppCoreConfigError(error)) {
917
+ process.stderr.write(ui.errorLine(error.message));
918
+ if (jsonResult.missing) {
919
+ process.stderr.write(ui.hint(`No mono-agent config found at ${configPath}. Run \`mono-agent init\` to scaffold one.`));
920
+ }
921
+ else {
922
+ process.stderr.write(ui.hint("Fix the config above, then re-run `mono-agent config`."));
923
+ }
924
+ return 1;
925
+ }
926
+ throw error;
927
+ }
928
+ const sections = buildMonoAgentConfigView({
929
+ redacted: redactMonoAgentConfig(config),
930
+ json: jsonResult.json,
931
+ env,
932
+ });
933
+ const drivers = await resolveChannelDrivers({ env, cwd, configPath });
934
+ const channelViews = await collectChannelConfigViews(drivers, { env, cwd, configPath });
935
+ process.stdout.write(ui.banner("mono-agent", "resolved config") + "\n");
936
+ process.stdout.write(renderConfigView(sections));
937
+ if (channelViews.length > 0) {
938
+ process.stdout.write("\n" + ui.heading("Channels"));
939
+ process.stdout.write(renderConfigView(channelViews));
940
+ }
941
+ for (const warning of [
942
+ ...findUnknownAppConfigWarnings(jsonResult.json),
943
+ ...findJsonSecretConfigWarnings([...sections, ...channelViews]),
944
+ ...findRemovedConfigWarnings({ json: jsonResult.json, env }),
945
+ ]) {
946
+ process.stdout.write(`${ui.style.yellow(warning)}\n`);
947
+ }
948
+ const report = await validateMonoAgentFolder({ env, cwd, configPath, liveness: false, drivers });
949
+ const channels = report.sections.filter((section) => section.id.startsWith("channel:"));
950
+ if (channels.length > 0) {
951
+ process.stdout.write("\n" + ui.heading("Channel status"));
952
+ for (const section of channels) {
953
+ process.stdout.write(formatSection(section));
954
+ }
955
+ }
956
+ process.stdout.write("\n" + ui.style.dim("Source precedence: [env] > [json] > [default]. Edit mono-agent.config.json and run `mono-agent restart` to apply.") + "\n");
957
+ return 0;
958
+ }
959
+ /** Render one validation section: a status badge, a bold label, and its details. */
960
+ function formatSection(section) {
961
+ let out = `${ui.badge(section.status)}${ui.style.bold(section.label)}\n`;
962
+ for (const detail of section.details) {
963
+ out += ` ${colorDetail(section.status, detail)}\n`;
964
+ }
965
+ return out;
966
+ }
967
+ function colorDetail(status, detail) {
968
+ if (status === "error") {
969
+ return ui.style.red(detail);
970
+ }
971
+ if (detail.startsWith("[WARN]") || detail.startsWith("WARNING:")) {
972
+ return ui.style.yellow(detail);
973
+ }
974
+ return ui.style.dim(detail);
975
+ }
976
+ /**
977
+ * Gate for `start`/`restart`: refuse unless the directory has a present, valid
978
+ * config. First the config FILE must exist (env vars alone are not enough — a
979
+ * folder without a config is not a configured agent). Then run the structural
980
+ * validation with `liveness:false` (network probes only yield `waiting`, never
981
+ * `error`, so skipping them keeps the verdict but avoids ~6s of timeouts) and
982
+ * refuse on any `error` section. `waiting` (e.g. Ollama/Phoenix not up yet) is
983
+ * runtime-soft and never blocks.
984
+ */
985
+ export async function ensureStartable(args) {
986
+ const cwd = process.cwd();
987
+ const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
988
+ if (!(await pathExists(configPath))) {
989
+ return { ok: false, code: 2, kind: "missing-config", configPath };
990
+ }
991
+ const report = await validateMonoAgentFolder({ env: process.env, cwd, configPath, liveness: false });
992
+ if (!report.ok) {
993
+ return { ok: false, code: 1, kind: "validation", report };
994
+ }
995
+ return { ok: true };
996
+ }
997
+ function printPreflightFailure(result) {
998
+ if (result.kind === "missing-config") {
999
+ process.stderr.write(ui.errorLine(`No mono-agent config found at ${result.configPath}.`));
1000
+ process.stderr.write(ui.hint("Run `mono-agent init` to scaffold one, or pass --config <path>."));
1001
+ return;
1002
+ }
1003
+ process.stderr.write(ui.heading("Cannot start: config has errors"));
1004
+ for (const section of result.report.sections) {
1005
+ if (section.status === "error") {
1006
+ process.stderr.write(formatSection(section));
1007
+ }
1008
+ }
1009
+ process.stderr.write(ui.hint("Run `mono-agent validate` for the full report, fix the errors, then retry."));
1010
+ }
1011
+ async function pathExists(path) {
1012
+ try {
1013
+ await stat(path);
1014
+ return true;
1015
+ }
1016
+ catch {
1017
+ return false;
285
1018
  }
286
1019
  }
287
1020
  async function runStart(args) {
@@ -292,33 +1025,47 @@ async function runStart(args) {
292
1025
  }
293
1026
  /**
294
1027
  * The blocking worker: builds the responder, starts every configured channel
295
- * plus the operator console and traceability, and stays alive until a signal.
296
- * This is what launchd invokes (via `start --foreground`) and what users get
297
- * with `--foreground`/`-f`.
1028
+ * plus traceability, and stays alive until a signal. This is what launchd
1029
+ * invokes (via `start --foreground`) and what users get with `--foreground`/`-f`.
298
1030
  */
299
1031
  async function runForeground(args) {
1032
+ const pre = await ensureStartable(args);
1033
+ if (!pre.ok) {
1034
+ printPreflightFailure(pre);
1035
+ return pre.code;
1036
+ }
300
1037
  const app = await startMonoAgentApp({
301
1038
  cwd: process.cwd(),
302
1039
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
303
- ...(args.port === undefined ? {} : { operatorConsolePort: args.port }),
304
- ...(args.noConsole ? { operatorConsole: false } : {}),
305
1040
  logger: consoleLogger(),
306
1041
  });
307
- printAppStatus(app);
308
- installSignalHandlers(app);
309
- return 0;
1042
+ await printAppStatus(app);
1043
+ // Block until a shutdown signal. Returning here (the old behavior) let the
1044
+ // process exit immediately whenever no channel owned a live handle — e.g. a
1045
+ // traceability-only config, now that the operator console is retired and the
1046
+ // trace heartbeat timer is unref'd.
1047
+ return await waitForShutdownSignal(app);
310
1048
  }
311
1049
  async function runBackgroundCommand(args, command) {
312
1050
  const guard = requireDarwin(command);
313
1051
  if (guard !== undefined) {
314
1052
  return guard;
315
1053
  }
1054
+ // Refuse to launch (or relaunch) an unconfigured/broken folder BEFORE writing
1055
+ // the plist and bootstrapping launchctl — otherwise the worker would crash and
1056
+ // launchd's KeepAlive would retry it forever. stop/status/logs stay ungated so
1057
+ // a broken instance can still be inspected and torn down.
1058
+ if (command === "start" || command === "restart") {
1059
+ const pre = await ensureStartable(args);
1060
+ if (!pre.ok) {
1061
+ printPreflightFailure(pre);
1062
+ return pre.code;
1063
+ }
1064
+ }
316
1065
  const target = await resolveInstanceTarget({
317
1066
  args: {
318
1067
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
319
1068
  ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
320
- ...(args.port === undefined ? {} : { port: args.port }),
321
- noConsole: args.noConsole,
322
1069
  },
323
1070
  env: process.env,
324
1071
  cwd: process.cwd(),
@@ -329,7 +1076,7 @@ async function runBackgroundCommand(args, command) {
329
1076
  case "start":
330
1077
  return await startBackground(target, deps);
331
1078
  case "restart":
332
- return await restartBackground(target, deps);
1079
+ return args.force ? await runForceRestart(target, deps) : await restartBackground(target, deps);
333
1080
  case "stop":
334
1081
  return await stopBackground(target, deps);
335
1082
  case "status":
@@ -338,6 +1085,27 @@ async function runBackgroundCommand(args, command) {
338
1085
  return await tailLogs(target, deps, { follow: args.follow, lines: args.lines ?? DEFAULT_LOG_LINES });
339
1086
  }
340
1087
  }
1088
+ /**
1089
+ * `restart --force`: stop the worker, purge its persisted pi-session store, then
1090
+ * start fresh. Stopping first guarantees the worker is not writing sessions while
1091
+ * they are deleted; the runtime recreates the store on the next session, and the
1092
+ * agent's durable memory lives elsewhere, so only resumable transcripts are dropped.
1093
+ */
1094
+ async function runForceRestart(target, deps) {
1095
+ const stopCode = await stopBackground(target, deps);
1096
+ if (stopCode !== 0) {
1097
+ return stopCode;
1098
+ }
1099
+ const result = await purgeSessions({ env: process.env, cwd: target.cwd, configPath: target.configPath });
1100
+ if (result.removed) {
1101
+ const count = result.files === 0 ? "" : ` (${result.files} session file${result.files === 1 ? "" : "s"})`;
1102
+ process.stdout.write(`${ui.badge("ok")}${ui.style.bold("Cleared persisted sessions")}${count}.\n`);
1103
+ }
1104
+ else {
1105
+ process.stdout.write(ui.style.dim("No persisted sessions to clear (in-memory or none on disk).") + "\n");
1106
+ }
1107
+ return await startBackground(target, deps);
1108
+ }
341
1109
  /**
342
1110
  * Background service mode is launchd-specific. On other platforms point the
343
1111
  * user at the still-supported blocking foreground path.
@@ -346,22 +1114,111 @@ function requireDarwin(command) {
346
1114
  if (process.platform === "darwin") {
347
1115
  return undefined;
348
1116
  }
349
- process.stderr.write(`Background service mode (mono-agent ${command}) requires macOS (launchd).\n` +
350
- "Run `mono-agent start --foreground` to run in the foreground on this platform.\n");
1117
+ process.stderr.write(ui.errorLine(`Background service mode (mono-agent ${command}) requires macOS (launchd).`));
1118
+ process.stderr.write(ui.hint("Run `mono-agent start --foreground` to run in the foreground on this platform."));
351
1119
  return 1;
352
1120
  }
353
- function printAppStatus(app) {
354
- if (app.operatorConsole !== undefined) {
355
- process.stdout.write(`operator console ${app.operatorConsole.appUrl}\n`);
356
- }
357
- process.stdout.write(`config ${app.configPath}\n`);
1121
+ export async function printAppStatus(app, options = {}) {
358
1122
  const trace = app.traceabilityStatus;
359
- process.stdout.write(trace.kind === "running"
360
- ? `traceability running (source ${trace.sourceId})\n`
361
- : `traceability ${trace.kind}: ${trace.reason}\n`);
362
- for (const [id, status] of app.channelStatuses()) {
363
- process.stdout.write(`${id.padEnd(17)} ${describeChannelStatus(status)}\n`);
1123
+ process.stdout.write(ui.rule("instance"));
1124
+ process.stdout.write(ui.keyValue([
1125
+ ["config", app.configPath],
1126
+ [
1127
+ "traceability",
1128
+ trace.kind === "running" ? `running (source ${trace.sourceId})` : `${trace.kind}: ${trace.reason}`,
1129
+ ],
1130
+ ], 2));
1131
+ const artifactDir = app.traceabilityStatus.kind === "running" ? app.traceabilityStatus.artifactDir : undefined;
1132
+ process.stdout.write(ui.rule("sandbox"));
1133
+ process.stdout.write(` ${describeSandboxStatus(app.sandboxStatus)}\n`);
1134
+ process.stdout.write(ui.rule("observability"));
1135
+ process.stdout.write(` ${describeExporter(app.exporterStatus, artifactDir)}\n`);
1136
+ const channels = [...app.channelStatuses()];
1137
+ if (channels.length > 0) {
1138
+ process.stdout.write(ui.rule("channels"));
1139
+ for (const [id, status] of channels) {
1140
+ process.stdout.write(` ${ui.channelBadge(status.kind)}${ui.style.bold(id.padEnd(11))} ${describeChannelStatus(status)}\n`);
1141
+ }
1142
+ }
1143
+ await writeAppRunsHealthDetail(app, options);
1144
+ }
1145
+ async function writeAppRunsHealthDetail(app, options) {
1146
+ const artifactDir = app.traceabilityStatus.kind === "running" ? app.traceabilityStatus.artifactDir : undefined;
1147
+ if (artifactDir === undefined || artifactDir.trim().length === 0) {
1148
+ return;
1149
+ }
1150
+ const reader = options.listRecordedRuns ?? listRecordedRuns;
1151
+ let result;
1152
+ try {
1153
+ result = await reader({ artifactDir, maxRuns: RUNS_HEALTH_MAX_RUNS, scope: "agent" });
1154
+ }
1155
+ catch (error) {
1156
+ result = {
1157
+ totalRuns: 0,
1158
+ runs: [],
1159
+ warnings: [`Unable to read run summaries: ${reasonOf(error)}`],
1160
+ };
1161
+ }
1162
+ const display = buildRunsHealthDisplay({
1163
+ artifactDir,
1164
+ totalRuns: result.totalRuns,
1165
+ runs: result.runs,
1166
+ warnings: result.warnings,
1167
+ includeSelectedSkills: true,
1168
+ runOwnerAlive: true,
1169
+ maxRuns: RUNS_HEALTH_MAX_RUNS,
1170
+ ...(app.selectedSkills === undefined ? {} : { selectedSkills: app.selectedSkills }),
1171
+ ...(options.nowMs === undefined ? {} : { nowMs: options.nowMs }),
1172
+ });
1173
+ process.stdout.write(ui.rule("runs health"));
1174
+ for (const detail of display.details) {
1175
+ process.stdout.write(` ${detail}\n`);
1176
+ }
1177
+ }
1178
+ function reasonOf(error) {
1179
+ return error instanceof Error ? error.message : String(error);
1180
+ }
1181
+ function describeExporter(status, artifactDir) {
1182
+ if (status.kind !== "configured") {
1183
+ return `${status.kind}: ${status.reason}`;
1184
+ }
1185
+ const parts = [`phoenix ${status.endpoint}`];
1186
+ const appUrl = phoenixAppBaseUrl(status.endpoint);
1187
+ if (appUrl !== undefined) {
1188
+ parts.push(`app ${appUrl}`);
1189
+ }
1190
+ if (status.includeSensitiveData) {
1191
+ parts.push(ui.style.yellow(describeSensitiveDataExportWarning(status.endpoint)));
1192
+ }
1193
+ if (status.lastWarning !== undefined) {
1194
+ parts.push(`last warning: ${status.lastWarning}`);
1195
+ }
1196
+ if (status.lastError !== undefined) {
1197
+ parts.push(`last error: ${status.lastError}`);
1198
+ }
1199
+ parts.push(artifactDir === undefined
1200
+ ? "JSONL artifacts remain local"
1201
+ : `JSONL artifacts remain local at ${artifactDir}`);
1202
+ return parts.join("; ");
1203
+ }
1204
+ function describeSandboxStatus(status) {
1205
+ const engineAvailability = status.engineAvailable === true
1206
+ ? "present"
1207
+ : status.engineAvailable === false
1208
+ ? "absent"
1209
+ : "not checked";
1210
+ const parts = [
1211
+ `effective: ${status.effective}`,
1212
+ `engine: ${status.engine ?? "none"} (${engineAvailability})`,
1213
+ ...(status.fallback === undefined ? [] : [`fallback: ${status.fallback}`]),
1214
+ `fallback active: ${status.fallbackActive ? "yes" : "no"}`,
1215
+ status.detail,
1216
+ ];
1217
+ const warning = status.warning ?? sandboxEffectiveStateWarning(status);
1218
+ if (warning !== undefined) {
1219
+ parts.push(ui.style.yellow(warning));
364
1220
  }
1221
+ return parts.join("; ");
365
1222
  }
366
1223
  function describeChannelStatus(status) {
367
1224
  if (status.kind === "running") {
@@ -372,19 +1229,34 @@ function describeChannelStatus(status) {
372
1229
  }
373
1230
  return `${status.kind}: ${status.reason}`;
374
1231
  }
375
- function installSignalHandlers(app) {
376
- let stopping = false;
377
- const stop = async (signal) => {
378
- if (stopping) {
379
- return;
380
- }
381
- stopping = true;
382
- process.stdout.write(`\nReceived ${signal}; stopping mono agent app...\n`);
383
- await app.stop();
384
- process.exit(0);
385
- };
386
- process.on("SIGINT", () => void stop("SIGINT"));
387
- process.on("SIGTERM", () => void stop("SIGTERM"));
1232
+ /**
1233
+ * Block the foreground process until SIGINT/SIGTERM, then stop the app and
1234
+ * resolve the exit code. A referenced no-op timer owns the event loop so the
1235
+ * process stays alive even with no channel handle (signal listeners alone do
1236
+ * NOT keep Node running, and the trace heartbeat is unref'd). Cleared on stop so
1237
+ * the loop drains cleanly without a forceful `process.exit`. Exported for tests.
1238
+ */
1239
+ export function waitForShutdownSignal(app) {
1240
+ return new Promise((resolve) => {
1241
+ const keepAlive = setInterval(() => { }, KEEP_ALIVE_INTERVAL_MS);
1242
+ let stopping = false;
1243
+ const onSignal = (signal) => {
1244
+ if (stopping) {
1245
+ return;
1246
+ }
1247
+ stopping = true;
1248
+ process.off("SIGINT", onSignal);
1249
+ process.off("SIGTERM", onSignal);
1250
+ clearInterval(keepAlive);
1251
+ void (async () => {
1252
+ process.stdout.write("\n" + ui.hint(`Received ${signal}; stopping mono agent app…`));
1253
+ await app.stop();
1254
+ resolve(0);
1255
+ })();
1256
+ };
1257
+ process.on("SIGINT", onSignal);
1258
+ process.on("SIGTERM", onSignal);
1259
+ });
388
1260
  }
389
1261
  function consoleLogger() {
390
1262
  return {
@@ -409,7 +1281,7 @@ if (isDirectCliInvocation) {
409
1281
  }
410
1282
  })
411
1283
  .catch((error) => {
412
- process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
1284
+ process.stderr.write(`${ui.style.red("✗")} ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
413
1285
  process.exitCode = 1;
414
1286
  });
415
1287
  }