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,34 @@
1
+ // Hardcoded safety constants — NEVER overridable via config or env vars.
2
+ // These are the absolute minimum safety guarantees of the system.
3
+ // Changing these requires a code change and code review.
4
+
5
+ // Max tool result size in bytes (1 MB) — prevents context flooding
6
+ export const MAX_RESULT_BYTES = 1_048_576;
7
+
8
+ // Max agent spawn depth — prevents recursive agent chain attacks
9
+ export const MAX_AGENT_DEPTH = 5;
10
+
11
+ // Max content length for memory entries
12
+ export const MAX_MEMORY_CONTENT = 10_000;
13
+
14
+ // HMAC algorithm — always SHA-256
15
+ export const HMAC_ALGORITHM = "sha256";
16
+
17
+ // HMAC key length in bytes
18
+ export const HMAC_KEY_BYTES = 32;
19
+
20
+ // Security module cannot be disabled in production
21
+ export const ALLOW_SECURITY_DISABLE = process.env.NODE_ENV === "test";
22
+
23
+ // Minimum security policy — can't go below "normal" in production
24
+ export const MIN_SECURITY_POLICY = "normal";
25
+
26
+ // These injection patterns are always checked, regardless of config
27
+ export const CORE_INJECTION_PATTERNS = [
28
+ /\bignore\s+(all\s+)?previous\s+instructions?\b/i,
29
+ /\byou\s+are\s+now\b.*\bassistant\b/i,
30
+ /\bforget\s+(everything|all)\b/i,
31
+ /<\/?system>/i,
32
+ /\bjailbreak\b/i,
33
+ /\boverride\s+safety\b/i,
34
+ ];
@@ -0,0 +1,107 @@
1
+ // Watchdog — periodic checks for memory usage and self-modification
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import crypto from "node:crypto";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const AGENT_SRC_DIR = path.resolve(__dirname, "..");
10
+
11
+ const HEAP_WARN_BYTES = 500 * 1024 * 1024; // 500 MB
12
+ const CHECK_INTERVAL = 30000; // 30 seconds
13
+
14
+ let watchdogTimer = null;
15
+ let initialHash = null;
16
+
17
+ /**
18
+ * Compute a hash of all file mtimes and sizes in src/.
19
+ * This detects self-modification of the agent source.
20
+ */
21
+ function computeSrcHash() {
22
+ const hasher = crypto.createHash("sha256");
23
+ try {
24
+ collectFiles(AGENT_SRC_DIR, hasher, 0);
25
+ } catch {
26
+ // If we can't read src, return a sentinel
27
+ return "unreadable";
28
+ }
29
+ return hasher.digest("hex");
30
+ }
31
+
32
+ function collectFiles(dir, hasher, depth) {
33
+ if (depth > 10) return;
34
+ let entries;
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ } catch {
38
+ return;
39
+ }
40
+ // Sort for consistent hash
41
+ entries.sort((a, b) => a.name.localeCompare(b.name));
42
+ for (const entry of entries) {
43
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
44
+ const full = path.join(dir, entry.name);
45
+ if (entry.isDirectory()) {
46
+ collectFiles(full, hasher, depth + 1);
47
+ } else {
48
+ try {
49
+ const stat = fs.statSync(full);
50
+ hasher.update(`${full}:${stat.size}:${stat.mtimeMs}\n`);
51
+ } catch {
52
+ // skip unreadable files
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Start the watchdog timer.
60
+ * @param {object} store - Zustand store
61
+ * @param {object} config - App config
62
+ * @param {object} [auditFns] - { auditLog } for logging alerts
63
+ * @returns {Function} stopWatchdog — call to stop the timer
64
+ */
65
+ export function startWatchdog(store, config, auditFns) {
66
+ const auditLog = auditFns?.auditLog || (() => {});
67
+
68
+ // Capture initial state
69
+ initialHash = computeSrcHash();
70
+
71
+ watchdogTimer = setInterval(() => {
72
+ try {
73
+ // 1. Memory check
74
+ const mem = process.memoryUsage();
75
+ if (mem.heapUsed > HEAP_WARN_BYTES) {
76
+ const mb = (mem.heapUsed / 1024 / 1024).toFixed(0);
77
+ auditLog("WATCHDOG_ALERT", null, {}, {
78
+ alert: "high_memory",
79
+ heapUsedMB: mb,
80
+ });
81
+ }
82
+
83
+ // 2. Self-modification detection
84
+ const currentHash = computeSrcHash();
85
+ if (initialHash && currentHash !== initialHash && currentHash !== "unreadable") {
86
+ auditLog("WATCHDOG_ALERT", null, {}, {
87
+ alert: "src_modified",
88
+ message: "Agent source files have been modified since startup",
89
+ });
90
+ // Update hash so we don't spam alerts
91
+ initialHash = currentHash;
92
+ }
93
+ } catch {
94
+ // Watchdog failures are non-fatal
95
+ }
96
+ }, CHECK_INTERVAL);
97
+
98
+ // Don't block Node.js exit
99
+ if (watchdogTimer.unref) watchdogTimer.unref();
100
+
101
+ return function stopWatchdog() {
102
+ if (watchdogTimer) {
103
+ clearInterval(watchdogTimer);
104
+ watchdogTimer = null;
105
+ }
106
+ };
107
+ }
@@ -0,0 +1,130 @@
1
+ import fs from "node:fs/promises";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
4
+ import path from "node:path";
5
+ import { config } from "./config.js";
6
+ import { stripTimeStamp } from "./agent/time-stamp.js";
7
+
8
+ // ── HMAC Integrity ──────────────────────────────────────────
9
+
10
+ function getSessionKey() {
11
+ if (process.env.AGENT_MEMORY_HMAC_KEY) return process.env.AGENT_MEMORY_HMAC_KEY;
12
+ const keyFile = path.join(config.sessionsDir, ".hmac-key");
13
+ if (existsSync(keyFile)) return readFileSync(keyFile, "utf-8").trim();
14
+ mkdirSync(config.sessionsDir, { recursive: true });
15
+ const key = randomBytes(32).toString("hex");
16
+ writeFileSync(keyFile, key, { encoding: "utf-8", mode: 0o600 });
17
+ return key;
18
+ }
19
+
20
+ function computeSessionHmac(data) {
21
+ return createHmac("sha256", getSessionKey()).update(data, "utf-8").digest("hex");
22
+ }
23
+
24
+ function hmacPath(sessionId) {
25
+ return path.join(config.sessionsDir, `${sessionId}.hmac`);
26
+ }
27
+
28
+ export function generateSessionId() {
29
+ return new Date().toISOString().slice(0, 19).replace(/[:.]/g, "-");
30
+ }
31
+
32
+ export async function saveSession(id, { messages, model, inputHistory, profile, plan, pastedImages, lastSummary, provider, sessionCost, sessionPromptTokens, sessionCompletionTokens }) {
33
+ await fs.mkdir(config.sessionsDir, { recursive: true });
34
+ const filePath = path.join(config.sessionsDir, `${id}.json`);
35
+ const data = {
36
+ id,
37
+ model,
38
+ provider: provider || null,
39
+ updated: new Date().toISOString(),
40
+ cwd: process.cwd(),
41
+ messages,
42
+ inputHistory: inputHistory || [],
43
+ profile: profile || null,
44
+ plan: plan || null,
45
+ pastedImages: pastedImages || [],
46
+ lastSummary: lastSummary || null,
47
+ sessionCost: sessionCost || 0,
48
+ sessionPromptTokens: sessionPromptTokens || 0,
49
+ sessionCompletionTokens: sessionCompletionTokens || 0,
50
+ };
51
+ const json = JSON.stringify(data, null, 2);
52
+ await fs.writeFile(filePath, json, "utf-8");
53
+ // Update HMAC
54
+ writeFileSync(hmacPath(id), computeSessionHmac(json), "utf-8");
55
+ }
56
+
57
+ export async function loadSession(id) {
58
+ const filePath = path.join(config.sessionsDir, `${id}.json`);
59
+ const content = await fs.readFile(filePath, "utf-8");
60
+
61
+ // Verify integrity
62
+ const hp = hmacPath(id);
63
+ if (existsSync(hp)) {
64
+ const stored = readFileSync(hp, "utf-8").trim();
65
+ const computed = computeSessionHmac(content);
66
+ try {
67
+ const storedBuf = Buffer.from(stored, "hex");
68
+ const computedBuf = Buffer.from(computed, "hex");
69
+ if (storedBuf.length !== computedBuf.length || !timingSafeEqual(storedBuf, computedBuf)) {
70
+ console.error(`[SECURITY] Session integrity check FAILED for ${id} — possible tampering`);
71
+ throw new Error(`Session ${id} failed integrity check`);
72
+ }
73
+ } catch (err) {
74
+ if (err.message.includes("integrity check")) throw err;
75
+ console.error(`[SECURITY] Session HMAC error for ${id} — rejecting`);
76
+ throw new Error(`Session ${id} failed integrity check`);
77
+ }
78
+ } else if (content.trim().length > 0) {
79
+ // Session data exists without HMAC → auto-heal (migration)
80
+ writeFileSync(hmacPath(id), computeSessionHmac(content), "utf-8");
81
+ }
82
+
83
+ return JSON.parse(content);
84
+ }
85
+
86
+ /** A message's text, whether its content is a string or a list of parts. */
87
+ function textOf(content) {
88
+ if (typeof content === "string") return stripTimeStamp(content);
89
+ if (Array.isArray(content)) return stripTimeStamp(content.filter((p) => p?.type === "text").map((p) => p.text).join(" ")).trim();
90
+ return "";
91
+ }
92
+
93
+ export async function listSessions() {
94
+ try {
95
+ const files = await fs.readdir(config.sessionsDir);
96
+ const sessions = [];
97
+ for (const file of files) {
98
+ if (!file.endsWith(".json")) continue;
99
+ const id = file.replace(".json", "");
100
+ try {
101
+ const filePath = path.join(config.sessionsDir, file);
102
+ const raw = await fs.readFile(filePath, "utf-8");
103
+ const data = JSON.parse(raw);
104
+ const userMsg = data.messages?.find((m) => m.role === "user");
105
+ // The last thing the operator asked, for /resume: the first message
106
+ // of a long session says little about where it got to.
107
+ const userMsgs = (data.messages || []).filter((m) => m.role === "user");
108
+ const lastUser = userMsgs[userMsgs.length - 1];
109
+ sessions.push({
110
+ id,
111
+ model: data.model || "?",
112
+ updated: data.updated || "",
113
+ userMessages: userMsgs.length,
114
+ last: textOf(lastUser?.content),
115
+ preview: userMsg
116
+ ? textOf(userMsg.content).length > 60
117
+ ? textOf(userMsg.content).slice(0, 60) + "..."
118
+ : textOf(userMsg.content)
119
+ : "(empty)",
120
+ });
121
+ } catch {
122
+ sessions.push({ id, model: "?", updated: "", preview: "(corrupt)" });
123
+ }
124
+ }
125
+ sessions.sort((a, b) => b.updated.localeCompare(a.updated));
126
+ return sessions;
127
+ } catch {
128
+ return [];
129
+ }
130
+ }
package/src/spend.js ADDED
@@ -0,0 +1,97 @@
1
+ // Spend modes: economy, normal, generous. One switch for how many tokens go
2
+ // into each model call (docs/spend-modes.md).
3
+ //
4
+ // Every saving rule had its own variable and a default tuned for cheap,
5
+ // small-window runs, so a first try with a capable model met an agent that
6
+ // summarised what it had read and searched for tools it had been given.
7
+ // A mode sets them together, the way the care level sets which tools ask.
8
+ // A variable for a single setting still wins over the mode.
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import path from "node:path";
13
+
14
+ export const SPEND_NAMES = ["economy", "normal", "generous"];
15
+ export const DEFAULT_SPEND = "normal";
16
+
17
+ /**
18
+ * The table of docs/spend-modes.md, as data. Fractions are of the model's
19
+ * context window; `cap` is a ceiling in tokens (null: none); `unknown` is
20
+ * used when the window is not known.
21
+ */
22
+ export const SPEND_LEVELS = {
23
+ economy: {
24
+ description: "fewest tokens a call: tools through search, early compression and swap, saving advice for the model",
25
+ mcpInlineMax: 10,
26
+ compress: { fraction: 0.25, cap: 64000, unknown: 32000 },
27
+ swapFromShare: 0.5,
28
+ swap: { resultMax: 2048, budgetTokens: 8000, headBytes: 1024 },
29
+ conv: { fraction: 0.4, cap: 150000, unknown: 60000 },
30
+ advice: true,
31
+ },
32
+ normal: {
33
+ description: "balanced: up to 30 MCP tools whole, compression at half the window, swap just before it",
34
+ mcpInlineMax: 30,
35
+ compress: { fraction: 0.5, cap: 128000, unknown: 64000 },
36
+ swapFromShare: 0.75,
37
+ swap: { resultMax: 4096, budgetTokens: 16000, headBytes: 1500 },
38
+ conv: { fraction: 0.6, cap: 300000, unknown: 100000 },
39
+ advice: false,
40
+ },
41
+ generous: {
42
+ description: "most context: every tool whole, compression and swap only near the window's edge",
43
+ mcpInlineMax: 200,
44
+ compress: { fraction: 0.8, cap: null, unknown: 200000 },
45
+ swapFromShare: 0.9,
46
+ swap: { resultMax: 16384, budgetTokens: 64000, headBytes: 4096 },
47
+ conv: { fraction: 0.85, cap: null, unknown: 200000 },
48
+ advice: false,
49
+ },
50
+ };
51
+
52
+ function spendFile() {
53
+ const dir = process.env.FLINT_DATA_DIR ? path.resolve(process.env.FLINT_DATA_DIR) : path.join(homedir(), ".flint");
54
+ return path.join(dir, "spend.json");
55
+ }
56
+
57
+ /** A level name from loose input ("e", "Economy", "gen"), or null. */
58
+ export function parseSpendName(input) {
59
+ const s = String(input || "").trim().toLowerCase();
60
+ if (!s) return null;
61
+ return SPEND_NAMES.find((n) => n === s || n.startsWith(s)) || null;
62
+ }
63
+
64
+ /** FLINT_SPEND, else the saved choice, else normal. An unknown name reads as normal. */
65
+ export function getSpendLevel(env = process.env) {
66
+ const fromEnv = parseSpendName(env.FLINT_SPEND);
67
+ if (fromEnv) return fromEnv;
68
+ try {
69
+ const f = spendFile();
70
+ if (existsSync(f)) {
71
+ const saved = parseSpendName(JSON.parse(readFileSync(f, "utf8")).level);
72
+ if (saved) return saved;
73
+ }
74
+ } catch {}
75
+ return DEFAULT_SPEND;
76
+ }
77
+
78
+ /** Save a level; returns the level, or null for a name that is not one. */
79
+ export function setSpendLevel(name) {
80
+ const level = parseSpendName(name);
81
+ if (!level) return null;
82
+ const f = spendFile();
83
+ mkdirSync(path.dirname(f), { recursive: true });
84
+ writeFileSync(f, JSON.stringify({ level }, null, 2) + "\n", "utf8");
85
+ return level;
86
+ }
87
+
88
+ export function spendSettings(level = getSpendLevel()) {
89
+ return SPEND_LEVELS[level] || SPEND_LEVELS[DEFAULT_SPEND];
90
+ }
91
+
92
+ /** A window share with a ceiling, or the fallback when the window is unknown. */
93
+ export function windowShare({ fraction, cap, unknown }, window) {
94
+ if (!window) return unknown;
95
+ const v = Math.floor(window * fraction);
96
+ return cap ? Math.min(v, cap) : v;
97
+ }
@@ -0,0 +1,59 @@
1
+ // Startup watchdog: if Flint has not finished starting within the timeout, it
2
+ // exits rather than hang. Time spent waiting for the operator is not startup.
3
+ //
4
+ // Owner, 2026-10-01, first run on a fresh machine: the first-run wizard and the
5
+ // "How careful should Flint be?" question were on screen, and the watchdog
6
+ // killed Flint at 30 s while it waited for an answer. The launcher's
7
+ // "loading..." spinner was also still repainting over both prompts, because the
8
+ // child only told it to stop once the main UI had drawn.
9
+
10
+ const TIMEOUT_MS = 30000;
11
+
12
+ let timer = null;
13
+ let done = false;
14
+
15
+ function arm() {
16
+ if (done) return;
17
+ if (timer) clearTimeout(timer);
18
+ timer = setTimeout(() => {
19
+ process.stderr.write(`\n[WATCHDOG] Startup timeout (${TIMEOUT_MS / 1000}s) -- force exit\n`);
20
+ process.exit(1);
21
+ }, TIMEOUT_MS);
22
+ timer.unref?.();
23
+ }
24
+
25
+ /** Start counting. Called once, at the top of index.js. */
26
+ export function startStartupWatchdog() {
27
+ done = false;
28
+ arm();
29
+ }
30
+
31
+ /** Startup finished: stop counting for good. */
32
+ export function clearStartupWatchdog() {
33
+ done = true;
34
+ if (timer) clearTimeout(timer);
35
+ timer = null;
36
+ }
37
+
38
+ /** For tests: is the watchdog currently counting? */
39
+ export function isStartupWatchdogArmed() {
40
+ return timer !== null;
41
+ }
42
+
43
+ /**
44
+ * Run an interactive startup step: no countdown while the operator answers,
45
+ * and the launcher spinner is told to stop before the question is drawn.
46
+ * The countdown restarts in full once the step is over.
47
+ */
48
+ export async function whileWaitingForOperator(fn) {
49
+ if (timer) clearTimeout(timer);
50
+ timer = null;
51
+ // Same message the main UI sends when it has drawn; the launcher only
52
+ // stops its spinner, so sending it early is harmless.
53
+ try { process.send?.({ type: "flint:ready" }); } catch { /* no IPC channel */ }
54
+ try {
55
+ return await fn();
56
+ } finally {
57
+ arm();
58
+ }
59
+ }
@@ -0,0 +1,71 @@
1
+ // Command-line flags of the stdio mode.
2
+ //
3
+ // Flint takes the flags agent CLIs commonly take for the same job, so a host that
4
+ // drives `claude` as a long-lived subprocess can start Flint with the same command line:
5
+ //
6
+ // flint --print --verbose --model <id> \
7
+ // --input-format stream-json --output-format stream-json \
8
+ // [--session-id <id> | --resume <id>] [--dangerously-skip-permissions] \
9
+ // [--system-prompt-file <path>] [--append-system-prompt <text>] \
10
+ // [--mcp-config <path>]
11
+ //
12
+ // `--stdio` alone means the same thing. Flags such CLIs have and Flint does
13
+ // not need (--add-dir, --max-turns, ...) are accepted and ignored, so a host
14
+ // passing them does not have to know which agent it is talking to.
15
+
16
+ const VALUE_FLAGS = new Set([
17
+ "--model", "--session-id", "--resume", "-r", "--system-prompt", "--system-prompt-file",
18
+ "--append-system-prompt", "--append-system-prompt-file", "--mcp-config", "--input-format",
19
+ "--output-format", "--cwd", "--max-turns", "--add-dir", "--permission-mode",
20
+ "--allowedTools", "--allowed-tools", "--disallowedTools", "--disallowed-tools",
21
+ "--fallback-model", "--settings",
22
+ ]);
23
+
24
+ /** Session ids become file names: letters, digits, dash and underscore only. */
25
+ export function isSafeSessionId(id) {
26
+ return typeof id === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(id);
27
+ }
28
+
29
+ /**
30
+ * The stdio-mode options in argv, or null when Flint was not started in it.
31
+ * Throws on a value that cannot be used, so the host sees the reason on
32
+ * stderr and a non-zero exit instead of a session that quietly ignores it.
33
+ */
34
+ export function parseStdioArgs(argv) {
35
+ const args = argv.slice(2);
36
+ const values = {};
37
+ const flags = new Set();
38
+ for (let i = 0; i < args.length; i++) {
39
+ const a = args[i];
40
+ const eq = a.startsWith("--") ? a.indexOf("=") : -1;
41
+ if (eq > 0) { values[a.slice(0, eq)] = a.slice(eq + 1); continue; }
42
+ if (VALUE_FLAGS.has(a)) { values[a] = args[i + 1]; i++; continue; }
43
+ flags.add(a);
44
+ }
45
+ const streaming = values["--input-format"] === "stream-json" || values["--output-format"] === "stream-json";
46
+ if (!flags.has("--stdio") && !streaming) return null;
47
+ if (values["--input-format"] && values["--input-format"] !== "stream-json") {
48
+ throw new Error(`--input-format ${values["--input-format"]} is not supported; use stream-json`);
49
+ }
50
+ if (values["--output-format"] && values["--output-format"] !== "stream-json") {
51
+ throw new Error(`--output-format ${values["--output-format"]} is not supported; use stream-json`);
52
+ }
53
+ const resume = values["--resume"] ?? values["-r"] ?? null;
54
+ const sessionId = resume ?? values["--session-id"] ?? null;
55
+ if (sessionId != null && !isSafeSessionId(sessionId)) {
56
+ throw new Error(`session id "${sessionId}" may contain only letters, digits, - and _`);
57
+ }
58
+ return {
59
+ model: values["--model"] || null,
60
+ sessionId,
61
+ resume: resume != null,
62
+ systemPrompt: values["--system-prompt"] ?? null,
63
+ systemPromptFile: values["--system-prompt-file"] ?? null,
64
+ appendSystemPrompt: values["--append-system-prompt"] ?? null,
65
+ appendSystemPromptFile: values["--append-system-prompt-file"] ?? null,
66
+ mcpConfig: values["--mcp-config"] ?? null,
67
+ cwd: values["--cwd"] ?? null,
68
+ skipPermissions: flags.has("--dangerously-skip-permissions") || values["--permission-mode"] === "bypassPermissions",
69
+ verbose: flags.has("--verbose"),
70
+ };
71
+ }
@@ -0,0 +1,59 @@
1
+ // First thing index.js imports, so it runs before any other module is
2
+ // evaluated. In the stdio mode stdout carries the protocol and nothing else:
3
+ // one stray console.log from any module would be a line the host cannot
4
+ // parse. So here, before anything can print:
5
+ //
6
+ // - the real stdout writer is kept for the protocol (protocolWrite);
7
+ // - process.stdout.write and console.log/info/warn/error go to stderr;
8
+ // - --cwd is applied, so every module sees the agent's own folder;
9
+ // - FLINT_DATA_DIR defaults to ~/.flint, so sessions and the task database
10
+ // live in the home of the user the host runs this agent as (a host that
11
+ // gives every agent its own user and HOME) instead of the checkout.
12
+ //
13
+ // Outside the stdio mode this module does nothing.
14
+
15
+ import { format } from "node:util";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { parseStdioArgs } from "./args.js";
19
+
20
+ let parsed = null;
21
+ let parseError = null;
22
+ try {
23
+ parsed = parseStdioArgs(process.argv);
24
+ } catch (err) {
25
+ parseError = err;
26
+ }
27
+
28
+ export const stdioArgs = parsed;
29
+ export const stdioArgsError = parseError;
30
+
31
+ const realWrite = process.stdout.write.bind(process.stdout);
32
+
33
+ /** Write one protocol object as a line on the real stdout. */
34
+ export function protocolWrite(obj) {
35
+ realWrite(JSON.stringify(obj) + "\n");
36
+ }
37
+
38
+ if (parsed || parseError) {
39
+ const toStderr = (...args) => { process.stderr.write(format(...args) + "\n"); };
40
+ process.stdout.write = (chunk, encoding, cb) => process.stderr.write(chunk, encoding, cb);
41
+ console.log = toStderr;
42
+ console.info = toStderr;
43
+ console.warn = toStderr;
44
+ console.error = toStderr;
45
+ console.debug = toStderr;
46
+ if (parseError) {
47
+ process.stderr.write(`[flint] ${parseError.message}\n`);
48
+ process.exit(2);
49
+ }
50
+ if (parsed.cwd) {
51
+ try {
52
+ process.chdir(parsed.cwd);
53
+ } catch (err) {
54
+ process.stderr.write(`[flint] cannot use --cwd ${parsed.cwd}: ${err.message}\n`);
55
+ process.exit(2);
56
+ }
57
+ }
58
+ if (!process.env.FLINT_DATA_DIR) process.env.FLINT_DATA_DIR = join(homedir(), ".flint");
59
+ }