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.
Files changed (171) hide show
  1. package/.env.example +108 -0
  2. package/CHANGELOG.md +55 -0
  3. package/FEATURES.md +298 -0
  4. package/LICENSE +21 -0
  5. package/README.md +435 -0
  6. package/bin/flint.js +47 -0
  7. package/config/classifier-prompt.md +218 -0
  8. package/config/models-curated.json +4 -0
  9. package/config/providers.json +74 -0
  10. package/package.json +92 -0
  11. package/patches/ink+6.8.0.patch +78 -0
  12. package/profiles/desktop.md +65 -0
  13. package/profiles/generic.md +20 -0
  14. package/profiles/marketer.md +20 -0
  15. package/profiles/profiles.json +34 -0
  16. package/profiles/ux-reviewer.md +25 -0
  17. package/src/agent/agent.js +1743 -0
  18. package/src/agent/auto.js +346 -0
  19. package/src/agent/backoff.js +143 -0
  20. package/src/agent/compression.js +310 -0
  21. package/src/agent/content-resolver.js +180 -0
  22. package/src/agent/flow-controller.js +309 -0
  23. package/src/agent/intent-manifest.js +231 -0
  24. package/src/agent/intent-timeout.js +46 -0
  25. package/src/agent/intent.js +633 -0
  26. package/src/agent/knowledge.js +114 -0
  27. package/src/agent/learning.js +180 -0
  28. package/src/agent/modes.js +187 -0
  29. package/src/agent/outcome-ask.js +91 -0
  30. package/src/agent/project-context.js +76 -0
  31. package/src/agent/prompt-budget.js +117 -0
  32. package/src/agent/reflection-extractor.js +140 -0
  33. package/src/agent/steering.js +86 -0
  34. package/src/agent/supervisor.js +430 -0
  35. package/src/agent/swap.js +443 -0
  36. package/src/agent/system-prompt.js +446 -0
  37. package/src/agent/time-stamp.js +48 -0
  38. package/src/agent/tool-guard.js +201 -0
  39. package/src/agent/toolcall-text.js +162 -0
  40. package/src/agent/usage.js +297 -0
  41. package/src/agent/vision.js +94 -0
  42. package/src/agent/watchdog.js +139 -0
  43. package/src/agent/workspace-changes.js +177 -0
  44. package/src/api/address.js +14 -0
  45. package/src/api/client.js +280 -0
  46. package/src/api/server.js +535 -0
  47. package/src/api/stream-pipe.js +113 -0
  48. package/src/app-state.js +39 -0
  49. package/src/bootstrap.js +501 -0
  50. package/src/bus/drain-loop.js +497 -0
  51. package/src/bus/index.js +270 -0
  52. package/src/bus/plugins.js +65 -0
  53. package/src/child-idle.js +14 -0
  54. package/src/cli.js +118 -0
  55. package/src/commands/commands.js +1297 -0
  56. package/src/commands/registry.js +132 -0
  57. package/src/components/App.js +491 -0
  58. package/src/components/CarefulMenu.js +145 -0
  59. package/src/components/HistoryWriter.js +86 -0
  60. package/src/components/LineInput.js +69 -0
  61. package/src/components/LiveZone.js +294 -0
  62. package/src/components/OverlayMenu.js +179 -0
  63. package/src/components/SystemPanel.js +156 -0
  64. package/src/components/Table.js +54 -0
  65. package/src/config.js +249 -0
  66. package/src/free-models.js +230 -0
  67. package/src/index.js +1111 -0
  68. package/src/input-handler.js +13 -0
  69. package/src/input-text.js +123 -0
  70. package/src/launcher.js +129 -0
  71. package/src/logging/api-log.js +95 -0
  72. package/src/logging/chat-log-follower.js +113 -0
  73. package/src/logging/chat-log.js +15 -0
  74. package/src/logging/log-collector.js +182 -0
  75. package/src/logging/logger.js +112 -0
  76. package/src/logging/tool-log.js +20 -0
  77. package/src/mcp-client.js +314 -0
  78. package/src/memory/conversation-digest.js +113 -0
  79. package/src/memory/extract-facts.js +98 -0
  80. package/src/memory/facts.js +181 -0
  81. package/src/memory/inbox.js +63 -0
  82. package/src/memory/markdown.js +38 -0
  83. package/src/memory/patterns.js +185 -0
  84. package/src/memory/project.js +66 -0
  85. package/src/memory/reflections.js +74 -0
  86. package/src/memory/retrieval.js +84 -0
  87. package/src/memory/rules.js +105 -0
  88. package/src/memory/session-facts.js +125 -0
  89. package/src/memory/skills.js +191 -0
  90. package/src/memory/sqlite-store.js +653 -0
  91. package/src/memory/store.js +208 -0
  92. package/src/memory/tools.js +196 -0
  93. package/src/memory/user-model.js +86 -0
  94. package/src/message-handler.js +775 -0
  95. package/src/model-check.js +218 -0
  96. package/src/plugins/loader.js +120 -0
  97. package/src/plugins/manager.js +88 -0
  98. package/src/production-env.js +22 -0
  99. package/src/profiles.js +42 -0
  100. package/src/providers/adapters/anthropic.js +270 -0
  101. package/src/providers/adapters/openai.js +120 -0
  102. package/src/providers/keys-dpapi.js +41 -0
  103. package/src/providers/keys-fallback.js +31 -0
  104. package/src/providers/keys.js +132 -0
  105. package/src/providers/models.js +154 -0
  106. package/src/providers/registry.js +56 -0
  107. package/src/providers/state.js +56 -0
  108. package/src/registry.js +96 -0
  109. package/src/restart.js +29 -0
  110. package/src/sandbox/backend.js +130 -0
  111. package/src/security/api-auth.js +132 -0
  112. package/src/security/audit.js +98 -0
  113. package/src/security/child-policy.js +41 -0
  114. package/src/security/command-guard.js +173 -0
  115. package/src/security/content-fence.js +250 -0
  116. package/src/security/content-validator.js +132 -0
  117. package/src/security/index.js +143 -0
  118. package/src/security/network-guard.js +126 -0
  119. package/src/security/pairing.js +180 -0
  120. package/src/security/path-guard.js +140 -0
  121. package/src/security/persona-guard.js +67 -0
  122. package/src/security/policies.js +452 -0
  123. package/src/security/safety-constants.js +34 -0
  124. package/src/security/watchdog.js +107 -0
  125. package/src/sessions.js +130 -0
  126. package/src/spend.js +97 -0
  127. package/src/startup-watchdog.js +59 -0
  128. package/src/stdio/args.js +71 -0
  129. package/src/stdio/guard.js +59 -0
  130. package/src/stdio/protocol.js +167 -0
  131. package/src/stdio/run.js +106 -0
  132. package/src/stdio/session.js +180 -0
  133. package/src/store/agent-slice.js +306 -0
  134. package/src/store/dataset-slice.js +73 -0
  135. package/src/store/index.js +22 -0
  136. package/src/store/process-slice.js +135 -0
  137. package/src/store/session-slice.js +191 -0
  138. package/src/store/ui-slice.js +119 -0
  139. package/src/tasks/db.js +184 -0
  140. package/src/tasks/queries.js +589 -0
  141. package/src/tools/agent-tools.js +473 -0
  142. package/src/tools/checkpoint.js +152 -0
  143. package/src/tools/command-approvals.js +180 -0
  144. package/src/tools/dataset.js +50 -0
  145. package/src/tools/filesystem.js +682 -0
  146. package/src/tools/inbox-tools.js +48 -0
  147. package/src/tools/mesh.js +135 -0
  148. package/src/tools/own-env.js +136 -0
  149. package/src/tools/permissions.js +681 -0
  150. package/src/tools/plugin-tools.js +123 -0
  151. package/src/tools/process-tools.js +595 -0
  152. package/src/tools/registry.js +307 -0
  153. package/src/tools/swap-tools.js +72 -0
  154. package/src/tools/system.js +662 -0
  155. package/src/tools/tasks.js +532 -0
  156. package/src/tools/tool-search.js +171 -0
  157. package/src/ui/header.js +140 -0
  158. package/src/ui/input-cursor.js +23 -0
  159. package/src/ui/last-line.js +25 -0
  160. package/src/ui/line-edit.js +135 -0
  161. package/src/ui/output.js +399 -0
  162. package/src/ui/paste-tokens.js +131 -0
  163. package/src/ui/prompt-attention.js +134 -0
  164. package/src/ui/render-options.js +13 -0
  165. package/src/ui/replay.js +94 -0
  166. package/src/ui/splash.js +49 -0
  167. package/src/ui/status-level.js +36 -0
  168. package/src/ui/tool-ledger.js +203 -0
  169. package/src/ui/window-title.js +150 -0
  170. package/src/update.js +205 -0
  171. package/system.md +63 -0
@@ -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
+ }