flint-agent 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- package/system.md +63 -0
|
@@ -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();
|