@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.
- package/README.md +24 -10
- package/dist/adapter-send-tools-main.d.ts +3 -0
- package/dist/adapter-send-tools-main.d.ts.map +1 -0
- package/dist/adapter-send-tools-main.js +20 -0
- package/dist/adapter-send-tools-main.js.map +1 -0
- package/dist/adapter-send-tools.d.ts +125 -0
- package/dist/adapter-send-tools.d.ts.map +1 -0
- package/dist/adapter-send-tools.js +629 -0
- package/dist/adapter-send-tools.js.map +1 -0
- package/dist/app-config.d.ts +86 -26
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +284 -90
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +51 -20
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +889 -119
- package/dist/app.js.map +1 -1
- package/dist/artifact-retention.d.ts +32 -0
- package/dist/artifact-retention.d.ts.map +1 -0
- package/dist/artifact-retention.js +111 -0
- package/dist/artifact-retention.js.map +1 -0
- package/dist/audit-runs.d.ts +12 -0
- package/dist/audit-runs.d.ts.map +1 -0
- package/dist/audit-runs.js +68 -0
- package/dist/audit-runs.js.map +1 -0
- package/dist/backfill.d.ts +75 -0
- package/dist/backfill.d.ts.map +1 -0
- package/dist/backfill.js +350 -0
- package/dist/backfill.js.map +1 -0
- package/dist/background.d.ts +8 -5
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +279 -53
- package/dist/background.js.map +1 -1
- package/dist/broadcast-recorder.d.ts +27 -0
- package/dist/broadcast-recorder.d.ts.map +1 -0
- package/dist/broadcast-recorder.js +82 -0
- package/dist/broadcast-recorder.js.map +1 -0
- package/dist/channel-config-view.d.ts +28 -0
- package/dist/channel-config-view.d.ts.map +1 -0
- package/dist/channel-config-view.js +79 -0
- package/dist/channel-config-view.js.map +1 -0
- package/dist/channel-gate.d.ts +22 -0
- package/dist/channel-gate.d.ts.map +1 -0
- package/dist/channel-gate.js +35 -0
- package/dist/channel-gate.js.map +1 -0
- package/dist/channel-plugins.d.ts +39 -0
- package/dist/channel-plugins.d.ts.map +1 -0
- package/dist/channel-plugins.js +346 -0
- package/dist/channel-plugins.js.map +1 -0
- package/dist/channels.d.ts +62 -66
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +850 -120
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +103 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +984 -112
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts +24 -0
- package/dist/config-reference.d.ts.map +1 -0
- package/dist/config-reference.js +580 -0
- package/dist/config-reference.js.map +1 -0
- package/dist/configured-agent.d.ts +104 -0
- package/dist/configured-agent.d.ts.map +1 -0
- package/dist/configured-agent.js +578 -0
- package/dist/configured-agent.js.map +1 -0
- package/dist/consumer-contract.d.ts +36 -0
- package/dist/consumer-contract.d.ts.map +1 -0
- package/dist/consumer-contract.js +290 -0
- package/dist/consumer-contract.js.map +1 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +392 -95
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +23 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +91 -26
- package/dist/init.js.map +1 -1
- package/dist/install-skill.d.ts +1 -1
- package/dist/install-skill.js +2 -2
- package/dist/install-skill.js.map +1 -1
- package/dist/interaction-bridge.d.ts +72 -0
- package/dist/interaction-bridge.d.ts.map +1 -0
- package/dist/interaction-bridge.js +354 -0
- package/dist/interaction-bridge.js.map +1 -0
- package/dist/launchd.d.ts +0 -2
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +0 -2
- package/dist/launchd.js.map +1 -1
- package/dist/memory-command.d.ts +10 -0
- package/dist/memory-command.d.ts.map +1 -0
- package/dist/memory-command.js +649 -0
- package/dist/memory-command.js.map +1 -0
- package/dist/memory-recall-main.d.ts +3 -0
- package/dist/memory-recall-main.d.ts.map +1 -0
- package/dist/memory-recall-main.js +30 -0
- package/dist/memory-recall-main.js.map +1 -0
- package/dist/memory-recall.d.ts +147 -0
- package/dist/memory-recall.d.ts.map +1 -0
- package/dist/memory-recall.js +345 -0
- package/dist/memory-recall.js.map +1 -0
- package/dist/memory-rituals.d.ts +8 -11
- package/dist/memory-rituals.d.ts.map +1 -1
- package/dist/memory-rituals.js +26 -25
- package/dist/memory-rituals.js.map +1 -1
- package/dist/metrics.d.ts +13 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +105 -0
- package/dist/metrics.js.map +1 -0
- package/dist/notify-destinations.d.ts +23 -0
- package/dist/notify-destinations.d.ts.map +1 -0
- package/dist/notify-destinations.js +79 -0
- package/dist/notify-destinations.js.map +1 -0
- package/dist/posted-message-index.d.ts +61 -0
- package/dist/posted-message-index.d.ts.map +1 -0
- package/dist/posted-message-index.js +178 -0
- package/dist/posted-message-index.js.map +1 -0
- package/dist/proactive-notify.d.ts +32 -0
- package/dist/proactive-notify.d.ts.map +1 -0
- package/dist/proactive-notify.js +62 -0
- package/dist/proactive-notify.js.map +1 -0
- package/dist/recipes/base.d.ts +15 -0
- package/dist/recipes/base.d.ts.map +1 -0
- package/dist/recipes/base.js +51 -0
- package/dist/recipes/base.js.map +1 -0
- package/dist/recipes/catalog.d.ts +4 -0
- package/dist/recipes/catalog.d.ts.map +1 -0
- package/dist/recipes/catalog.js +509 -0
- package/dist/recipes/catalog.js.map +1 -0
- package/dist/recipes/index.d.ts +11 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +14 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/recipes/types.d.ts +70 -0
- package/dist/recipes/types.d.ts.map +1 -0
- package/dist/recipes/types.js +15 -0
- package/dist/recipes/types.js.map +1 -0
- package/dist/request-model-override.d.ts +67 -0
- package/dist/request-model-override.d.ts.map +1 -0
- package/dist/request-model-override.js +96 -0
- package/dist/request-model-override.js.map +1 -0
- package/dist/runs-health.d.ts +22 -0
- package/dist/runs-health.d.ts.map +1 -0
- package/dist/runs-health.js +144 -0
- package/dist/runs-health.js.map +1 -0
- package/dist/seen-conversations.d.ts +22 -0
- package/dist/seen-conversations.d.ts.map +1 -0
- package/dist/seen-conversations.js +120 -0
- package/dist/seen-conversations.js.map +1 -0
- package/dist/sessions.d.ts +21 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +45 -0
- package/dist/sessions.js.map +1 -0
- package/dist/setup.d.ts +29 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +97 -0
- package/dist/setup.js.map +1 -0
- package/dist/trigger-overrides.d.ts +17 -0
- package/dist/trigger-overrides.d.ts.map +1 -0
- package/dist/trigger-overrides.js +30 -0
- package/dist/trigger-overrides.js.map +1 -0
- package/dist/tui-command.d.ts +56 -0
- package/dist/tui-command.d.ts.map +1 -0
- package/dist/tui-command.js +160 -0
- package/dist/tui-command.js.map +1 -0
- package/dist/ui.d.ts +54 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +157 -0
- package/dist/ui.js.map +1 -0
- package/dist/web-command.d.ts +39 -0
- package/dist/web-command.d.ts.map +1 -0
- package/dist/web-command.js +151 -0
- package/dist/web-command.js.map +1 -0
- package/package.json +22 -17
- package/schema/mono-agent.config.schema.json +1686 -0
- package/skills/mono-agent-composer/SKILL.md +38 -12
- package/skills/mono-agent-composer/agents/openai.yaml +4 -0
- package/skills/mono-agent-composer/references/config-blueprint.md +84 -31
- package/skills/mono-agent-composer/references/discovery-questions.md +49 -36
- package/skills/mono-agent-composer/references/feature-coverage.md +26 -19
- package/skills/mono-agent-composer/references/package-map.md +20 -22
- package/skills/mono-agent-composer/references/playbooks.md +209 -0
- 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",
|
|
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
|
-
|
|
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 >
|
|
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
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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(
|
|
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(
|
|
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
|
|
746
|
+
const context = resolveValidateContext(args, process.cwd());
|
|
261
747
|
const report = await validateMonoAgentFolder({
|
|
262
748
|
env: process.env,
|
|
263
|
-
cwd,
|
|
264
|
-
configPath:
|
|
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(
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
296
|
-
*
|
|
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
|
-
|
|
309
|
-
|
|
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)
|
|
350
|
-
|
|
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(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
}
|