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,179 @@
1
+ // OverlayMenu — interactive selection list rendered in place of tab content
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import stringWidth from "string-width";
5
+
6
+ const { createElement: h } = React;
7
+
8
+ /** Cut to a display width, with an ellipsis; wide characters count as two. */
9
+ export function fitEnd(str, width) {
10
+ const s = String(str || "");
11
+ if (stringWidth(s) <= width) return s;
12
+ let out = "";
13
+ for (const ch of s) {
14
+ if (stringWidth(out + ch) > width - 1) break;
15
+ out += ch;
16
+ }
17
+ return out + "…";
18
+ }
19
+
20
+ /** "10-02 14:24" for a session's last change, local time. */
21
+ export function sessionWhen(date) {
22
+ if (!date || Number.isNaN(date.getTime?.())) return "";
23
+ const p = (n) => String(n).padStart(2, "0");
24
+ return `${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`;
25
+ }
26
+
27
+ function pad(str, len) {
28
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
29
+ }
30
+
31
+ export function OverlayMenu({ overlay, height }) {
32
+ if (!overlay) return null;
33
+
34
+ const { title, items: rawItems, index, loading, type, sortMode } = overlay;
35
+
36
+ // Sort items for model view
37
+ const items = type === "model" && sortMode && sortMode !== "name"
38
+ ? [...rawItems].sort((a, b) => {
39
+ if (sortMode === "price") {
40
+ return (a._priceNum ?? Infinity) - (b._priceNum ?? Infinity);
41
+ }
42
+ if (sortMode === "context") {
43
+ return (b._ctxNum ?? 0) - (a._ctxNum ?? 0);
44
+ }
45
+ return 0;
46
+ })
47
+ : rawItems;
48
+ const cols = process.stdout.columns || 80;
49
+
50
+ if (loading) {
51
+ return h(Box, { flexDirection: "column", height },
52
+ h(Text, { bold: true, color: "cyan" }, ` ${title}`),
53
+ h(Text, { dimColor: true }, ""),
54
+ h(Text, { color: "yellow" }, " Loading..."),
55
+ );
56
+ }
57
+
58
+ if (!items || !items.length) {
59
+ return h(Box, { flexDirection: "column", height },
60
+ h(Text, { bold: true, color: "cyan" }, ` ${title}`),
61
+ h(Text, { dimColor: true }, ""),
62
+ h(Text, { color: "red" }, " No items available"),
63
+ h(Text, { dimColor: true }, ""),
64
+ h(Text, { dimColor: true }, " Esc: back"),
65
+ );
66
+ }
67
+
68
+ // Viewport scrolling for long lists
69
+ const maxVisible = Math.max(3, height - 6); // title + header + footer + hints
70
+ let startIdx = 0;
71
+ if (items.length > maxVisible) {
72
+ startIdx = Math.max(0, Math.min(index - Math.floor(maxVisible / 2), items.length - maxVisible));
73
+ }
74
+ const visibleItems = items.slice(startIdx, startIdx + maxVisible);
75
+
76
+ // Calculate column widths from all items
77
+ const isModel = type === "model";
78
+
79
+ let headerLine = null;
80
+ let renderItem;
81
+
82
+ if (type === "free") {
83
+ // marker | model | window | speed | first token | uptime | note
84
+ const idW = Math.min(44, Math.max(12, ...items.map((it) => (it.id || "").length)) + 1);
85
+ const num = (v, unit, digits = 0) => (v == null ? "—" : `${Number(v).toFixed(digits)}${unit}`);
86
+ const win = (c) => (c ? (c >= 1_000_000 ? `${Math.round(c / 1_000_000)}M` : `${Math.round(c / 1000)}k`) : "—");
87
+ headerLine = h(Text, { key: "hdr", dimColor: true },
88
+ ` ${pad("Model", idW)} ${pad("Check", 6)} ${pad("Window", 7)} ${pad("Speed", 10)} ${pad("First token", 12)} Uptime`
89
+ );
90
+ renderItem = (item, realIdx) => {
91
+ const selected = realIdx === index;
92
+ const marker = selected ? ">" : " ";
93
+ const note = [item.light ? "light" : "", item.stealth ? "stealth: prompts may be logged" : ""].filter(Boolean).join(" · ");
94
+ return h(Text, { key: realIdx, bold: selected, color: selected ? "green" : "white", wrap: "truncate" },
95
+ ` ${marker} ${pad(item.id || "", idW)} ${pad(item.checkScore == null ? "—" : `${item.checkScore}/6${item.scoreFresh ? "" : "*"}`, 6)} ${pad(win(item.context), 7)} ${pad(num(item.tps, " tok/s"), 10)} ${pad(item.firstMs == null ? "—" : num(item.firstMs / 1000, " s", 1), 12)} ${num(item.uptime, "%", 1)}${note ? ` ${note}` : ""}`
96
+ );
97
+ };
98
+ } else if (type === "session") {
99
+ // marker | when | messages | the last thing asked in it
100
+ const whenW = 11, countW = 5;
101
+ const lastW = Math.max(10, Math.min(cols, 140) - whenW - countW - 8);
102
+ headerLine = h(Text, { key: "hdr", dimColor: true },
103
+ ` ${pad("When", whenW)} ${pad("Msgs", countW)} Last message`
104
+ );
105
+ renderItem = (item, realIdx) => {
106
+ const selected = realIdx === index;
107
+ const marker = selected ? ">" : " ";
108
+ return h(Text, { key: realIdx, bold: selected, color: selected ? "green" : "white", wrap: "truncate" },
109
+ ` ${marker} ${pad(sessionWhen(item.when), whenW)} ${pad(String(item.count ?? ""), countW)} ${fitEnd(item.last || "(no text)", lastW)}`
110
+ );
111
+ };
112
+ } else if (isModel) {
113
+ // Table layout: marker | model id | context | price in / price out
114
+ const idW = Math.min(45, Math.max(10, ...items.map((it) => (it.id || "").length)) + 1);
115
+ const ctxW = 6;
116
+ const priceW = 22;
117
+
118
+ headerLine = h(Text, { key: "hdr", dimColor: true },
119
+ ` ${pad("Model", idW)} ${pad("Ctx", ctxW)} ${pad("Price (per 1M tok)", priceW)}`
120
+ );
121
+
122
+ renderItem = (item, realIdx) => {
123
+ const selected = realIdx === index;
124
+ const marker = selected ? ">" : " ";
125
+ const id = pad(item.id || "", idW);
126
+ const ctx = pad(item.ctx || "", ctxW);
127
+ const price = item.price || "";
128
+ const cur = item.current ? " *" : "";
129
+ const color = selected ? "green" : item.current ? "cyan" : "white";
130
+
131
+ return h(Text, { key: realIdx, bold: selected, color },
132
+ ` ${marker} ${id} ${ctx} ${price}${cur}`
133
+ );
134
+ };
135
+ } else {
136
+ // Provider list: marker | id | name | key status
137
+ const idW = Math.max(12, ...items.map((it) => (it.id || "").length)) + 1;
138
+ const nameW = Math.max(16, ...items.map((it) => (it.name || "").length)) + 1;
139
+
140
+ headerLine = h(Text, { key: "hdr", dimColor: true },
141
+ ` ${pad("ID", idW)} ${pad("Name", nameW)} Key`
142
+ );
143
+
144
+ renderItem = (item, realIdx) => {
145
+ const selected = realIdx === index;
146
+ const marker = selected ? ">" : " ";
147
+ const id = pad(item.id || "", idW);
148
+ const name = pad(item.name || "", nameW);
149
+ const info = item.info || "";
150
+ const cur = item.current ? " *" : "";
151
+ const color = selected ? "green" : item.disabled ? "gray" : "white";
152
+
153
+ return h(Text, { key: realIdx, bold: selected, color },
154
+ ` ${marker} ${id} ${name} ${info}${cur}`
155
+ );
156
+ };
157
+ }
158
+
159
+ const lines = visibleItems.map((item, i) => renderItem(item, startIdx + i));
160
+
161
+ const scrollInfo = items.length > maxVisible
162
+ ? ` ${startIdx + 1}-${Math.min(startIdx + maxVisible, items.length)} / ${items.length}`
163
+ : ` ${items.length} items`;
164
+
165
+ const sep = " " + "-".repeat(Math.min(70, cols - 4));
166
+
167
+ return h(Box, { flexDirection: "column", height },
168
+ h(Text, { bold: true, color: "cyan" }, ` ${title}`),
169
+ headerLine,
170
+ h(Text, { dimColor: true }, sep),
171
+ ...lines,
172
+ h(Text, { dimColor: true }, sep),
173
+ h(Text, { dimColor: true },
174
+ isModel
175
+ ? ` Up/Down: navigate | Enter: select | Ctrl+S: sort (${sortMode || "name"}) | Esc: back${scrollInfo}`
176
+ : ` Up/Down: navigate | Enter: select | Esc: back${scrollInfo}`
177
+ ),
178
+ );
179
+ }
@@ -0,0 +1,156 @@
1
+ // SystemPanel — system info tab (model, session, context, config, MCP, plugins, datasets)
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { getDefinitions, getMcpStatus, getLoadedPlugins } from "../tools/registry.js";
5
+
6
+ const { createElement: h, useSyncExternalStore, useRef } = React;
7
+
8
+ function useStoreSelector(store, selector) {
9
+ const prev = useRef(null);
10
+ return useSyncExternalStore(store.subscribe, () => {
11
+ const next = selector(store.getState());
12
+ if (prev.current !== null && shallowEqual(prev.current, next)) {
13
+ return prev.current;
14
+ }
15
+ prev.current = next;
16
+ return next;
17
+ });
18
+ }
19
+
20
+ function shallowEqual(a, b) {
21
+ if (a === b) return true;
22
+ if (!a || !b) return false;
23
+ const keysA = Object.keys(a);
24
+ if (keysA.length !== Object.keys(b).length) return false;
25
+ for (const k of keysA) {
26
+ if (a[k] !== b[k]) return false;
27
+ }
28
+ return true;
29
+ }
30
+
31
+ export function SystemPanel({ store }) {
32
+ const info = useStoreSelector(store, (s) => ({
33
+ model: s.model,
34
+ pricing: s.pricing,
35
+ contextLimit: s.contextLimit,
36
+ lastContextTokens: s.lastContextTokens,
37
+ sessionId: s.sessionId,
38
+ messageCount: s.userMessageCount || 0,
39
+ sessionCost: s.sessionCost,
40
+ sessionPromptTokens: s.sessionPromptTokens,
41
+ sessionCompletionTokens: s.sessionCompletionTokens,
42
+ port: s._port || 3000,
43
+ version: s._version || "?",
44
+ profile: s._profile || null,
45
+ agentStatus: s.agentStatus,
46
+ datasetCount: Object.keys(s.datasets || {}).length,
47
+ }));
48
+
49
+ const section = (title, rows) =>
50
+ h(Box, { flexDirection: "column", marginBottom: 1 },
51
+ h(Text, { bold: true, color: "cyan" }, ` ${title}`),
52
+ ...rows.map((r, i) => h(Text, { key: i, dimColor: !r.highlight }, ` ${r.label}: ${r.value}`)),
53
+ );
54
+
55
+ const sections = [];
56
+
57
+ // Model
58
+ const modelRows = [];
59
+ if (info.model) modelRows.push({ label: "Name", value: info.model });
60
+ if (info.pricing) {
61
+ modelRows.push({
62
+ label: "Pricing",
63
+ value: `$${(info.pricing.prompt * 1e6).toFixed(2)} / $${(info.pricing.completion * 1e6).toFixed(2)} per 1M tok`,
64
+ });
65
+ }
66
+ if (modelRows.length) sections.push(section("Model", modelRows));
67
+
68
+ // Session
69
+ const sessRows = [];
70
+ if (info.sessionId) sessRows.push({ label: "ID", value: info.sessionId });
71
+ sessRows.push({ label: "Messages", value: String(info.messageCount) });
72
+ sessRows.push({ label: "Cost", value: `$${info.sessionCost.toFixed(4)}`, highlight: info.sessionCost > 0 });
73
+ sessRows.push({ label: "Tokens", value: `${info.sessionPromptTokens + info.sessionCompletionTokens}` });
74
+ sections.push(section("Session", sessRows));
75
+
76
+ // Context
77
+ if (info.contextLimit) {
78
+ const pct = info.lastContextTokens
79
+ ? ((info.lastContextTokens / info.contextLimit) * 100).toFixed(1)
80
+ : "0.0";
81
+ const cur = info.lastContextTokens >= 1000
82
+ ? (info.lastContextTokens / 1000).toFixed(1) + "k"
83
+ : String(info.lastContextTokens || 0);
84
+ const lim = info.contextLimit >= 1e6
85
+ ? (info.contextLimit / 1e6).toFixed(0) + "M"
86
+ : (info.contextLimit / 1000).toFixed(0) + "k";
87
+ sections.push(section("Context", [
88
+ { label: "Used", value: `${cur} / ${lim} (${pct}%)`, highlight: parseFloat(pct) > 80 },
89
+ ]));
90
+ }
91
+
92
+ // Runtime
93
+ const mem = process.memoryUsage();
94
+ const rssMB = (mem.rss / 1024 / 1024).toFixed(0);
95
+ const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(0);
96
+ const heapTotalMB = (mem.heapTotal / 1024 / 1024).toFixed(0);
97
+ const uptimeSec = Math.floor(process.uptime());
98
+ const uptimeStr = uptimeSec < 60 ? `${uptimeSec}s`
99
+ : uptimeSec < 3600 ? `${Math.floor(uptimeSec / 60)}m ${uptimeSec % 60}s`
100
+ : `${Math.floor(uptimeSec / 3600)}h ${Math.floor((uptimeSec % 3600) / 60)}m`;
101
+
102
+ sections.push(section("Runtime", [
103
+ { label: "Memory", value: `${rssMB} MB RSS (heap: ${heapMB}/${heapTotalMB} MB)`, highlight: parseInt(rssMB) > 500 },
104
+ { label: "Uptime", value: uptimeStr },
105
+ { label: "PID", value: String(process.pid) },
106
+ { label: "Node", value: process.version },
107
+ ]));
108
+
109
+ // Config
110
+ const configRows = [
111
+ { label: "Version", value: `v${info.version}` },
112
+ { label: "API port", value: String(info.port) },
113
+ ];
114
+ if (info.profile) configRows.push({ label: "Profile", value: info.profile });
115
+ configRows.push({ label: "Tools", value: String(getDefinitions().length) });
116
+ configRows.push({ label: "Security", value: process.env.AGENT_SECURITY_DISABLE === "1" ? "disabled" : (process.env.AGENT_SECURITY_POLICY || "normal") });
117
+ sections.push(section("Config", configRows));
118
+
119
+ // Agent
120
+ sections.push(section("Agent", [
121
+ { label: "Status", value: info.agentStatus, highlight: info.agentStatus !== "idle" },
122
+ ]));
123
+
124
+ // MCP Servers
125
+ const mcpServers = getMcpStatus();
126
+ if (mcpServers.length) {
127
+ const mcpRows = mcpServers.map((s) => ({
128
+ label: s.name,
129
+ value: s.ok ? `${s.tools} tools` : `error: ${s.error}`,
130
+ highlight: !s.ok,
131
+ }));
132
+ sections.push(section("MCP Servers", mcpRows));
133
+ }
134
+
135
+ // Plugins
136
+ const plugins = getLoadedPlugins();
137
+ if (plugins.length) {
138
+ const pluginRows = plugins.map((p) => ({
139
+ label: p.name,
140
+ value: `${p.description || p.type} (${p.toolCount} tools)`,
141
+ }));
142
+ sections.push(section("Plugins", pluginRows));
143
+ }
144
+
145
+ // Datasets
146
+ if (info.datasetCount > 0) {
147
+ const datasets = store.getState().datasets;
148
+ const dsRows = Object.values(datasets).map((ds) => ({
149
+ label: ds.label,
150
+ value: `${ds.rows.length} rows (${ds.source})`,
151
+ }));
152
+ sections.push(section("Datasets", dsRows));
153
+ }
154
+
155
+ return h(Box, { flexDirection: "column" }, ...sections);
156
+ }
@@ -0,0 +1,54 @@
1
+ // Table — Ink React component for tabular data
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+
5
+ const { createElement: h } = React;
6
+
7
+ export function Table({ columns, rows, title, footer }) {
8
+ const cols = process.stdout.columns || 80;
9
+
10
+ // Calculate column widths from data
11
+ const widths = columns.map((col, i) => {
12
+ const headerLen = String(col).length;
13
+ const maxData = rows.reduce((max, row) => Math.max(max, String(row[i] || "").length), 0);
14
+ return Math.min(Math.max(headerLen, maxData) + 1, 40);
15
+ });
16
+
17
+ // Adjust last column to fill remaining space
18
+ const usedWidth = widths.slice(0, -1).reduce((s, w) => s + w + 3, 4);
19
+ widths[widths.length - 1] = Math.max(widths[widths.length - 1], cols - usedWidth - 2);
20
+
21
+ const pad = (s, n) => {
22
+ const str = String(s);
23
+ return str.length > n ? str.slice(0, n - 1) + "..." : str.padEnd(n);
24
+ };
25
+
26
+ const makeLine = (cells, sep) =>
27
+ " | " + cells.map((c, i) => pad(c, widths[i])).join(` ${sep} `) + " |";
28
+
29
+ const borderTop = " +" + widths.map((w) => "-".repeat(w + 2)).join("+") + "+";
30
+ const borderBot = borderTop;
31
+ const divider = " +" + widths.map((w) => "-".repeat(w + 2)).join("+") + "+";
32
+
33
+ const elements = [];
34
+
35
+ if (title) {
36
+ elements.push(h(Text, { key: "title", color: "cyan" }, ` ${title}`));
37
+ }
38
+
39
+ elements.push(h(Text, { key: "top", dimColor: true }, borderTop));
40
+ elements.push(h(Text, { key: "header", dimColor: true }, makeLine(columns, "|")));
41
+ elements.push(h(Text, { key: "div", dimColor: true }, divider));
42
+
43
+ for (let r = 0; r < rows.length; r++) {
44
+ elements.push(h(Text, { key: `r-${r}`, dimColor: true }, makeLine(rows[r], "|")));
45
+ }
46
+
47
+ elements.push(h(Text, { key: "bot", dimColor: true }, borderBot));
48
+
49
+ if (footer) {
50
+ elements.push(h(Text, { key: "footer", dimColor: true }, ` ${footer}`));
51
+ }
52
+
53
+ return h(Box, { flexDirection: "column" }, ...elements);
54
+ }
package/src/config.js ADDED
@@ -0,0 +1,249 @@
1
+ import "dotenv/config";
2
+ import path from "node:path";
3
+ import { statSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getProvider } from "./providers/registry.js";
6
+ import { getActiveProvider, getLastModel } from "./providers/state.js";
7
+ import { getKey, hasKey } from "./providers/keys.js";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
11
+
12
+ const DEFAULT_PORT = 3000;
13
+
14
+ // OpenRouter provider preference for the main model, from the environment
15
+ // OPENROUTER_PROVIDER_ONLY / _IGNORE / _ORDER are comma lists of provider slugs
16
+ // (as on the model's endpoints page, e.g. "xiaomi,atlas-cloud"), _SORT is
17
+ // price | throughput | latency. The same four keys other agents take as
18
+ // provider_routing, so both agents can be pinned alike. null when none is set.
19
+ function openrouterProviderFromEnv() {
20
+ const list = (v) => (v || "").split(",").map((s) => s.trim()).filter(Boolean);
21
+ const pref = {};
22
+ const only = list(process.env.OPENROUTER_PROVIDER_ONLY);
23
+ const ignore = list(process.env.OPENROUTER_PROVIDER_IGNORE);
24
+ const order = list(process.env.OPENROUTER_PROVIDER_ORDER);
25
+ const sort = (process.env.OPENROUTER_PROVIDER_SORT || "").trim();
26
+ if (only.length) pref.only = only;
27
+ if (ignore.length) pref.ignore = ignore;
28
+ if (order.length) pref.order = order;
29
+ if (sort) pref.sort = sort;
30
+ return Object.keys(pref).length ? pref : null;
31
+ }
32
+
33
+ function getArgValue(name) {
34
+ const idx = process.argv.indexOf(name);
35
+ if (idx !== -1 && process.argv[idx + 1]) {
36
+ return process.argv[idx + 1];
37
+ }
38
+ return null;
39
+ }
40
+
41
+ // Resolve provider — CLI > env > persisted state > default
42
+ const cliProvider = getArgValue("--provider");
43
+ const envProvider = process.env.FLINT_PROVIDER;
44
+ const initialProviderId = cliProvider || envProvider || getActiveProvider() || "openrouter";
45
+
46
+ const provider = getProvider(initialProviderId) || getProvider("openrouter");
47
+
48
+ // Resolve model — CLI > env > last used for provider > provider default
49
+ const cliModel = getArgValue("--model");
50
+ const envModel = process.env.OPENROUTER_MODEL;
51
+ const lastModel = getLastModel(provider.id);
52
+ const initialModel = cliModel || envModel || lastModel || provider.defaultModel;
53
+
54
+ export const config = {
55
+ // Provider-aware fields (mutable — updated when provider switches)
56
+ provider: provider.id,
57
+ model: initialModel,
58
+
59
+ // API key — resolved lazily; falls back to env vars for backward compat
60
+ _apiKeyCache: null,
61
+ _apiKeyCacheProvider: null,
62
+
63
+ get apiKey() {
64
+ // Sync getter — returns cached key or env fallback
65
+ // Call config.resolveApiKey at startup for async encrypted key resolution
66
+ if (this._apiKeyCache && this._apiKeyCacheProvider === this.provider) {
67
+ return this._apiKeyCache;
68
+ }
69
+ // Env var fallbacks for backward compat
70
+ if (this.provider === "openrouter") return process.env.OPENROUTER_API_KEY || null;
71
+ if (this.provider === "openai") return process.env.OPENAI_API_KEY || null;
72
+ if (this.provider === "anthropic") return process.env.ANTHROPIC_API_KEY || null;
73
+ return null;
74
+ },
75
+
76
+ set apiKey(val) {
77
+ this._apiKeyCache = val;
78
+ this._apiKeyCacheProvider = this.provider;
79
+ },
80
+
81
+ // Resolve API key from encrypted storage (async — call at startup)
82
+ async resolveApiKey() {
83
+ const key = await getKey(this.provider);
84
+ if (key) {
85
+ this._apiKeyCache = key;
86
+ this._apiKeyCacheProvider = this.provider;
87
+ return key;
88
+ }
89
+ // Fallback to env vars
90
+ return this.apiKey;
91
+ },
92
+
93
+ get apiUrl() {
94
+ const p = getProvider(this.provider);
95
+ if (!p) return "https://openrouter.ai/api/v1/chat/completions";
96
+ if (p.format === "anthropic") return `${p.baseUrl}/messages`;
97
+ if (p.ollamaCompat) return `${p.baseUrl}/v1/chat/completions`;
98
+ return `${p.baseUrl}/chat/completions`;
99
+ },
100
+
101
+ port: parseInt(getArgValue("--port") || (process.env.AGENT_PARENT_PORT ? process.env.AGENT_PORT : null) || DEFAULT_PORT, 10),
102
+ // Whether somebody named that port on purpose. When they did, the server
103
+ // binds it or fails, instead of quietly scanning upwards: a bench subject
104
+ // that lands on a different port than the harness was told about measures
105
+ // whatever else is listening. On 2026-09-21 two runs were asked for 3001 and
106
+ // reported themselves on 3002 and 3003.
107
+ portExplicit: !!getArgValue("--port"),
108
+ memoryUrl: process.env.MEMORY_API_URL ? process.env.MEMORY_API_URL.replace(/\/+$/, "") : null,
109
+ // 2048 starved reasoning models: on the 2026-09-22 auto-budget run the
110
+ // final turn and the summary both came back with completion=2048 and zero
111
+ // characters of content, the whole allowance spent on thinking.
112
+ maxResponseTokens: parseInt(process.env.AGENT_MAX_RESPONSE_TOKENS || "16384", 10),
113
+ maxDisplayLines: parseInt(process.env.AGENT_MAX_LINES || "1000", 10),
114
+ maxResponseLines: parseInt(process.env.AGENT_MAX_RESPONSE_LINES || "500", 10),
115
+ mcpServers: process.env.MCP_SERVERS || null,
116
+ // Steps per turn. 50 cut real work short: on 2026-09-29 the open-source
117
+ // self-audit hit it 8 times in one afternoon, while everyday tasks need a
118
+ // median of 3 and at most 15 (71-task run). The ceiling is a runaway guard,
119
+ // not a budget; money is capped by AGENT_MAX_COST.
120
+ maxIterations: parseInt(process.env.AGENT_MAX_ITERATIONS || "150", 10),
121
+ maxCostPerAction: parseFloat(process.env.AGENT_MAX_COST || "0"),
122
+ sessionBudget: parseFloat(process.env.AGENT_SESSION_BUDGET || "0"), // session-level $ limit (0=unlimited)
123
+ // Unset means "derive from the model's window", see compressThreshold in
124
+ // agent/compression.js. A fixed 50000 with an eager pass at half of it
125
+ // rewrote every read file into a one-line summary at about 25k tokens, and
126
+ // the agent spent the rest of the turn reading the same files again.
127
+ compressAfterTokens: process.env.COMPRESS_AFTER_TOKENS ? parseInt(process.env.COMPRESS_AFTER_TOKENS, 10) : null,
128
+ headless: false,
129
+ // Escape hatch back to the old fallback that handed the model every live tool.
130
+ // See fallbackManifest in intent.js for the numbers that closed it.
131
+ fallbackAllTools: process.env.FLINT_FALLBACK_ALL_TOOLS === "1",
132
+ apiAutoApprove: process.env.AGENT_API_AUTO_APPROVE !== "0", // auto-approve tools for authenticated API callers (default: true)
133
+ // Whether the agent may install or reload a plugin without asking. "ask"
134
+ // unless FLINT_PLUGIN_INSTALL=allow; any other value asks too. A plugin is
135
+ // code with the agent's rights, so it is asked even when apiAutoApprove
136
+ // opens everything else, and a run with no operator is refused.
137
+ pluginInstall: process.env.FLINT_PLUGIN_INSTALL === "allow" ? "allow" : "ask",
138
+ openrouterProvider: openrouterProviderFromEnv(),
139
+ // The self-verification gate: after "done" on a turn that changed
140
+ // files and ran nothing, one more round asks the model to show the change
141
+ // works. Built for weaker models (Gemini Flash) that stop before checking.
142
+ // On mimo it cost a round in 34 of 43 benchmark tasks with no gain in
143
+ // passes.
144
+ //
145
+ // Off by default: the models Flint runs on now check on
146
+ // their own; FLINT_SELF_VERIFY=on brings it back for a weaker one.
147
+ selfVerify: process.env.FLINT_SELF_VERIFY === "on" ? "on" : "off",
148
+ autoMaxIterations: parseInt(process.env.AGENT_AUTO_MAX_ITERATIONS || "50", 10),
149
+ autoMaxCost: parseFloat(process.env.AGENT_AUTO_MAX_COST || "0.50"),
150
+ // Where this instance keeps what it writes. FLINT_DATA_DIR moves it off the
151
+ // shared defaults, which is what a bench subject needs: on 2026-09-21 the
152
+ // readiness subject and the operator's own live agent wrote into the same
153
+ // sessions/ directory and the same memory database, so a measured run
154
+ // carried another process's turns and another process's facts, and a unit
155
+ // test that clears the facts table raced a live agent writing to it.
156
+ sessionsDir: process.env.FLINT_DATA_DIR
157
+ ? path.join(path.resolve(process.env.FLINT_DATA_DIR), "sessions")
158
+ : process.env.AGENT_PARENT_PORT
159
+ ? path.join(PROJECT_ROOT, "sessions", "children")
160
+ : path.join(PROJECT_ROOT, "sessions"),
161
+ projectRoot: PROJECT_ROOT,
162
+ // Where .permissions.json lives — the operator's own saved permissions,
163
+ // onboarding answer, per-file "[a]lways" approvals.
164
+ //
165
+ // Split out from projectRoot because the two answer different questions.
166
+ // projectRoot is "where the code and the resources are", and moving it
167
+ // changes what a run reads: profiles.js, memory/store.js, bus/plugins.js all
168
+ // resolve their directories from it at module load, so a test run with a
169
+ // redirected projectRoot would load no profiles and no memories, and pass
170
+ // against an empty sandbox rather than the real thing. That was tried and
171
+ // reverted for exactly this reason.
172
+ //
173
+ // The one thing that genuinely must not be shared is the file a run WRITES.
174
+ // A test writing config.projectRoot/.permissions.json overwrote the
175
+ // developer's own saved permissions on every suite run, and because the file
176
+ // is in .gitignore the overwrite left `git status` clean — silent damage that
177
+ // only a container diff of the work tree ever noticed.
178
+ //
179
+ // Production is unchanged: unset means exactly the path it has always meant.
180
+ // A test run points it at a temp dir, so the bytes land there and the
181
+ // checkout is left byte-for-byte as it was found.
182
+ permissionsFile: process.env.FLINT_TEST_PERMISSIONS_FILE
183
+ ? path.resolve(process.env.FLINT_TEST_PERMISSIONS_FILE)
184
+ : path.join(PROJECT_ROOT, ".permissions.json"),
185
+ // Working directory for agent file operations (created per session)
186
+ // If set, relative paths in file tools resolve from here instead of cwd
187
+ // If empty, defaults to sessions/<sessionId>/workspace/
188
+ workdirBase: process.env.AGENT_WORKDIR || "",
189
+ // Filesystem sandbox: comma-separated list of allowed directories
190
+ // If empty/null — unrestricted access
191
+ // Example: AGENT_ALLOWED_PATHS=C:\Projects,C:\tmp,D:\data
192
+ allowedPaths: (process.env.AGENT_ALLOWED_PATHS || "")
193
+ .split(",")
194
+ .map((p) => p.trim())
195
+ .filter(Boolean)
196
+ .map((p) => path.resolve(p)),
197
+ maxChildAgents: parseInt(process.env.AGENT_MAX_CHILDREN || "5", 10),
198
+ childIdleTimeout: parseInt(process.env.AGENT_CHILD_IDLE_TIMEOUT || "60", 10), // seconds
199
+ childCleanupDelay: parseInt(process.env.AGENT_CHILD_CLEANUP_DELAY || "30000", 10), // ms before removing stopped agent from registry
200
+ shell: process.env.AGENT_SHELL || (() => {
201
+ if (process.platform !== "win32") return "/bin/bash";
202
+ // Prefer Git Bash over WSL bash on Windows (check standard install paths)
203
+ const candidates = [
204
+ process.env.ProgramFiles && `${process.env.ProgramFiles}\\Git\\usr\\bin\\bash.exe`,
205
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
206
+ ].filter(Boolean);
207
+ for (const p of candidates) { try { statSync(p); return p; } catch {} }
208
+ return "bash";
209
+ })(),
210
+ maxBatchFiles: parseInt(process.env.AGENT_MAX_BATCH_FILES || "20", 10),
211
+ budgetWarningThresholds: (process.env.AGENT_BUDGET_WARNINGS || "0.5,0.8").split(",").map(Number),
212
+ securityPolicy: process.env.AGENT_SECURITY_POLICY || "normal",
213
+ extractionModel: process.env.EXTRACTION_MODEL || "google/gemini-2.0-flash-001",
214
+ compressThreshold: parseInt(process.env.COMPRESS_THRESHOLD || "500", 10),
215
+ maxContextChars: parseInt(process.env.MAX_CONTEXT_CHARS || "20000", 10),
216
+ maxPromptTokens: parseInt(process.env.MAX_PROMPT_TOKENS || "100000", 10),
217
+ // Classifier model, from INTENT_MODEL. Optional: when it is not set the
218
+ // classifier is off and every turn gets the built-in tools (the same path
219
+ // as FLINT_NO_CLASSIFIER=1). Owner, 2026-10-01: a missing classifier model
220
+ // must not stop Flint from starting. No default model is guessed.
221
+ //
222
+ // Measured on 2026-04-21 (.env.example has the full ranking; short list)
223
+ // openai/gpt-5.4-mini 4/4 1.5-2s $0.75/M best value
224
+ // x-ai/grok-4.20 4/4 1.0-1.3s $2/M fastest
225
+ // anthropic/claude-sonnet-4.6 4/4 2.5-3s $3/M
226
+ // qwen/qwen3.5-flash-02-23 4/4 4-15s $0.07/M cheapest, slow
227
+ intentModel: (process.env.INTENT_MODEL || "").trim() || null,
228
+ };
229
+
230
+ // Log config at boot (logger imports dotenv too, so it's ready)
231
+ import { createLogger } from "./logging/logger.js";
232
+ const _configLog = createLogger("config");
233
+ _configLog.info("Config loaded", { port: config.port, model: config.model, provider: config.provider, sessionsDir: config.sessionsDir });
234
+
235
+ // Check if any key is available — don't exit, defer to first-run wizard
236
+ function hasAnyKey() {
237
+ // Check encrypted storage
238
+ if (hasKey(config.provider)) return true;
239
+ // Check env vars
240
+ if (process.env.OPENROUTER_API_KEY) return true;
241
+ if (process.env.OPENAI_API_KEY) return true;
242
+ if (process.env.ANTHROPIC_API_KEY) return true;
243
+ // Ollama doesn't need a key
244
+ if (config.provider === "ollama") return true;
245
+ return false;
246
+ }
247
+
248
+ // Export for first-run wizard check
249
+ export const needsFirstRunSetup = !hasAnyKey();