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,218 @@
1
+ // Model check: six small agent tasks with answers a program can verify, run
2
+ // on a model, scored and saved (docs/model-check.md). OpenRouter tells how
3
+ // fast a model is, not whether it can do an agent's work; the free list
4
+ // ranked light models by their names until this measured it.
5
+
6
+ import { spawn } from "node:child_process";
7
+ import { randomUUID } from "node:crypto";
8
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir, tmpdir } from "node:os";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const read = (dir, f) => { try { return readFileSync(path.join(dir, f), "utf8"); } catch { return null; } };
14
+
15
+ const NUMBERS = [17, 25, 8, 13];
16
+ export const SUM_EXPECTED = NUMBERS.reduce((a, b) => a + b, 0);
17
+
18
+ export const CHECK_TASKS = [
19
+ {
20
+ id: "write-file",
21
+ prompt: "Create a file named hello.txt in the current folder containing exactly: flint-check",
22
+ check: ({ dir }) => (read(dir, "hello.txt") || "").trim() === "flint-check",
23
+ },
24
+ {
25
+ id: "read-sum",
26
+ prompt: "The file data.txt in the current folder holds numbers, one per line. Read it and reply with their sum, digits only.",
27
+ setup: (dir) => writeFileSync(path.join(dir, "data.txt"), NUMBERS.join("\n") + "\n"),
28
+ check: ({ answer }) => new RegExp(`\\b${SUM_EXPECTED}\\b`).test(String(answer || "")),
29
+ },
30
+ {
31
+ id: "edit-json",
32
+ prompt: "In config.json in the current folder, change the port to 8080. Keep the file valid JSON and keep the other fields.",
33
+ setup: (dir) => writeFileSync(path.join(dir, "config.json"), JSON.stringify({ port: 3000, name: "demo" }, null, 2) + "\n"),
34
+ check: ({ dir }) => {
35
+ try { const j = JSON.parse(read(dir, "config.json")); return j.port === 8080 && j.name === "demo"; } catch { return false; }
36
+ },
37
+ },
38
+ {
39
+ id: "run-command",
40
+ prompt: 'Run this command: node -e "console.log(6*7)" and reply with its output only.',
41
+ check: ({ answer, tools = [] }) =>
42
+ tools.some((t) => /run_command|run_background|exec|shell|bash/.test(t.name)) && /\b42\b/.test(String(answer || "")),
43
+ },
44
+ {
45
+ id: "multi-step",
46
+ prompt: "Create a folder named out in the current folder with two files: a.txt containing A and b.txt containing B. Then list the folder and reply with the file names, comma-separated.",
47
+ check: ({ dir, answer }) =>
48
+ (read(path.join(dir, "out"), "a.txt") || "").trim() === "A"
49
+ && (read(path.join(dir, "out"), "b.txt") || "").trim() === "B"
50
+ && /a\.txt/.test(answer || "") && /b\.txt/.test(answer || ""),
51
+ },
52
+ {
53
+ id: "restraint",
54
+ prompt: "Reply with the word ready and do nothing else.",
55
+ check: ({ answer, tools = [] }) => tools.length === 0 && String(answer || "").trim().replace(/[.!*"'`]/g, "").toLowerCase() === "ready",
56
+ },
57
+ ];
58
+
59
+ export const scoreOf = (tasks) => tasks.filter((t) => t.passed).length;
60
+
61
+ // ── Saved results ───────────────────────────────────────────
62
+
63
+ function checksFile() {
64
+ const dir = process.env.FLINT_DATA_DIR ? path.resolve(process.env.FLINT_DATA_DIR) : path.join(homedir(), ".flint");
65
+ return path.join(dir, "model-checks.json");
66
+ }
67
+ export function loadChecks() {
68
+ try { return existsSync(checksFile()) ? JSON.parse(readFileSync(checksFile(), "utf8")) : {}; } catch { return {}; }
69
+ }
70
+ export function saveCheck(result) {
71
+ const all = loadChecks();
72
+ all[result.model] = result;
73
+ mkdirSync(path.dirname(checksFile()), { recursive: true });
74
+ writeFileSync(checksFile(), JSON.stringify(all, null, 2) + "\n");
75
+ }
76
+
77
+ const FRESH_DAYS = 14;
78
+ /** The list with each model's saved score; only a fresh one ranks. */
79
+ export function withScores(list, now = new Date()) {
80
+ const checks = loadChecks();
81
+ return (list || []).map((m) => {
82
+ const c = checks[m.id];
83
+ if (!c) return m;
84
+ const fresh = now - new Date(c.checkedAt) <= FRESH_DAYS * 86400e3;
85
+ return { ...m, checkScore: c.score, scoreFresh: fresh, score: fresh ? c.score : undefined };
86
+ });
87
+ }
88
+
89
+ // ── Running ─────────────────────────────────────────────────
90
+
91
+ const withTimeout = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error(`timed out after ${Math.round(ms / 1000)} s`)), ms))]);
92
+
93
+ /** Run the six tasks on one model; saves and returns { model, score, checkedAt, tasks }. */
94
+ export async function runCheck(model, { startAgent = agentFactory || startStdioAgent, timeoutMs = 120000 } = {}) {
95
+ const dir = mkdtempSync(path.join(tmpdir(), "flint-check-"));
96
+ const agent = await startAgent({ model, cwd: dir });
97
+ const tasks = [];
98
+ try {
99
+ for (const t of CHECK_TASKS) {
100
+ t.setup?.(dir);
101
+ const t0 = Date.now();
102
+ let r = null;
103
+ let error;
104
+ try {
105
+ r = await withTimeout(agent.ask(t.prompt, { timeoutMs }), timeoutMs + 15000);
106
+ } catch (err) {
107
+ error = err.message;
108
+ }
109
+ const tools = r?.tools || [];
110
+ tasks.push({
111
+ id: t.id,
112
+ passed: r ? !!t.check({ dir, answer: r.answer || "", tools }) : false,
113
+ seconds: Math.round((Date.now() - t0) / 100) / 10,
114
+ toolCalls: tools.length,
115
+ toolErrors: tools.filter((x) => x.isError).length,
116
+ tokensIn: r?.usage?.input_tokens || 0,
117
+ tokensOut: r?.usage?.output_tokens || 0,
118
+ ...(error ? { error } : {}),
119
+ });
120
+ }
121
+ } finally {
122
+ try { await agent.close(); } catch {}
123
+ }
124
+ const result = { model, score: scoreOf(tasks), checkedAt: new Date().toISOString(), tasks };
125
+ saveCheck(result);
126
+ return result;
127
+ }
128
+
129
+ let agentFactory = null;
130
+ /** For tests: replace how an agent is started. */
131
+ export function setCheckAgentFactory(fn) { agentFactory = fn; }
132
+
133
+ const FLINT_BIN = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "flint.js");
134
+
135
+ /**
136
+ * A Flint in stdio mode for one model, in `cwd`, with its own data folder,
137
+ * no MCP servers and every tool allowed. ask() sends one turn and returns
138
+ * the answer, the tool calls (with whether they failed) and the usage.
139
+ */
140
+ export async function startStdioAgent({ model, cwd }) {
141
+ const data = mkdtempSync(path.join(tmpdir(), "flint-check-data-"));
142
+ const child = spawn(process.execPath, [FLINT_BIN, "--print", "--input-format", "stream-json", "--output-format", "stream-json",
143
+ "--model", model, "--session-id", randomUUID(), "--dangerously-skip-permissions"], {
144
+ cwd, stdio: ["pipe", "pipe", "pipe"],
145
+ env: { ...process.env, FLINT_DATA_DIR: data, MCP_SERVERS: "", FLINT_UPDATE_CHECK: "0", FLINT_SWAP_FROM: "100000000" },
146
+ });
147
+ child.stderr.on("data", () => {});
148
+ let buf = "";
149
+ let waiting = null; // { tools, resolve }
150
+ const ids = new Map();
151
+ child.stdout.on("data", (d) => {
152
+ buf += d;
153
+ let i;
154
+ while ((i = buf.indexOf("\n")) >= 0) {
155
+ const line = buf.slice(0, i); buf = buf.slice(i + 1);
156
+ let ev; try { ev = JSON.parse(line); } catch { continue; }
157
+ if (!waiting) continue;
158
+ if (ev.type === "assistant") {
159
+ for (const b of ev.message?.content || []) if (b.type === "tool_use") { const t = { name: b.name, isError: false }; ids.set(b.id, t); waiting.tools.push(t); }
160
+ } else if (ev.type === "user") {
161
+ for (const b of ev.message?.content || []) if (b.type === "tool_result" && b.is_error && ids.get(b.tool_use_id)) ids.get(b.tool_use_id).isError = true;
162
+ } else if (ev.type === "result") {
163
+ const w = waiting; waiting = null;
164
+ w.resolve({ answer: ev.result || "", tools: w.tools, usage: ev.usage || {}, subtype: ev.subtype });
165
+ }
166
+ }
167
+ });
168
+ const send = (o) => child.stdin.write(JSON.stringify(o) + "\n");
169
+ return {
170
+ ask(prompt, { timeoutMs = 120000 } = {}) {
171
+ return new Promise((resolve, reject) => {
172
+ const timer = setTimeout(() => {
173
+ // Stop the turn, wait for its end, and fail the task.
174
+ send({ type: "control_request", request_id: `stop-${Date.now()}`, request: { subtype: "interrupt" } });
175
+ const w = waiting;
176
+ setTimeout(() => { if (waiting === w) waiting = null; }, 10000);
177
+ if (w) w.resolve = () => reject(new Error(`timed out after ${Math.round(timeoutMs / 1000)} s`));
178
+ else reject(new Error("timed out"));
179
+ }, timeoutMs);
180
+ waiting = { tools: [], resolve: (r) => { clearTimeout(timer); resolve(r); } };
181
+ send({ type: "user", message: { role: "user", content: prompt } });
182
+ });
183
+ },
184
+ async close() {
185
+ child.stdin.end();
186
+ await new Promise((r) => { const t = setTimeout(() => { child.kill(); r(); }, 8000); child.on("exit", () => { clearTimeout(t); r(); }); });
187
+ },
188
+ };
189
+ }
190
+
191
+ // ── The command's background run ────────────────────────────
192
+
193
+ let lastRun = Promise.resolve();
194
+ export function lastCheckRun() { return lastRun; }
195
+
196
+ /** Check `models` one by one in the background; `log` gets a line per model and a table. */
197
+ export function startCheckRun(models, log) {
198
+ lastRun = (async () => {
199
+ const results = [];
200
+ for (const model of models) {
201
+ log(`checking ${model} (6 tasks)...`);
202
+ try {
203
+ const r = await runCheck(model);
204
+ results.push(r);
205
+ const secs = r.tasks.reduce((s, t) => s + t.seconds, 0);
206
+ const failed = r.tasks.filter((t) => !t.passed).map((t) => t.id).join(", ");
207
+ log(`${model} ${r.score}/6 ${secs.toFixed(0)} s${failed ? ` failed: ${failed}` : ""}`);
208
+ } catch (err) {
209
+ log(`${model} could not run: ${err.message}`);
210
+ }
211
+ }
212
+ if (results.length > 1) {
213
+ log("results, best first:");
214
+ for (const r of [...results].sort((a, b) => b.score - a.score)) log(` ${String(r.score).padStart(1)}/6 ${r.model}`);
215
+ }
216
+ })();
217
+ return lastRun;
218
+ }
@@ -0,0 +1,120 @@
1
+ // Plugin loader for Flint
2
+ // Scans ~/.flint/plugins/ and loads plugin modules
3
+
4
+ import { readdirSync, existsSync, readFileSync, mkdirSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { homedir } from "node:os";
8
+
9
+ // Read on every call, not fixed at import: FLINT_PLUGINS_DIR lets a test (or a
10
+ // bench subject) keep its plugins out of the user's ~/.flint/plugins.
11
+ function pluginsDir() {
12
+ return process.env.FLINT_PLUGINS_DIR || join(homedir(), ".flint", "plugins");
13
+ }
14
+
15
+ // Ensure plugins directory exists
16
+ export function ensurePluginsDir() {
17
+ const dir = pluginsDir();
18
+ if (!existsSync(dir)) {
19
+ mkdirSync(dir, { recursive: true });
20
+ }
21
+ return dir;
22
+ }
23
+
24
+ // Load a single plugin from its directory. `fresh` bypasses the ESM module
25
+ // cache: a reload after the file changed must run the new code, and import()
26
+ // of the same URL hands back the module it loaded the first time.
27
+ export async function loadPlugin(pluginDir, { fresh = false } = {}) {
28
+ const manifestPath = join(pluginDir, "package.json");
29
+ const indexPath = join(pluginDir, "index.js");
30
+
31
+ if (!existsSync(indexPath)) return null;
32
+
33
+ let manifest = {};
34
+ if (existsSync(manifestPath)) {
35
+ try {
36
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
37
+ } catch {}
38
+ }
39
+
40
+ try {
41
+ const url = pathToFileURL(indexPath).href + (fresh ? `?v=${Date.now()}-${Math.random().toString(36).slice(2)}` : "");
42
+ const mod = await import(url);
43
+ const plugin = mod.default || mod;
44
+
45
+ // Validate minimum contract
46
+ if (!plugin.name) {
47
+ plugin.name = manifest.name || pluginDir.split(/[\\/]/).pop();
48
+ }
49
+ if (!plugin.type) plugin.type = "tool";
50
+ if (!plugin.version) plugin.version = manifest.version || "0.0.0";
51
+
52
+ return {
53
+ name: plugin.name,
54
+ type: plugin.type, // "tool" | "channel" | "environment"
55
+ version: plugin.version,
56
+ description: plugin.description || manifest.description || "",
57
+ tools: plugin.tools || [],
58
+ handlers: plugin.handlers || {},
59
+ // Channel-specific
60
+ connect: plugin.connect || null,
61
+ onMessage: plugin.onMessage || null,
62
+ send: plugin.send || null,
63
+ // Lifecycle
64
+ init: plugin.init || null,
65
+ destroy: plugin.destroy || null,
66
+ // Raw module reference
67
+ _module: plugin,
68
+ _dir: pluginDir,
69
+ };
70
+ } catch (err) {
71
+ return { name: pluginDir.split(/[\\/]/).pop(), error: err.message };
72
+ }
73
+ }
74
+
75
+ // Load all plugins from ~/.flint/plugins/
76
+ export async function loadPlugins() {
77
+ const dir = ensurePluginsDir();
78
+ const loaded = [];
79
+ const errors = [];
80
+
81
+ let entries;
82
+ try {
83
+ entries = readdirSync(dir, { withFileTypes: true });
84
+ } catch {
85
+ return { loaded, errors };
86
+ }
87
+
88
+ for (const entry of entries) {
89
+ if (!entry.isDirectory()) continue;
90
+
91
+ const pluginDir = join(dir, entry.name);
92
+ const plugin = await loadPlugin(pluginDir);
93
+
94
+ if (!plugin) continue;
95
+
96
+ if (plugin.error) {
97
+ errors.push({ name: plugin.name, error: plugin.error });
98
+ } else {
99
+ loaded.push(plugin);
100
+ }
101
+ }
102
+
103
+ return { loaded, errors };
104
+ }
105
+
106
+ // Get list of installed plugin names
107
+ export function listInstalledPlugins() {
108
+ const dir = ensurePluginsDir();
109
+ try {
110
+ return readdirSync(dir, { withFileTypes: true })
111
+ .filter((e) => e.isDirectory())
112
+ .map((e) => e.name);
113
+ } catch {
114
+ return [];
115
+ }
116
+ }
117
+
118
+ export function getPluginsDir() {
119
+ return pluginsDir();
120
+ }
@@ -0,0 +1,88 @@
1
+ // Plugin manager for Flint
2
+ // Install/uninstall plugins via npm
3
+
4
+ import { execSync } from "node:child_process";
5
+ import { existsSync, rmSync, mkdirSync, writeFileSync, cpSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { ensurePluginsDir, getPluginsDir } from "./loader.js";
8
+
9
+ // Install a plugin by npm package name or local path
10
+ export async function installPlugin(nameOrPath) {
11
+ const pluginsDir = ensurePluginsDir();
12
+
13
+ // Local path install
14
+ if (existsSync(nameOrPath)) {
15
+ const dirName = nameOrPath.split(/[\\/]/).pop();
16
+ const targetDir = join(pluginsDir, dirName);
17
+ if (existsSync(targetDir)) {
18
+ return { ok: false, error: `Plugin "${dirName}" already installed` };
19
+ }
20
+ // Copy directory (no shell — safe from injection)
21
+ cpSync(nameOrPath, targetDir, { recursive: true });
22
+ // Install deps if package.json exists
23
+ if (existsSync(join(targetDir, "package.json"))) {
24
+ try {
25
+ execSync("npm install --omit=dev", { cwd: targetDir, stdio: "pipe" });
26
+ } catch {}
27
+ }
28
+ return { ok: true, name: dirName, path: targetDir };
29
+ }
30
+
31
+ // npm package install
32
+ const packageName = nameOrPath.startsWith("flint-plugin-")
33
+ ? nameOrPath
34
+ : `flint-plugin-${nameOrPath}`;
35
+
36
+ const targetDir = join(pluginsDir, packageName);
37
+ if (existsSync(targetDir)) {
38
+ return { ok: false, error: `Plugin "${packageName}" already installed` };
39
+ }
40
+
41
+ try {
42
+ mkdirSync(targetDir, { recursive: true });
43
+ // Create minimal package.json so npm install works
44
+ writeFileSync(
45
+ join(targetDir, "package.json"),
46
+ JSON.stringify({ name: packageName, version: "0.0.0", dependencies: { [packageName]: "latest" } }, null, 2),
47
+ );
48
+ execSync("npm install --omit=dev", { cwd: targetDir, stdio: "pipe", timeout: 60000 });
49
+
50
+ // Check if the package has an index.js or main entry
51
+ const pkgDir = join(targetDir, "node_modules", packageName);
52
+ if (!existsSync(pkgDir)) {
53
+ rmSync(targetDir, { recursive: true, force: true });
54
+ return { ok: false, error: `Package "${packageName}" not found on npm` };
55
+ }
56
+
57
+ // Create index.js that re-exports the package
58
+ writeFileSync(
59
+ join(targetDir, "index.js"),
60
+ `export { default } from "${packageName}";\n`,
61
+ );
62
+
63
+ return { ok: true, name: packageName, path: targetDir };
64
+ } catch (err) {
65
+ // Cleanup on failure
66
+ if (existsSync(targetDir)) {
67
+ rmSync(targetDir, { recursive: true, force: true });
68
+ }
69
+ return { ok: false, error: err.message };
70
+ }
71
+ }
72
+
73
+ // Uninstall a plugin
74
+ export function uninstallPlugin(name) {
75
+ const pluginsDir = getPluginsDir();
76
+ const packageName = name.startsWith("flint-plugin-") ? name : `flint-plugin-${name}`;
77
+
78
+ // Try both with and without prefix
79
+ for (const dirName of [name, packageName]) {
80
+ const targetDir = join(pluginsDir, dirName);
81
+ if (existsSync(targetDir)) {
82
+ rmSync(targetDir, { recursive: true, force: true });
83
+ return { ok: true, name: dirName };
84
+ }
85
+ }
86
+
87
+ return { ok: false, error: `Plugin "${name}" not found` };
88
+ }
@@ -0,0 +1,22 @@
1
+ // Imported first by index.js: React and Ink load their production builds.
2
+ //
3
+ // With NODE_ENV unset they loaded the development builds, and React 19's
4
+ // development build records a performance.measure for every component render
5
+ // (its "Components" profiling track). Nothing clears that buffer. The
6
+ // overnight soak of 2026-10-02 ended with 72,784 PerformanceMeasure entries
7
+ // and their strings making up most of a 97 MB live heap after /new and a
8
+ // full GC, and private bytes rising about 6 MB an hour.
9
+ //
10
+ // The variable is set only while the modules load (React picks its build
11
+ // then) and restored by restoreNodeEnv() afterwards: commands the agent runs
12
+ // inherit the environment, and NODE_ENV=production there would make
13
+ // `npm install` in the operator's project skip devDependencies.
14
+
15
+ export const originalNodeEnv = process.env.NODE_ENV;
16
+ if (!originalNodeEnv) process.env.NODE_ENV = "production";
17
+
18
+ /** Put NODE_ENV back as it was before Flint started. */
19
+ export function restoreNodeEnv() {
20
+ if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
21
+ else process.env.NODE_ENV = originalNodeEnv;
22
+ }
@@ -0,0 +1,42 @@
1
+ // Profile loader — reads profile configs and prompt files
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { config } from "./config.js";
5
+
6
+ const PROFILES_DIR = join(config.projectRoot, "profiles");
7
+
8
+ export function loadProfilesConfig() {
9
+ try {
10
+ return JSON.parse(readFileSync(join(PROFILES_DIR, "profiles.json"), "utf-8"));
11
+ } catch {
12
+ return {};
13
+ }
14
+ }
15
+
16
+ export function loadProfile(name) {
17
+ const configs = loadProfilesConfig();
18
+ const cfg = configs[name];
19
+ if (!cfg) throw new Error(`Profile "${name}" not found in profiles.json`);
20
+ const filePath = join(PROFILES_DIR, cfg.prompt);
21
+ try {
22
+ const content = readFileSync(filePath, "utf-8").trim();
23
+ return {
24
+ content,
25
+ contextMode: cfg.contextMode || "mini",
26
+ windowSize: cfg.windowSize || 10,
27
+ description: cfg.description || "",
28
+ };
29
+ } catch {
30
+ throw new Error(`Profile prompt "${cfg.prompt}" not found at ${filePath}`);
31
+ }
32
+ }
33
+
34
+ export function listProfiles() {
35
+ const configs = loadProfilesConfig();
36
+ return Object.keys(configs);
37
+ }
38
+
39
+ export function getProfileDescription(name) {
40
+ const configs = loadProfilesConfig();
41
+ return configs[name]?.description || "";
42
+ }