flint-agent 1.14.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/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- package/system.md +63 -0
package/src/bootstrap.js
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { config } from "./config.js";
|
|
3
|
+
import { store } from "./store/index.js";
|
|
4
|
+
import { app } from "./app-state.js";
|
|
5
|
+
import { getArgValue } from "./cli.js";
|
|
6
|
+
import { initRegistry, registerMcpTools, registerMeshTools, registerTaskTools, registerDatasetTools, registerScheduleTools, registerInboxTools, registerPlugin, setMcpManagement } from "./tools/registry.js";
|
|
7
|
+
import { loadPlugins } from "./plugins/loader.js";
|
|
8
|
+
import { ensureOwnEnv } from "./tools/own-env.js";
|
|
9
|
+
import { setPluginContext } from "./tools/plugin-tools.js";
|
|
10
|
+
import { getSandboxMode } from "./sandbox/backend.js";
|
|
11
|
+
import { initPermissions, askOnboardingIfNeeded, getOnboardingState, getConfirmTimeoutMs } from "./tools/permissions.js";
|
|
12
|
+
import { whileWaitingForOperator } from "./startup-watchdog.js";
|
|
13
|
+
import { initSecurity } from "./security/index.js";
|
|
14
|
+
import { initCommands } from "./commands/registry.js";
|
|
15
|
+
import { getSystemMessage } from "./agent/system-prompt.js";
|
|
16
|
+
import { fetchModelInfo } from "./api/client.js";
|
|
17
|
+
import { initLogger, createLogger } from "./logging/logger.js";
|
|
18
|
+
import { loadProfile } from "./profiles.js";
|
|
19
|
+
import { createTaskTools, formatPlanForPrompt } from "./tools/tasks.js";
|
|
20
|
+
// schedule-tools.js removed — reminders are now add_task with next_run/repeat
|
|
21
|
+
import { createInboxTools, handleInboxTool } from "./tools/inbox-tools.js";
|
|
22
|
+
import { datasetToolDefs, createDatasetHandlers } from "./tools/dataset.js";
|
|
23
|
+
import { syncPlanToStore } from "./tasks/queries.js";
|
|
24
|
+
import { setPricingSource, seedSessionSpend } from "./agent/usage.js";
|
|
25
|
+
import {
|
|
26
|
+
generateSessionId,
|
|
27
|
+
saveSession,
|
|
28
|
+
loadSession,
|
|
29
|
+
listSessions,
|
|
30
|
+
} from "./sessions.js";
|
|
31
|
+
import { initOutput, printWarning } from "./ui/output.js";
|
|
32
|
+
import { formatToolArgsFull } from "./ui/header.js";
|
|
33
|
+
import { approvalArgLines, approvalFitsLive } from "./components/LiveZone.js";
|
|
34
|
+
import { promptAttentionStart, promptAttentionStop } from "./ui/prompt-attention.js";
|
|
35
|
+
|
|
36
|
+
export function buildSystemMessage(profileName, sessionId) {
|
|
37
|
+
app.profileConfig = loadProfile(profileName);
|
|
38
|
+
// A host's own instructions (stdio mode, --system-prompt-file and friends)
|
|
39
|
+
// come after the profile. They add to Flint's prompt rather than replace
|
|
40
|
+
// it: without the core prompt the model would not know Flint's tools.
|
|
41
|
+
const profileContent = [app.profileConfig.content, app.hostPrompt].filter(Boolean).join("\n\n");
|
|
42
|
+
return getSystemMessage(sessionId || null, {
|
|
43
|
+
profile: profileName,
|
|
44
|
+
profileContent,
|
|
45
|
+
datasets: store.getState().datasets,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Bootstrap phases ──
|
|
50
|
+
|
|
51
|
+
function initProfile() {
|
|
52
|
+
const explicitProfile = getArgValue("--profile") || process.env.AGENT_PROFILE;
|
|
53
|
+
app.activeProfile = explicitProfile || "generic";
|
|
54
|
+
try {
|
|
55
|
+
app.profileConfig = loadProfile(app.activeProfile);
|
|
56
|
+
} catch {
|
|
57
|
+
app.activeProfile = "generic";
|
|
58
|
+
app.profileConfig = loadProfile(app.activeProfile);
|
|
59
|
+
}
|
|
60
|
+
app.systemMessage = null; // deferred until tools are registered
|
|
61
|
+
return explicitProfile;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function initStore() {
|
|
65
|
+
initOutput(store);
|
|
66
|
+
initRegistry(store);
|
|
67
|
+
initCommands(store);
|
|
68
|
+
initPermissions({
|
|
69
|
+
confirm: (toolName, args) => new Promise((resolve) => {
|
|
70
|
+
// Announce the wait, so a prompt that needs the operator reaches one
|
|
71
|
+
// rather than sitting in a window nobody is looking at. Start before the
|
|
72
|
+
// prompt is stored and stop when it is answered — including on the
|
|
73
|
+
// timeout path, where stop() runs from the same wrapped resolve.
|
|
74
|
+
const attention = promptAttentionStart();
|
|
75
|
+
let settled = false;
|
|
76
|
+
let timer = null;
|
|
77
|
+
const wrappedResolve = (answer) => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
if (timer) clearTimeout(timer);
|
|
81
|
+
promptAttentionStop(attention);
|
|
82
|
+
const current = store.getState().pendingConfirmation;
|
|
83
|
+
if (current && current.toolName === toolName) {
|
|
84
|
+
store.getState().clearPendingConfirmation();
|
|
85
|
+
}
|
|
86
|
+
resolve(answer);
|
|
87
|
+
};
|
|
88
|
+
// The question is shown in the live zone only (LiveZone confirmText) and
|
|
89
|
+
// answered with one key; history gets one line with the answer
|
|
90
|
+
// (printConfirmResult). It used to print a three-line block that stayed
|
|
91
|
+
// in history for good (owner, 2026-10-01).
|
|
92
|
+
// The whole command, never a cut one (ui/header.js formatToolArgsFull).
|
|
93
|
+
const argsText = formatToolArgsFull(args);
|
|
94
|
+
// Too long for the live zone: printed in full into the history, once.
|
|
95
|
+
const argLines = approvalArgLines(argsText);
|
|
96
|
+
if (!approvalFitsLive(argLines.length)) {
|
|
97
|
+
store.getState().addLine(chalk.yellow(` ? ${toolName} asks for approval, full command:`));
|
|
98
|
+
for (const l of argLines) store.getState().addLine(l);
|
|
99
|
+
}
|
|
100
|
+
store.getState().setPendingConfirmation({ id: Date.now(), toolName, args, argsText, resolve: wrappedResolve });
|
|
101
|
+
// permissions.js gives up after its window; take the question off the
|
|
102
|
+
// screen at the same moment, or the input would stay waiting for y/n.
|
|
103
|
+
timer = setTimeout(() => wrappedResolve("timeout"), getConfirmTimeoutMs());
|
|
104
|
+
timer.unref?.();
|
|
105
|
+
}),
|
|
106
|
+
// No timeout passed on purpose.
|
|
107
|
+
//
|
|
108
|
+
// This used to pass `timeout: 30000`, which overrode the 600 s the module
|
|
109
|
+
// sets for itself — and the module's own comment explains at length why
|
|
110
|
+
// 30 s was abandoned: it caused a retry-storm, because a prompt that gave up
|
|
111
|
+
// after half a minute sent every denied tool straight back to the model,
|
|
112
|
+
// which retried, which timed out again. The prompt text was then corrected
|
|
113
|
+
// to read from getConfirmTimeoutMs() (68e1f21), which made the UI say "10m
|
|
114
|
+
// timeout" while the answer was still being cut off at 30 s by this line.
|
|
115
|
+
// Two sources of truth, and this was the wrong one. Leaving it unset lets
|
|
116
|
+
// permissions.js own the number, which is what the fix in 68e1f21 assumed
|
|
117
|
+
// was already true.
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
app.securityApi = initSecurity(store, config);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error(`[security] Outer init failed: ${err.message}\n${err.stack}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The one onboarding question, on a fresh home.
|
|
128
|
+
*
|
|
129
|
+
* Asked here and nowhere else, before the session takes any work, because the
|
|
130
|
+
* level it records governs the guard registered a few lines above in initStore.
|
|
131
|
+
* Asking later would mean the first turn of a first run was decided by a
|
|
132
|
+
* default the operator had not been asked to accept.
|
|
133
|
+
*
|
|
134
|
+
* Backlog item 13. It used to be one readline line — a heading, three bracketed
|
|
135
|
+
* options and "pick s, n or p", all on one row — and on 2026-09-30 11:05 the
|
|
136
|
+
* owner read it as a wall of text and did not know what was being asked. It is
|
|
137
|
+
* now a menu with a heading, the three options one per line each with a plain
|
|
138
|
+
* sentence of what it means, the default marked and highlighted, arrows and
|
|
139
|
+
* Enter to choose and a letter still accepted.
|
|
140
|
+
*
|
|
141
|
+
* The menu is rendered with ink, like everything else on screen, so it is in
|
|
142
|
+
* colour and takes the keyboard the operator is already holding. ink is
|
|
143
|
+
* imported here rather than at the top of the file so a headless run never
|
|
144
|
+
* loads React: this is the only thing in bootstrap that needs it.
|
|
145
|
+
*
|
|
146
|
+
* The "already answered" check happens BEFORE anything is rendered or any
|
|
147
|
+
* interface is taken over the terminal — otherwise the menu would be painted
|
|
148
|
+
* over the top of a normal session start on every run after the first.
|
|
149
|
+
*
|
|
150
|
+
* Non-interactive stdin (a pipe, CI, a test) is still not asked, and the
|
|
151
|
+
* documented default applies. That direction of failing is deliberate: the
|
|
152
|
+
* level nobody chose is never written on their behalf.
|
|
153
|
+
*/
|
|
154
|
+
export async function initOnboarding() {
|
|
155
|
+
if (!process.stdin.isTTY) return;
|
|
156
|
+
// Asked on the very first run only. See the note above.
|
|
157
|
+
if (getOnboardingState().asked) return;
|
|
158
|
+
// No startup countdown while the operator reads the question.
|
|
159
|
+
await whileWaitingForOperator(askCarefulLevel);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function askCarefulLevel() {
|
|
163
|
+
// Declared out here, not inside the try: the try only covers rendering, and
|
|
164
|
+
// the answer is read after it. A `let answer` inside the block would be out
|
|
165
|
+
// of scope at the confirm below and every run would silently answer null.
|
|
166
|
+
let answer = null;
|
|
167
|
+
let instance = null;
|
|
168
|
+
try {
|
|
169
|
+
const [{ render }, { CarefulMenu }, { createElement: h }] = await Promise.all([
|
|
170
|
+
import("ink"),
|
|
171
|
+
import("./components/CarefulMenu.js"),
|
|
172
|
+
import("react"),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
// One promise for the whole question, answered by whichever way the menu
|
|
176
|
+
// closes. `answer` stays null unless the menu reports a choice, which is
|
|
177
|
+
// what makes Esc and a crash both mean "not answered".
|
|
178
|
+
let resolveAnswer;
|
|
179
|
+
const answered = new Promise((r) => { resolveAnswer = r; });
|
|
180
|
+
|
|
181
|
+
instance = render(
|
|
182
|
+
h(CarefulMenu, {
|
|
183
|
+
// askOnboardingIfNeeded still owns validity and persistence, so there
|
|
184
|
+
// is one place that decides what an answer is worth. The menu only
|
|
185
|
+
// reports which option was chosen.
|
|
186
|
+
onSelect: (level) => { answer = level; resolveAnswer(); },
|
|
187
|
+
onCancel: () => resolveAnswer(),
|
|
188
|
+
}),
|
|
189
|
+
{ exitOnCtrlC: false },
|
|
190
|
+
);
|
|
191
|
+
// Either way, both promises settle on the same event, so the menu is never
|
|
192
|
+
// left holding the terminal after this returns.
|
|
193
|
+
await Promise.race([answered, instance.waitUntilExit()]);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// A first-run question must never be the reason Flint does not start.
|
|
196
|
+
console.error(`[onboarding] question not shown: ${err.message}`);
|
|
197
|
+
} finally {
|
|
198
|
+
// The menu must let go of stdin before the session's own ink instance is
|
|
199
|
+
// created a few steps later in index.js. Two ink instances reading the same
|
|
200
|
+
// terminal both get the keystrokes, so arrows and letters land in the wrong
|
|
201
|
+
// place roughly half the time — and the leftover listener is one of the
|
|
202
|
+
// per-call AbortSignal-style listeners that once leaked. Unmounting
|
|
203
|
+
// here is what makes the question a moment rather than a second owner of
|
|
204
|
+
// the keyboard.
|
|
205
|
+
try { instance?.unmount(); } catch { /* already gone */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// askOnboardingIfNeeded owns validity and persistence, so there is one place
|
|
209
|
+
// that decides what an answer is worth: parseLevelAnswer refuses anything
|
|
210
|
+
// unknown, and a refused or absent answer is not saved, so the question comes
|
|
211
|
+
// back next start and the documented default applies meanwhile.
|
|
212
|
+
await askOnboardingIfNeeded({ confirm: async () => answer });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function initTools() {
|
|
216
|
+
const { tools: taskTools, handlers: taskHandlers } = createTaskTools({
|
|
217
|
+
getStore: () => store,
|
|
218
|
+
});
|
|
219
|
+
registerTaskTools(taskTools, taskHandlers);
|
|
220
|
+
|
|
221
|
+
const inboxTools = createInboxTools();
|
|
222
|
+
registerInboxTools(inboxTools, { check_inbox: (args) => handleInboxTool("check_inbox", args) });
|
|
223
|
+
|
|
224
|
+
registerDatasetTools(datasetToolDefs, createDatasetHandlers(store));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function initMesh() {
|
|
228
|
+
if (config.memoryUrl) {
|
|
229
|
+
try {
|
|
230
|
+
const { tools: meshTools, handlers: meshHandlers } = await import("./tools/mesh.js");
|
|
231
|
+
registerMeshTools(meshTools, meshHandlers);
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function initMcp() {
|
|
237
|
+
app.mcpReady = false;
|
|
238
|
+
if (!config.mcpServers) {
|
|
239
|
+
app.mcpStatusList = [];
|
|
240
|
+
// Nothing to load is loaded. Left false, every API message waited the
|
|
241
|
+
// full 15 s for tools that were never coming (every benchmark task).
|
|
242
|
+
app.mcpReady = true;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Parse server names immediately for "connecting..." display in header
|
|
247
|
+
try {
|
|
248
|
+
const names = Array.isArray(config.mcpServers)
|
|
249
|
+
? config.mcpServers.map(s => s.name)
|
|
250
|
+
: config.mcpServers.split(",").map(s => s.split("|")[0].trim()).filter(Boolean);
|
|
251
|
+
app.mcpStatusList = names.map(name => ({ name, tools: 0, ok: false, connecting: true }));
|
|
252
|
+
} catch {
|
|
253
|
+
app.mcpStatusList = [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Connect in background — TUI already visible
|
|
257
|
+
(async () => {
|
|
258
|
+
try {
|
|
259
|
+
const { connectMcpServers, disconnectServer, reconnectServer, getServerStatus } = await import("./mcp-client.js");
|
|
260
|
+
const { tools: mcpTools, handlers: mcpHandlers, results } = await connectMcpServers(config.mcpServers);
|
|
261
|
+
registerMcpTools(mcpTools, mcpHandlers, results);
|
|
262
|
+
app.mcpStatusList = results;
|
|
263
|
+
app.mcpReady = true;
|
|
264
|
+
setMcpManagement({
|
|
265
|
+
getServerStatus: () => getServerStatus(config.mcpServers),
|
|
266
|
+
disconnect: disconnectServer,
|
|
267
|
+
reconnect: (name) => reconnectServer(name, config.mcpServers),
|
|
268
|
+
});
|
|
269
|
+
app.systemMessage = buildSystemMessage(app.activeProfile);
|
|
270
|
+
// Show per-server results
|
|
271
|
+
for (const mc of results) {
|
|
272
|
+
if (mc.ok) {
|
|
273
|
+
store.getState().addLine(chalk.green(` mcp: ${mc.name} connected (${mc.tools} tools)`));
|
|
274
|
+
} else {
|
|
275
|
+
store.getState().addLine(chalk.yellow(` mcp: ${mc.name} -- ${mc.error || "failed"}`));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MCP health check + auto-reconnect (every 60s)
|
|
280
|
+
const mcpHealthInterval = setInterval(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const statuses = getServerStatus(config.mcpServers);
|
|
283
|
+
for (const s of statuses) {
|
|
284
|
+
if (!s.connected) {
|
|
285
|
+
store.getState().addLine(chalk.yellow(` mcp: ${s.name} disconnected — reconnecting...`));
|
|
286
|
+
try {
|
|
287
|
+
const result = await reconnectServer(s.name, config.mcpServers);
|
|
288
|
+
if (result.ok) {
|
|
289
|
+
registerMcpTools(result.tools ? [result] : [], result.handlers || {}, [result]);
|
|
290
|
+
app.systemMessage = buildSystemMessage(app.activeProfile);
|
|
291
|
+
store.getState().addLine(chalk.green(` mcp: ${s.name} reconnected (${result.tools || 0} tools)`));
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
store.getState().addLine(chalk.yellow(` mcp: ${s.name} reconnect failed: ${e.message}`));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch {}
|
|
299
|
+
}, 60000);
|
|
300
|
+
mcpHealthInterval.unref(); // don't prevent process exit
|
|
301
|
+
} catch (err) {
|
|
302
|
+
app.mcpStatusList = [{ name: "mcp", tools: 0, ok: false, error: err.message }];
|
|
303
|
+
store.getState().addLine(chalk.yellow(` MCP: ${err.message}`));
|
|
304
|
+
// Loading is over, it failed. Waiting on it would only delay every message.
|
|
305
|
+
app.mcpReady = true;
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function initPlugins() {
|
|
311
|
+
// The same context for a plugin installed later from the conversation.
|
|
312
|
+
setPluginContext({ config, store });
|
|
313
|
+
const pluginResult = await loadPlugins();
|
|
314
|
+
for (const plugin of pluginResult.loaded) {
|
|
315
|
+
try {
|
|
316
|
+
if (plugin.init) await plugin.init({ config, store });
|
|
317
|
+
registerPlugin(plugin);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
pluginResult.errors.push({ name: plugin.name, error: err.message });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (pluginResult.errors.length) {
|
|
323
|
+
for (const e of pluginResult.errors) {
|
|
324
|
+
printWarning(`Plugin ${e.name}: ${e.error}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function initSystemMessage() {
|
|
330
|
+
try {
|
|
331
|
+
const { prefetchAgentMemory } = await import("./agent/system-prompt.js");
|
|
332
|
+
await prefetchAgentMemory();
|
|
333
|
+
} catch {}
|
|
334
|
+
app.systemMessage = buildSystemMessage(app.activeProfile);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function initSession(cli, explicitProfile) {
|
|
338
|
+
store.setState({ _version: cli._version, _port: config.port, _profile: app.activeProfile });
|
|
339
|
+
|
|
340
|
+
let initialMessages = null;
|
|
341
|
+
let initialInputHistory = [];
|
|
342
|
+
|
|
343
|
+
function restoreProfile(data) {
|
|
344
|
+
if (!explicitProfile && data.profile) {
|
|
345
|
+
app.activeProfile = data.profile;
|
|
346
|
+
try {
|
|
347
|
+
app.profileConfig = loadProfile(app.activeProfile);
|
|
348
|
+
app.systemMessage = buildSystemMessage(app.activeProfile);
|
|
349
|
+
} catch {}
|
|
350
|
+
}
|
|
351
|
+
store.setState({ _profile: app.activeProfile });
|
|
352
|
+
store.getState().setProfile(app.activeProfile);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function restoreSession(sessionId) {
|
|
356
|
+
const data = await loadSession(sessionId);
|
|
357
|
+
if (data.messages?.[0]?.role === "system") {
|
|
358
|
+
data.messages[0] = app.systemMessage;
|
|
359
|
+
}
|
|
360
|
+
const s = store.getState();
|
|
361
|
+
s.setSession(sessionId, data.messages, data.inputHistory || []);
|
|
362
|
+
s.setPlan(data.plan || null);
|
|
363
|
+
s.setLastSummary(data.lastSummary || null);
|
|
364
|
+
if (data.pastedImages) store.setState({ pastedImages: data.pastedImages });
|
|
365
|
+
// Restore session cost — budget tracks across restarts. The ledger
|
|
366
|
+
// is told too, and it is the one the session ceiling is enforced against:
|
|
367
|
+
// a resumed session that starts the count at zero has no ceiling.
|
|
368
|
+
if (data.sessionCost) {
|
|
369
|
+
store.setState({
|
|
370
|
+
sessionCost: data.sessionCost,
|
|
371
|
+
sessionPromptTokens: data.sessionPromptTokens || 0,
|
|
372
|
+
sessionCompletionTokens: data.sessionCompletionTokens || 0,
|
|
373
|
+
});
|
|
374
|
+
seedSessionSpend(data.sessionCost);
|
|
375
|
+
}
|
|
376
|
+
restoreProfile(data);
|
|
377
|
+
initialMessages = data.messages;
|
|
378
|
+
initialInputHistory = data.inputHistory || [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (cli.action === "last") {
|
|
382
|
+
const sessions = await listSessions();
|
|
383
|
+
if (sessions.length) await restoreSession(sessions[0].id);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (cli.action === "resume") {
|
|
387
|
+
try {
|
|
388
|
+
await restoreSession(cli.id);
|
|
389
|
+
} catch {
|
|
390
|
+
console.error(chalk.red(`Session "${cli.id}" not found.`));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// stdio mode: the host names the session. Its id is kept as given, and an
|
|
396
|
+
// existing one is continued whichever flag named it: a host decides between
|
|
397
|
+
// --session-id and --resume from its own records, and a wrong guess must
|
|
398
|
+
// not cost the conversation (some CLIs refuse both ways, and their hosts
|
|
399
|
+
// carry workarounds for that).
|
|
400
|
+
if (cli.action === "stdio" && cli.sessionId) {
|
|
401
|
+
try {
|
|
402
|
+
await restoreSession(cli.sessionId);
|
|
403
|
+
} catch {
|
|
404
|
+
store.getState().setSession(cli.sessionId, [app.systemMessage], []);
|
|
405
|
+
store.getState().setProfile(app.activeProfile);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!store.getState().sessionId) {
|
|
410
|
+
const newId = generateSessionId();
|
|
411
|
+
store.getState().setSession(newId, [app.systemMessage], []);
|
|
412
|
+
store.getState().setProfile(app.activeProfile);
|
|
413
|
+
listSessions().then(sessions => {
|
|
414
|
+
if (sessions.length) {
|
|
415
|
+
return loadSession(sessions[0].id).then(data => {
|
|
416
|
+
if (data.inputHistory?.length) {
|
|
417
|
+
store.setState({ inputHistory: data.inputHistory });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}).catch(() => {});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
store.getState().setModel(config.model);
|
|
425
|
+
store.getState().setProvider(config.provider);
|
|
426
|
+
|
|
427
|
+
return { initialMessages, initialInputHistory };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function initWorkspace() {
|
|
431
|
+
const sessionId = store.getState().sessionId;
|
|
432
|
+
if (config.workdirBase) {
|
|
433
|
+
config.workdir = config.workdirBase;
|
|
434
|
+
} else {
|
|
435
|
+
config.workdir = (await import("node:path")).default.join(config.sessionsDir, sessionId, "workspace");
|
|
436
|
+
}
|
|
437
|
+
await import("node:fs").then(fs => fs.promises.mkdir(config.workdir, { recursive: true }));
|
|
438
|
+
process.env._FLINT_WORKDIR = config.workdir;
|
|
439
|
+
|
|
440
|
+
initLogger({ sessionsDir: config.sessionsDir, sessionId });
|
|
441
|
+
return createLogger("main");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Main bootstrap ──
|
|
445
|
+
|
|
446
|
+
export async function bootstrap(cli, pkg) {
|
|
447
|
+
const _bootTimes = [];
|
|
448
|
+
const _t = (label) => _bootTimes.push([label, performance.now()]);
|
|
449
|
+
|
|
450
|
+
_t("start");
|
|
451
|
+
const explicitProfile = initProfile();
|
|
452
|
+
_t("initProfile");
|
|
453
|
+
// Before initStore, and that ordering is the entire point.
|
|
454
|
+
//
|
|
455
|
+
// initStore is what registers the command guard, and the guard reads the
|
|
456
|
+
// level once, at registration. Asked afterwards, the answer is recorded and
|
|
457
|
+
// consulted by nothing for the rest of the session: the question would look
|
|
458
|
+
// like it had worked and change no prompt at all.
|
|
459
|
+
await initOnboarding();
|
|
460
|
+
_t("initOnboarding");
|
|
461
|
+
initStore();
|
|
462
|
+
_t("initStore");
|
|
463
|
+
initTools();
|
|
464
|
+
_t("initTools");
|
|
465
|
+
await initMesh();
|
|
466
|
+
_t("initMesh");
|
|
467
|
+
initMcp();
|
|
468
|
+
_t("initMcp");
|
|
469
|
+
await initPlugins();
|
|
470
|
+
_t("initPlugins");
|
|
471
|
+
// Not awaited: making the venv takes seconds, and the first shell command
|
|
472
|
+
// waits on the same promise anyway. Started here so it is usually ready.
|
|
473
|
+
if (getSandboxMode() !== "docker") ensureOwnEnv();
|
|
474
|
+
await initSystemMessage();
|
|
475
|
+
_t("initSystemMessage");
|
|
476
|
+
|
|
477
|
+
cli._version = pkg.version;
|
|
478
|
+
const { initialMessages, initialInputHistory } = await initSession(cli, explicitProfile);
|
|
479
|
+
_t("initSession");
|
|
480
|
+
|
|
481
|
+
const log = await initWorkspace();
|
|
482
|
+
_t("initWorkspace");
|
|
483
|
+
|
|
484
|
+
syncPlanToStore(store);
|
|
485
|
+
_t("syncPlan");
|
|
486
|
+
|
|
487
|
+
// The ledger prices a call itself only when the provider reported no cost.
|
|
488
|
+
// It reads the rate card from here rather than importing the store, so the
|
|
489
|
+
// accounting stays testable on its own.
|
|
490
|
+
setPricingSource(() => store.getState().pricing);
|
|
491
|
+
|
|
492
|
+
fetchModelInfo(config.model).then(modelInfo => {
|
|
493
|
+
if (modelInfo) store.getState().setPricing(modelInfo);
|
|
494
|
+
}).catch(() => {});
|
|
495
|
+
_t("fetchModelInfo");
|
|
496
|
+
|
|
497
|
+
log.info("boot-timings", Object.fromEntries(_bootTimes.map(([k, v]) => [k, Math.round(v)])));
|
|
498
|
+
log.info(`Flint starting`, { port: config.port, model: config.model, profile: app.activeProfile });
|
|
499
|
+
|
|
500
|
+
return { log, initialMessages, initialInputHistory, explicitProfile };
|
|
501
|
+
}
|