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,270 @@
1
+ // Unified Message Bus — SQLite-backed priority queue
2
+ // All input channels (TUI, API, child agents, scheduler) push here.
3
+ // Agent loop drains by priority. Crash-safe: pending messages survive restart.
4
+
5
+ import { getDb } from "../tasks/db.js";
6
+ import { createLogger } from "../logging/logger.js";
7
+
8
+ const log = createLogger("bus");
9
+
10
+ // Ring buffer for recent bus events (for /bus/log endpoint)
11
+ const _eventLog = [];
12
+ const MAX_EVENTS = 100;
13
+ function logEvent(type, msg) {
14
+ _eventLog.push({ type, ts: new Date().toISOString(), ...msg });
15
+ if (_eventLog.length > MAX_EVENTS) _eventLog.shift();
16
+ }
17
+
18
+ /**
19
+ * Get recent bus events (last N).
20
+ * @param {number} [limit=50]
21
+ */
22
+ export function recentEvents(limit = 50) {
23
+ return _eventLog.slice(-limit);
24
+ }
25
+
26
+ // Priority levels (lower = higher priority)
27
+ export const PRIORITY = {
28
+ USER: 0, // TUI input, /commands
29
+ API: 1, // HTTP /message from paired client
30
+ AGENT: 2, // Child agent responses
31
+ TASK: 3, // Scheduled tasks firing
32
+ EVENT: 4, // Email, Telegram, external events
33
+ SYSTEM: 5, // BG process alerts, heartbeat
34
+ };
35
+
36
+ /**
37
+ * Push a message onto the bus.
38
+ * @param {object} msg
39
+ * @param {string} msg.channel - 'user' | 'api' | 'agent' | 'scheduler' | 'telegram' | 'email' | 'voice'
40
+ * @param {string} msg.content - Message text
41
+ * @param {number} [msg.priority=5] - 0=highest, 10=lowest
42
+ * @param {string} [msg.source] - Agent port, email address, etc.
43
+ * @param {object} [msg.metadata] - Channel-specific data (stored as JSON)
44
+ * @param {string} [msg.sessionId] - Session context
45
+ * @returns {{ id: number }} Inserted message ID
46
+ */
47
+ export function push({ channel, content, priority, source, metadata, sessionId }) {
48
+ const db = getDb();
49
+ const result = db.prepare(
50
+ `INSERT INTO message_queue (channel, content, priority, source, metadata, session_id)
51
+ VALUES (?, ?, ?, ?, ?, ?)`
52
+ ).run(
53
+ channel,
54
+ content,
55
+ priority ?? PRIORITY.SYSTEM,
56
+ source || null,
57
+ metadata ? JSON.stringify(metadata) : null,
58
+ sessionId || null,
59
+ );
60
+ const id = result.lastInsertRowid;
61
+ log.debug("PUSH", { id, channel, priority: priority ?? PRIORITY.SYSTEM, source });
62
+ logEvent("push", { id: Number(id), channel, priority: priority ?? PRIORITY.SYSTEM, source, contentPreview: content.slice(0, 80) });
63
+ return { id: Number(id) };
64
+ }
65
+
66
+ /**
67
+ * Drain the highest-priority pending message.
68
+ * Atomically sets status = 'processing'.
69
+ * @returns {object|null} Message row or null if queue empty
70
+ */
71
+ export function drain() {
72
+ const db = getDb();
73
+ // Single atomic operation: select + update
74
+ const msg = db.prepare(
75
+ `SELECT * FROM message_queue
76
+ WHERE status = 'pending'
77
+ ORDER BY priority ASC, created_at ASC
78
+ LIMIT 1`
79
+ ).get();
80
+
81
+ if (!msg) return null;
82
+
83
+ db.prepare(
84
+ `UPDATE message_queue SET status = 'processing', processed_at = datetime('now') WHERE id = ?`
85
+ ).run(msg.id);
86
+
87
+ log.debug("DRAIN", { id: msg.id, channel: msg.channel, priority: msg.priority });
88
+ logEvent("drain", { id: msg.id, channel: msg.channel, priority: msg.priority });
89
+
90
+ return {
91
+ ...msg,
92
+ status: "processing",
93
+ metadata: msg.metadata ? JSON.parse(msg.metadata) : null,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Mark a message as successfully processed.
99
+ * @param {number} id - Message ID
100
+ * @param {string} [result] - Agent response summary (for audit)
101
+ */
102
+ export function complete(id, result) {
103
+ const db = getDb();
104
+ db.prepare(
105
+ `UPDATE message_queue SET status = 'done', result = ?, processed_at = datetime('now') WHERE id = ?`
106
+ ).run(result || null, id);
107
+ log.debug("DONE", { id, resultLen: result?.length || 0 });
108
+ logEvent("done", { id, resultPreview: (result || "").slice(0, 80) });
109
+ }
110
+
111
+ /**
112
+ * Mark a message as failed.
113
+ * @param {number} id - Message ID
114
+ * @param {string} [error] - Error description
115
+ */
116
+ /**
117
+ * Take back a message that is still waiting, for the operator to edit it
118
+ * (Esc on an unread message). Only a pending message: once the agent has
119
+ * taken it, it is read and cannot be recalled.
120
+ * @returns {boolean} true if it was still pending and is now cancelled
121
+ */
122
+ export function cancelPending(id) {
123
+ const db = getDb();
124
+ const r = db.prepare(
125
+ `UPDATE message_queue SET status = 'failed', result = 'recalled for editing', processed_at = datetime('now') WHERE id = ? AND status = 'pending'`
126
+ ).run(id);
127
+ if (r.changes) logEvent("cancel", { id });
128
+ return r.changes > 0;
129
+ }
130
+
131
+ export function fail(id, error) {
132
+ const db = getDb();
133
+ db.prepare(
134
+ `UPDATE message_queue SET status = 'failed', result = ?, processed_at = datetime('now') WHERE id = ?`
135
+ ).run(error || "unknown error", id);
136
+ log.warn("FAIL", { id, error });
137
+ logEvent("fail", { id, error });
138
+ }
139
+
140
+ /**
141
+ * Crash recovery: reset stale 'processing' messages back to 'pending'.
142
+ * Called on startup to recover from mid-processing crashes.
143
+ * @param {number} [staleMinutes=5] - Messages processing longer than this are stale
144
+ * @returns {number} Number of recovered messages
145
+ */
146
+ export function recover(staleMinutes = 5) {
147
+ const db = getDb();
148
+ const result = db.prepare(
149
+ `UPDATE message_queue
150
+ SET status = 'pending', processed_at = NULL
151
+ WHERE status = 'processing'
152
+ AND processed_at < datetime('now', ? || ' minutes')`
153
+ ).run(`-${staleMinutes}`);
154
+ const count = result.changes;
155
+ if (count > 0) {
156
+ log.info("RECOVER", { count, staleMinutes });
157
+ logEvent("recover", { count, staleMinutes });
158
+ }
159
+ return count;
160
+ }
161
+
162
+ // Older than this, nobody is still waiting for the answer: an API caller's
163
+ // sync wait is 180 s, a child agent's report is about work long gone.
164
+ const LEFTOVER_MAX_AGE_MINUTES = 10;
165
+
166
+ /**
167
+ * Start-up: put this session's half-done messages back in the queue, and
168
+ * expire what an earlier run left for someone else.
169
+ *
170
+ * Owner, 2026-10-02: a console started with --new drained a benchmark task an
171
+ * API client had sent five days earlier to another session, and ran it with
172
+ * the API's automatic approval. A message belongs to the session it was sent
173
+ * to and to the minutes after it was sent. Expired: any message of another
174
+ * session, and any message older than LEFTOVER_MAX_AGE_MINUTES. Reminders
175
+ * (channel 'scheduler') are kept: they are the operator's own schedule.
176
+ *
177
+ * @param {object} opts
178
+ * @param {string} opts.sessionId - The session this start runs
179
+ * @returns {{ recovered: number, expired: number }}
180
+ */
181
+ export function prepareQueueAtStart({ sessionId }) {
182
+ const recovered = recover();
183
+ const db = getDb();
184
+ const r = db.prepare(
185
+ `UPDATE message_queue
186
+ SET status = 'failed', result = 'expired: left in the queue by an earlier run', processed_at = datetime('now')
187
+ WHERE status IN ('pending', 'processing')
188
+ AND channel != 'scheduler'
189
+ AND ((session_id IS NOT NULL AND session_id != ?)
190
+ OR created_at < datetime('now', ? || ' minutes'))`
191
+ ).run(sessionId || "", `-${LEFTOVER_MAX_AGE_MINUTES}`);
192
+ const expired = r.changes;
193
+ if (expired > 0) {
194
+ log.info("EXPIRE", { expired, sessionId });
195
+ logEvent("expire", { count: expired });
196
+ }
197
+ return { recovered, expired };
198
+ }
199
+
200
+ /**
201
+ * Cleanup old processed messages.
202
+ * @param {number} [doneHours=24] - Delete 'done' messages older than this
203
+ * @param {number} [failedDays=7] - Delete 'failed' messages older than this
204
+ * @returns {{ done: number, failed: number }} Counts of deleted messages
205
+ */
206
+ export function cleanup(doneHours = 24, failedDays = 7) {
207
+ const db = getDb();
208
+ const doneResult = db.prepare(
209
+ `DELETE FROM message_queue
210
+ WHERE status = 'done'
211
+ AND processed_at < datetime('now', ? || ' hours')`
212
+ ).run(`-${doneHours}`);
213
+ const failedResult = db.prepare(
214
+ `DELETE FROM message_queue
215
+ WHERE status = 'failed'
216
+ AND processed_at < datetime('now', ? || ' days')`
217
+ ).run(`-${failedDays}`);
218
+ const counts = { done: doneResult.changes, failed: failedResult.changes };
219
+ if (counts.done + counts.failed > 0) {
220
+ log.debug("CLEANUP", counts);
221
+ }
222
+ return counts;
223
+ }
224
+
225
+ /**
226
+ * Get queue statistics for StatusBar and monitoring.
227
+ * @returns {{ pending: number, processing: number, done: number, failed: number, total: number }}
228
+ */
229
+ export function stats() {
230
+ const db = getDb();
231
+ const rows = db.prepare(
232
+ `SELECT status, COUNT(*) as count FROM message_queue GROUP BY status`
233
+ ).all();
234
+ const s = { pending: 0, processing: 0, done: 0, failed: 0, total: 0 };
235
+ for (const row of rows) {
236
+ s[row.status] = row.count;
237
+ s.total += row.count;
238
+ }
239
+ return s;
240
+ }
241
+
242
+ /**
243
+ * Get all pending messages (for inspection, not processing).
244
+ * @param {number} [limit=50]
245
+ * @returns {object[]}
246
+ */
247
+ export function pending(limit = 50) {
248
+ const db = getDb();
249
+ return db.prepare(
250
+ `SELECT * FROM message_queue
251
+ WHERE status = 'pending'
252
+ ORDER BY priority ASC, created_at ASC
253
+ LIMIT ?`
254
+ ).all(limit).map(row => ({
255
+ ...row,
256
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
257
+ }));
258
+ }
259
+
260
+ /**
261
+ * Get message by ID (for polling result).
262
+ * @param {number} id
263
+ * @returns {object|null}
264
+ */
265
+ export function getMessage(id) {
266
+ const db = getDb();
267
+ const row = db.prepare("SELECT * FROM message_queue WHERE id = ?").get(id);
268
+ if (!row) return null;
269
+ return { ...row, metadata: row.metadata ? JSON.parse(row.metadata) : null };
270
+ }
@@ -0,0 +1,65 @@
1
+ // Channel plugin loader
2
+ // Scans plugins/ directory for channel plugins.
3
+ // Each plugin: { name, priority, start(bus), stop() }
4
+
5
+ import { createLogger } from "../logging/logger.js";
6
+ import { readdirSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { config } from "../config.js";
9
+
10
+ const log = createLogger("bus-plugins");
11
+
12
+ const _plugins = new Map(); // name → plugin instance
13
+
14
+ /**
15
+ * Load and start all channel plugins from plugins/ directory.
16
+ * @param {object} bus - bus module (push, drain, etc.)
17
+ */
18
+ export async function loadPlugins(bus) {
19
+ const pluginsDir = path.join(config.projectRoot, "plugins");
20
+ let files;
21
+ try {
22
+ files = readdirSync(pluginsDir).filter(f => f.endsWith(".js"));
23
+ } catch {
24
+ // No plugins directory — that's fine
25
+ return;
26
+ }
27
+
28
+ for (const file of files) {
29
+ try {
30
+ const mod = await import(path.join(pluginsDir, file));
31
+ const plugin = mod.default || mod;
32
+ if (!plugin.name || !plugin.start) {
33
+ log.warn(`Plugin ${file} missing name or start(), skipping`);
34
+ continue;
35
+ }
36
+ await plugin.start(bus);
37
+ _plugins.set(plugin.name, plugin);
38
+ log.info(`Plugin loaded: ${plugin.name}`, { file, priority: plugin.priority });
39
+ } catch (err) {
40
+ log.error(`Failed to load plugin ${file}: ${err.message}`);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Stop all loaded plugins.
47
+ */
48
+ export async function stopPlugins() {
49
+ for (const [name, plugin] of _plugins) {
50
+ try {
51
+ if (plugin.stop) await plugin.stop();
52
+ log.debug(`Plugin stopped: ${name}`);
53
+ } catch (err) {
54
+ log.error(`Failed to stop plugin ${name}: ${err.message}`);
55
+ }
56
+ }
57
+ _plugins.clear();
58
+ }
59
+
60
+ /**
61
+ * Get list of loaded plugin names.
62
+ */
63
+ export function listPlugins() {
64
+ return [..._plugins.keys()];
65
+ }
@@ -0,0 +1,14 @@
1
+ // When a child agent counts as idle, for its idle timeout (index.js).
2
+ //
3
+ // The timeout was reset only when the store's message count changed, and a
4
+ // turn writes its messages back at the end. A child given a research task
5
+ // shut itself down one minute in, between two browser calls, with nothing
6
+ // written (2026-10-02, agent@3010). Working is not idle.
7
+
8
+ /** A turn, queued work, or a stoppable task in flight: not idle. */
9
+ export function childBusy({ processing, processingCount, agentStatus, abortController } = {}) {
10
+ return !!processing
11
+ || (processingCount || 0) > 0
12
+ || (!!agentStatus && agentStatus !== "idle")
13
+ || !!abortController;
14
+ }
package/src/cli.js ADDED
@@ -0,0 +1,118 @@
1
+ import chalk from "chalk";
2
+ import { listSessions } from "./sessions.js";
3
+ import { migrateEnvKey, setKey, hasKey } from "./providers/keys.js";
4
+ import { listProviders, getProvider } from "./providers/registry.js";
5
+ import { setActiveProvider, setLastModel } from "./providers/state.js";
6
+ import { config, needsFirstRunSetup } from "./config.js";
7
+
8
+ // -- CLI argument parsing --
9
+
10
+ export function getArgValue(name) {
11
+ const idx = process.argv.indexOf(name);
12
+ return (idx !== -1 && process.argv[idx + 1]) ? process.argv[idx + 1] : null;
13
+ }
14
+
15
+ export function parseCLI() {
16
+ const args = process.argv;
17
+ if (args.includes("--list")) return { action: "list" };
18
+ if (args.includes("--headless")) {
19
+ const taskIdx = args.indexOf("--task");
20
+ const task = (taskIdx !== -1 && args[taskIdx + 1]) ? args[taskIdx + 1] : null;
21
+ const cwdIdx = args.indexOf("--cwd");
22
+ const cwd = (cwdIdx !== -1 && args[cwdIdx + 1]) ? args[cwdIdx + 1] : null;
23
+ const budgetIdx = args.indexOf("--budget");
24
+ const budget = (budgetIdx !== -1 && args[budgetIdx + 1]) ? parseFloat(args[budgetIdx + 1]) : null;
25
+ return { action: "headless", task, cwd, budget };
26
+ }
27
+ if (args.includes("--new")) return { action: "new" };
28
+ if (args.includes("--last")) return { action: "last" };
29
+ const idx = args.indexOf("--session");
30
+ if (idx !== -1 && args[idx + 1])
31
+ return { action: "resume", id: args[idx + 1] };
32
+ return { action: "new" };
33
+ }
34
+
35
+ export async function runListSessions() {
36
+ const sessions = await listSessions();
37
+ if (!sessions.length) {
38
+ console.log(chalk.gray("No sessions found."));
39
+ } else {
40
+ console.log(chalk.cyan.bold("\n Sessions:\n"));
41
+ for (const s of sessions) {
42
+ console.log(` ${chalk.yellow(s.id)} ${chalk.gray(s.model)} ${s.preview}`);
43
+ }
44
+ console.log();
45
+ }
46
+ process.exit(0);
47
+ }
48
+
49
+ export async function migrateKeys() {
50
+ await migrateEnvKey("OPENROUTER_API_KEY", "openrouter");
51
+ await migrateEnvKey("OPENAI_API_KEY", "openai");
52
+ await migrateEnvKey("ANTHROPIC_API_KEY", "anthropic");
53
+ }
54
+
55
+ /** Real provider keys are long tokens; a few characters is a stray keypress. */
56
+ export function looksLikeApiKey(key) {
57
+ const k = String(key || "").trim();
58
+ return k.length >= 20 && !/\s/.test(k);
59
+ }
60
+
61
+ export async function runFirstRunSetup(cli) {
62
+ if (!needsFirstRunSetup || cli.action === "list") return;
63
+
64
+ const readline = await import("node:readline");
65
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
66
+ const ask = (q) => new Promise((r) => rl.question(q, r));
67
+
68
+ console.log(chalk.cyan("\n +-------------------------------------+"));
69
+ console.log(chalk.cyan(" | Flint Agent -- First Run |"));
70
+ console.log(chalk.cyan(" +-------------------------------------+\n"));
71
+ console.log(chalk.gray(" No API keys found. Let's set one up.\n"));
72
+
73
+ const providers = listProviders().filter((p) => p.keyRequired);
74
+ for (let i = 0; i < providers.length; i++) {
75
+ console.log(chalk.white(` ${i + 1}. ${providers[i].name} (${providers[i].id})`));
76
+ }
77
+ console.log(chalk.white(` ${providers.length + 1}. Ollama (local, no key needed)`));
78
+ console.log();
79
+
80
+ const choice = await ask(chalk.cyan(" Choose provider (number): "));
81
+ const idx = parseInt(choice.trim(), 10) - 1;
82
+
83
+ if (idx === providers.length) {
84
+ config.provider = "ollama";
85
+ setActiveProvider("ollama");
86
+ config.model = "llama3.2";
87
+ console.log(chalk.green("\n Selected Ollama. Make sure it's running on localhost:11434.\n"));
88
+ } else if (idx >= 0 && idx < providers.length) {
89
+ const selected = providers[idx];
90
+ // Owner, 2026-10-01: the spinner hid this prompt, a stray "1" was saved as
91
+ // the OpenRouter key, and the wizard said "Key saved". Anything this short
92
+ // is not a key, so ask again rather than store it.
93
+ let trimmedKey = "";
94
+ for (let attempt = 0; attempt < 3; attempt++) {
95
+ trimmedKey = (await ask(chalk.cyan(` Enter ${selected.name} API key: `))).trim();
96
+ if (!trimmedKey || looksLikeApiKey(trimmedKey)) break;
97
+ console.log(chalk.yellow(" That does not look like an API key (too short). Paste the full key."));
98
+ trimmedKey = "";
99
+ }
100
+ if (trimmedKey) {
101
+ await setKey(selected.id, trimmedKey);
102
+ config.provider = selected.id;
103
+ config.apiKey = trimmedKey;
104
+ setActiveProvider(selected.id);
105
+ config.model = selected.defaultModel;
106
+ console.log(chalk.green(`\n Key saved (encrypted). Provider: ${selected.name}, Model: ${selected.defaultModel}\n`));
107
+ } else {
108
+ console.log(chalk.yellow("\n No key entered. You can add one later with /key <provider>.\n"));
109
+ }
110
+ } else {
111
+ console.log(chalk.yellow("\n Invalid choice. You can set up later with /key <provider>.\n"));
112
+ }
113
+
114
+ // Once, at the end of the first run: where Flint lives (owner, 2026-10-02:
115
+ // one line, no nagging).
116
+ console.log(chalk.gray(" Flint is free and open source: https://klymentiev.com/projects/flint\n"));
117
+ rl.close();
118
+ }