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