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,280 @@
1
+ // The one door to the provider.
2
+ //
3
+ // Every paid call leaves from here, and leaves only after the budget has been
4
+ // consulted. There used to be four other ways out — the intent classifier, the
5
+ // fact extractor, the outcome ask, and a stray copy of the benchmark judge —
6
+ // each with its own `fetch` and none of them within sight of a ceiling. The
7
+ // ceilings, meanwhile, were checked after the fact against numbers that did not
8
+ // include those calls. See src/agent/usage.js for the notebook side of it.
9
+ //
10
+ // A new caller must come through here. tests/unit/api/one-door.test.js walks
11
+ // src/ and fails the build if a second door appears.
12
+
13
+ import { freeRequestFields, paymentRequiredMessage, recordFreeRequest, createServedModelNotifier } from "../free-models.js";
14
+ import { config } from "../config.js";
15
+ import { getProvider } from "../providers/registry.js";
16
+ import * as openaiAdapter from "../providers/adapters/openai.js";
17
+ import * as anthropicAdapter from "../providers/adapters/anthropic.js";
18
+ import { fetchModelInfo as fetchModelInfoFromProvider } from "../providers/models.js";
19
+ import { assertWithinBudget, recordUsage } from "../agent/usage.js";
20
+
21
+ function getAdapter() {
22
+ const provider = getProvider(config.provider);
23
+ if (!provider) return { adapter: openaiAdapter, provider: getProvider("openrouter") };
24
+ if (provider.format === "anthropic") return { adapter: anthropicAdapter, provider };
25
+ return { adapter: openaiAdapter, provider };
26
+ }
27
+
28
+ /**
29
+ * RX-1 fix: Combine external AbortSignal with an internal timeout signal.
30
+ * Returns a signal that aborts when either fires. Compatible with Node 18+.
31
+ */
32
+ function combineAbortSignals(external, timeoutMs) {
33
+ const combined = new AbortController();
34
+ let timeoutHandle = null;
35
+ let externalHandler = null;
36
+
37
+ const cleanup = () => {
38
+ if (timeoutHandle) clearTimeout(timeoutHandle);
39
+ if (external && externalHandler) {
40
+ try { external.removeEventListener("abort", externalHandler); } catch {}
41
+ }
42
+ };
43
+
44
+ // External signal already aborted — return immediately
45
+ if (external?.aborted) {
46
+ combined.abort(external.reason);
47
+ return { signal: combined.signal, cleanup };
48
+ }
49
+
50
+ // Set up timeout
51
+ timeoutHandle = setTimeout(() => {
52
+ combined.abort(new Error(`API call exceeded timeout of ${timeoutMs}ms`));
53
+ }, timeoutMs);
54
+
55
+ // Forward external abort
56
+ if (external) {
57
+ externalHandler = () => combined.abort(external.reason);
58
+ external.addEventListener("abort", externalHandler, { once: true });
59
+ }
60
+
61
+ return { signal: combined.signal, cleanup };
62
+ }
63
+
64
+ /**
65
+ * Call the provider. The only function in src/ that does.
66
+ *
67
+ * @param {Array} messages
68
+ * @param {Array} tools
69
+ * @param {Function} onToken
70
+ * @param {object} [opts]
71
+ * @param {AbortSignal} [opts.signal]
72
+ * @param {string} [opts.model] model for this call; defaults to config.model
73
+ * @param {string} [opts.source="agent"] which part of Flint is spending. Must
74
+ * be one of USAGE_SOURCES — it is what the bill is broken down by.
75
+ * @param {number} [opts.maxTokens] defaults to config.maxResponseTokens
76
+ * @param {number} [opts.temperature]
77
+ * @param {number} [opts.seed]
78
+ * @param {object} [opts.responseFormat] e.g. {type: "json_object"}
79
+ * @param {number} [opts.timeoutMs] hard ceiling for this call
80
+ * @param {boolean} [opts.stream=true] side calls want the whole answer at once
81
+ * @throws {BudgetExceededError} before sending anything, when a ceiling is spent
82
+ */
83
+ // Free mode notices go to the console through this (index.js sets it).
84
+ const notifyServedModel = createServedModelNotifier();
85
+ let freeNoticeSink = null;
86
+ export function setFreeNoticeSink(fn) { freeNoticeSink = fn; }
87
+
88
+ export async function chatCompletion(messages, tools, onToken, {
89
+ signal,
90
+ model: modelOverride,
91
+ source = "agent",
92
+ maxTokens,
93
+ temperature,
94
+ seed,
95
+ responseFormat,
96
+ timeoutMs,
97
+ stream: streamOpt = true,
98
+ } = {}) {
99
+ // FIRST, before anything is built or sent. The ceilings are read live because
100
+ // /budget can move them mid-session; 0 means unlimited, which is the default.
101
+ // The run ceiling of an autonomous run lives in the ledger and is checked in
102
+ // the same call, so all three refuse in one place.
103
+ assertWithinBudget({ perAction: config.maxCostPerAction, session: config.sessionBudget });
104
+
105
+ // Strip internal metadata fields before sending to API
106
+ const cleanMessages = messages.map((m) => {
107
+ if (m._toolName || m._toolArgs || m._compressed) {
108
+ const { _toolName, _toolArgs, _compressed, ...clean } = m;
109
+ return clean;
110
+ }
111
+ return m;
112
+ });
113
+
114
+ // Detect if messages contain images — disable streaming for image requests
115
+ const hasImages = cleanMessages.some((m) =>
116
+ Array.isArray(m.content) && m.content.some((p) => p.type === "image_url")
117
+ );
118
+
119
+ const { adapter, provider } = getAdapter();
120
+ const apiKey = config.apiKey;
121
+ const stream = streamOpt && !hasImages;
122
+
123
+ const headers = adapter.buildHeaders(provider, apiKey);
124
+ const effectiveModel = modelOverride || config.model;
125
+ const body = adapter.buildBody(
126
+ cleanMessages,
127
+ effectiveModel,
128
+ tools,
129
+ maxTokens || config.maxResponseTokens,
130
+ stream,
131
+ { temperature, seed, responseFormat },
132
+ );
133
+ // OpenRouter's provider preference, for the main model only. One model is
134
+ // served by several hosts that bill and answer differently (DigitalOcean
135
+ // billed cache reads at 27x Xiaomi's rate on 2026-09-28 and once answered a
136
+ // file-sorting task about Kubernetes). A side call on another model must not
137
+ // inherit "only these hosts": they may not serve it.
138
+ if (config.openrouterProvider && provider?.id === "openrouter" && effectiveModel === config.model) {
139
+ body.provider = config.openrouterProvider;
140
+ }
141
+ // Free mode: the main model's request names the fallbacks, and OpenRouter
142
+ // answers from the first that works (free-models.js).
143
+ Object.assign(body, freeRequestFields({ chain: config.freeChain, provider, model: effectiveModel, mainModel: config.model }));
144
+ const freeCall = !!config.freeChain?.includes(effectiveModel);
145
+ const url = adapter.getChatUrl(provider);
146
+
147
+ // Env-gated snapshot of the exact prompt going to the LLM. Used to compare
148
+ // Flint's wrapped prompt vs. raw model behaviour on the same task. Writes
149
+ // one JSON per call to ~/.flint/agent-prompt-snapshots/.
150
+ // Only the main loop: this dump is an instrument for comparing Flint's
151
+ // wrapped prompt against a raw one, and now the classifier, the fact
152
+ // extractor and the outcome ask come through here too. Mixing their payloads
153
+ // into the same directory would quietly change what the measurement means.
154
+ if (process.env.FLINT_DUMP_AGENT_PROMPT === "1" && source === "agent") {
155
+ try {
156
+ const { writeFileSync, existsSync, mkdirSync } = await import("node:fs");
157
+ const { join } = await import("node:path");
158
+ const { homedir } = await import("node:os");
159
+ const dir = join(homedir(), ".flint", "agent-prompt-snapshots");
160
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
161
+ const ts = Date.now();
162
+ const file = join(dir, `${ts}.json`);
163
+ writeFileSync(file, JSON.stringify({
164
+ ts,
165
+ model: effectiveModel,
166
+ url,
167
+ messages: cleanMessages,
168
+ tools: tools,
169
+ body_summary: {
170
+ message_count: cleanMessages.length,
171
+ tool_count: Array.isArray(tools) ? tools.length : 0,
172
+ total_chars: JSON.stringify(cleanMessages).length,
173
+ },
174
+ }, null, 2));
175
+ } catch {}
176
+ }
177
+
178
+ // RX-1 fix: hard timeout on API call (configurable).
179
+ // Prevents agent loop from hanging indefinitely when provider is slow
180
+ // or when streaming never terminates. Combines with external abort signal.
181
+ //
182
+ // 600s, not 120: the ceiling is on the whole call, and a reasoning model on a
183
+ // long context was cut off mid-answer and paid for again. The SWE pilot of
184
+ // 2026-09-26 lost three calls in a row that way and the turn ended with
185
+ // "API error (3x)". 600 is the OpenAI SDK default.
186
+ const API_TIMEOUT = timeoutMs || parseInt(process.env.AGENT_API_TIMEOUT || "600", 10) * 1000;
187
+ const { signal: effectiveSignal, cleanup } = combineAbortSignals(signal, API_TIMEOUT);
188
+
189
+ try {
190
+ const response = await fetch(url, {
191
+ method: "POST",
192
+ headers,
193
+ body: JSON.stringify(body),
194
+ signal: effectiveSignal,
195
+ });
196
+
197
+ if (!response.ok) {
198
+ const text = await response.text();
199
+ // Surface rate-limit / quota errors explicitly so the user sees them.
200
+ // Fail-fast on daily / monthly quota (no retry would help).
201
+ if (response.status === 429) {
202
+ const retryAfter = response.headers.get("retry-after");
203
+ const err = new Error(
204
+ `Rate limit (429): ${text.slice(0, 300)}${retryAfter ? ` | retry-after: ${retryAfter}s` : ""}`
205
+ );
206
+ err.statusCode = 429;
207
+ err.isRateLimit = true;
208
+ err.retryAfter = retryAfter ? parseInt(retryAfter, 10) : null;
209
+ throw err;
210
+ }
211
+ if (response.status === 401 || response.status === 403) {
212
+ const err = new Error(`Auth failed (${response.status}): check API key for provider '${provider?.id || "unknown"}'. ${text.slice(0, 200)}`);
213
+ err.statusCode = response.status;
214
+ err.isAuthError = true;
215
+ throw err;
216
+ }
217
+ if (response.status === 402) {
218
+ const err = new Error(paymentRequiredMessage(freeCall) || `Payment required (402): insufficient credits. ${text.slice(0, 200)}`);
219
+ err.statusCode = 402;
220
+ err.isQuotaError = true;
221
+ throw err;
222
+ }
223
+ // The status travels with the error: a caller deciding whether to retry
224
+ // needs to tell a client error from a transient one, and parsing it back
225
+ // out of the message is not a decision, it is a guess.
226
+ throw Object.assign(new Error(`API ${response.status}: ${text}`), { statusCode: response.status });
227
+ }
228
+
229
+ const result = !stream
230
+ ? adapter.parseResponse(await response.json(), onToken)
231
+ : await adapter.parseStreamResponse(response, onToken);
232
+
233
+ // Free mode: today's count for the footer, and one line when a fallback
234
+ // answered instead of the primary.
235
+ if (freeCall) {
236
+ recordFreeRequest();
237
+ if (effectiveModel === config.model) {
238
+ const note = notifyServedModel(config.model, result.servedModel);
239
+ if (note) freeNoticeSink?.(note);
240
+ }
241
+ }
242
+
243
+ // The caller does not record anything. Two sources were declared and never
244
+ // wired to a counter before this moved here, both times
245
+ // because recording was somebody else's job.
246
+ recordUsage(source, result.usage);
247
+ return result;
248
+ } finally {
249
+ cleanup();
250
+ }
251
+ }
252
+
253
+ export async function fetchModelInfo(modelId) {
254
+ // Use provider-aware model info fetching
255
+ const providerId = config.provider;
256
+ const info = await fetchModelInfoFromProvider(providerId, modelId);
257
+ if (info) return info;
258
+
259
+ // Fallback: try OpenRouter directly (backward compat)
260
+ if (providerId === "openrouter") {
261
+ try {
262
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
263
+ headers: { Authorization: `Bearer ${config.apiKey}` },
264
+ });
265
+ if (!res.ok) return null;
266
+ const data = await res.json();
267
+ const model = data.data?.find((m) => m.id === modelId);
268
+ if (!model?.pricing) return null;
269
+ return {
270
+ prompt: parseFloat(model.pricing.prompt),
271
+ completion: parseFloat(model.pricing.completion),
272
+ contextLength: model.context_length || null,
273
+ };
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ return null;
280
+ }