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,1297 @@
1
+ // All slash commands implementation
2
+ import chalk from "chalk";
3
+ import { readFileSync } from "node:fs";
4
+ import { config } from "../config.js";
5
+ import {
6
+ generateSessionId,
7
+ saveSession,
8
+ loadSession,
9
+ listSessions,
10
+ } from "../sessions.js";
11
+ import { getSystemMessage } from "../agent/system-prompt.js";
12
+ import { clearDigest } from "../memory/conversation-digest.js";
13
+ import { clearSessionFacts } from "../memory/session-facts.js";
14
+ import { getPermissionMap, setPermission, resetSessionOverrides, bulkSetPermission } from "../tools/permissions.js";
15
+ import { getMemoryStats, listRecentMemories, clearAllMemories } from "../memory/store.js";
16
+ import { updateMemoryMd } from "../memory/markdown.js";
17
+ import { loadProfile, listProfiles as listProfileNames } from "../profiles.js";
18
+ import { formatPlanForPrompt } from "../tools/tasks.js";
19
+ import {
20
+ getActiveGoal, listGoals, getTasksByGoal, getTaskStats,
21
+ getDashboard, goalToPlan, syncPlanToStore, abandonAllActiveGoals,
22
+ } from "../tasks/queries.js";
23
+ import { listAgents } from "../registry.js";
24
+ import { getMcpServerStatus, mcpDisconnect, mcpReconnect, getLoadedPlugins, unregisterPlugin } from "../tools/registry.js";
25
+ import { activatePlugin } from "../tools/plugin-tools.js";
26
+ import { join as pathJoin } from "node:path";
27
+ import { installPlugin, uninstallPlugin } from "../plugins/manager.js";
28
+ import { listInstalledPlugins, getPluginsDir } from "../plugins/loader.js";
29
+ import { rewind, rewindAll, getCheckpointStack, clearCheckpoints } from "../tools/checkpoint.js";
30
+ import { printTable } from "../ui/output.js";
31
+ import * as bus from "../bus/index.js";
32
+ import { notify as busPush } from "../bus/drain-loop.js";
33
+ import { apiUrl } from "../api/address.js";
34
+
35
+ export function registerCommands(store) {
36
+ function log(text) {
37
+ store.getState().addLine(text);
38
+ }
39
+
40
+ const commands = {
41
+ async "/help"() {
42
+ log("");
43
+ log(chalk.cyan(" Commands:"));
44
+ log(chalk.white(" /sessions") + chalk.gray(" -- list saved sessions"));
45
+ log(chalk.white(" /resume") + chalk.gray(" -- pick a recent session to continue (or /resume <id>)"));
46
+ log(chalk.white(" /spend") + chalk.gray(" -- tokens per call: economy, normal, generous"));
47
+ log(chalk.white(" /update") + chalk.gray(" -- install a newer Flint and restart, same session"));
48
+ log(chalk.white(" /load <id>") + chalk.gray(" -- load a session"));
49
+ log(chalk.white(" /new") + chalk.gray(" -- start new session"));
50
+ log(chalk.white(" /clear") + chalk.gray(" -- clear context (keep session)"));
51
+ log(chalk.white(" /queue") + chalk.gray(" -- list queued messages"));
52
+ log(chalk.white(" /later <q>") + chalk.gray(" -- queue a question for later"));
53
+ log(chalk.white(" /paired") + chalk.gray(" -- programs paired with the API (/paired revoke <name>)"));
54
+ log(chalk.white(" /careful") + chalk.gray(" -- how often Flint asks: safe, normal, permissive"));
55
+ log(chalk.white(" /ps") + chalk.gray(" -- background processes"));
56
+ log(chalk.white(" /logs <id>") + chalk.gray(" -- last output of a background process"));
57
+ log(chalk.white(" /kill <id>") + chalk.gray(" -- stop a background process (/kill all)"));
58
+ log(chalk.white(" /tools [n]") + chalk.gray(" -- last n tool calls"));
59
+ log(chalk.white(" /sys") + chalk.gray(" -- model, cost, context, MCP, session"));
60
+ log(chalk.white(" /plan") + chalk.gray(" -- show current task plan"));
61
+ log(chalk.white(" /tasks") + chalk.gray(" -- task dashboard (SQLite)"));
62
+ log(chalk.white(" /continue") + chalk.gray(" -- resume unfinished auto work"));
63
+ log(chalk.white(" /rewind") + chalk.gray(" -- undo last file change"));
64
+ log(chalk.white(" /rewind N") + chalk.gray(" -- undo last N changes"));
65
+ log(chalk.white(" /rewind all") + chalk.gray(" -- undo all changes this session"));
66
+ log(chalk.white(" /model") + chalk.gray(" -- show current AI model"));
67
+ log(chalk.white(" /provider") + chalk.gray(" -- show/switch LLM provider"));
68
+ log(chalk.white(" /key") + chalk.gray(" -- manage API keys"));
69
+ log(chalk.white(" /profile") + chalk.gray(" -- switch agent profile"));
70
+ log(chalk.white(" /agents") + chalk.gray(" -- list running agent instances"));
71
+ log(chalk.white(" /mcp") + chalk.gray(" -- manage MCP server connections"));
72
+ log(chalk.white(" /paste") + chalk.gray(" -- send clipboard image to AI"));
73
+ log(chalk.white(" /copy") + chalk.gray(" -- copy chat output to clipboard"));
74
+ log(chalk.white(" /restart") + chalk.gray(" -- restart with updated code"));
75
+ log(chalk.white(" /stats") + chalk.gray(" -- show current session info"));
76
+ log(chalk.white(" /budget") + chalk.gray(" -- show session budget & spending"));
77
+ log(chalk.white(" /stop") + chalk.gray(" -- abort running agent"));
78
+ log(chalk.white(" /memory") + chalk.gray(" -- show persistent memory stats"));
79
+ log(chalk.white(" /memory clear") + chalk.gray(" -- clear all memories"));
80
+ log(chalk.white(" /permissions") + chalk.gray(" -- show tool permission map"));
81
+ log(chalk.white(" /allow <t>") + chalk.gray(" -- allow a tool without confirmation"));
82
+ log(chalk.white(" /deny <t>") + chalk.gray(" -- deny a tool completely"));
83
+ log(chalk.white(" /confirm <t>") + chalk.gray(" -- require confirmation for a tool"));
84
+ log(chalk.white(" /allow-all") + chalk.gray(" -- allow all tools (YOLO mode)"));
85
+ log(chalk.white(" /deny-all") + chalk.gray(" -- deny all tools"));
86
+ log(chalk.white(" /reset-permissions") + chalk.gray(" -- reset to defaults"));
87
+ log(chalk.white(" /auto <task>") + chalk.gray(" -- run task autonomously"));
88
+ log(chalk.white(" /plugins") + chalk.gray(" -- list installed plugins"));
89
+ log(chalk.white(" /install <p>") + chalk.gray(" -- install a plugin"));
90
+ log(chalk.white(" /uninstall <p>") + chalk.gray(" -- remove a plugin"));
91
+ log(chalk.white(" /next") + chalk.gray(" -- next page of last dataset"));
92
+ log(chalk.white(" /prev") + chalk.gray(" -- previous page of last dataset"));
93
+ log(chalk.white(" /page N") + chalk.gray(" -- jump to page N (or /page <name> N)"));
94
+ log(chalk.white(" exit") + chalk.gray(" -- quit"));
95
+ log("");
96
+ log(chalk.cyan(" Keys:"));
97
+ log(chalk.white(" Alt+V") + chalk.gray(" -- paste a picture (or text) from the clipboard into the input as [Image #N]"));
98
+ log(chalk.gray(" Ctrl+V is the terminal's own paste: with a picture in the clipboard it sends nothing."));
99
+ log(chalk.white(" Esc") + chalk.gray(" -- clear the input / stop the turn / stop background processes, newest first"));
100
+ log(chalk.white(" Ctrl+C") + chalk.gray(" -- stop the turn; twice within 2 s exits"));
101
+ log("");
102
+ log(chalk.gray(" Flint is free and open source: https://klymentiev.com/projects/flint"));
103
+ log("");
104
+ log(chalk.cyan(" Stop agent:"));
105
+ log(chalk.white(" Escape") + chalk.gray(" -- stop the current task, keep the queue"));
106
+ log(chalk.white(' "stop"') + chalk.gray(" -- type and press Enter to abort"));
107
+ log(chalk.white(" POST /stop") + chalk.gray(" -- HTTP endpoint"));
108
+ log("");
109
+ log(chalk.cyan(" Shortcuts:"));
110
+ log(chalk.white(" Ctrl+U") + chalk.gray(" -- clear input line"));
111
+ log(chalk.white(" Ctrl+W") + chalk.gray(" -- delete last word"));
112
+ log(chalk.white(" Up/Down") + chalk.gray(" -- input history"));
113
+ log("");
114
+ },
115
+
116
+ async "/sessions"() {
117
+ const sessions = await listSessions();
118
+ const s = store.getState();
119
+ if (!sessions.length) {
120
+ log(chalk.gray(" No sessions."));
121
+ } else {
122
+ log("");
123
+ for (const sess of sessions) {
124
+ const marker = sess.id === s.sessionId ? chalk.green(" <") : "";
125
+ log(` ${chalk.yellow(sess.id)} ${chalk.gray(sess.model)} ${sess.preview}${marker}`);
126
+ }
127
+ log("");
128
+ }
129
+ },
130
+
131
+ async "/load"(arg) {
132
+ const id = (arg || "").trim();
133
+ if (!id) {
134
+ log(chalk.red(" Usage: /load <session-id>"));
135
+ return;
136
+ }
137
+ try {
138
+ const data = await loadSession(id);
139
+ // Restore working directory BEFORE rebuilding system prompt so project
140
+ // auto-detection in getSystemMessage reflects the loaded session's CWD,
141
+ // not the current one. If the saved cwd no longer exists (renamed/
142
+ // deleted), warn and keep the current cwd.
143
+ if (data.cwd && data.cwd !== process.cwd()) {
144
+ const { existsSync } = await import("node:fs");
145
+ const { dirname } = await import("node:path");
146
+ if (existsSync(data.cwd)) {
147
+ try {
148
+ process.chdir(data.cwd);
149
+ log(chalk.gray(` cwd → ${data.cwd}`));
150
+ } catch (err) {
151
+ log(chalk.yellow(` warn: could not chdir to ${data.cwd}: ${err.message}`));
152
+ }
153
+ } else {
154
+ // Structure changed — walk up to find closest existing ancestor.
155
+ let ancestor = dirname(data.cwd);
156
+ while (ancestor && ancestor !== dirname(ancestor) && !existsSync(ancestor)) {
157
+ ancestor = dirname(ancestor);
158
+ }
159
+ if (ancestor && existsSync(ancestor)) {
160
+ try {
161
+ process.chdir(ancestor);
162
+ log(chalk.yellow(` warn: saved cwd gone: ${data.cwd}`));
163
+ log(chalk.gray(` cwd → ${ancestor} (closest existing ancestor)`));
164
+ } catch {
165
+ log(chalk.yellow(` warn: saved cwd gone: ${data.cwd}. staying in ${process.cwd()}`));
166
+ }
167
+ } else {
168
+ log(chalk.yellow(` warn: saved cwd gone: ${data.cwd}. staying in ${process.cwd()}`));
169
+ }
170
+ }
171
+ }
172
+ if (data.messages?.[0]?.role === "system") {
173
+ data.messages[0] = getSystemMessage(id);
174
+ }
175
+ const s = store.getState();
176
+ s.setSession(id, data.messages, data.inputHistory || []);
177
+ s.setPlan(data.plan || null);
178
+ s.setLastSummary(data.lastSummary || null);
179
+ if (data.pastedImages) store.setState({ pastedImages: data.pastedImages });
180
+ if (data.profile) {
181
+ s.setProfile(data.profile);
182
+ store.setState({ _profile: data.profile });
183
+ }
184
+ const userMsgs = data.messages.filter((m) => m.role === "user").length;
185
+ const { loadSessionFacts } = await import("../memory/session-facts.js");
186
+ const facts = loadSessionFacts(id);
187
+ const factsInfo = facts.length ? `, ${facts.length} facts` : "";
188
+ // Where the conversation got to, as it was on screen (ui/replay.js).
189
+ // Never the reason a load fails: the session is loaded already.
190
+ try {
191
+ const { replaySessionTail } = await import("../ui/replay.js");
192
+ replaySessionTail(store, id);
193
+ } catch {}
194
+ log(chalk.green(` Loaded session ${id} (${userMsgs} messages${factsInfo})`));
195
+ } catch {
196
+ log(chalk.red(` Session "${id}" not found.`));
197
+ }
198
+ },
199
+
200
+ // Owner, 2026-10-02: "does it have something like /resume?" It had
201
+ // /sessions and /load <id>, which means reading ids off a list and typing
202
+ // one back. /resume picks from the recent sessions with the arrows, newest
203
+ // first, each with the last thing asked in it.
204
+ async "/resume"(arg) {
205
+ const id = (arg || "").trim();
206
+ if (id) return commands["/load"](id);
207
+ const current = store.getState().sessionId;
208
+ const sessions = (await listSessions())
209
+ .filter((x) => x.id !== current && x.userMessages > 0)
210
+ .slice(0, 30);
211
+ if (!sessions.length) {
212
+ log(chalk.gray(" No other sessions to resume."));
213
+ return;
214
+ }
215
+ const items = sessions.map((x) => ({
216
+ id: x.id,
217
+ when: x.updated ? new Date(x.updated) : null,
218
+ count: x.userMessages,
219
+ last: String(x.last || "").replace(/\s+/g, " ").trim(),
220
+ }));
221
+ store.getState().openOverlay("session", "Resume a session", items);
222
+ },
223
+
224
+ async "/new"() {
225
+ const killed = store.getState().killAllRunning();
226
+ if (killed > 0) log(chalk.gray(` killed ${killed} background process${killed > 1 ? "es" : ""}`));
227
+ const { flush, abortAll } = await import("../bus/drain-loop.js");
228
+ // Abort any in-flight API messages BEFORE clearing the screen,
229
+ // otherwise their tokens keep streaming and render on top of the fresh banner.
230
+ const aborted = abortAll();
231
+ if (aborted > 0) {
232
+ // Give abort handlers one tick to flush their final error lines.
233
+ await new Promise(r => setTimeout(r, 50));
234
+ }
235
+ flush(); // clear bus queue
236
+ // Run-level flow state (interrupt, retry caps) no longer resets per turn.
237
+ const { resetFlow } = await import("../agent/flow-controller.js");
238
+ resetFlow();
239
+ const oldId = store.getState().sessionId;
240
+ const oldMessages = store.getState().messages || [];
241
+
242
+ // Extract session-end memory BEFORE we reset. Both reflections (L1) and
243
+ // facts (L4) must persist so the NEXT session's system prompt includes them.
244
+ // Fire-and-forget used to race the session reset — next session built its
245
+ // prompt before extraction finished. Fix: await with a hard timeout cap
246
+ // (6s) so /new cannot hang forever if LLM call stalls.
247
+ if (oldId && oldMessages.length > 2) {
248
+ const timeoutMs = 6000;
249
+ const withTimeout = (p) => Promise.race([
250
+ p,
251
+ new Promise(resolve => setTimeout(() => resolve(null), timeoutMs)),
252
+ ]);
253
+ const { extractAndStoreReflection } = await import("../agent/reflection-extractor.js");
254
+ const { extractFacts } = await import("../memory/extract-facts.js");
255
+ const { addFact } = await import("../memory/facts.js");
256
+ const { getCurrentProject } = await import("../memory/project.js");
257
+ const currentProject = getCurrentProject();
258
+ await Promise.allSettled([
259
+ withTimeout(extractAndStoreReflection(oldMessages, oldId).catch(() => {})),
260
+ withTimeout((async () => {
261
+ try {
262
+ const facts = await extractFacts(oldMessages);
263
+ if (Array.isArray(facts)) {
264
+ for (const f of facts) {
265
+ // Route to project scope: project/tech/env/decision → scoped;
266
+ // preference/person/general about user → global.
267
+ const scopedCats = new Set(["project", "tech", "env", "decision", "bug"]);
268
+ const projectScope = scopedCats.has(f.category) ? currentProject : null;
269
+ try { addFact(f.content, f.category || "general", "user", 0.8, projectScope); } catch {}
270
+ }
271
+ }
272
+ } catch {}
273
+ })()),
274
+ ]);
275
+ }
276
+
277
+ const newId = generateSessionId();
278
+ store.getState().resetSession(newId, [getSystemMessage()]);
279
+ store.getState().clearDatasets();
280
+ abandonAllActiveGoals();
281
+ if (oldId) clearSessionFacts(oldId);
282
+ clearDigest(newId);
283
+ store.getState().clearScreen();
284
+ log(chalk.cyan(` Flint v${store.getState()._version || "?"}`));
285
+ log(chalk.gray(` model: ${config.model} (${config.provider})`));
286
+ log(chalk.gray(` session: ${newId} (new)`));
287
+ log("");
288
+ },
289
+
290
+ async "/queue"() {
291
+ const msgs = bus.pending();
292
+ const s = bus.stats();
293
+ if (!msgs.length && s.processing === 0) {
294
+ log(chalk.gray(" No queued messages."));
295
+ return;
296
+ }
297
+ log("");
298
+ log(chalk.cyan(` Bus queue: ${s.pending} pending, ${s.processing} processing`));
299
+ for (const msg of msgs) {
300
+ const preview = msg.content.length > 60 ? msg.content.slice(0, 60) + "..." : msg.content;
301
+ log(` ${chalk.yellow(`#${msg.id}`)} ${chalk.gray(`[${msg.channel}]`)} ${chalk.white(preview)}`);
302
+ }
303
+ log("");
304
+ },
305
+
306
+ async "/queue clear"() {
307
+ const { flush } = await import("../bus/drain-loop.js");
308
+ flush();
309
+ log(chalk.gray(" Bus queue flushed."));
310
+ },
311
+
312
+ // Programs paired with the HTTP API, which is the only way in by default.
313
+ async "/paired"(arg) {
314
+ const { listPairedClients, revokePairedClients } = await import("../security/pairing.js");
315
+ const m = (arg || "").trim().match(/^revoke\s+(.+)$/);
316
+ if (m) {
317
+ const n = revokePairedClients(m[1].trim());
318
+ log(chalk.gray(n ? ` Revoked ${n} pairing(s): ${m[1].trim()}` : ` No pairing named ${m[1].trim()}.`));
319
+ return;
320
+ }
321
+ const list = listPairedClients();
322
+ if (!list.length) {
323
+ log(chalk.gray(" No paired programs. A program asks with POST /pair/request; the PIN shows here."));
324
+ return;
325
+ }
326
+ log("");
327
+ log(chalk.cyan(` Paired programs: ${list.length}`));
328
+ for (const c of list) {
329
+ log(` ${chalk.white(c.name)} ${chalk.gray(`from ${c.address}, paired ${c.pairedAt.slice(0, 16).replace("T", " ")}`)}`);
330
+ }
331
+ log(chalk.gray(" /paired revoke <name> or /paired revoke all"));
332
+ log("");
333
+ },
334
+
335
+ async "/later"(arg) {
336
+ const text = (arg || "").trim();
337
+ if (!text) {
338
+ log(chalk.red(" Usage: /later <question or task>"));
339
+ return;
340
+ }
341
+ const { id } = bus.push({ channel: "user", content: text, priority: bus.PRIORITY.USER, source: "later" });
342
+ log(chalk.gray(` queued (bus #${id}): ${text.length > 60 ? text.slice(0, 60) + "..." : text}`));
343
+ busPush(); // notify drain loop
344
+ },
345
+
346
+ async "/clear"() {
347
+ const s = store.getState();
348
+ s.resetSession(s.sessionId, [getSystemMessage()]);
349
+ s.clearDatasets();
350
+ clearSessionFacts(s.sessionId);
351
+ clearDigest(s.sessionId);
352
+
353
+ // Re-read provider state from disk (provider.json may have changed)
354
+ const { getActiveProvider, getLastModel } = await import("../providers/state.js");
355
+ const { getProvider, reloadProviders } = await import("../providers/registry.js");
356
+ reloadProviders();
357
+ const freshProvider = getActiveProvider();
358
+ const providerDef = getProvider(freshProvider);
359
+ if (providerDef) {
360
+ config.provider = freshProvider;
361
+ const lastModel = getLastModel(freshProvider);
362
+ config.model = lastModel || providerDef.defaultModel;
363
+ s.setModel(config.model);
364
+ s.setProvider(freshProvider);
365
+ await config.resolveApiKey();
366
+ const { fetchModelInfo } = await import("../api/client.js");
367
+ const info = await fetchModelInfo(config.model);
368
+ if (info) s.setPricing(info);
369
+ }
370
+
371
+ s.clearScreen();
372
+ log(chalk.cyan(` Flint v${s._version || "?"}`));
373
+ log(chalk.gray(` model: ${config.model} (${config.provider})`));
374
+ log(chalk.gray(` session: ${s.sessionId} (cleared)`));
375
+ log("");
376
+ await saveSession(s.sessionId, {
377
+ messages: [getSystemMessage()],
378
+ model: config.model,
379
+ inputHistory: s.inputHistory,
380
+ });
381
+ },
382
+
383
+ // Install a newer Flint, then restart in the same session
384
+ // (docs/self-update.md). Refuses when it would overwrite local work.
385
+ async "/update"() {
386
+ const { runUpdate, installKind } = await import("../update.js");
387
+ const { restartKeepingSession } = await import("../restart.js");
388
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
389
+ const root = config.projectRoot;
390
+ const s = store.getState();
391
+ log("");
392
+ await runUpdate({
393
+ root,
394
+ kind: installKind(root),
395
+ current: pkg.version,
396
+ log: (l) => log(chalk.gray(` ${l}`)),
397
+ restart: async () => {
398
+ await saveSession(s.sessionId, {
399
+ messages: s.messages, model: config.model, profile: s._profile, inputHistory: s.inputHistory,
400
+ lastSummary: s.lastSummary, pastedImages: s.pastedImages, provider: config.provider,
401
+ sessionCost: s.sessionCost, sessionPromptTokens: s.sessionPromptTokens, sessionCompletionTokens: s.sessionCompletionTokens,
402
+ });
403
+ restartKeepingSession(s.sessionId, 300);
404
+ },
405
+ });
406
+ log("");
407
+ },
408
+
409
+ async "/restart"() {
410
+ const s = store.getState();
411
+ // Do NOT persist `plan` or pendingAction on /restart — those are
412
+ // in-flight execution state. Carrying them through exit code 42
413
+ // into the fresh process caused an "autonomous loop sticky across
414
+ // restart" bug (observed 2026-04-21): a plan-style probe entered
415
+ // autonomous mode, /restart was used to stop it, and the new
416
+ // process picked up the saved plan and immediately resumed the
417
+ // same loop. Conversation history + profile + input history are
418
+ // fine to preserve (they're idempotent, no active state). Plan
419
+ // stays cleared on restart — user can /continue or /plan to
420
+ // resume explicitly.
421
+ await saveSession(s.sessionId, {
422
+ messages: s.messages,
423
+ model: config.model,
424
+ profile: s._profile,
425
+ inputHistory: s.inputHistory,
426
+ lastSummary: s.lastSummary,
427
+ pastedImages: s.pastedImages,
428
+ // The restart continues this session now, cost included.
429
+ provider: config.provider,
430
+ sessionCost: s.sessionCost,
431
+ sessionPromptTokens: s.sessionPromptTokens,
432
+ sessionCompletionTokens: s.sessionCompletionTokens,
433
+ });
434
+ log(chalk.yellow("\n Restarting agent, same session...\n"));
435
+ const { restartKeepingSession } = await import("../restart.js");
436
+ restartKeepingSession(s.sessionId, 100);
437
+ },
438
+
439
+ async "/stats"() {
440
+ const s = store.getState();
441
+ const userMsgs = s.messages.filter((m) => m.role === "user").length;
442
+ const totalMsgs = s.messages.length;
443
+ const totalTokens = s.sessionPromptTokens + s.sessionCompletionTokens;
444
+ log("");
445
+ log(chalk.gray(` session: ${s.sessionId}`));
446
+ log(chalk.gray(` model: ${config.model}`));
447
+ log(chalk.gray(` provider: ${config.provider}`));
448
+ log(chalk.gray(` profile: ${s._profile || "?"}`));
449
+ log(chalk.gray(` messages: ${totalMsgs} total, ${userMsgs} from user`));
450
+ log(chalk.gray(` tokens: ${totalTokens} (^${s.sessionPromptTokens} v${s.sessionCompletionTokens})`));
451
+ log(chalk.gray(` spent: $${s.sessionCost.toFixed(6)}`));
452
+ log(chalk.gray(` api: ${apiUrl(s._port || config.port)}`));
453
+ log("");
454
+ },
455
+
456
+ async "/budget"() {
457
+ const s = store.getState();
458
+ const spent = s.sessionCost || 0;
459
+ const budget = config.sessionBudget;
460
+ const perAction = config.maxCostPerAction;
461
+ log("");
462
+ log(chalk.white(" Session Budget"));
463
+ log(chalk.gray(` spent: $${spent.toFixed(6)}`));
464
+ if (budget > 0) {
465
+ const pct = (spent / budget * 100).toFixed(1);
466
+ const remaining = Math.max(0, budget - spent);
467
+ const color = pct > 80 ? chalk.red : pct > 50 ? chalk.yellow : chalk.green;
468
+ log(chalk.gray(` budget: $${budget.toFixed(2)}`));
469
+ log(color(` remaining: $${remaining.toFixed(6)} (${pct}% used)`));
470
+ } else {
471
+ log(chalk.gray(" budget: unlimited (set AGENT_SESSION_BUDGET)"));
472
+ }
473
+ if (perAction > 0) {
474
+ log(chalk.gray(` per-action: $${perAction.toFixed(2)} max`));
475
+ }
476
+ const totalTokens = (s.sessionPromptTokens || 0) + (s.sessionCompletionTokens || 0);
477
+ log(chalk.gray(` tokens: ${totalTokens} (^${s.sessionPromptTokens || 0} v${s.sessionCompletionTokens || 0})`));
478
+ log("");
479
+ },
480
+
481
+ async "/model"(arg) {
482
+ const modelId = (arg || "").trim();
483
+ // Model check (docs/model-check.md): six small agent tasks on a model,
484
+ // in the background. /model test [free | <id> ...]
485
+ if (/^test(\s|$)/i.test(modelId)) {
486
+ const rest = modelId.slice(4).trim();
487
+ const mc = await import("../model-check.js");
488
+ let models = rest ? rest.split(/\s+/) : [config.model];
489
+ if (/^free$/i.test(rest)) {
490
+ const fm = await import("../free-models.js");
491
+ const found = await fm.loadFreeModels();
492
+ if (found.limit <= 50) {
493
+ log(chalk.yellow(" Your OpenRouter account is on the free tier (50 free requests a day), and a check takes about 15 requests a model. Check one at a time: /model test <id>."));
494
+ return;
495
+ }
496
+ models = found.models.map((m) => m.id);
497
+ }
498
+ log(chalk.gray(` Checking ${models.length} model${models.length === 1 ? "" : "s"} in the background, six tasks each (about 15 requests a model).`));
499
+ mc.startCheckRun(models, (l) => log(chalk.cyan(` check: ${l}`)));
500
+ return;
501
+ }
502
+ // Free mode (docs/free-mode.md): the free models with their speed, the
503
+ // best one with two fallbacks. "/model free auto" takes it without a list.
504
+ if (/^free(\s+auto)?$/i.test(modelId)) {
505
+ const fm = await import("../free-models.js");
506
+ log(chalk.gray(" Looking up OpenRouter's free models and their speed..."));
507
+ let found;
508
+ try {
509
+ found = await fm.loadFreeModels();
510
+ } catch (err) {
511
+ log(chalk.red(` Could not list the free models: ${err.message}`));
512
+ return;
513
+ }
514
+ if (!found.models.length) {
515
+ log(chalk.yellow(" No free model with tool calling is listed right now."));
516
+ return;
517
+ }
518
+ if (/auto$/i.test(modelId)) {
519
+ await fm.applyFreeChain({ chain: fm.freeChain(found.models), limit: found.limit, store, log: (l) => log(chalk.green(` ${l}`)) });
520
+ return;
521
+ }
522
+ const items = found.models.map((m) => ({ ...m, chain: fm.freeChain(found.models, m.id), limit: found.limit }));
523
+ store.getState().openOverlay("free", `Free models · OpenRouter · last 30 min · your limit: ${found.limit} requests/day`, items);
524
+ return;
525
+ }
526
+ if (modelId) {
527
+ // Any other model leaves free mode.
528
+ if (config.freeChain) {
529
+ const { clearFreeChain } = await import("../free-models.js");
530
+ await clearFreeChain();
531
+ }
532
+ // Switch model
533
+ const { fetchModelInfo } = await import("../api/client.js");
534
+ const prev = config.model;
535
+ config.model = modelId;
536
+ store.getState().setModel(modelId);
537
+ // Remembered per provider, as the pick list already did; without this
538
+ // "/model <id>" was forgotten on the next start (owner, 2026-10-01).
539
+ try {
540
+ const { setLastModel } = await import("../providers/state.js");
541
+ setLastModel(config.provider, modelId);
542
+ } catch {}
543
+ try {
544
+ const info = await fetchModelInfo(modelId);
545
+ if (info) store.getState().setPricing(info);
546
+ } catch {}
547
+ log("");
548
+ log(chalk.green(` Switched: ${prev} → ${modelId}`));
549
+ log("");
550
+ return;
551
+ }
552
+ const s = store.getState();
553
+ log("");
554
+ log(chalk.cyan(` Model: ${config.model}`));
555
+ log(chalk.gray(` Provider: ${config.provider}`));
556
+ if (s.pricing) {
557
+ log(chalk.gray(` Price: $${(s.pricing.prompt * 1e6).toFixed(2)} / $${(s.pricing.completion * 1e6).toFixed(2)} per 1M tokens (in/out)`));
558
+ }
559
+ if (s.contextLimit) {
560
+ log(chalk.gray(` Context: ${(s.contextLimit / 1000).toFixed(0)}k tokens`));
561
+ }
562
+ log(chalk.gray(' Usage: /model <model-id> to switch, e.g. /model google/gemini-2.5-flash'));
563
+ log("");
564
+ },
565
+
566
+ async "/project"(arg) {
567
+ const { getCurrentProject, setCurrentProject, clearCurrentProject } = await import("../memory/project.js");
568
+ const parts = (arg || "").trim().split(/\s+/);
569
+ const sub = parts[0]?.toLowerCase();
570
+ if (!sub || sub === "show") {
571
+ const current = getCurrentProject();
572
+ if (current) log(chalk.cyan(` Current project: ${current}`));
573
+ else log(chalk.gray(" No project scope (global only)."));
574
+ log(chalk.gray(" Usage: /project [show | set <name> | clear]"));
575
+ return;
576
+ }
577
+ if (sub === "set") {
578
+ const name = parts[1];
579
+ if (!name) { log(chalk.red(" Usage: /project set <name>")); return; }
580
+ setCurrentProject(name);
581
+ log(chalk.green(` Project scope set to: ${name.toLowerCase()}`));
582
+ return;
583
+ }
584
+ if (sub === "clear") {
585
+ clearCurrentProject();
586
+ const current = getCurrentProject();
587
+ log(chalk.green(` Project override cleared. Auto-detected: ${current || "(none)"}`));
588
+ return;
589
+ }
590
+ log(chalk.red(` Unknown subcommand: ${sub}. Use show | set <name> | clear`));
591
+ },
592
+
593
+ async "/provider"(arg) {
594
+ const { listProviders, getProvider } = await import("../providers/registry.js");
595
+ const { hasKey } = await import("../providers/keys.js");
596
+ const { setActiveProvider, getLastModel } = await import("../providers/state.js");
597
+
598
+ const name = (arg || "").trim().toLowerCase();
599
+ if (name) {
600
+ // Switch to provider
601
+ const provider = getProvider(name);
602
+ if (!provider) {
603
+ log(chalk.red(` Unknown provider: ${name}`));
604
+ return;
605
+ }
606
+ if (provider.keyRequired && !hasKey(name)) {
607
+ log(chalk.red(` No API key for ${provider.name}. Use /key ${name} to add one.`));
608
+ return;
609
+ }
610
+ config.provider = name;
611
+ setActiveProvider(name);
612
+ const lastModel = getLastModel(name);
613
+ config.model = lastModel || provider.defaultModel;
614
+ store.getState().setModel(config.model);
615
+ store.getState().setProvider(name);
616
+ await config.resolveApiKey();
617
+
618
+ // Fetch pricing
619
+ const { fetchModelInfo } = await import("../api/client.js");
620
+ const info = await fetchModelInfo(config.model);
621
+ if (info) store.getState().setPricing(info);
622
+
623
+ log(chalk.green(` Switched to ${provider.name} (${config.model})`));
624
+ } else {
625
+ // Open interactive provider menu
626
+ const providers = listProviders();
627
+ const items = providers.map((p) => {
628
+ const keyInfo = p.keyRequired
629
+ ? (hasKey(p.id) ? "yes" : "no")
630
+ : "local";
631
+ return {
632
+ id: p.id,
633
+ name: p.name,
634
+ info: keyInfo,
635
+ current: p.id === config.provider,
636
+ disabled: p.keyRequired && !hasKey(p.id),
637
+ };
638
+ });
639
+ store.getState().openOverlay("provider", "Select Provider", items);
640
+ }
641
+ },
642
+
643
+ async "/key"(arg) {
644
+ const { hasKey, setKey, deleteKey, listConfiguredProviders } = await import("../providers/keys.js");
645
+ const { getProvider } = await import("../providers/registry.js");
646
+
647
+ const parts = (arg || "").trim().split(/\s+/);
648
+ const providerId = parts[0]?.toLowerCase();
649
+ const action = parts[1]?.toLowerCase();
650
+
651
+ if (!providerId) {
652
+ // Show which providers have keys
653
+ const configured = listConfiguredProviders();
654
+ log("");
655
+ log(chalk.cyan(" API Keys:"));
656
+ if (configured.length === 0) {
657
+ log(chalk.gray(" No keys configured."));
658
+ } else {
659
+ for (const id of configured) {
660
+ const p = getProvider(id);
661
+ log(` ${chalk.green("+")} ${chalk.white(id)} ${chalk.gray(p?.name || "")}`);
662
+ }
663
+ }
664
+ log(chalk.gray("\n /key <provider> -- set API key"));
665
+ log(chalk.gray(" /key <provider> delete -- remove API key"));
666
+ log("");
667
+ return;
668
+ }
669
+
670
+ const provider = getProvider(providerId);
671
+ if (!provider) {
672
+ log(chalk.red(` Unknown provider: ${providerId}`));
673
+ return;
674
+ }
675
+
676
+ if (action === "delete") {
677
+ await deleteKey(providerId);
678
+ log(chalk.green(` Key deleted for ${provider.name}.`));
679
+ return;
680
+ }
681
+
682
+ // Prompt for key input (read from stdin)
683
+ log(chalk.cyan(` Enter API key for ${provider.name}:`));
684
+ log(chalk.gray(" (paste key and press Enter)"));
685
+
686
+ // The key is read through the app's own input box in secret mode, NOT
687
+ // through a second readline on stdin. Owner, 2026-10-01: readline and
688
+ // the Ink input both received the pasted line; readline saved the key
689
+ // and the Ink input submitted it as a chat message, so the key was sent
690
+ // to the model and written to the session log. Secret mode masks the
691
+ // text and hands it only to this resolver (see App.js).
692
+ const key = await new Promise((resolve) => {
693
+ store.setState({ secretPrompt: { label: `${provider.name} API key`, resolve } });
694
+ });
695
+ const trimmed = String(key || "").trim();
696
+ if (!trimmed) {
697
+ log(chalk.gray(" Cancelled."));
698
+ return;
699
+ }
700
+ await setKey(providerId, trimmed);
701
+ log(chalk.green(` Key saved for ${provider.name} (encrypted).`));
702
+
703
+ // If this is the current provider, refresh the cached key
704
+ if (providerId === config.provider) {
705
+ await config.resolveApiKey();
706
+ }
707
+ },
708
+
709
+ // The onboarding menu promised "you can change it later with /careful",
710
+ // and there was no such command (owner, 2026-10-01).
711
+ async "/careful"(arg) {
712
+ const { getOnboardingAnswer, saveOnboardingAnswer } = await import("../tools/permissions.js");
713
+ const { levelOptions, parseLevelAnswer, DEFAULT_ONBOARDING_LEVEL } = await import("../security/policies.js");
714
+ const current = getOnboardingAnswer() || DEFAULT_ONBOARDING_LEVEL;
715
+ const wanted = (arg || "").trim();
716
+ if (!wanted) {
717
+ log("");
718
+ for (const o of levelOptions()) {
719
+ const mark = o.level === current ? chalk.green(" > ") : " ";
720
+ log(`${mark}${chalk.white(o.level.padEnd(11))}${chalk.gray(o.description)}`);
721
+ }
722
+ log(chalk.gray(" /careful safe | normal | permissive (or s, n, p)"));
723
+ log("");
724
+ return;
725
+ }
726
+ const level = parseLevelAnswer(wanted);
727
+ if (!level) {
728
+ log(chalk.red(` Unknown level: ${wanted}. Use safe, normal or permissive.`));
729
+ return;
730
+ }
731
+ saveOnboardingAnswer(level);
732
+ store.setState({ _careLevel: level }); // redraw the status line
733
+ log(chalk.green(` care: ${level}`));
734
+ },
735
+
736
+ // How many tokens go into each call: economy, normal, generous
737
+ // (docs/spend-modes.md). The next model call uses the new level.
738
+ async "/spend"(arg) {
739
+ const { getSpendLevel, setSpendLevel, parseSpendName, SPEND_NAMES, SPEND_LEVELS } = await import("../spend.js");
740
+ const current = getSpendLevel();
741
+ const wanted = (arg || "").trim();
742
+ if (!wanted) {
743
+ log("");
744
+ for (const name of SPEND_NAMES) {
745
+ const mark = name === current ? chalk.green(" > ") : " ";
746
+ log(`${mark}${chalk.white(name.padEnd(10))}${chalk.gray(SPEND_LEVELS[name].description)}`);
747
+ }
748
+ if (process.env.FLINT_SPEND) log(chalk.yellow(` FLINT_SPEND=${process.env.FLINT_SPEND} is set and wins over /spend.`));
749
+ log(chalk.gray(" /spend economy | normal | generous (or e, n, g)"));
750
+ log("");
751
+ return;
752
+ }
753
+ if (!parseSpendName(wanted)) {
754
+ log(chalk.red(` Unknown level: ${wanted}. Use economy, normal or generous.`));
755
+ return;
756
+ }
757
+ const level = setSpendLevel(wanted);
758
+ store.setState({ _spendLevel: level }); // redraw the footer
759
+ log(chalk.green(` spend: ${level}`));
760
+ },
761
+
762
+ // The former Processes / Tool Log / System tabs, as commands that print a
763
+ // snapshot into history once (docs/console-spec.md).
764
+ async "/ps"() {
765
+ const procs = store.getState().processes || [];
766
+ if (!procs.length) {
767
+ log(chalk.gray(" No background processes."));
768
+ return;
769
+ }
770
+ log("");
771
+ for (const p of [...procs].reverse()) {
772
+ const exit = p.exitCode != null ? ` (exit ${p.exitCode})` : "";
773
+ const color = p.status === "running" ? chalk.yellow : p.status === "done" ? chalk.green : chalk.red;
774
+ log(` [bg ${p.id}] ${color(p.status.padEnd(7))} ${p.command || p.cmd}${chalk.gray(exit)}`);
775
+ }
776
+ log(chalk.gray(" /logs <id> for output, /kill <id> or /kill all to stop."));
777
+ log("");
778
+ },
779
+
780
+ async "/logs"(arg) {
781
+ const id = parseInt((arg || "").trim(), 10);
782
+ const p = (store.getState().processes || []).find((x) => x.id === id);
783
+ if (!p) {
784
+ log(chalk.red(" Usage: /logs <id> (ids are in /ps)"));
785
+ return;
786
+ }
787
+ log("");
788
+ log(chalk.cyan(` [bg ${p.id}] ${p.command || p.cmd} ${p.status}`));
789
+ if (!p.output.length) log(chalk.gray(" (no output)"));
790
+ for (const line of p.output) log(` ${line}`);
791
+ log("");
792
+ },
793
+
794
+ async "/kill"(arg) {
795
+ const a = (arg || "").trim().toLowerCase();
796
+ if (a === "all") {
797
+ const n = store.getState().killAllRunning();
798
+ log(chalk.yellow(` Stopped ${n} background process${n === 1 ? "" : "es"}.`));
799
+ return;
800
+ }
801
+ const id = parseInt(a, 10);
802
+ if (!id) {
803
+ log(chalk.red(" Usage: /kill <id> or /kill all (ids are in /ps)"));
804
+ return;
805
+ }
806
+ const ok = store.getState().killProcess(id);
807
+ log(ok ? chalk.yellow(` Stopped bg ${id}.`) : chalk.gray(` bg ${id} is not running.`));
808
+ },
809
+
810
+ async "/tools"(arg) {
811
+ const n = parseInt((arg || "").trim(), 10) || 20;
812
+ const acts = (store.getState().toolActivities || []).filter((a) => a.name && a.name !== "---").slice(-n);
813
+ if (!acts.length) {
814
+ log(chalk.gray(" No tool calls yet."));
815
+ return;
816
+ }
817
+ log("");
818
+ for (const a of acts) {
819
+ const outcome = a.status === "running" ? "running" : a.result === "DENIED" ? "denied" : String(a.result || "done");
820
+ const args = String(a.args || "").replace(/\s+/g, " ").slice(0, 60);
821
+ log(` ${chalk.white(a.name)} ${chalk.gray(args)} ${chalk.gray("->")} ${outcome.replace(/\s+/g, " ").slice(0, 60)}`);
822
+ }
823
+ log("");
824
+ },
825
+
826
+ async "/sys"() {
827
+ const [{ renderToString }, React, { SystemPanel }] = await Promise.all([
828
+ import("ink"), import("react"), import("../components/SystemPanel.js"),
829
+ ]);
830
+ const text = renderToString(React.createElement(SystemPanel, { store }), { columns: process.stdout.columns || 80 });
831
+ log("");
832
+ for (const line of text.split("\n")) log(line);
833
+ log("");
834
+ },
835
+
836
+ async "/plan"() {
837
+ const plan = syncPlanToStore(store);
838
+ if (!plan) {
839
+ log(chalk.gray(" No active plan."));
840
+ } else {
841
+ log("");
842
+ log(chalk.cyan(` Plan #${plan.goalId}: ${plan.goal} (${plan.project})`));
843
+ for (const t of plan.tasks) {
844
+ let icon, color;
845
+ if (t.status === "done") { icon = "+"; color = chalk.green; }
846
+ else if (t.status === "in_progress") { icon = ">"; color = chalk.yellow; }
847
+ else if (t.status === "skipped") { icon = "-"; color = chalk.gray; }
848
+ else { icon = "o"; color = chalk.white; }
849
+ let line = ` [${t.id}] ${icon} ${t.title}`;
850
+ if (t.result) line += chalk.gray(` - ${t.result}`);
851
+ log(color(line));
852
+ }
853
+ log("");
854
+ }
855
+ },
856
+
857
+ async "/tasks"(arg) {
858
+ const filter = (arg || "").trim();
859
+ if (filter === "all") {
860
+ const goals = listGoals();
861
+ if (!goals.length) {
862
+ log(chalk.gray(" No goals in database."));
863
+ return;
864
+ }
865
+ log("");
866
+ for (const g of goals) {
867
+ const stats = getTaskStats(g.id);
868
+ const statusColor = g.status === "active" ? chalk.green : g.status === "completed" ? chalk.gray : chalk.red;
869
+ log(` ${statusColor(`[${g.status}]`)} #${chalk.yellow(g.id)} ${chalk.white(g.title)} ${chalk.gray(`(${g.project})`)} ${chalk.gray(`${stats.done}/${stats.total} done`)}`);
870
+ }
871
+ log(chalk.gray("\n /tasks -- active goals with task details"));
872
+ log(chalk.gray(" /tasks all -- all goals (active + completed + abandoned)"));
873
+ log("");
874
+ return;
875
+ }
876
+
877
+ const dashboard = getDashboard();
878
+ if (!dashboard.length) {
879
+ log(chalk.gray(" No active tasks. Use /auto or ask AI to create_plan."));
880
+ return;
881
+ }
882
+ log("");
883
+ for (const g of dashboard) {
884
+ log(chalk.cyan(` Goal #${g.id}: ${g.title} (${g.project})`));
885
+ log(chalk.gray(` Created: ${g.created_at} | ${g.stats.done}/${g.stats.total} done, ${g.stats.pending} pending`));
886
+ for (const t of g.tasks) {
887
+ let icon, color;
888
+ if (t.status === "done") { icon = "+"; color = chalk.green; }
889
+ else if (t.status === "in_progress") { icon = ">"; color = chalk.yellow; }
890
+ else if (t.status === "skipped") { icon = "-"; color = chalk.gray; }
891
+ else { icon = "o"; color = chalk.white; }
892
+ let line = ` [${t.id}] ${icon} ${t.title}`;
893
+ if (t.result) line += chalk.gray(` - ${t.result}`);
894
+ log(color(line));
895
+ }
896
+ log("");
897
+ }
898
+ },
899
+
900
+ async "/rewind"(arg) {
901
+ const param = (arg || "").trim().toLowerCase();
902
+ const stack = getCheckpointStack();
903
+
904
+ if (!stack.length) {
905
+ log(chalk.gray(" No changes to rewind."));
906
+ return;
907
+ }
908
+
909
+ if (param === "all") {
910
+ const restored = await rewindAll();
911
+ log("");
912
+ log(chalk.cyan(` Rewound ${restored.length} change(s):`));
913
+ for (const r of restored) {
914
+ log(chalk.green(` ${r.action}: ${r.path}`));
915
+ }
916
+ log("");
917
+ return;
918
+ }
919
+
920
+ // Show stack if no arg, or rewind N
921
+ const count = parseInt(param) || 1;
922
+
923
+ if (!param) {
924
+ // Show what can be rewound
925
+ log("");
926
+ log(chalk.cyan(` Checkpoint stack: ${stack.length} change(s)`));
927
+ const show = stack.slice(-10).reverse();
928
+ for (const s of show) {
929
+ const age = Math.round((Date.now() - s.timestamp) / 1000);
930
+ const label = s.existed ? s.type : "create";
931
+ log(chalk.gray(` [${label}] ${s.path} (${age}s ago)`));
932
+ }
933
+ if (stack.length > 10) log(chalk.gray(` ... and ${stack.length - 10} more`));
934
+ log(chalk.gray("\n /rewind -- undo last change"));
935
+ log(chalk.gray(" /rewind N -- undo last N changes"));
936
+ log(chalk.gray(" /rewind all -- undo everything"));
937
+ log("");
938
+ return;
939
+ }
940
+
941
+ const restored = await rewind(count);
942
+ log("");
943
+ log(chalk.cyan(` Rewound ${restored.length} change(s):`));
944
+ for (const r of restored) {
945
+ log(chalk.green(` ${r.action}: ${r.path}`));
946
+ }
947
+ log("");
948
+ },
949
+
950
+ async "/profile"(arg) {
951
+ const profileName = (arg || "").trim();
952
+ if (profileName) {
953
+ try {
954
+ const profile = loadProfile(profileName);
955
+ store.setState({ _profile: profileName });
956
+ store.getState().setProfile(profileName);
957
+ // Switch agent-memory workspace to match profile
958
+ try {
959
+ const { execFileSync } = await import("node:child_process");
960
+ const bin = process.env.AGENT_MEMORY_BIN || "agent-memory";
961
+ execFileSync(bin, ["focus", profileName], { timeout: 3000, stdio: "ignore" });
962
+ // Prefetch new workspace context
963
+ const { prefetchAgentMemory } = await import("../agent/system-prompt.js");
964
+ await prefetchAgentMemory();
965
+ log(chalk.gray(` agent-memory workspace: ${profileName}`));
966
+ } catch {}
967
+ // Rebuild system message with profile + agent-memory context
968
+ const { getSystemMessage } = await import("../agent/system-prompt.js");
969
+ const app = (await import("../app-state.js")).app;
970
+ app.activeProfile = profileName;
971
+ app.profileConfig = profile;
972
+ const sysMsg = { role: "system", content: getSystemMessage(store.getState().sessionId, { profile: profileName, profileContent: profile.content, datasets: store.getState().datasets }) };
973
+ app.systemMessage = sysMsg;
974
+ const s = store.getState();
975
+ s.resetSession(s.sessionId, [sysMsg]);
976
+ log(chalk.green(` Switched to profile: ${profileName}`));
977
+ log("");
978
+ } catch (err) {
979
+ log(chalk.red(` ${err.message}`));
980
+ }
981
+ } else {
982
+ const profiles = listProfileNames();
983
+ const current = store.getState()._profile || "desktop";
984
+ log("");
985
+ log(chalk.cyan(" Profiles:"));
986
+ for (const p of profiles) {
987
+ const marker = p === current ? chalk.green(" (current)") : "";
988
+ try {
989
+ const cfg = loadProfile(p);
990
+ log(` ${chalk.white(p)}${marker} ${chalk.gray("-- " + cfg.description)}`);
991
+ } catch {
992
+ log(` ${chalk.white(p)}${marker}`);
993
+ }
994
+ }
995
+ log(chalk.gray("\n Usage: /profile <name>"));
996
+ log("");
997
+ }
998
+ },
999
+
1000
+ async "/agents"() {
1001
+ log(chalk.gray(" Scanning..."));
1002
+ const agents = await listAgents({ checkAlive: true });
1003
+ if (!agents.length) {
1004
+ log(chalk.gray(" No running agents found."));
1005
+ } else {
1006
+ log("");
1007
+ for (const a of agents) {
1008
+ const isSelf = a.pid === process.pid;
1009
+ const tag = isSelf ? chalk.green(" (this)") : "";
1010
+ log(
1011
+ ` :${chalk.yellow(a.port)} ${chalk.gray(a.model || "?")} ${a.profile || "?"} ${a.sessionId || "?"}${tag}`,
1012
+ );
1013
+ }
1014
+ }
1015
+ log("");
1016
+ },
1017
+
1018
+ async "/mcp"() {
1019
+ const servers = getMcpServerStatus();
1020
+ if (!servers.length) {
1021
+ log(chalk.gray(" No MCP servers configured."));
1022
+ log("");
1023
+ return;
1024
+ }
1025
+ log("");
1026
+ log(chalk.cyan(" MCP Servers:"));
1027
+ for (const s of servers) {
1028
+ const status = s.connected ? chalk.green("[+]") : chalk.red("[ ]");
1029
+ log(` ${status} ${chalk.white(s.name)} ${chalk.gray(s.url)}`);
1030
+ }
1031
+ log(chalk.gray("\n Reconnect/disconnect via the agent or MCP_SERVERS env var."));
1032
+ log("");
1033
+ },
1034
+
1035
+ async "/memory"() {
1036
+ const stats = getMemoryStats();
1037
+ log("");
1038
+ log(chalk.cyan(` Memory: ${stats.total} entries`));
1039
+ if (stats.total > 0) {
1040
+ for (const [cat, count] of Object.entries(stats.byCategory)) {
1041
+ log(chalk.gray(` ${cat}: ${count}`));
1042
+ }
1043
+ const recent = listRecentMemories(3);
1044
+ log(chalk.gray(" Recent:"));
1045
+ for (const m of recent) {
1046
+ log(chalk.gray(` [#${m.id}] ${m.content.slice(0, 60)}`));
1047
+ }
1048
+ }
1049
+ log("");
1050
+ },
1051
+
1052
+ async "/memory clear"() {
1053
+ clearAllMemories();
1054
+ updateMemoryMd();
1055
+ log(chalk.green(" All memories cleared."));
1056
+ },
1057
+
1058
+ async "/permissions"() {
1059
+ const map = getPermissionMap();
1060
+ const groups = { allow: [], confirm: [], deny: [] };
1061
+ for (const [name, level] of Object.entries(map)) {
1062
+ (groups[level] || (groups[level] = [])).push(name);
1063
+ }
1064
+ log("");
1065
+ log(chalk.cyan(" Permissions:"));
1066
+ if (groups.allow.length) {
1067
+ log(chalk.green(" allow: ") + chalk.gray(groups.allow.join(", ")));
1068
+ }
1069
+ if (groups.confirm.length) {
1070
+ log(chalk.yellow(" confirm: ") + chalk.gray(groups.confirm.join(", ")));
1071
+ }
1072
+ if (groups.deny.length) {
1073
+ log(chalk.red(" deny: ") + chalk.gray(groups.deny.join(", ")));
1074
+ }
1075
+ log("");
1076
+ log(chalk.gray(" /allow|deny|confirm <tool> /allow-all /deny-all /reset-permissions"));
1077
+ log("");
1078
+ },
1079
+
1080
+ async "/allow"(arg) {
1081
+ const tool = (arg || "").trim();
1082
+ if (!tool) { log(chalk.red(" Usage: /allow <tool>")); return; }
1083
+ setPermission(tool, "allow");
1084
+ log(chalk.green(` ${tool} -> allow`));
1085
+ },
1086
+
1087
+ async "/deny"(arg) {
1088
+ const tool = (arg || "").trim();
1089
+ if (!tool) { log(chalk.red(" Usage: /deny <tool>")); return; }
1090
+ setPermission(tool, "deny");
1091
+ log(chalk.red(` ${tool} -> deny`));
1092
+ },
1093
+
1094
+ async "/confirm"(arg) {
1095
+ const tool = (arg || "").trim();
1096
+ if (!tool) { log(chalk.red(" Usage: /confirm <tool>")); return; }
1097
+ setPermission(tool, "confirm");
1098
+ log(chalk.yellow(` ${tool} -> confirm`));
1099
+ },
1100
+
1101
+ async "/allow-all"() {
1102
+ bulkSetPermission("allow");
1103
+ log(chalk.green(" All tools -> allow (no confirmations)"));
1104
+ },
1105
+
1106
+ async "/deny-all"() {
1107
+ bulkSetPermission("deny");
1108
+ log(chalk.red(" All tools -> deny (nothing executes)"));
1109
+ },
1110
+
1111
+ async "/reset-permissions"() {
1112
+ resetSessionOverrides();
1113
+ log(chalk.cyan(" Permissions reset to defaults"));
1114
+ },
1115
+
1116
+ async "/auto"(task) {
1117
+ const { app } = await import("../app-state.js");
1118
+ if (!task) {
1119
+ // Toggle or show status
1120
+ if (app.autonomous) {
1121
+ app.autonomous = false;
1122
+ log(chalk.yellow(" Autonomous mode: OFF (interactive)"));
1123
+ } else {
1124
+ log(chalk.gray(" Autonomous mode is OFF. Use: /auto <task> to start autonomous work."));
1125
+ }
1126
+ return;
1127
+ }
1128
+ // Start autonomous mode: create plan via message, enable self-continue
1129
+ app.autonomous = true;
1130
+ bulkSetPermission("allow"); // autonomous needs tool access
1131
+ log(chalk.green(" Autonomous mode: ON"));
1132
+ log(chalk.gray(` Task: ${task}`));
1133
+ log(chalk.gray(" Tools: auto-approved. Use /auto to stop."));
1134
+ // Push the task as a message
1135
+ const busMod = await import("../bus/index.js");
1136
+ const { notify, resetAutonomous } = await import("../bus/drain-loop.js");
1137
+ resetAutonomous();
1138
+ busMod.push({
1139
+ channel: "user",
1140
+ content: `Work autonomously on this task. Create a plan with create_plan, break into subtasks, and complete everything. When all tasks are done, report results.\n\nTask: ${task}`,
1141
+ priority: busMod.PRIORITY?.USER || 1,
1142
+ source: "auto",
1143
+ });
1144
+ notify();
1145
+ },
1146
+
1147
+ async "/plugins"() {
1148
+ const loaded = getLoadedPlugins();
1149
+ const installed = listInstalledPlugins();
1150
+ log("");
1151
+ if (!installed.length) {
1152
+ log(chalk.gray(" No plugins installed."));
1153
+ log(chalk.gray(` Directory: ${getPluginsDir()}`));
1154
+ log(chalk.gray(" Install: /install <name-or-path>"));
1155
+ } else {
1156
+ log(chalk.cyan(" Plugins:"));
1157
+ for (const name of installed) {
1158
+ const info = loaded.find((p) => p.name === name);
1159
+ if (info) {
1160
+ log(` ${chalk.green("[+]")} ${chalk.white(name)} ${chalk.gray(`v${info.version}`)} ${chalk.gray(`(${info.toolCount} tools, ${info.type})`)}`);
1161
+ } else {
1162
+ log(` ${chalk.red("[ ]")} ${chalk.white(name)} ${chalk.gray("(not loaded)")}`);
1163
+ }
1164
+ }
1165
+ log(chalk.gray(`\n Directory: ${getPluginsDir()}`));
1166
+ }
1167
+ log("");
1168
+ },
1169
+
1170
+ async "/install"(arg) {
1171
+ const target = (arg || "").trim();
1172
+ if (!target) {
1173
+ log(chalk.red(" Usage: /install <plugin-name-or-path>"));
1174
+ return;
1175
+ }
1176
+ log(chalk.yellow(" WARNING: Plugins run with full system access (filesystem, commands, network)."));
1177
+ log(chalk.yellow(" Only install plugins from trusted sources."));
1178
+ log(chalk.gray(` Installing ${target}...`));
1179
+ const result = await installPlugin(target);
1180
+ if (result.ok) {
1181
+ // Loaded now, the way install_plugin does it for the agent.
1182
+ const r = await activatePlugin(result.path);
1183
+ if (r.error) {
1184
+ log(chalk.red(` Installed ${result.name}, but it did not load: ${r.error}`));
1185
+ } else {
1186
+ log(chalk.green(` Installed and loaded: ${result.name}`) + chalk.gray(r.tools.length ? ` (${r.tools.join(", ")})` : ""));
1187
+ }
1188
+ } else {
1189
+ log(chalk.red(` Error: ${result.error}`));
1190
+ }
1191
+ },
1192
+
1193
+ async "/uninstall"(arg) {
1194
+ const target = (arg || "").trim();
1195
+ if (!target) {
1196
+ log(chalk.red(" Usage: /uninstall <plugin-name>"));
1197
+ return;
1198
+ }
1199
+ const result = uninstallPlugin(target);
1200
+ if (result.ok) {
1201
+ const old = unregisterPlugin(pathJoin(getPluginsDir(), result.name));
1202
+ if (old?.destroy) { try { await old.destroy(); } catch {} }
1203
+ log(chalk.green(` Removed: ${result.name}`));
1204
+ } else {
1205
+ log(chalk.red(` Error: ${result.error}`));
1206
+ }
1207
+ },
1208
+
1209
+ // -- Dataset navigation --
1210
+
1211
+ async "/next"() {
1212
+ const entries = Object.values(store.getState().datasets);
1213
+ if (!entries.length) { log(chalk.gray(" No active datasets.")); return; }
1214
+ const ds = entries[entries.length - 1];
1215
+ const totalPages = Math.ceil(ds.rows.length / ds.pageSize);
1216
+ const next = Math.min(ds.page + 1, totalPages);
1217
+ store.getState().setDatasetPage(ds.id, next);
1218
+ const start = (next - 1) * ds.pageSize;
1219
+ printTable(ds.columns, ds.rows.slice(start, start + ds.pageSize),
1220
+ `${ds.label} (page ${next}/${totalPages}, ${ds.rows.length} total)`,
1221
+ `Page ${next}/${totalPages} | /next /prev /page ${ds.label} N`);
1222
+ },
1223
+
1224
+ async "/prev"() {
1225
+ const entries = Object.values(store.getState().datasets);
1226
+ if (!entries.length) { log(chalk.gray(" No active datasets.")); return; }
1227
+ const ds = entries[entries.length - 1];
1228
+ const totalPages = Math.ceil(ds.rows.length / ds.pageSize);
1229
+ const prev = Math.max(ds.page - 1, 1);
1230
+ store.getState().setDatasetPage(ds.id, prev);
1231
+ const start = (prev - 1) * ds.pageSize;
1232
+ printTable(ds.columns, ds.rows.slice(start, start + ds.pageSize),
1233
+ `${ds.label} (page ${prev}/${totalPages}, ${ds.rows.length} total)`,
1234
+ `Page ${prev}/${totalPages} | /next /prev /page ${ds.label} N`);
1235
+ },
1236
+
1237
+ async "/page"(arg) {
1238
+ const parts = (arg || "").trim().split(/\s+/);
1239
+ const datasets = store.getState().datasets;
1240
+ const entries = Object.values(datasets);
1241
+ if (!entries.length) { log(chalk.gray(" No active datasets.")); return; }
1242
+
1243
+ let ds, pageNum;
1244
+ if (parts.length === 1 && /^\d+$/.test(parts[0])) {
1245
+ // /page 3 — last dataset
1246
+ ds = entries[entries.length - 1];
1247
+ pageNum = parseInt(parts[0], 10);
1248
+ } else if (parts.length >= 2) {
1249
+ // /page emails 3 — find by label or id
1250
+ const name = parts[0];
1251
+ ds = entries.find((d) => d.label === name || d.id === name);
1252
+ pageNum = parseInt(parts[1], 10);
1253
+ }
1254
+
1255
+ if (!ds) { log(chalk.red(` Dataset not found. Available: ${entries.map(d => d.label).join(", ")}`)); return; }
1256
+ if (!pageNum || isNaN(pageNum)) { log(chalk.red(" Usage: /page [name] <number>")); return; }
1257
+
1258
+ const totalPages = Math.ceil(ds.rows.length / ds.pageSize);
1259
+ const clamped = Math.max(1, Math.min(pageNum, totalPages));
1260
+ store.getState().setDatasetPage(ds.id, clamped);
1261
+ const start = (clamped - 1) * ds.pageSize;
1262
+ printTable(ds.columns, ds.rows.slice(start, start + ds.pageSize),
1263
+ `${ds.label} (page ${clamped}/${totalPages}, ${ds.rows.length} total)`,
1264
+ `Page ${clamped}/${totalPages} | /next /prev /page ${ds.label} N`);
1265
+ },
1266
+ async "/copy"() {
1267
+ const lines = s.lines || [];
1268
+ // Strip ANSI codes and join
1269
+ const ansiRe = /\x1b\[[0-9;]*m/g;
1270
+ const text = lines
1271
+ .map((l) => (l.text || "").replace(ansiRe, ""))
1272
+ .filter((t) => t.trim())
1273
+ .join("\n");
1274
+
1275
+ if (!text) {
1276
+ log(chalk.gray(" Nothing to copy."));
1277
+ return;
1278
+ }
1279
+
1280
+ try {
1281
+ const { execSync } = await import("node:child_process");
1282
+ if (process.platform === "win32") {
1283
+ execSync("clip", { input: text, timeout: 3000 });
1284
+ } else if (process.platform === "darwin") {
1285
+ execSync("pbcopy", { input: text, timeout: 3000 });
1286
+ } else {
1287
+ execSync("xclip -selection clipboard", { input: text, timeout: 3000 });
1288
+ }
1289
+ log(chalk.green(` Copied ${lines.length} lines to clipboard.`));
1290
+ } catch (err) {
1291
+ log(chalk.red(` Failed to copy: ${err.message}`));
1292
+ }
1293
+ },
1294
+ };
1295
+
1296
+ return commands;
1297
+ }