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,307 @@
1
+ import { tools as fsTools, handlers as fsHandlers } from "./filesystem.js";
2
+ import { createSystemTools } from "./system.js";
3
+ import { tools as memTools, handlers as memHandlers } from "../memory/tools.js";
4
+ // screenbox.js removed — desktop tools come only via MCP
5
+ import { createLogger } from "../logging/logger.js";
6
+ import { TOOL_SEARCH_NAME, toolSearchDef, createToolSearchHandler, resetLoadedTools } from "./tool-search.js";
7
+ import { swapToolDefs, createSwapHandlers } from "./swap-tools.js";
8
+ import { swapEnabled, getCurrentSwapStore } from "../agent/swap.js";
9
+ import { tools as pluginTools, handlers as pluginHandlers } from "./plugin-tools.js";
10
+
11
+ const log = createLogger("registry");
12
+
13
+ let allTools = [];
14
+ let handlerMap = {};
15
+ let mcpStatusList = [];
16
+ let mcpGetServerStatusFn = null;
17
+ let mcpDisconnectFn = null;
18
+ let mcpReconnectFn = null;
19
+
20
+ export function initRegistry(store) {
21
+ const { tools: sysTools, handlers: sysHandlers } = createSystemTools(store);
22
+
23
+ handlerMap = { ...fsHandlers, ...sysHandlers, ...memHandlers, ...pluginHandlers };
24
+ allTools = [...fsTools, ...sysTools, ...memTools, ...pluginTools];
25
+ loadedPlugins = [];
26
+ pluginToolOwner = new Map();
27
+
28
+ // tool_search is always registered: with the classifier off, MCP tools past
29
+ // the inline limit are offered through it (intent.js fallbackManifest), and
30
+ // FLINT_TOOL_MODE=search uses it for everything outside the core set. The
31
+ // handler searches allTools live, so MCP tools registered later are found.
32
+ resetLoadedTools();
33
+ handlerMap[TOOL_SEARCH_NAME] = createToolSearchHandler(() => allTools);
34
+ allTools.push(toolSearchDef);
35
+
36
+ // Context swap (docs/context-swap.md): reading back what the loop moved out
37
+ // of the context. Not offered when swap is off, so FLINT_SWAP=0 leaves the
38
+ // tool list as it was.
39
+ if (swapEnabled()) {
40
+ Object.assign(handlerMap, createSwapHandlers(getCurrentSwapStore));
41
+ allTools.push(...swapToolDefs);
42
+ }
43
+ }
44
+
45
+ // Called after async MCP init
46
+ export function registerMcpTools(tools, handlers, status) {
47
+ Object.assign(handlerMap, handlers);
48
+ allTools.push(...tools);
49
+ mcpStatusList = status;
50
+ }
51
+
52
+ // Mesh tools registration
53
+ export function registerMeshTools(tools, handlers) {
54
+ Object.assign(handlerMap, handlers);
55
+ allTools.push(...tools);
56
+ }
57
+
58
+ // Task planning tools registration
59
+ export function registerTaskTools(tools, handlers) {
60
+ Object.assign(handlerMap, handlers);
61
+ allTools.push(...tools);
62
+ }
63
+
64
+ // Dataset tools registration
65
+ export function registerDatasetTools(tools, handlers) {
66
+ Object.assign(handlerMap, handlers);
67
+ allTools.push(...tools);
68
+ }
69
+
70
+ // Schedule / reminder tools registration
71
+ export function registerScheduleTools(tools, handlers) {
72
+ Object.assign(handlerMap, handlers);
73
+ allTools.push(...tools);
74
+ }
75
+
76
+ // Inbox tools registration
77
+ export function registerInboxTools(tools, handlers) {
78
+ Object.assign(handlerMap, handlers);
79
+ allTools.push(...tools);
80
+ }
81
+
82
+ // Plugin tools registration
83
+ let loadedPlugins = [];
84
+ // tool name -> the plugin folder that registered it, so a reload can take a
85
+ // plugin's tools out again and a name clash can be told from a re-register.
86
+ let pluginToolOwner = new Map();
87
+
88
+ /**
89
+ * Register a loaded plugin. Throws when one of its tool names is already
90
+ * taken by a built-in, an MCP tool or another plugin.
91
+ *
92
+ * WHY the refusal: handlers used to be Object.assign'ed over the map,
93
+ * so a plugin with a tool named read_file silently replaced the real one.
94
+ * While only the user installed plugins that was a foot-gun; now the model can
95
+ * install one from the conversation, and the plugin is code nobody reviewed.
96
+ */
97
+ export function registerPlugin(plugin) {
98
+ const key = plugin._dir || plugin.name;
99
+ const names = (plugin.tools || []).map((t) => t.function?.name).filter(Boolean);
100
+ const taken = names.filter((n) => (handlerMap[n] || allTools.some((t) => t.function?.name === n)) && pluginToolOwner.get(n) !== key);
101
+ if (taken.length) {
102
+ throw new Error(`tool name already taken: ${taken.join(", ")}`);
103
+ }
104
+ if (plugin.tools && plugin.tools.length) {
105
+ allTools.push(...plugin.tools);
106
+ }
107
+ if (plugin.handlers) {
108
+ Object.assign(handlerMap, plugin.handlers);
109
+ }
110
+ for (const n of names) pluginToolOwner.set(n, key);
111
+ loadedPlugins.push({
112
+ name: plugin.name,
113
+ type: plugin.type,
114
+ version: plugin.version,
115
+ description: plugin.description,
116
+ toolCount: names.length,
117
+ tools: names,
118
+ dir: plugin._dir || null,
119
+ destroy: plugin.destroy || null,
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Take a plugin's tools and handlers out again, found by its folder.
125
+ * @returns {object|null} the record that was removed (its `destroy` included)
126
+ */
127
+ export function unregisterPlugin(dir) {
128
+ const rec = loadedPlugins.find((p) => p.dir === dir);
129
+ if (!rec) return null;
130
+ const names = new Set(rec.tools);
131
+ allTools = allTools.filter((t) => !names.has(t.function?.name));
132
+ for (const n of names) {
133
+ delete handlerMap[n];
134
+ pluginToolOwner.delete(n);
135
+ }
136
+ loadedPlugins = loadedPlugins.filter((p) => p !== rec);
137
+ return rec;
138
+ }
139
+
140
+ export function getLoadedPlugins() {
141
+ return loadedPlugins;
142
+ }
143
+
144
+ // MCP management functions
145
+ export function setMcpManagement(fns) {
146
+ mcpGetServerStatusFn = fns.getServerStatus;
147
+ mcpDisconnectFn = fns.disconnect;
148
+ mcpReconnectFn = fns.reconnect;
149
+ }
150
+
151
+ export function getMcpStatus() {
152
+ return mcpStatusList;
153
+ }
154
+
155
+ export function getMcpServerStatus() {
156
+ return mcpGetServerStatusFn ? mcpGetServerStatusFn() : [];
157
+ }
158
+
159
+ export async function mcpDisconnect(name) {
160
+ if (!mcpDisconnectFn) return false;
161
+ const ok = await mcpDisconnectFn(name);
162
+ if (ok) {
163
+ mcpStatusList = mcpStatusList.map((s) =>
164
+ s.name === name ? { ...s, ok: false, error: "disconnected" } : s,
165
+ );
166
+ }
167
+ return ok;
168
+ }
169
+
170
+ export async function mcpReconnect(name) {
171
+ if (!mcpReconnectFn) throw new Error("MCP not configured");
172
+ const result = await mcpReconnectFn(name);
173
+ Object.assign(handlerMap, result.handlers);
174
+ mcpStatusList = mcpStatusList.map((s) =>
175
+ s.name === name ? { name, tools: result.toolCount, ok: true } : s,
176
+ );
177
+ for (const tool of result.tools) {
178
+ if (!allTools.find((t) => t.function.name === tool.function.name)) {
179
+ allTools.push(tool);
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+
185
+ export function getDefinitions() {
186
+ // FLINT_TOOL_ALLOWLIST: comma-separated tool names. When set, the agent sees
187
+ // ONLY these tools -- least-privilege mode for embedded/headless executors
188
+ // (e.g. a host that runs Flint with just its delivery plugin: a full
189
+ // toolset let the executor shell around the host and chase stale tasks).
190
+ const allow = (process.env.FLINT_TOOL_ALLOWLIST || "").trim();
191
+ if (allow) {
192
+ const names = new Set(allow.split(",").map((x) => x.trim()).filter(Boolean));
193
+ return allTools.filter((t) => names.has(t.function?.name));
194
+ }
195
+ return allTools;
196
+ }
197
+
198
+ // ── Schema Validation ────────────────────────────────────────
199
+
200
+ function validateArgs(name, args) {
201
+ const def = allTools.find((t) => t.function?.name === name);
202
+ if (!def?.function?.parameters) return null; // no schema = skip
203
+ const schema = def.function.parameters;
204
+ const errors = [];
205
+
206
+ // Check required fields
207
+ if (schema.required) {
208
+ for (const field of schema.required) {
209
+ if (args[field] === undefined || args[field] === null) {
210
+ errors.push(`missing required parameter: "${field}"`);
211
+ }
212
+ }
213
+ }
214
+
215
+ // Type-check each provided property
216
+ if (schema.properties) {
217
+ for (const [key, value] of Object.entries(args)) {
218
+ const prop = schema.properties[key];
219
+ if (!prop) continue; // unknown props are tolerated (LLMs add extra fields)
220
+ const expected = prop.type;
221
+ if (!expected) continue;
222
+ const actual = typeof value;
223
+ if (expected === "string" && actual !== "string") {
224
+ errors.push(`"${key}" must be string, got ${actual}`);
225
+ } else if (expected === "integer" && (!Number.isInteger(value))) {
226
+ errors.push(`"${key}" must be integer, got ${actual} (${value})`);
227
+ } else if (expected === "number" && actual !== "number") {
228
+ errors.push(`"${key}" must be number, got ${actual}`);
229
+ } else if (expected === "boolean" && actual !== "boolean") {
230
+ errors.push(`"${key}" must be boolean, got ${actual}`);
231
+ } else if (expected === "array" && !Array.isArray(value)) {
232
+ errors.push(`"${key}" must be array, got ${actual}`);
233
+ } else if (expected === "object" && (actual !== "object" || Array.isArray(value) || value === null)) {
234
+ errors.push(`"${key}" must be object, got ${actual}`);
235
+ }
236
+ }
237
+ }
238
+
239
+ return errors.length ? errors : null;
240
+ }
241
+
242
+ // Levenshtein distance — small helper for tool-name auto-repair.
243
+ function _levDistance(a, b) {
244
+ if (a === b) return 0;
245
+ const m = a.length, n = b.length;
246
+ if (m === 0) return n;
247
+ if (n === 0) return m;
248
+ const dp = new Array(n + 1);
249
+ for (let j = 0; j <= n; j++) dp[j] = j;
250
+ for (let i = 1; i <= m; i++) {
251
+ let prev = dp[0];
252
+ dp[0] = i;
253
+ for (let j = 1; j <= n; j++) {
254
+ const tmp = dp[j];
255
+ dp[j] = a[i - 1] === b[j - 1] ? prev : Math.min(prev, dp[j], dp[j - 1]) + 1;
256
+ prev = tmp;
257
+ }
258
+ }
259
+ return dp[n];
260
+ }
261
+
262
+ // Fuzzy-match a tool name against the registry. Returns the closest registered
263
+ // name if a unique match exists within maxDistance, else null.
264
+ function _repairToolName(name, maxDistance = 2) {
265
+ const candidates = Object.keys(handlerMap);
266
+ if (candidates.includes(name)) return name;
267
+ const scored = candidates
268
+ .map((c) => ({ name: c, dist: _levDistance(name, c) }))
269
+ .filter((x) => x.dist <= maxDistance)
270
+ .sort((a, b) => a.dist - b.dist);
271
+ if (scored.length === 0) return null;
272
+ // Require unambiguous match: best is strictly closer than second-best.
273
+ if (scored.length === 1 || scored[0].dist < scored[1].dist) return scored[0].name;
274
+ return null;
275
+ }
276
+
277
+ export async function executeTool(name, args) {
278
+ let handler = handlerMap[name];
279
+ let repairedName = null;
280
+ if (!handler) {
281
+ const repaired = _repairToolName(name);
282
+ if (repaired) {
283
+ repairedName = repaired;
284
+ handler = handlerMap[repaired];
285
+ }
286
+ }
287
+ if (!handler) {
288
+ return `Error: unknown tool "${name}"`;
289
+ }
290
+ if (repairedName) {
291
+ // Log the repair so we can measure frequency; caller is agent.js which logs tool calls.
292
+ console.error(`[tool-repair] '${name}' -> '${repairedName}'`);
293
+ name = repairedName;
294
+ }
295
+
296
+ // Validate args against tool schema
297
+ const validationErrors = validateArgs(name, args);
298
+ if (validationErrors) {
299
+ return `Error: invalid parameters for "${name}": ${validationErrors.join("; ")}`;
300
+ }
301
+
302
+ try {
303
+ return await handler(args);
304
+ } catch (err) {
305
+ return `Error: ${err.message}`;
306
+ }
307
+ }
@@ -0,0 +1,72 @@
1
+ // swap_list and swap_read: what the session's context swap holds, and reading
2
+ // it back (docs/context-swap.md). The store is the current session's
3
+ // (agent/swap.js getCurrentSwapStore), handed in so the tools test alone.
4
+
5
+ import { stubFor } from "../agent/swap.js";
6
+
7
+ export const swapToolDefs = [
8
+ {
9
+ type: "function",
10
+ function: {
11
+ name: "swap_list",
12
+ description:
13
+ "List what this session has moved out of the context to its swap: pages, files and command output " +
14
+ "read earlier, newest first, one line each ([swap #N ...]). Filter by turn, by time ('1h', '30m'), " +
15
+ "by a part of the source (URL, path, command) or by words in the title. Look here before fetching " +
16
+ "something again.",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {
20
+ turn: { type: "number", description: "Only entries from this turn" },
21
+ since: { type: "string", description: "Only entries newer than this, e.g. '1h', '30m', '2d'" },
22
+ source: { type: "string", description: "Part of the URL, path or command" },
23
+ text: { type: "string", description: "Words that must all be in the title" },
24
+ limit: { type: "number", description: "At most this many (default 30)" },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ {
30
+ type: "function",
31
+ function: {
32
+ name: "swap_read",
33
+ description:
34
+ "Read back a swapped entry by its number (the N of [swap #N ...]): all of it, or `limit` lines " +
35
+ "from line `offset` for a long one. To search inside swapped text, use search_in_files on the " +
36
+ "session's swap folder.",
37
+ parameters: {
38
+ type: "object",
39
+ properties: {
40
+ id: { type: "number", description: "The entry number" },
41
+ offset: { type: "number", description: "First line to read, from 1" },
42
+ limit: { type: "number", description: "How many lines" },
43
+ },
44
+ required: ["id"],
45
+ },
46
+ },
47
+ },
48
+ ];
49
+
50
+ const UNAVAILABLE = "Swap is not available in this session.";
51
+
52
+ export function createSwapHandlers(getStore) {
53
+ return {
54
+ async swap_list({ turn, since, source, text, limit = 30 } = {}) {
55
+ const store = getStore();
56
+ if (!store) return UNAVAILABLE;
57
+ const entries = store.list({ turn, since, source, text, limit });
58
+ if (!entries.length) return "Nothing in swap matches.";
59
+ return `${entries.length} swapped entr${entries.length === 1 ? "y" : "ies"}, newest first (folder: ${store.dir}):\n` +
60
+ entries.map(stubFor).join("\n");
61
+ },
62
+ async swap_read({ id, offset, limit } = {}) {
63
+ const store = getStore();
64
+ if (!store) return UNAVAILABLE;
65
+ const e = store.get(id);
66
+ if (!e) return `No swap entry #${id}. swap_list shows what there is.`;
67
+ const text = store.read(id, { offset, limit });
68
+ if (text == null) return `Swap entry #${id} is listed but its file could not be read (${e.file}).`;
69
+ return `${stubFor(e)}\n${text}`;
70
+ },
71
+ };
72
+ }