@mono-agent/agent-app 0.6.1 → 0.8.0
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 +210 -17
- package/dist/adapter-send-proxy.d.ts +18 -0
- package/dist/adapter-send-proxy.d.ts.map +1 -0
- package/dist/adapter-send-proxy.js +229 -0
- package/dist/adapter-send-proxy.js.map +1 -0
- package/dist/adapter-send-tools-main.js +62 -4
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +41 -8
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +563 -108
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +8 -0
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +532 -134
- package/dist/app.js.map +1 -1
- package/dist/broadcast-recorder.d.ts.map +1 -1
- package/dist/broadcast-recorder.js +24 -2
- package/dist/broadcast-recorder.js.map +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +18 -7
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +77 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2106 -99
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +195 -6
- package/dist/config-reference.js.map +1 -1
- package/dist/configuration-proposal-main.d.ts +3 -0
- package/dist/configuration-proposal-main.d.ts.map +1 -0
- package/dist/configuration-proposal-main.js +13 -0
- package/dist/configuration-proposal-main.js.map +1 -0
- package/dist/configuration-proposal-tool.d.ts +26 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -0
- package/dist/configuration-proposal-tool.js +139 -0
- package/dist/configuration-proposal-tool.js.map +1 -0
- package/dist/configured-agent.d.ts +12 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +150 -18
- package/dist/configured-agent.js.map +1 -1
- package/dist/consumer-contract.d.ts +3 -0
- package/dist/consumer-contract.d.ts.map +1 -1
- package/dist/consumer-contract.js +44 -1
- package/dist/consumer-contract.js.map +1 -1
- package/dist/doctor.d.ts +27 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1198 -96
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-managed-memory.d.ts +39 -0
- package/dist/first-run-managed-memory.d.ts.map +1 -0
- package/dist/first-run-managed-memory.js +521 -0
- package/dist/first-run-managed-memory.js.map +1 -0
- package/dist/first-run-readiness.d.ts +111 -0
- package/dist/first-run-readiness.d.ts.map +1 -0
- package/dist/first-run-readiness.js +757 -0
- package/dist/first-run-readiness.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +88 -3
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1212 -23
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +32 -2
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +309 -28
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/local-configuration.d.ts +56 -0
- package/dist/local-configuration.d.ts.map +1 -0
- package/dist/local-configuration.js +914 -0
- package/dist/local-configuration.js.map +1 -0
- package/dist/memory-command.d.ts +4 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +751 -27
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-recall.d.ts +38 -10
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +82 -25
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-retrieval.d.ts +66 -0
- package/dist/memory-retrieval.d.ts.map +1 -0
- package/dist/memory-retrieval.js +332 -0
- package/dist/memory-retrieval.js.map +1 -0
- package/dist/modules/base.d.ts +6 -5
- package/dist/modules/base.d.ts.map +1 -1
- package/dist/modules/base.js +11 -7
- package/dist/modules/base.js.map +1 -1
- package/dist/modules/catalog.d.ts.map +1 -1
- package/dist/modules/catalog.js +54 -20
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/index.d.ts +2 -2
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/known-tools.d.ts +3 -0
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +11 -1
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/modules/types.d.ts +20 -6
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/package-version.d.ts +3 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +14 -0
- package/dist/package-version.js.map +1 -0
- package/dist/pi-auth-store-inspection.d.ts +21 -0
- package/dist/pi-auth-store-inspection.d.ts.map +1 -0
- package/dist/pi-auth-store-inspection.js +120 -0
- package/dist/pi-auth-store-inspection.js.map +1 -0
- package/dist/pi-oauth-login-main.d.ts +3 -0
- package/dist/pi-oauth-login-main.d.ts.map +1 -0
- package/dist/pi-oauth-login-main.js +14 -0
- package/dist/pi-oauth-login-main.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +22 -0
- package/dist/pi-oauth-login.d.ts.map +1 -0
- package/dist/pi-oauth-login.js +79 -0
- package/dist/pi-oauth-login.js.map +1 -0
- package/dist/project-skills.d.ts +40 -0
- package/dist/project-skills.d.ts.map +1 -0
- package/dist/project-skills.js +573 -0
- package/dist/project-skills.js.map +1 -0
- package/dist/provider-setup.d.ts +136 -5
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +1552 -33
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +12 -0
- package/dist/readiness-probe-worker.d.ts.map +1 -0
- package/dist/readiness-probe-worker.js +276 -0
- package/dist/readiness-probe-worker.js.map +1 -0
- package/dist/readiness-probe.d.ts +100 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +780 -0
- package/dist/readiness-probe.js.map +1 -0
- package/dist/request-model-override.d.ts +43 -1
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +178 -19
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +32 -0
- package/dist/run-history.d.ts.map +1 -0
- package/dist/run-history.js +859 -0
- package/dist/run-history.js.map +1 -0
- package/dist/runs-health.d.ts.map +1 -1
- package/dist/runs-health.js +12 -4
- package/dist/runs-health.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +14 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -0
- package/dist/runtime-option-extensions.js +98 -0
- package/dist/runtime-option-extensions.js.map +1 -0
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -0
- package/dist/runtime-routes.js +13 -0
- package/dist/runtime-routes.js.map +1 -0
- package/dist/sandbox-manager.d.ts +69 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +856 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/supermemory-plugin.d.ts +54 -0
- package/dist/supermemory-plugin.d.ts.map +1 -0
- package/dist/supermemory-plugin.js +136 -0
- package/dist/supermemory-plugin.js.map +1 -0
- package/dist/tui-command.d.ts +15 -0
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +46 -6
- package/dist/tui-command.js.map +1 -1
- package/dist/web-command.d.ts +8 -0
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +174 -17
- package/dist/web-command.js.map +1 -1
- package/dist/wizard/answers.d.ts +31 -4
- package/dist/wizard/answers.d.ts.map +1 -1
- package/dist/wizard/answers.js +145 -16
- package/dist/wizard/answers.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +7 -0
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +36 -2
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +4 -4
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +2 -2
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/model-discovery.d.ts +38 -3
- package/dist/wizard/model-discovery.d.ts.map +1 -1
- package/dist/wizard/model-discovery.js +927 -102
- package/dist/wizard/model-discovery.js.map +1 -1
- package/dist/wizard/presets.d.ts +1 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +7 -12
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts +66 -11
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +190 -20
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.d.ts +60 -3
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +1161 -145
- package/dist/wizard/run.js.map +1 -1
- package/package.json +21 -16
- package/resources/srt/package-lock.json +66 -0
- package/resources/srt/package.json +9 -0
- package/schema/mono-agent.config.schema.json +381 -17
- package/skills/mono-agent-composer/SKILL.md +4 -4
- package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
- package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
- package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
- package/skills/mono-agent-composer/references/package-map.md +1 -1
- package/skills/mono-agent-composer/references/playbooks.md +8 -7
- package/skills/mono-agent-composer/references/validation.md +1 -1
package/dist/wizard/run.js
CHANGED
|
@@ -1,15 +1,113 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
4
|
import * as p from "@clack/prompts";
|
|
5
|
+
import { parseMonoRuntimeModelReference } from "@mono-agent/runtime-adapter";
|
|
4
6
|
import { findModule } from "../modules/catalog.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { hasSensitivePersistedEnvironmentValue } from "../first-run-readiness.js";
|
|
8
|
+
import { DEFAULT_MODEL } from "../modules/base.js";
|
|
9
|
+
import { ALLOW_ALL_TOOLS, APP_TOOL_NAMES, BUILTIN_TOOL_NAMES, isAllowAllTools } from "../modules/known-tools.js";
|
|
10
|
+
import { hasDurableProviderEnvironmentCredential, isProviderSetupPiApiKeyAction, planProviderSetup, providerSetupActionCommandLine, } from "../provider-setup.js";
|
|
11
|
+
import { isSupermemoryPluginInstalled } from "../supermemory-plugin.js";
|
|
12
|
+
import { alwaysOnTools, composeWizardPlan, defaultAnswers, effectiveFallbacks, humanizeAgentName, recommendedToolSelection, referencedSetupModelRefs, } from "./answers.js";
|
|
8
13
|
import { findPreset, presetAnswers } from "./presets.js";
|
|
9
|
-
import { channelSelectOptions, effortSelectOptions,
|
|
10
|
-
import { discoverWizardModelCandidates, formatModelDiscoveryStatus } from "./model-discovery.js";
|
|
11
|
-
const LOCAL_PROVIDER_MODEL = /^pi:(?:ollama|lmstudio):/u;
|
|
14
|
+
import { assertConcreteWizardModelRef, channelSelectOptions, creationReviewOptions, CUSTOM_PI_MODEL_OPTION, effortSelectOptions, fallbackModelSelectOptions, formatRouteSafetyMatrix, isMixedRouteChain, memorySelectOptions, MODEL_AUTOCOMPLETE_MAX_ITEMS, modelSelectOptions, piModelSelectOptions, presetSelectOptions, ROUTE_SAFETY_OPTIONS, toolMultiselectOptions, validateWizardAgentName, validateWizardAgentPurpose, wizardCancelIntentForKey, WizardBack, WizardCancelled, WizardExitRequested, } from "./prompts.js";
|
|
15
|
+
import { discoverWizardModelCandidates, formatModelDiscoveryStatus, guidedPiProviderProblem, } from "./model-discovery.js";
|
|
12
16
|
const SANDBOXABLE_TOOLS = new Set(["Bash", "Write", "Edit"]);
|
|
17
|
+
/**
|
|
18
|
+
* Clack intentionally coalesces Escape and Ctrl-C into one cancel sentinel. The
|
|
19
|
+
* wizard owns a scoped keypress observer so the state machine can interpret
|
|
20
|
+
* Escape as Back and Ctrl-C as Exit without changing Clack globally.
|
|
21
|
+
*/
|
|
22
|
+
async function runPrompt(invoke) {
|
|
23
|
+
let intent;
|
|
24
|
+
const input = process.stdin;
|
|
25
|
+
const listener = (_text, key) => {
|
|
26
|
+
intent = wizardCancelIntentForKey(key) ?? intent;
|
|
27
|
+
};
|
|
28
|
+
if (input.isTTY) {
|
|
29
|
+
emitKeypressEvents(input);
|
|
30
|
+
input.on("keypress", listener);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const value = await invoke();
|
|
34
|
+
if (!p.isCancel(value))
|
|
35
|
+
return value;
|
|
36
|
+
if (intent === "back")
|
|
37
|
+
throw new WizardBack();
|
|
38
|
+
throw new WizardExitRequested();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (input.isTTY)
|
|
42
|
+
input.off("keypress", listener);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function discoverModelsInterruptibly(options) {
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
let intent;
|
|
48
|
+
const input = process.stdin;
|
|
49
|
+
const wasRaw = input.isRaw === true;
|
|
50
|
+
const wasFlowing = input.readableFlowing;
|
|
51
|
+
const interrupt = (nextIntent) => {
|
|
52
|
+
intent ??= nextIntent;
|
|
53
|
+
controller.abort();
|
|
54
|
+
};
|
|
55
|
+
const onKeypress = (_text, key) => {
|
|
56
|
+
const nextIntent = wizardCancelIntentForKey(key);
|
|
57
|
+
if (nextIntent !== undefined)
|
|
58
|
+
interrupt(nextIntent);
|
|
59
|
+
};
|
|
60
|
+
const onSigint = () => interrupt("exit");
|
|
61
|
+
if (input.isTTY) {
|
|
62
|
+
emitKeypressEvents(input);
|
|
63
|
+
input.setRawMode?.(true);
|
|
64
|
+
input.resume();
|
|
65
|
+
input.on("keypress", onKeypress);
|
|
66
|
+
}
|
|
67
|
+
process.on("SIGINT", onSigint);
|
|
68
|
+
try {
|
|
69
|
+
const result = await discoverWizardModelCandidates({
|
|
70
|
+
...options,
|
|
71
|
+
abortSignal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
if (!controller.signal.aborted)
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!controller.signal.aborted)
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
process.off("SIGINT", onSigint);
|
|
82
|
+
if (input.isTTY) {
|
|
83
|
+
input.off("keypress", onKeypress);
|
|
84
|
+
input.setRawMode?.(wasRaw);
|
|
85
|
+
if (wasFlowing !== true)
|
|
86
|
+
input.pause();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (intent === "back")
|
|
90
|
+
throw new WizardBack();
|
|
91
|
+
throw new WizardExitRequested();
|
|
92
|
+
}
|
|
93
|
+
async function select(options) {
|
|
94
|
+
return runPrompt(() => p.select(options));
|
|
95
|
+
}
|
|
96
|
+
async function autocomplete(options) {
|
|
97
|
+
return runPrompt(() => p.autocomplete(options));
|
|
98
|
+
}
|
|
99
|
+
async function confirm(options) {
|
|
100
|
+
return runPrompt(() => p.confirm(options));
|
|
101
|
+
}
|
|
102
|
+
async function multiselect(options) {
|
|
103
|
+
return runPrompt(() => p.multiselect(options));
|
|
104
|
+
}
|
|
105
|
+
async function textPrompt(options) {
|
|
106
|
+
return runPrompt(() => p.text(options));
|
|
107
|
+
}
|
|
108
|
+
async function passwordPrompt(options) {
|
|
109
|
+
return runPrompt(() => p.password(options));
|
|
110
|
+
}
|
|
13
111
|
/**
|
|
14
112
|
* The interactive `init` wizard: a colourful, step-by-step flow that COLLECTS a
|
|
15
113
|
* {@link WizardAnswers} and hands it back — it never writes anything. The caller
|
|
@@ -20,7 +118,16 @@ const SANDBOXABLE_TOOLS = new Set(["Bash", "Write", "Edit"]);
|
|
|
20
118
|
export async function runInitWizard(ctx) {
|
|
21
119
|
try {
|
|
22
120
|
const result = await collectAnswers(ctx);
|
|
23
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
status: "answers",
|
|
123
|
+
answers: result.answers,
|
|
124
|
+
runProviderSetup: result.runProviderSetup,
|
|
125
|
+
providerSetupSecrets: result.providerSetupSecrets,
|
|
126
|
+
providerEnvironmentSecrets: result.providerEnvironmentSecrets,
|
|
127
|
+
piApiKeyPersistenceByProvider: result.piApiKeyPersistenceByProvider,
|
|
128
|
+
credentialStates: result.credentialStates,
|
|
129
|
+
moduleSecrets: result.moduleSecrets,
|
|
130
|
+
};
|
|
24
131
|
}
|
|
25
132
|
catch (error) {
|
|
26
133
|
if (error instanceof WizardCancelled) {
|
|
@@ -30,95 +137,287 @@ export async function runInitWizard(ctx) {
|
|
|
30
137
|
throw error;
|
|
31
138
|
}
|
|
32
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Re-open an already reviewed setup at the Creation review. The caller keeps
|
|
142
|
+
* ownership of the original state: Escape returns `cancelled`, and no mutable
|
|
143
|
+
* credential or answer state is exposed unless the operator confirms a repair.
|
|
144
|
+
*/
|
|
145
|
+
export async function runSetupRepairWizard(ctx) {
|
|
146
|
+
try {
|
|
147
|
+
p.log.step("Repair setup choices");
|
|
148
|
+
const initialStep = ctx.initialStep ?? 8;
|
|
149
|
+
const result = await collectInteractiveFromSeed(ctx, ctx.answers, {
|
|
150
|
+
initialStep,
|
|
151
|
+
...(initialStep === 8 ? {} : { initialReturnToReviewStep: initialStep }),
|
|
152
|
+
returnToCallerOnReviewBack: true,
|
|
153
|
+
repairState: ctx,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
status: "answers",
|
|
157
|
+
answers: result.answers,
|
|
158
|
+
runProviderSetup: result.runProviderSetup,
|
|
159
|
+
providerSetupSecrets: result.providerSetupSecrets,
|
|
160
|
+
providerEnvironmentSecrets: result.providerEnvironmentSecrets,
|
|
161
|
+
piApiKeyPersistenceByProvider: result.piApiKeyPersistenceByProvider,
|
|
162
|
+
credentialStates: result.credentialStates,
|
|
163
|
+
moduleSecrets: result.moduleSecrets,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof WizardBack || error instanceof WizardCancelled) {
|
|
168
|
+
p.log.info("Returning to the preflight recovery menu; previous setup choices were kept.");
|
|
169
|
+
return { status: "cancelled" };
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Re-select only primary/fallback models and effort after a failed model check.
|
|
176
|
+
* Capabilities, module inputs, and observability are copied from `ctx.answers`,
|
|
177
|
+
* and no module secret is requested again. Tool/sandbox choices are re-confirmed
|
|
178
|
+
* when the runtime family crosses the direct-Codex boundary or changes whether
|
|
179
|
+
* a provider-owned Claude/direct-OpenCode tool loop is present.
|
|
180
|
+
*/
|
|
181
|
+
export async function runModelRepairWizard(ctx) {
|
|
182
|
+
try {
|
|
183
|
+
p.log.step("Repair model configuration");
|
|
184
|
+
for (;;) {
|
|
185
|
+
const draft = draftFrom(ctx.answers);
|
|
186
|
+
try {
|
|
187
|
+
const previousFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
|
|
188
|
+
await promptModelSettings(draft, ctx);
|
|
189
|
+
const nextFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
|
|
190
|
+
if (previousFamilies !== nextFamilies
|
|
191
|
+
|| !hasExactAllowAllTools(draft)
|
|
192
|
+
|| hasInvalidUniformManagedSrtChain(draft)) {
|
|
193
|
+
p.log.info("The runtime route changed, so tool and sandbox safety choices must be confirmed again.");
|
|
194
|
+
await promptTools(draft);
|
|
195
|
+
await promptSafetyPolicy(draft);
|
|
196
|
+
}
|
|
197
|
+
const answers = toWizardAnswers(draft);
|
|
198
|
+
const plan = await composePlanForCwd(answers, ctx.cwd);
|
|
199
|
+
const providerSetup = await promptProviderSetup(plan, ctx, draft.credentialStates);
|
|
200
|
+
if (!await confirm({ message: "Use this model configuration?", initialValue: true })) {
|
|
201
|
+
return { status: "cancelled" };
|
|
202
|
+
}
|
|
203
|
+
return { status: "answers", answers, ...providerSetup, credentialStates: { ...draft.credentialStates } };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof WizardBack) {
|
|
207
|
+
p.log.info("Returning to the preflight recovery menu; previous model choices were kept.");
|
|
208
|
+
return { status: "cancelled" };
|
|
209
|
+
}
|
|
210
|
+
if (error instanceof WizardExitRequested) {
|
|
211
|
+
if (await confirmExitSetup())
|
|
212
|
+
throw new WizardCancelled();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error instanceof WizardCancelled) {
|
|
221
|
+
p.cancel("Model repair cancelled — previous choices were kept.");
|
|
222
|
+
return { status: "cancelled" };
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
33
227
|
/** Walk the flow (preset or custom), returning the fully collected answers. */
|
|
34
228
|
async function collectAnswers(ctx) {
|
|
35
229
|
p.intro("mono-agent init — let's build your agent");
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
230
|
+
for (;;) {
|
|
231
|
+
let choice;
|
|
232
|
+
try {
|
|
233
|
+
choice = await select({
|
|
234
|
+
message: "Start from…",
|
|
235
|
+
options: presetSelectOptions(),
|
|
236
|
+
initialValue: "__custom__",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error instanceof WizardBack || error instanceof WizardExitRequested) {
|
|
241
|
+
if (await confirmExitSetup())
|
|
242
|
+
throw new WizardCancelled();
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return choice === "__custom__"
|
|
249
|
+
? await collectCustom(ctx)
|
|
250
|
+
: await collectFromPreset(ctx, choice);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
// Escape from the first detail step returns to the preset chooser. Ctrl-C
|
|
254
|
+
// is handled inside the detail state machine and should not normally leak.
|
|
255
|
+
if (error instanceof WizardBack)
|
|
256
|
+
continue;
|
|
257
|
+
if (error instanceof WizardExitRequested) {
|
|
258
|
+
if (await confirmExitSetup())
|
|
259
|
+
throw new WizardCancelled();
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
44
265
|
}
|
|
45
266
|
/**
|
|
46
267
|
* The full custom flow: model → effort → channels → memory → per-module inputs
|
|
47
268
|
* → tools → sandbox (only if code tools were chosen) → observability → summary.
|
|
48
269
|
*/
|
|
49
270
|
async function collectCustom(ctx) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const discovery = await discoverWizardModelCandidates();
|
|
53
|
-
p.note(formatModelDiscoveryStatus(discovery.statuses), "Model discovery");
|
|
54
|
-
const model = guard(await p.select({
|
|
55
|
-
message: "Which model?",
|
|
56
|
-
options: modelSelectOptions(discovery.candidates),
|
|
57
|
-
initialValue: "claude:claude-sonnet-4-6",
|
|
271
|
+
return await collectInteractiveFromSeed(ctx, defaultAnswers({
|
|
272
|
+
name: humanizeAgentName(basename(ctx.cwd)),
|
|
58
273
|
}));
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
274
|
+
}
|
|
275
|
+
async function resolveModelSelection(model, opts) {
|
|
276
|
+
if (model === "__pi_other__") {
|
|
277
|
+
return await promptPiModelSelection(opts);
|
|
278
|
+
}
|
|
279
|
+
if (model === "__other__") {
|
|
280
|
+
const resolved = (await textPrompt({
|
|
281
|
+
message: opts.context === "primary" ? "Model reference" : "Fallback model reference",
|
|
62
282
|
placeholder: "pi:ollama:llama3.1:8b",
|
|
63
|
-
validate:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
283
|
+
validate: validateFullModelReference,
|
|
284
|
+
})).trim();
|
|
285
|
+
assertConcreteWizardModelRef(resolved);
|
|
286
|
+
assertGuidedModelRef(resolved);
|
|
287
|
+
return resolved;
|
|
288
|
+
}
|
|
289
|
+
assertConcreteWizardModelRef(model);
|
|
290
|
+
assertGuidedModelRef(model);
|
|
291
|
+
return model;
|
|
292
|
+
}
|
|
293
|
+
async function promptPiModelSelection(opts) {
|
|
294
|
+
const options = piModelSelectOptions(opts.candidates, opts.excludedModels ?? []);
|
|
295
|
+
const choice = await autocomplete({
|
|
296
|
+
message: opts.context === "primary" ? "Other Pi model" : "Other Pi fallback model",
|
|
297
|
+
options,
|
|
298
|
+
initialValue: options[0]?.value ?? CUSTOM_PI_MODEL_OPTION,
|
|
299
|
+
placeholder: "Type to search Pi providers and models…",
|
|
300
|
+
maxItems: MODEL_AUTOCOMPLETE_MAX_ITEMS,
|
|
301
|
+
});
|
|
302
|
+
if (choice !== CUSTOM_PI_MODEL_OPTION) {
|
|
303
|
+
assertConcreteWizardModelRef(choice);
|
|
304
|
+
return choice;
|
|
305
|
+
}
|
|
306
|
+
return await promptManualPiModelRef();
|
|
307
|
+
}
|
|
308
|
+
async function promptManualPiModelRef() {
|
|
309
|
+
const provider = (await textPrompt({
|
|
310
|
+
message: "Pi provider id",
|
|
311
|
+
placeholder: "openai-codex",
|
|
312
|
+
validate: (v) => {
|
|
313
|
+
const value = (v ?? "").trim();
|
|
314
|
+
if (value.length === 0) {
|
|
315
|
+
return "Enter a supported Pi provider id (anthropic, github-copilot, openai-codex, opencode-go, ollama, or lmstudio)";
|
|
316
|
+
}
|
|
317
|
+
if (value.includes(":"))
|
|
318
|
+
return "Provider id cannot contain ':'.";
|
|
319
|
+
return guidedPiProviderProblem(value);
|
|
320
|
+
},
|
|
321
|
+
})).trim();
|
|
322
|
+
const modelId = (await textPrompt({
|
|
323
|
+
message: "Pi model id",
|
|
324
|
+
placeholder: provider === "openai-codex" ? "gpt-5.6-terra" : "llama3.1:8b",
|
|
325
|
+
validate: (v) => (v ?? "").trim().length === 0
|
|
326
|
+
? "Enter the provider-specific model id (e.g. gpt-5.6-terra, gpt-5.6-sol, kimi-k2.6, llama3.1:8b)"
|
|
327
|
+
: undefined,
|
|
328
|
+
})).trim();
|
|
329
|
+
return `pi:${provider}:${modelId}`;
|
|
330
|
+
}
|
|
331
|
+
function validateFullModelReference(value) {
|
|
332
|
+
const trimmed = (value ?? "").trim();
|
|
333
|
+
if (trimmed.length === 0) {
|
|
334
|
+
return "Enter a provider:model reference (e.g. pi:ollama:llama3.1:8b)";
|
|
335
|
+
}
|
|
336
|
+
if (trimmed.includes(",")) {
|
|
337
|
+
return "Enter one model reference. Add more fallbacks one at a time.";
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
assertConcreteWizardModelRef(trimmed);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
return error instanceof Error ? error.message : "Enter a concrete model reference.";
|
|
344
|
+
}
|
|
345
|
+
return guidedModelRefProblem(trimmed);
|
|
346
|
+
}
|
|
347
|
+
function assertGuidedModelRef(model) {
|
|
348
|
+
const problem = guidedModelRefProblem(model);
|
|
349
|
+
if (problem !== undefined) {
|
|
350
|
+
throw new Error(problem);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export function guidedModelRefProblem(model) {
|
|
354
|
+
let parsed;
|
|
355
|
+
try {
|
|
356
|
+
parsed = parseMonoRuntimeModelReference(model);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
return error instanceof Error ? error.message : "Enter a concrete model reference.";
|
|
360
|
+
}
|
|
361
|
+
if (parsed.sdk === "opencode") {
|
|
362
|
+
return "Direct OpenCode is scaffold/config-only. Choose pi:opencode-go:* for guided readiness.";
|
|
363
|
+
}
|
|
364
|
+
if (parsed.sdk === "pi" && parsed.provider !== undefined) {
|
|
365
|
+
return guidedPiProviderProblem(parsed.provider);
|
|
366
|
+
}
|
|
367
|
+
return parsed.sdk === "claude" || parsed.sdk === "codex"
|
|
368
|
+
? undefined
|
|
369
|
+
: "Guided init supports Claude, Codex, supported Pi providers, and local Pi routes.";
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Build an ordered fallback chain one model at a time. Each step reuses the same
|
|
373
|
+
* discovered model labels as the primary picker while hiding the selected primary
|
|
374
|
+
* and any fallback already chosen. `Other…` is the explicit path for custom refs.
|
|
375
|
+
*/
|
|
376
|
+
async function promptFallbackModels(draft, candidates) {
|
|
377
|
+
const byValue = new Map(candidates.map((candidate) => [candidate.value, candidate]));
|
|
378
|
+
for (;;) {
|
|
379
|
+
const selectedModels = draft.fallbacks.map((fallback) => fallback.model);
|
|
380
|
+
let choice;
|
|
381
|
+
try {
|
|
382
|
+
choice = await autocomplete({
|
|
383
|
+
message: `Fallback model #${draft.fallbacks.length + 1}`,
|
|
384
|
+
options: fallbackModelSelectOptions(candidates, draft.model, selectedModels),
|
|
385
|
+
initialValue: "__done__",
|
|
386
|
+
placeholder: "Type to search all supported models…",
|
|
387
|
+
maxItems: MODEL_AUTOCOMPLETE_MAX_ITEMS,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (!(error instanceof WizardBack) || draft.fallbacks.length === 0)
|
|
392
|
+
throw error;
|
|
393
|
+
draft.fallbacks.pop();
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (choice === "__done__") {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const resolved = await resolveModelSelection(choice, {
|
|
400
|
+
candidates,
|
|
401
|
+
excludedModels: [draft.model, ...selectedModels],
|
|
402
|
+
context: "fallback",
|
|
403
|
+
});
|
|
404
|
+
if (resolved !== draft.model && !selectedModels.includes(resolved)) {
|
|
405
|
+
let effort;
|
|
406
|
+
try {
|
|
407
|
+
effort = await promptEffortForModel(resolved, byValue.get(resolved));
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (error instanceof WizardBack)
|
|
411
|
+
continue;
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
draft.fallbacks.push({ model: resolved, ...(effort === undefined ? {} : { effort }) });
|
|
415
|
+
}
|
|
108
416
|
}
|
|
109
|
-
// 7. Observability.
|
|
110
|
-
draft.observability = guard(await p.confirm({
|
|
111
|
-
message: "Export traces to Phoenix (best-effort OTLP, sensitive data excluded)?",
|
|
112
|
-
initialValue: false,
|
|
113
|
-
}));
|
|
114
|
-
// 8. Summary + final confirm.
|
|
115
|
-
const runProviderSetup = await confirmSummary(draft, ctx);
|
|
116
|
-
return { answers: toWizardAnswers(draft), runProviderSetup };
|
|
117
417
|
}
|
|
118
418
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* confirm the summary.
|
|
419
|
+
* Presets seed every choice, but do not silently freeze them. A first run must
|
|
420
|
+
* still choose its model, channels, tool policy, and sandbox safety posture.
|
|
122
421
|
*/
|
|
123
422
|
async function collectFromPreset(ctx, presetId) {
|
|
124
423
|
const preset = findPreset(presetId);
|
|
@@ -127,19 +426,428 @@ async function collectFromPreset(ctx, presetId) {
|
|
|
127
426
|
throw new WizardCancelled();
|
|
128
427
|
}
|
|
129
428
|
p.log.step(`Preset: ${preset.title}`);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
429
|
+
return await collectInteractiveFromSeed(ctx, defaultAnswers({
|
|
430
|
+
...presetAnswers(preset),
|
|
431
|
+
name: humanizeAgentName(basename(ctx.cwd)),
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Whether a logical wizard step will actually ask the operator anything for
|
|
436
|
+
* the current draft. Some runtime families make Tools and Safety informative
|
|
437
|
+
* only; treating those as back-navigation destinations traps Escape in a
|
|
438
|
+
* silent forward loop.
|
|
439
|
+
*/
|
|
440
|
+
function wizardStepHasInteractivePrompt(step, draft) {
|
|
441
|
+
switch (step) {
|
|
442
|
+
case 0:
|
|
443
|
+
case 1:
|
|
444
|
+
case 2:
|
|
445
|
+
case 3:
|
|
446
|
+
case 7:
|
|
447
|
+
case 8:
|
|
448
|
+
return true;
|
|
449
|
+
case 4:
|
|
450
|
+
return [...draft.channels, ...(draft.memory === undefined ? [] : [draft.memory])]
|
|
451
|
+
.some((id) => findModule(id)?.inputs.some((input) => input.secret !== true) === true);
|
|
452
|
+
case 5:
|
|
453
|
+
return !selectedRuntimeModels(draft).every(hasFixedAllowAllToolPolicyRef);
|
|
454
|
+
case 6:
|
|
455
|
+
return safetyPolicyHasInteractivePrompt(draft);
|
|
456
|
+
default:
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function safetyPolicyHasInteractivePrompt(draft) {
|
|
461
|
+
const models = selectedRuntimeModels(draft);
|
|
462
|
+
const mixed = isMixedRouteChain(models);
|
|
463
|
+
if (mixed)
|
|
464
|
+
return true;
|
|
465
|
+
const hasSandboxableTools = isAllowAllTools(draft.allowedTools)
|
|
466
|
+
|| draft.allowedTools.some((tool) => SANDBOXABLE_TOOLS.has(tool));
|
|
467
|
+
const hasPiRoute = models.some((model) => model.startsWith("pi:"));
|
|
468
|
+
const providerNative = models.some((model) => !model.startsWith("pi:"));
|
|
469
|
+
if (hasSandboxableTools && hasPiRoute)
|
|
470
|
+
return true;
|
|
471
|
+
if (draft.routeSafety === "per-route-native" && providerNative)
|
|
472
|
+
return true;
|
|
473
|
+
return hasSandboxableTools
|
|
474
|
+
&& providerNative
|
|
475
|
+
&& !models.every(isDirectCodexRef)
|
|
476
|
+
&& isAllowAllTools(draft.allowedTools);
|
|
477
|
+
}
|
|
478
|
+
function previousInteractiveWizardStep(step, draft) {
|
|
479
|
+
for (let candidate = step - 1; candidate >= 0; candidate -= 1) {
|
|
480
|
+
if (wizardStepHasInteractivePrompt(candidate, draft))
|
|
481
|
+
return candidate;
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
/** Shared custom/preset first-run chooser; seed answers only set sensible defaults. */
|
|
486
|
+
async function collectInteractiveFromSeed(ctx, seed, options = {}) {
|
|
487
|
+
const draft = draftFrom(seed);
|
|
488
|
+
if (options.repairState !== undefined) {
|
|
489
|
+
draft.credentialStates = { ...options.repairState.credentialStates };
|
|
490
|
+
}
|
|
491
|
+
const modelDiscoveryCache = {};
|
|
492
|
+
let step = options.initialStep ?? 0;
|
|
493
|
+
let returnToReviewAfterStep = options.initialReturnToReviewStep;
|
|
494
|
+
const finalStep = 8;
|
|
495
|
+
const advanceAfter = (completedStep) => {
|
|
496
|
+
if (returnToReviewAfterStep === completedStep) {
|
|
497
|
+
returnToReviewAfterStep = undefined;
|
|
498
|
+
step = finalStep;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
step = completedStep + 1;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
for (;;) {
|
|
505
|
+
const snapshot = cloneDraft(draft);
|
|
506
|
+
let finalizing = false;
|
|
507
|
+
try {
|
|
508
|
+
switch (step) {
|
|
509
|
+
case 0:
|
|
510
|
+
draft.name = (await textPrompt({
|
|
511
|
+
message: "What should this agent be called?",
|
|
512
|
+
// Keep the current answer visible and editable when Escape returns
|
|
513
|
+
// here from model selection. `defaultValue` only substitutes an
|
|
514
|
+
// empty submission, so Clack would otherwise render the folder-name
|
|
515
|
+
// placeholder and make a preserved custom name look lost.
|
|
516
|
+
initialValue: draft.name,
|
|
517
|
+
placeholder: humanizeAgentName(basename(ctx.cwd)),
|
|
518
|
+
validate: validateWizardAgentName,
|
|
519
|
+
})).trim();
|
|
520
|
+
draft.purpose = (await textPrompt({
|
|
521
|
+
message: "What should this agent help with?",
|
|
522
|
+
initialValue: draft.purpose,
|
|
523
|
+
placeholder: "Help with work in this folder",
|
|
524
|
+
validate: validateWizardAgentPurpose,
|
|
525
|
+
})).trim();
|
|
526
|
+
advanceAfter(0);
|
|
527
|
+
break;
|
|
528
|
+
case 1: {
|
|
529
|
+
const previousFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
|
|
530
|
+
await promptModelSettings(draft, ctx, modelDiscoveryCache);
|
|
531
|
+
const nextFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
|
|
532
|
+
if (returnToReviewAfterStep === 1
|
|
533
|
+
&& (previousFamilies !== nextFamilies
|
|
534
|
+
|| !hasExactAllowAllTools(draft)
|
|
535
|
+
|| hasInvalidUniformManagedSrtChain(draft))) {
|
|
536
|
+
p.log.info("The runtime route changed, so tool and sandbox safety choices must be confirmed again.");
|
|
537
|
+
await promptTools(draft);
|
|
538
|
+
await promptSafetyPolicy(draft);
|
|
539
|
+
}
|
|
540
|
+
advanceAfter(1);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case 2: {
|
|
544
|
+
const previousChannels = new Set(draft.channels);
|
|
545
|
+
draft.channels = [...await multiselect({
|
|
546
|
+
message: "How will you talk to this agent?",
|
|
547
|
+
options: channelSelectOptions({ readyOnly: true }),
|
|
548
|
+
initialValues: draft.channels,
|
|
549
|
+
required: false,
|
|
550
|
+
})];
|
|
551
|
+
if (returnToReviewAfterStep === 2) {
|
|
552
|
+
await promptModuleInputs(draft, draft.channels.filter((channel) => !previousChannels.has(channel)));
|
|
553
|
+
await promptTools(draft);
|
|
554
|
+
await promptSafetyPolicy(draft);
|
|
555
|
+
}
|
|
556
|
+
advanceAfter(2);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case 3: {
|
|
560
|
+
const previousMemory = draft.memory;
|
|
561
|
+
const memory = await select({
|
|
562
|
+
message: "Should the agent remember across conversations?",
|
|
563
|
+
options: memorySelectOptions({
|
|
564
|
+
includeOptionalPlugins: draft.memory === "memory:supermemory"
|
|
565
|
+
|| isSupermemoryPluginInstalled({ cwd: ctx.cwd }),
|
|
566
|
+
}),
|
|
567
|
+
initialValue: draft.memory ?? "",
|
|
568
|
+
});
|
|
569
|
+
draft.memory = memory === "" ? undefined : memory;
|
|
570
|
+
if (returnToReviewAfterStep === 3
|
|
571
|
+
&& draft.memory !== undefined
|
|
572
|
+
&& draft.memory !== previousMemory) {
|
|
573
|
+
await promptModuleInputs(draft, [draft.memory]);
|
|
574
|
+
}
|
|
575
|
+
advanceAfter(3);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case 4:
|
|
579
|
+
await promptModuleInputs(draft);
|
|
580
|
+
advanceAfter(4);
|
|
581
|
+
break;
|
|
582
|
+
case 5:
|
|
583
|
+
await promptTools(draft);
|
|
584
|
+
if (returnToReviewAfterStep === 5) {
|
|
585
|
+
await promptSafetyPolicy(draft);
|
|
586
|
+
}
|
|
587
|
+
advanceAfter(5);
|
|
588
|
+
break;
|
|
589
|
+
case 6:
|
|
590
|
+
await promptSafetyPolicy(draft);
|
|
591
|
+
advanceAfter(6);
|
|
592
|
+
break;
|
|
593
|
+
case 7:
|
|
594
|
+
draft.observability = await confirm({
|
|
595
|
+
message: "Export traces to Phoenix (best-effort OTLP, sensitive data excluded)?",
|
|
596
|
+
initialValue: draft.observability,
|
|
597
|
+
});
|
|
598
|
+
advanceAfter(7);
|
|
599
|
+
break;
|
|
600
|
+
case 8: {
|
|
601
|
+
const reviewed = await confirmSummary(draft, ctx, options.repairState === undefined
|
|
602
|
+
? undefined
|
|
603
|
+
: {
|
|
604
|
+
existing: options.repairState,
|
|
605
|
+
});
|
|
606
|
+
if (reviewed.status === "edit") {
|
|
607
|
+
step = reviewed.step;
|
|
608
|
+
returnToReviewAfterStep = reviewed.step === finalStep ? undefined : reviewed.step;
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
finalizing = true;
|
|
612
|
+
const answers = toWizardAnswers(draft);
|
|
613
|
+
const moduleSecrets = await promptRequiredModuleSecrets(answers, ctx.persistedEnv, options.repairState?.moduleSecrets);
|
|
614
|
+
return {
|
|
615
|
+
answers,
|
|
616
|
+
...reviewed.providerSetup,
|
|
617
|
+
moduleSecrets,
|
|
618
|
+
credentialStates: { ...draft.credentialStates },
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
default:
|
|
622
|
+
throw new Error(`Unknown wizard step ${step}.`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
restoreDraft(draft, snapshot);
|
|
627
|
+
if (error instanceof WizardBack) {
|
|
628
|
+
if (options.returnToCallerOnReviewBack === true)
|
|
629
|
+
throw error;
|
|
630
|
+
if (returnToReviewAfterStep === step) {
|
|
631
|
+
returnToReviewAfterStep = undefined;
|
|
632
|
+
step = finalStep;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const previous = previousInteractiveWizardStep(step, draft);
|
|
636
|
+
if (previous === undefined)
|
|
637
|
+
throw error;
|
|
638
|
+
// Secret/auth collection happens after the review choice. Escape there
|
|
639
|
+
// returns to the review rather than discarding unrelated answers.
|
|
640
|
+
step = step === finalStep && finalizing ? finalStep : previous;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (error instanceof WizardExitRequested) {
|
|
644
|
+
if (await confirmExitSetup())
|
|
645
|
+
throw new WizardCancelled();
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async function promptSafetyPolicy(draft) {
|
|
653
|
+
const models = selectedRuntimeModels(draft);
|
|
654
|
+
const mixed = isMixedRouteChain(models);
|
|
655
|
+
if (mixed) {
|
|
656
|
+
draft.routeSafety = await select({
|
|
657
|
+
message: "How should safety apply across this mixed fallback chain?",
|
|
658
|
+
options: [...ROUTE_SAFETY_OPTIONS],
|
|
659
|
+
initialValue: draft.routeSafety,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
draft.routeSafety = "uniform";
|
|
664
|
+
}
|
|
665
|
+
const hasSandboxableTools = isAllowAllTools(draft.allowedTools)
|
|
666
|
+
|| draft.allowedTools.some((tool) => SANDBOXABLE_TOOLS.has(tool));
|
|
667
|
+
const hasPiRoute = models.some((model) => model.startsWith("pi:"));
|
|
668
|
+
const providerNative = models.some((model) => !model.startsWith("pi:"));
|
|
669
|
+
const mixedPiProviderNativeChain = mixed && hasPiRoute && providerNative;
|
|
670
|
+
if (hasSandboxableTools && hasPiRoute) {
|
|
671
|
+
draft.sandbox = await confirm({
|
|
672
|
+
message: "Install and use managed SRT for Pi shell/file tools? (localhost-only network; setup verifies fail-closed enforcement)",
|
|
673
|
+
initialValue: true,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
draft.sandbox = false;
|
|
678
|
+
}
|
|
679
|
+
if (mixedPiProviderNativeChain && draft.routeSafety === "uniform" && draft.sandbox) {
|
|
680
|
+
p.note("Managed SRT applies to Pi routes only. A mixed Pi/provider-native chain therefore cannot promise one uniform managed-SRT contract.", "Safety choice required");
|
|
681
|
+
const resolution = await select({
|
|
682
|
+
message: "How should this mixed chain resolve the managed-SRT mismatch?",
|
|
683
|
+
options: [
|
|
684
|
+
{
|
|
685
|
+
value: "per-route-native",
|
|
686
|
+
label: "Use per-route native safety",
|
|
687
|
+
hint: "keep managed SRT for Pi and show every provider's native contract",
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
value: "disable-managed-srt",
|
|
691
|
+
label: "Disable managed SRT",
|
|
692
|
+
hint: "keep the uniform contract without claiming SRT on provider-native routes",
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
initialValue: "per-route-native",
|
|
696
|
+
});
|
|
697
|
+
if (resolution === "per-route-native")
|
|
698
|
+
draft.routeSafety = "per-route-native";
|
|
699
|
+
else
|
|
700
|
+
draft.sandbox = false;
|
|
701
|
+
}
|
|
702
|
+
if (draft.routeSafety === "per-route-native" && (mixed || providerNative)) {
|
|
703
|
+
p.note(formatRouteSafetyMatrix({ model: draft.model, ...(draft.effort === undefined ? {} : { effort: draft.effort }) }, draft.fallbacks, draft.sandbox), "Per-route safety contract");
|
|
704
|
+
const accepted = await confirm({
|
|
705
|
+
message: "Use these per-route safety contracts? Provider-native routes cannot enforce every mono-agent capability.",
|
|
706
|
+
initialValue: false,
|
|
707
|
+
});
|
|
708
|
+
if (!accepted) {
|
|
709
|
+
draft.routeSafety = "uniform";
|
|
710
|
+
if (mixedPiProviderNativeChain && draft.sandbox) {
|
|
711
|
+
draft.sandbox = false;
|
|
712
|
+
p.log.info("Managed SRT disabled because this mixed chain kept the uniform compatibility contract.");
|
|
713
|
+
}
|
|
714
|
+
p.log.info("Per-route native safety was not accepted; restored the uniform compatibility contract.");
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (hasSandboxableTools && hasPiRoute && isAllowAllTools(draft.allowedTools) && !draft.sandbox) {
|
|
721
|
+
const accepted = await confirmHighRiskUnsandboxedAccess();
|
|
722
|
+
if (!accepted) {
|
|
723
|
+
draft.sandbox = true;
|
|
724
|
+
if (mixedPiProviderNativeChain && draft.routeSafety === "uniform") {
|
|
725
|
+
draft.routeSafety = "per-route-native";
|
|
726
|
+
p.note(formatRouteSafetyMatrix({ model: draft.model, ...(draft.effort === undefined ? {} : { effort: draft.effort }) }, draft.fallbacks, draft.sandbox), "Per-route safety contract");
|
|
727
|
+
if (!await confirm({
|
|
728
|
+
message: "Use these per-route safety contracts? Provider-native routes cannot enforce every mono-agent capability.",
|
|
729
|
+
initialValue: false,
|
|
730
|
+
})) {
|
|
731
|
+
throw new WizardBack();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
p.log.info("Managed SRT enabled because unsandboxed Pi allow-all access was not confirmed.");
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
else if (hasSandboxableTools
|
|
740
|
+
&& providerNative
|
|
741
|
+
&& !models.every(isDirectCodexRef)
|
|
742
|
+
&& isAllowAllTools(draft.allowedTools)
|
|
743
|
+
&& !await confirmHighRiskUnsandboxedAccess()) {
|
|
744
|
+
throw new WizardBack();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function confirmHighRiskUnsandboxedAccess() {
|
|
748
|
+
return confirm({
|
|
749
|
+
message: "Proceed with high-risk unsandboxed access? The model may run shell commands, change files, access the web, and send through enabled channels.",
|
|
750
|
+
initialValue: false,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
/** Select primary, fallbacks, and effort without touching any other answer. */
|
|
754
|
+
async function promptModelSettings(draft, ctx, cache = {}) {
|
|
755
|
+
if (cache.result === undefined)
|
|
756
|
+
p.log.step("Discovering supported model catalogs…");
|
|
757
|
+
const discovery = cache.result ??= await discoverModelsInterruptibly({
|
|
758
|
+
...(ctx.piAuthPath === undefined ? {} : { piAuthPath: ctx.piAuthPath }),
|
|
759
|
+
...(ctx.persistedEnv === undefined ? {} : { persistedEnv: ctx.persistedEnv }),
|
|
760
|
+
});
|
|
761
|
+
const discoveredByValue = new Map(discovery.candidates.map((candidate) => [candidate.value, candidate]));
|
|
762
|
+
p.note(formatModelDiscoveryStatus(discovery.statuses), "Model discovery");
|
|
763
|
+
const previousModel = draft.model;
|
|
764
|
+
const previousEffort = draft.effort;
|
|
765
|
+
const providerDefault = discovery.candidates.find((candidate) => candidate.providerDefault === true);
|
|
766
|
+
const initialModel = draft.model === DEFAULT_MODEL ? providerDefault?.value ?? draft.model : draft.model;
|
|
767
|
+
let substep = 0;
|
|
768
|
+
for (;;) {
|
|
769
|
+
try {
|
|
770
|
+
if (substep === 0) {
|
|
771
|
+
const model = await autocomplete({
|
|
772
|
+
message: "Which model?",
|
|
773
|
+
options: modelSelectOptions(discovery.candidates, draft.model),
|
|
774
|
+
initialValue: draft.model === DEFAULT_MODEL ? initialModel : draft.model,
|
|
775
|
+
placeholder: "Type to search Pi, Codex, Claude, and local models…",
|
|
776
|
+
maxItems: MODEL_AUTOCOMPLETE_MAX_ITEMS,
|
|
777
|
+
});
|
|
778
|
+
draft.model = await resolveModelSelection(model, { candidates: discovery.candidates, context: "primary" });
|
|
779
|
+
substep = 1;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (substep === 1) {
|
|
783
|
+
draft.effort = await promptEffortForModel(draft.model, discoveredByValue.get(draft.model), draft.model === previousModel ? previousEffort : undefined);
|
|
784
|
+
substep = 2;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (substep === 2) {
|
|
788
|
+
draft.fallbacks = [];
|
|
789
|
+
if (!await confirm({ message: "Add fallback models?", initialValue: false }))
|
|
790
|
+
break;
|
|
791
|
+
substep = 3;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
await promptFallbackModels(draft, discovery.candidates);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
if (!(error instanceof WizardBack) || substep === 0)
|
|
799
|
+
throw error;
|
|
800
|
+
substep -= 1;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
draft.credentialStates = selectedCredentialStates(draft, discoveredByValue, ctx.persistedEnv);
|
|
804
|
+
}
|
|
805
|
+
function selectedCredentialStates(draft, candidates, persistedEnv = {}) {
|
|
806
|
+
const states = {};
|
|
807
|
+
for (const model of selectedRuntimeModels(draft)) {
|
|
808
|
+
const key = providerCredentialKey(model);
|
|
809
|
+
if (key === undefined)
|
|
810
|
+
continue;
|
|
811
|
+
const authState = candidates.get(model)?.authState;
|
|
812
|
+
states[key] = authState === "verified"
|
|
813
|
+
? "verified"
|
|
814
|
+
: hasDurableProviderEnvironmentCredential(model, persistedEnv)
|
|
815
|
+
? "credential_detected"
|
|
816
|
+
: authState === "credential_detected"
|
|
817
|
+
? "credential_detected"
|
|
818
|
+
: "auth_required";
|
|
819
|
+
}
|
|
820
|
+
return states;
|
|
821
|
+
}
|
|
822
|
+
function providerCredentialKey(model) {
|
|
823
|
+
if (model.startsWith("codex:"))
|
|
824
|
+
return "codex";
|
|
825
|
+
if (model.startsWith("claude:"))
|
|
826
|
+
return "claude";
|
|
827
|
+
if (!model.startsWith("pi:"))
|
|
828
|
+
return undefined;
|
|
829
|
+
const provider = model.split(":")[1];
|
|
830
|
+
return provider === undefined || provider === "ollama" || provider === "lmstudio" ? undefined : `pi:${provider}`;
|
|
831
|
+
}
|
|
832
|
+
async function promptEffortForModel(model, candidate, initial) {
|
|
833
|
+
const supported = candidate?.supportedEfforts ?? [];
|
|
834
|
+
const options = effortSelectOptions(supported, candidate?.defaultEffort);
|
|
835
|
+
const initialValue = initial !== undefined && supported.includes(initial) ? initial : "";
|
|
836
|
+
const effort = await select({
|
|
837
|
+
message: `Reasoning effort for ${model}`,
|
|
838
|
+
options,
|
|
839
|
+
initialValue,
|
|
840
|
+
});
|
|
841
|
+
return effort === "" ? undefined : effort;
|
|
135
842
|
}
|
|
136
843
|
/**
|
|
137
844
|
* Prompt every non-secret input of the selected channel + memory modules and store
|
|
138
|
-
* the answers into `draft.moduleInputs`.
|
|
139
|
-
*
|
|
845
|
+
* the answers into `draft.moduleInputs`. Required secrets are collected only after
|
|
846
|
+
* the write confirmation so they can never appear in the plan summary.
|
|
140
847
|
*/
|
|
141
|
-
async function promptModuleInputs(draft) {
|
|
142
|
-
const moduleIds =
|
|
848
|
+
async function promptModuleInputs(draft, requestedModuleIds) {
|
|
849
|
+
const moduleIds = requestedModuleIds
|
|
850
|
+
?? [...draft.channels, ...(draft.memory === undefined ? [] : [draft.memory])];
|
|
143
851
|
for (const id of moduleIds) {
|
|
144
852
|
const module = findModule(id);
|
|
145
853
|
if (module === undefined) {
|
|
@@ -149,21 +857,50 @@ async function promptModuleInputs(draft) {
|
|
|
149
857
|
if (input.secret === true) {
|
|
150
858
|
continue;
|
|
151
859
|
}
|
|
152
|
-
const
|
|
860
|
+
const currentValue = draft.moduleInputs[module.id]?.[input.id];
|
|
861
|
+
const answer = await textPrompt({
|
|
153
862
|
message: `${module.title}: ${input.label}`,
|
|
154
863
|
placeholder: input.description,
|
|
155
864
|
...(input.default === undefined ? {} : { defaultValue: input.default }),
|
|
156
|
-
|
|
865
|
+
...(currentValue === undefined ? {} : { initialValue: currentValue }),
|
|
866
|
+
...(input.validate === undefined ? {} : { validate: input.validate }),
|
|
867
|
+
});
|
|
157
868
|
const trimmed = answer.trim();
|
|
158
869
|
if (trimmed.length > 0) {
|
|
159
870
|
(draft.moduleInputs[module.id] ??= {})[input.id] = trimmed;
|
|
160
871
|
}
|
|
872
|
+
else {
|
|
873
|
+
delete draft.moduleInputs[module.id]?.[input.id];
|
|
874
|
+
}
|
|
161
875
|
}
|
|
162
876
|
}
|
|
163
877
|
}
|
|
878
|
+
/** Collect only typed required secrets, masked and retained solely for this run. */
|
|
879
|
+
async function promptRequiredModuleSecrets(answers, persistedEnv = {}, existing = {}) {
|
|
880
|
+
const plan = composeWizardPlan(answers, { dirBasename: "agent", skillsRootExists: false });
|
|
881
|
+
const secrets = {};
|
|
882
|
+
for (const secret of plan.secrets) {
|
|
883
|
+
if (!secret.required || hasNonEmptyValue(persistedEnv[secret.envVar]))
|
|
884
|
+
continue;
|
|
885
|
+
if (hasNonEmptyValue(existing[secret.envVar])) {
|
|
886
|
+
secrets[secret.envVar] = existing[secret.envVar];
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
secrets[secret.envVar] = await passwordPrompt({
|
|
890
|
+
message: `${secret.label} (${secret.envVar})`,
|
|
891
|
+
validate: (value) => (value ?? "").trim().length === 0 ? "This secret is required for the selected capability." : undefined,
|
|
892
|
+
clearOnError: true,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
return secrets;
|
|
896
|
+
}
|
|
897
|
+
function hasNonEmptyValue(value) {
|
|
898
|
+
return value !== undefined && value.trim().length > 0;
|
|
899
|
+
}
|
|
164
900
|
/** Short reasons annotating each always-on tool in the framing note. */
|
|
165
901
|
const ALWAYS_ON_TOOL_REASONS = {
|
|
166
902
|
MemoryRecall: "memory recall is on",
|
|
903
|
+
ReadSkill: "configuration skills load on demand",
|
|
167
904
|
};
|
|
168
905
|
/** The always-on tool names annotated with their reason, e.g. `MemoryRecall (memory recall is on)`. */
|
|
169
906
|
function alwaysOnDisplay(alwaysOn) {
|
|
@@ -179,7 +916,7 @@ function alwaysOnDisplay(alwaysOn) {
|
|
|
179
916
|
*/
|
|
180
917
|
function channelSendTools(channels) {
|
|
181
918
|
return toolMultiselectOptions(channels)
|
|
182
|
-
.slice(BUILTIN_TOOL_NAMES.length)
|
|
919
|
+
.slice(BUILTIN_TOOL_NAMES.length + APP_TOOL_NAMES.length)
|
|
183
920
|
.map((option) => option.value)
|
|
184
921
|
.filter((value) => value !== "AskUser");
|
|
185
922
|
}
|
|
@@ -188,7 +925,8 @@ function channelSendTools(channels) {
|
|
|
188
925
|
* knows what "Allow all" turns on and what is unaffected by the choice:
|
|
189
926
|
* 1. Always on — auto-provisioned, NOT gated by this choice (e.g. MemoryRecall).
|
|
190
927
|
* 2. Built-ins — file/shell/web tools.
|
|
191
|
-
* 3.
|
|
928
|
+
* 3. App tools — safe host capabilities such as bounded run inspection.
|
|
929
|
+
* 4. Channel tools — the send/ask tools that came with the channels you enabled,
|
|
192
930
|
* plus AskUser (ask the human, any channel).
|
|
193
931
|
*/
|
|
194
932
|
function toolSituationFraming(draft, alwaysOn) {
|
|
@@ -201,8 +939,9 @@ function toolSituationFraming(draft, alwaysOn) {
|
|
|
201
939
|
? `Always on (auto-provisioned, not affected by this choice): ${alwaysOnDisplay(alwaysOn).join(", ")}.`
|
|
202
940
|
: "Always on (auto-provisioned): none for this setup.",
|
|
203
941
|
"Built-ins: files (Read/Write/Edit/Glob/Grep), shell (Bash), web (WebFetch/WebSearch).",
|
|
942
|
+
"App tools: RunHistory (read-only evidence from completed prior runs in this conversation).",
|
|
204
943
|
channelLine,
|
|
205
|
-
'"Allow all"
|
|
944
|
+
'"Allow all" lets the model run shell commands, read/change files, access the web, and send through enabled channels. These actions can modify data or contact people; you can turn specific tools off later via tools.disallowedTools.',
|
|
206
945
|
].join("\n");
|
|
207
946
|
}
|
|
208
947
|
/**
|
|
@@ -214,10 +953,15 @@ function toolSituationFraming(draft, alwaysOn) {
|
|
|
214
953
|
async function promptTools(draft) {
|
|
215
954
|
const alwaysOn = alwaysOnTools(toWizardAnswers(draft));
|
|
216
955
|
p.note(toolSituationFraming(draft, alwaysOn), "Tools");
|
|
217
|
-
|
|
218
|
-
|
|
956
|
+
if (selectedRuntimeModels(draft).every(hasFixedAllowAllToolPolicyRef)) {
|
|
957
|
+
draft.allowedTools = [ALLOW_ALL_TOOLS];
|
|
958
|
+
p.log.info("Direct Codex uses its native app-server tool set and cannot enforce mono-agent per-tool allowlists. Tool policy is fixed to allow-all; use Pi or Claude for a restrictive tool list.");
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const allowAll = await confirm({
|
|
962
|
+
message: "Allow all tools? (shell commands, file changes, web access, and enabled-channel sends)",
|
|
219
963
|
initialValue: true,
|
|
220
|
-
})
|
|
964
|
+
});
|
|
221
965
|
if (allowAll) {
|
|
222
966
|
draft.allowedTools = [ALLOW_ALL_TOOLS];
|
|
223
967
|
return;
|
|
@@ -235,17 +979,17 @@ async function pickSpecificTools(draft) {
|
|
|
235
979
|
const optionOrder = new Map(options.map((option, index) => [option.value, index]));
|
|
236
980
|
const recommended = recommendedToolSelection(toWizardAnswers(draft));
|
|
237
981
|
for (;;) {
|
|
238
|
-
const tools =
|
|
982
|
+
const tools = await multiselect({
|
|
239
983
|
message: "Which tools may the model call?",
|
|
240
984
|
options,
|
|
241
985
|
initialValues: [...recommended],
|
|
242
986
|
required: false,
|
|
243
|
-
})
|
|
987
|
+
});
|
|
244
988
|
if (tools.length === 0) {
|
|
245
|
-
const proceed =
|
|
989
|
+
const proceed = await confirm({
|
|
246
990
|
message: "⚠ Zero tools selected — the agent will be chat-only (cannot read files, run commands, or send proactively). Continue?",
|
|
247
991
|
initialValue: false,
|
|
248
|
-
})
|
|
992
|
+
});
|
|
249
993
|
if (!proceed) {
|
|
250
994
|
continue;
|
|
251
995
|
}
|
|
@@ -254,66 +998,315 @@ async function pickSpecificTools(draft) {
|
|
|
254
998
|
return;
|
|
255
999
|
}
|
|
256
1000
|
}
|
|
257
|
-
|
|
258
|
-
* Render the compact plan summary and ask for the single write-time confirmation.
|
|
259
|
-
* Provider modules are implementation detail (auto-added for local models), so
|
|
260
|
-
* they are excluded from the user-facing capabilities line. A "no" cancels.
|
|
261
|
-
*/
|
|
262
|
-
async function confirmSummary(draft, ctx) {
|
|
263
|
-
if ([draft.model, ...draft.fallbackModels].some((model) => LOCAL_PROVIDER_MODEL.test(model))) {
|
|
264
|
-
p.note("A matching local provider block is added automatically.\nThe first turn may be slow while the model loads.", "Local model");
|
|
265
|
-
}
|
|
1001
|
+
async function confirmSummary(draft, ctx, options) {
|
|
266
1002
|
const answers = toWizardAnswers(draft);
|
|
267
|
-
const plan =
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1003
|
+
const plan = await composePlanForCwd(answers, ctx.cwd);
|
|
1004
|
+
const setupModelRefs = referencedSetupModelRefs(plan);
|
|
1005
|
+
const existingSetupModelRefs = options === undefined
|
|
1006
|
+
? undefined
|
|
1007
|
+
: referencedSetupModelRefs(await composePlanForCwd(options.existing.answers, ctx.cwd));
|
|
1008
|
+
const preserveProviderSetup = options !== undefined
|
|
1009
|
+
&& existingSetupModelRefs !== undefined
|
|
1010
|
+
&& sameOrderedValues(setupModelRefs, existingSetupModelRefs);
|
|
1011
|
+
const preliminarySetupPlan = providerSetupPlan(plan, ctx, draft.credentialStates);
|
|
1012
|
+
// Resolve destinations before the final review; collect masked values only
|
|
1013
|
+
// after the operator chooses Create.
|
|
1014
|
+
const piApiKeyPersistenceByProvider = preserveProviderSetup
|
|
1015
|
+
? { ...options.existing.piApiKeyPersistenceByProvider }
|
|
1016
|
+
: await selectPiApiKeyPersistence(preliminarySetupPlan, options?.existing.piApiKeyPersistenceByProvider);
|
|
1017
|
+
const setupPlan = providerSetupPlan(plan, ctx, draft.credentialStates, piApiKeyPersistenceByProvider);
|
|
1018
|
+
if (setupModelRefs.some((model) => /^pi:(?:ollama|lmstudio):/u.test(model))) {
|
|
1019
|
+
p.note("Local runtime or memory services are checked before readiness. The first load may take longer while models start.", "Local dependencies");
|
|
1020
|
+
}
|
|
271
1021
|
const capabilities = plan.selectedModules
|
|
272
1022
|
.filter((module) => module.kind !== "provider")
|
|
273
1023
|
.map((module) => module.title);
|
|
274
|
-
const
|
|
1024
|
+
const creates = ["mono-agent.config.json", "IDENTITY.md", ".mono-agent/artifacts/", ".mono-agent/workspace/"];
|
|
275
1025
|
if (plan.envExample !== undefined) {
|
|
276
|
-
|
|
1026
|
+
creates.push(".env.example (placeholders only)");
|
|
277
1027
|
}
|
|
278
1028
|
for (const file of plan.files) {
|
|
279
|
-
|
|
1029
|
+
creates.push(file.path);
|
|
1030
|
+
}
|
|
1031
|
+
const providerEnvironmentNames = setupPlan.actions
|
|
1032
|
+
.filter(isProviderSetupPiApiKeyAction)
|
|
1033
|
+
.filter((action) => action.persistence === "environment")
|
|
1034
|
+
.map((action) => action.envVar);
|
|
1035
|
+
const moduleSecretsToCollect = plan.secrets
|
|
1036
|
+
.filter((secret) => secret.required && !hasNonEmptyValue(ctx.persistedEnv?.[secret.envVar]))
|
|
1037
|
+
.map((secret) => secret.envVar);
|
|
1038
|
+
const secrets = [...new Set([
|
|
1039
|
+
...moduleSecretsToCollect,
|
|
1040
|
+
...providerEnvironmentNames,
|
|
1041
|
+
])];
|
|
1042
|
+
const hardenExistingDotenv = hasSensitivePersistedEnvironmentValue(ctx.persistedEnv ?? {});
|
|
1043
|
+
const mutableSecretFiles = [];
|
|
1044
|
+
if (secrets.length > 0 || hardenExistingDotenv) {
|
|
1045
|
+
mutableSecretFiles.push(secrets.length > 0 ? ".env (owner-only secret merge)" : ".env (owner-only permission hardening)", ".gitignore (ensure /.env is ignored)");
|
|
280
1046
|
}
|
|
281
|
-
const secrets = plan.secrets.map((secret) => secret.envVar);
|
|
282
1047
|
const toolsLine = isAllowAllTools(draft.allowedTools)
|
|
283
1048
|
? "all tools"
|
|
284
1049
|
: draft.allowedTools.length > 0
|
|
285
1050
|
? draft.allowedTools.join(", ")
|
|
286
1051
|
: "none (chat-only)";
|
|
287
1052
|
const alwaysOn = alwaysOnTools(answers);
|
|
1053
|
+
const runtimeRoutes = [
|
|
1054
|
+
{ model: draft.model, effort: draft.effort },
|
|
1055
|
+
...draft.fallbacks,
|
|
1056
|
+
];
|
|
1057
|
+
const potentiallyBilledCalls = runtimeRoutes.filter((route) => modelRefMayBill(route.model)).length;
|
|
1058
|
+
const providerActions = [
|
|
1059
|
+
...setupPlan.detectedModelRefs.map((modelRef) => `${modelRef}: credential/sign-in detected; skip initial auth and verify with the live readiness call`),
|
|
1060
|
+
...setupPlan.actions.map((action) => {
|
|
1061
|
+
if (isProviderSetupPiApiKeyAction(action)) {
|
|
1062
|
+
return action.persistence === "environment"
|
|
1063
|
+
? `${action.label}: read ${action.envVar} from owner-only .env; do not write Pi auth.json`
|
|
1064
|
+
: `${action.label}: save credential to owner-only ${action.piAuthPath}; do not copy it to .env`;
|
|
1065
|
+
}
|
|
1066
|
+
return `${action.label}: check the current credential/service state first; if needed run ${providerSetupActionCommandLine(action)} (cwd: ${action.cwd})`;
|
|
1067
|
+
}),
|
|
1068
|
+
...(draft.sandbox
|
|
1069
|
+
? ["Managed SRT: install the pinned tool in the private user cache and run the functional fail-closed preflight"]
|
|
1070
|
+
: []),
|
|
1071
|
+
];
|
|
288
1072
|
const lines = [
|
|
289
|
-
`
|
|
290
|
-
`
|
|
1073
|
+
`Agent: ${draft.name}`,
|
|
1074
|
+
`Purpose: ${draft.purpose}`,
|
|
1075
|
+
"Routes:",
|
|
1076
|
+
...runtimeRoutes.map((route, index) => ` ${index === 0 ? "Primary" : `Fallback ${index}`}: ${route.model} [${route.effort ?? "provider default"}]`),
|
|
291
1077
|
`Capabilities: ${capabilities.length > 0 ? capabilities.join(", ") : "none"}`,
|
|
292
1078
|
`Tools: ${toolsLine}`,
|
|
1079
|
+
`Route safety: ${draft.routeSafety}`,
|
|
1080
|
+
...(draft.routeSafety === "per-route-native" || !isMixedRouteChain(runtimeRoutes.map((route) => route.model))
|
|
1081
|
+
? [formatRouteSafetyMatrix({ model: draft.model, ...(draft.effort === undefined ? {} : { effort: draft.effort }) }, draft.fallbacks, draft.sandbox)]
|
|
1082
|
+
: [
|
|
1083
|
+
"Uniform contract: every route must satisfy the same mono-agent tool/sandbox capabilities; an incompatible route is rejected or skipped, never silently weakened.",
|
|
1084
|
+
]),
|
|
293
1085
|
...(alwaysOn.length > 0 ? [`Always on: ${alwaysOnDisplay(alwaysOn).join(", ")}`] : []),
|
|
294
|
-
`
|
|
295
|
-
`
|
|
1086
|
+
`Readiness: ${runtimeRoutes.length} real model call(s), one per selected route; ${potentiallyBilledCalls} potentially billed`,
|
|
1087
|
+
`Verify refs: ${setupModelRefs.join(", ")}`,
|
|
1088
|
+
`Provider actions: ${providerActions.length > 0 ? providerActions.join("\n ") : "none"}`,
|
|
1089
|
+
`Creates if missing (preserves existing scaffold paths): ${creates.join(", ")}`,
|
|
1090
|
+
...(mutableSecretFiles.length > 0
|
|
1091
|
+
? [`May create or update: ${mutableSecretFiles.join(", ")}`]
|
|
1092
|
+
: []),
|
|
1093
|
+
`Secret persistence: ${secrets.length > 0
|
|
1094
|
+
? `${secrets.join(", ")} -> owner-only .env merge (values hidden)`
|
|
1095
|
+
: hardenExistingDotenv
|
|
1096
|
+
? "existing sensitive .env -> owner-only permission/ignore hardening (values unchanged)"
|
|
1097
|
+
: "none"}`,
|
|
1098
|
+
...(Object.values(piApiKeyPersistenceByProvider).some((value) => value === "secure-store")
|
|
1099
|
+
? ["Pi credential persistence: selected keys -> owner-only Pi auth.json (values hidden; not copied to .env)"]
|
|
1100
|
+
: []),
|
|
1101
|
+
...(secrets.length > 0 || hardenExistingDotenv
|
|
1102
|
+
? [`Secret files: .env (${secrets.length > 0 ? "owner-only merge" : "owner-only hardening"}), .gitignore (ensure /.env is ignored)`]
|
|
1103
|
+
: []),
|
|
296
1104
|
];
|
|
297
|
-
p.note(lines.join("\n"), "
|
|
298
|
-
const
|
|
299
|
-
|
|
1105
|
+
p.note(lines.join("\n"), "Creation review");
|
|
1106
|
+
const choice = await select({
|
|
1107
|
+
message: `Create “${draft.name}”?`,
|
|
1108
|
+
options: creationReviewOptions({
|
|
1109
|
+
setupRequired: (preserveProviderSetup
|
|
1110
|
+
? options.existing.runProviderSetup
|
|
1111
|
+
: setupPlan.actions.length > 0) || draft.sandbox,
|
|
1112
|
+
}),
|
|
1113
|
+
initialValue: "create",
|
|
1114
|
+
});
|
|
1115
|
+
if (choice === "cancel") {
|
|
1116
|
+
throw new WizardCancelled();
|
|
1117
|
+
}
|
|
1118
|
+
if (choice === "edit") {
|
|
1119
|
+
try {
|
|
1120
|
+
const step = await select({
|
|
1121
|
+
message: "What would you like to edit?",
|
|
1122
|
+
options: [
|
|
1123
|
+
{ value: "0", label: "Agent name and purpose" },
|
|
1124
|
+
{ value: "1", label: "Models and efforts" },
|
|
1125
|
+
{ value: "2", label: "Channels" },
|
|
1126
|
+
{ value: "3", label: "Memory" },
|
|
1127
|
+
{ value: "4", label: "Capability details" },
|
|
1128
|
+
{ value: "5", label: "Tools" },
|
|
1129
|
+
{ value: "6", label: "Route safety and sandbox" },
|
|
1130
|
+
{ value: "7", label: "Observability" },
|
|
1131
|
+
{ value: "8", label: "Return to review" },
|
|
1132
|
+
],
|
|
1133
|
+
initialValue: "1",
|
|
1134
|
+
});
|
|
1135
|
+
return { status: "edit", step: Number(step) };
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
if (error instanceof WizardBack) {
|
|
1139
|
+
if (options !== undefined)
|
|
1140
|
+
throw error;
|
|
1141
|
+
return { status: "edit", step: 8 };
|
|
1142
|
+
}
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (preserveProviderSetup) {
|
|
1147
|
+
return {
|
|
1148
|
+
status: "create",
|
|
1149
|
+
providerSetup: {
|
|
1150
|
+
runProviderSetup: options.existing.runProviderSetup,
|
|
1151
|
+
providerSetupSecrets: { ...options.existing.providerSetupSecrets },
|
|
1152
|
+
providerEnvironmentSecrets: { ...options.existing.providerEnvironmentSecrets },
|
|
1153
|
+
piApiKeyPersistenceByProvider: { ...options.existing.piApiKeyPersistenceByProvider },
|
|
1154
|
+
},
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
let providerSetup;
|
|
1158
|
+
try {
|
|
1159
|
+
providerSetup = await collectProviderSetup(setupPlan, setupPlan.actions.length > 0, piApiKeyPersistenceByProvider, options?.existing.providerSetupSecrets, options?.existing.providerEnvironmentSecrets);
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
if (error instanceof WizardBack) {
|
|
1163
|
+
if (options !== undefined)
|
|
1164
|
+
throw error;
|
|
1165
|
+
return { status: "edit", step: 8 };
|
|
1166
|
+
}
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
status: "create",
|
|
1171
|
+
providerSetup,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function selectedRuntimeModels(draft) {
|
|
1175
|
+
return [draft.model, ...draft.fallbacks.map((fallback) => fallback.model)];
|
|
1176
|
+
}
|
|
1177
|
+
function sameOrderedValues(left, right) {
|
|
1178
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1179
|
+
}
|
|
1180
|
+
function hasFixedAllowAllToolPolicyRef(model) {
|
|
1181
|
+
return isDirectCodexRef(model) || isDirectOpenCodeRef(model);
|
|
1182
|
+
}
|
|
1183
|
+
function hasExactAllowAllTools(draft) {
|
|
1184
|
+
return draft.allowedTools.length === 1 && draft.allowedTools[0] === ALLOW_ALL_TOOLS;
|
|
1185
|
+
}
|
|
1186
|
+
function hasInvalidUniformManagedSrtChain(draft) {
|
|
1187
|
+
if (draft.routeSafety !== "uniform" || !draft.sandbox)
|
|
1188
|
+
return false;
|
|
1189
|
+
const models = selectedRuntimeModels(draft);
|
|
1190
|
+
return isMixedRouteChain(models)
|
|
1191
|
+
&& models.some((model) => model.startsWith("pi:"))
|
|
1192
|
+
&& models.some((model) => !model.startsWith("pi:"));
|
|
1193
|
+
}
|
|
1194
|
+
function isDirectCodexRef(model) {
|
|
1195
|
+
return model.startsWith("codex:");
|
|
1196
|
+
}
|
|
1197
|
+
function isDirectOpenCodeRef(model) {
|
|
1198
|
+
return model.startsWith("opencode:");
|
|
1199
|
+
}
|
|
1200
|
+
function runtimeFamily(model) {
|
|
1201
|
+
return model.split(":", 1)[0] ?? model;
|
|
1202
|
+
}
|
|
1203
|
+
function modelRefMayBill(model) {
|
|
1204
|
+
return !/^pi:(?:ollama|lmstudio):/u.test(model);
|
|
1205
|
+
}
|
|
1206
|
+
/** Build a plan using the destination-derived context for honest review/setup. */
|
|
1207
|
+
async function composePlanForCwd(answers, cwd) {
|
|
1208
|
+
return composeWizardPlan(answers, {
|
|
1209
|
+
dirBasename: basename(cwd),
|
|
1210
|
+
skillsRootExists: await pathExists(join(cwd, "skills")),
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
/** Offer auth/preflight for every runtime and hidden memory model dependency. */
|
|
1214
|
+
async function promptProviderSetup(plan, ctx, credentialStates = {}) {
|
|
1215
|
+
const modelRefs = referencedSetupModelRefs(plan);
|
|
1216
|
+
p.note(modelRefs.join("\n"), "Models and services to verify");
|
|
1217
|
+
const preliminarySetupPlan = providerSetupPlan(plan, ctx, credentialStates);
|
|
1218
|
+
if (preliminarySetupPlan.actions.length === 0) {
|
|
1219
|
+
return { runProviderSetup: false, providerSetupSecrets: {}, providerEnvironmentSecrets: {}, piApiKeyPersistenceByProvider: {} };
|
|
1220
|
+
}
|
|
1221
|
+
const piApiKeyPersistenceByProvider = await selectPiApiKeyPersistence(preliminarySetupPlan);
|
|
1222
|
+
const setupPlan = providerSetupPlan(plan, ctx, credentialStates, piApiKeyPersistenceByProvider);
|
|
1223
|
+
p.note(setupPlan.actions
|
|
1224
|
+
.map(providerSetupActionReviewLine)
|
|
1225
|
+
.join("\n"), "Provider setup");
|
|
1226
|
+
const runProviderSetup = await confirm({
|
|
1227
|
+
message: setupPlan.actions.some((action) => action.id.startsWith("pi-login:"))
|
|
1228
|
+
? "Run provider auth/preflight now? (detected credentials are reused and verified by live readiness; Pi OAuth may update the auth store)"
|
|
1229
|
+
: "Run provider auth/preflight now? (detected credentials are reused and verified by live readiness)",
|
|
1230
|
+
initialValue: false,
|
|
1231
|
+
});
|
|
1232
|
+
return collectProviderSetup(setupPlan, runProviderSetup, piApiKeyPersistenceByProvider);
|
|
1233
|
+
}
|
|
1234
|
+
function providerSetupPlan(plan, ctx, credentialStates = {}, piApiKeyPersistenceByProvider = {}) {
|
|
1235
|
+
const modelRefs = referencedSetupModelRefs(plan);
|
|
1236
|
+
const configuredPiAuthPath = typeof plan.configJson.providers?.piAuthPath === "string"
|
|
1237
|
+
? plan.configJson.providers.piAuthPath
|
|
1238
|
+
: undefined;
|
|
1239
|
+
const piAuthPath = ctx.piAuthPath ?? configuredPiAuthPath;
|
|
1240
|
+
return planProviderSetup({
|
|
1241
|
+
modelRefs,
|
|
300
1242
|
cwd: ctx.cwd,
|
|
301
|
-
|
|
1243
|
+
credentialStates,
|
|
1244
|
+
piApiKeyPersistenceByProvider,
|
|
1245
|
+
...(piAuthPath === undefined ? {} : { piAuthPath }),
|
|
302
1246
|
});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
1247
|
+
}
|
|
1248
|
+
async function collectProviderSetup(setupPlan, runProviderSetup, selectedPersistence, existingSetupSecrets = {}, existingEnvironmentSecrets = {}) {
|
|
1249
|
+
const providerSetupSecrets = {};
|
|
1250
|
+
const providerEnvironmentSecrets = {};
|
|
1251
|
+
if (!runProviderSetup) {
|
|
1252
|
+
return {
|
|
1253
|
+
runProviderSetup: false,
|
|
1254
|
+
providerSetupSecrets,
|
|
1255
|
+
providerEnvironmentSecrets,
|
|
1256
|
+
piApiKeyPersistenceByProvider: { ...(selectedPersistence ?? {}) },
|
|
1257
|
+
};
|
|
312
1258
|
}
|
|
313
|
-
|
|
314
|
-
|
|
1259
|
+
const piApiKeyPersistenceByProvider = { ...(selectedPersistence ?? {}) };
|
|
1260
|
+
for (const action of setupPlan.actions) {
|
|
1261
|
+
if (!isProviderSetupPiApiKeyAction(action))
|
|
1262
|
+
continue;
|
|
1263
|
+
if (action.persistence === "environment") {
|
|
1264
|
+
if (hasNonEmptyValue(existingEnvironmentSecrets[action.envVar])) {
|
|
1265
|
+
providerEnvironmentSecrets[action.envVar] = existingEnvironmentSecrets[action.envVar];
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
providerEnvironmentSecrets[action.envVar] = await passwordPrompt({
|
|
1269
|
+
message: `${action.label} (${action.envVar}, saved to owner-only .env)`,
|
|
1270
|
+
validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
|
|
1271
|
+
clearOnError: true,
|
|
1272
|
+
});
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
if (hasNonEmptyValue(existingSetupSecrets[action.id])) {
|
|
1276
|
+
providerSetupSecrets[action.id] = existingSetupSecrets[action.id];
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
providerSetupSecrets[action.id] = await passwordPrompt({
|
|
1280
|
+
message: `${action.label} (${action.envVar})`,
|
|
1281
|
+
validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
|
|
1282
|
+
clearOnError: true,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
return { runProviderSetup, providerSetupSecrets, providerEnvironmentSecrets, piApiKeyPersistenceByProvider };
|
|
1286
|
+
}
|
|
1287
|
+
async function selectPiApiKeyPersistence(setupPlan, existing = {}) {
|
|
1288
|
+
const selected = {};
|
|
1289
|
+
for (const action of setupPlan.actions) {
|
|
1290
|
+
if (!isProviderSetupPiApiKeyAction(action))
|
|
1291
|
+
continue;
|
|
1292
|
+
const label = action.label.replace(/ \((?:secure store|environment)\)$/u, "");
|
|
1293
|
+
const existingSelection = existing[action.provider];
|
|
1294
|
+
selected[action.provider] = existingSelection ?? await select({
|
|
1295
|
+
message: `Where should ${label} store ${action.envVar}?`,
|
|
1296
|
+
options: [
|
|
1297
|
+
{ value: "secure-store", label: "Store securely in Pi auth.json", hint: "owner-only Pi credential store" },
|
|
1298
|
+
{ value: "environment", label: "Use environment variable", hint: `owner-only .env (${action.envVar})` },
|
|
1299
|
+
],
|
|
1300
|
+
initialValue: "secure-store",
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
return selected;
|
|
1304
|
+
}
|
|
1305
|
+
function providerSetupActionReviewLine(action) {
|
|
1306
|
+
if (isProviderSetupPiApiKeyAction(action) && action.persistence === "environment") {
|
|
1307
|
+
return `${action.label}: read ${action.envVar} from the durable agent environment; Pi auth.json remains unchanged (cwd: ${action.cwd})`;
|
|
315
1308
|
}
|
|
316
|
-
return
|
|
1309
|
+
return `${action.label}: ${providerSetupActionCommandLine(action)} (cwd: ${action.cwd})`;
|
|
317
1310
|
}
|
|
318
1311
|
/** Seed a mutable draft from immutable answers (defaults or a preset). */
|
|
319
1312
|
function draftFrom(answers) {
|
|
@@ -328,9 +1321,13 @@ function draftFrom(answers) {
|
|
|
328
1321
|
moduleInputs[moduleId] = bag;
|
|
329
1322
|
}
|
|
330
1323
|
return {
|
|
1324
|
+
name: answers.name?.trim() || "Mono Agent",
|
|
1325
|
+
purpose: answers.purpose?.trim() || "Help the operator work effectively in this folder.",
|
|
331
1326
|
model: answers.model,
|
|
332
|
-
|
|
1327
|
+
fallbacks: effectiveFallbacks(answers).map((fallback) => ({ ...fallback })),
|
|
333
1328
|
effort: answers.effort,
|
|
1329
|
+
routeSafety: answers.routeSafety,
|
|
1330
|
+
credentialStates: {},
|
|
334
1331
|
channels: [...answers.channels],
|
|
335
1332
|
memory: answers.memory,
|
|
336
1333
|
sandbox: answers.sandbox,
|
|
@@ -345,10 +1342,16 @@ function draftFrom(answers) {
|
|
|
345
1342
|
* `undefined`).
|
|
346
1343
|
*/
|
|
347
1344
|
function toWizardAnswers(draft) {
|
|
1345
|
+
for (const modelRef of selectedRuntimeModels(draft)) {
|
|
1346
|
+
assertConcreteWizardModelRef(modelRef);
|
|
1347
|
+
}
|
|
348
1348
|
return {
|
|
1349
|
+
name: draft.name,
|
|
1350
|
+
purpose: draft.purpose,
|
|
349
1351
|
model: draft.model,
|
|
350
|
-
|
|
1352
|
+
fallbacks: draft.fallbacks.map((fallback) => ({ ...fallback })),
|
|
351
1353
|
...(draft.effort === undefined ? {} : { effort: draft.effort }),
|
|
1354
|
+
routeSafety: draft.routeSafety,
|
|
352
1355
|
channels: [...draft.channels],
|
|
353
1356
|
...(draft.memory === undefined ? {} : { memory: draft.memory }),
|
|
354
1357
|
sandbox: draft.sandbox,
|
|
@@ -357,9 +1360,22 @@ function toWizardAnswers(draft) {
|
|
|
357
1360
|
moduleInputs: draft.moduleInputs,
|
|
358
1361
|
};
|
|
359
1362
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1363
|
+
function cloneDraft(draft) {
|
|
1364
|
+
return {
|
|
1365
|
+
...draft,
|
|
1366
|
+
fallbacks: draft.fallbacks.map((fallback) => ({ ...fallback })),
|
|
1367
|
+
channels: [...draft.channels],
|
|
1368
|
+
allowedTools: [...draft.allowedTools],
|
|
1369
|
+
moduleInputs: Object.fromEntries(Object.entries(draft.moduleInputs).map(([id, values]) => [id, { ...values }])),
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function restoreDraft(target, source) {
|
|
1373
|
+
Object.assign(target, cloneDraft(source));
|
|
1374
|
+
}
|
|
1375
|
+
/** A second Esc/Ctrl-C while this default-No prompt is open exits cleanly. */
|
|
1376
|
+
async function confirmExitSetup() {
|
|
1377
|
+
const answer = await p.confirm({ message: "Exit setup?", initialValue: false });
|
|
1378
|
+
return p.isCancel(answer) ? true : answer;
|
|
363
1379
|
}
|
|
364
1380
|
/** True when `path` exists (a local mirror of the CLI's private helper). */
|
|
365
1381
|
async function pathExists(path) {
|