min-agent 0.2.1 → 0.3.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/README.md +146 -18
- package/dist/agent.js +293 -408
- package/dist/assistant-stream.js +11 -7
- package/dist/cli.js +397 -139
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +3 -3
- package/dist/compaction.js +182 -81
- package/dist/config.js +186 -35
- package/dist/confirm.js +55 -6
- package/dist/context-window.js +67 -54
- package/dist/doom-loop.js +19 -12
- package/dist/http.js +119 -0
- package/dist/instructions.js +51 -33
- package/dist/logger.js +66 -0
- package/dist/markdown.js +3 -44
- package/dist/mcp.js +547 -100
- package/dist/memory.js +48 -6
- package/dist/output.js +36 -27
- package/dist/paste-handler.js +3 -3
- package/dist/plugins.js +33 -6
- package/dist/pricing.js +119 -0
- package/dist/provider.js +17 -15
- package/dist/serve.js +658 -369
- package/dist/sessions.js +151 -13
- package/dist/skills.js +466 -76
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +2 -1
- package/dist/tool-display.js +173 -0
- package/dist/tool-output.js +54 -45
- package/dist/tools/apply_patch.js +191 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +147 -70
- package/dist/tools/code_search.js +6 -5
- package/dist/tools/edit.js +23 -7
- package/dist/tools/explore.js +80 -12
- package/dist/tools/glob.js +3 -3
- package/dist/tools/grep.js +146 -14
- package/dist/tools/index.js +7 -7
- package/dist/tools/question.js +4 -22
- package/dist/tools/read.js +71 -11
- package/dist/tools/task.js +33 -20
- package/dist/tools/todo.js +83 -73
- package/dist/tools/web_fetch.js +150 -46
- package/dist/tools/web_search.js +706 -28
- package/dist/tools/write.js +13 -7
- package/dist/tui/App.js +40 -6
- package/dist/tui/ConfirmBar.js +24 -3
- package/dist/tui/InputBar.js +390 -45
- package/dist/tui/MessageList.js +533 -20
- package/dist/tui/ModelPicker.js +108 -0
- package/dist/tui/QuestionBar.js +104 -0
- package/dist/tui/StatusBar.js +19 -11
- package/dist/tui/agent-runner.js +103 -0
- package/dist/tui/caret-pos.js +134 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +44 -0
- package/dist/tui/index.js +153 -24
- package/dist/tui/input-history.js +44 -0
- package/dist/tui/layout.js +17 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/selection.js +134 -0
- package/dist/tui/slash-commands.js +90 -0
- package/dist/tui/slash-handler.js +370 -0
- package/dist/tui/text-width.js +91 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +27 -0
- package/dist/tui-chat.js +111 -331
- package/dist/updater.js +57 -0
- package/docs/API.md +160 -14
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/package.json +7 -8
package/dist/sessions.js
CHANGED
|
@@ -1,41 +1,154 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, openSync, readSync, closeSync, statSync, } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
3
4
|
import { getConfigDir } from "./config.js";
|
|
4
5
|
import { generateTitle } from "./title-gen.js";
|
|
6
|
+
import { isSyntheticMessage } from "./synthetic.js";
|
|
5
7
|
function getSessionsDir() {
|
|
6
8
|
return path.join(getConfigDir(), "sessions");
|
|
7
9
|
}
|
|
10
|
+
/** Session ids are UUIDs; a strict whitelist blocks path traversal via `/v1/sessions/..%2F..%2Fconfig`. */
|
|
11
|
+
const SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
8
12
|
function sessionPath(id) {
|
|
13
|
+
if (!SESSION_ID_RE.test(id))
|
|
14
|
+
throw new Error(`invalid session id: ${id}`);
|
|
9
15
|
return path.join(getSessionsDir(), `${id}.json`);
|
|
10
16
|
}
|
|
17
|
+
/** Session meta lives at the head of the JSON file, so only the first bytes are needed. */
|
|
18
|
+
const META_READ_LIMIT = 64 * 1024;
|
|
19
|
+
function readHead(file, limit) {
|
|
20
|
+
const fd = openSync(file, "r");
|
|
21
|
+
try {
|
|
22
|
+
const buf = Buffer.alloc(Math.min(statSync(file).size, limit));
|
|
23
|
+
if (buf.length === 0)
|
|
24
|
+
return "";
|
|
25
|
+
readSync(fd, buf, 0, buf.length, 0);
|
|
26
|
+
return buf.toString("utf-8");
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Find the index of the `}` that closes the object opened at `start`, or -1. */
|
|
33
|
+
function matchBrace(text, start) {
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let inString = false;
|
|
36
|
+
let escaped = false;
|
|
37
|
+
for (let i = start; i < text.length; i++) {
|
|
38
|
+
const ch = text[i];
|
|
39
|
+
if (inString) {
|
|
40
|
+
if (escaped)
|
|
41
|
+
escaped = false;
|
|
42
|
+
else if (ch === "\\")
|
|
43
|
+
escaped = true;
|
|
44
|
+
else if (ch === '"')
|
|
45
|
+
inString = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (ch === '"')
|
|
49
|
+
inString = true;
|
|
50
|
+
else if (ch === "{")
|
|
51
|
+
depth++;
|
|
52
|
+
else if (ch === "}") {
|
|
53
|
+
depth--;
|
|
54
|
+
if (depth === 0)
|
|
55
|
+
return i;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
/** Extract the meta object from a (possibly truncated) session JSON head. */
|
|
61
|
+
function parseMetaFromHead(head) {
|
|
62
|
+
const key = head.indexOf('"meta"');
|
|
63
|
+
if (key < 0)
|
|
64
|
+
return null;
|
|
65
|
+
const brace = head.indexOf("{", key);
|
|
66
|
+
if (brace < 0)
|
|
67
|
+
return null;
|
|
68
|
+
const end = matchBrace(head, brace);
|
|
69
|
+
if (end < 0)
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(head.slice(brace, end + 1));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
11
78
|
function generateId() {
|
|
12
|
-
return
|
|
79
|
+
return randomUUID();
|
|
80
|
+
}
|
|
81
|
+
function textOfContent(content) {
|
|
82
|
+
if (typeof content === "string")
|
|
83
|
+
return content;
|
|
84
|
+
return content.map((p) => (p.type === "text" ? p.text : "")).join(" ").trim();
|
|
13
85
|
}
|
|
14
86
|
function deriveTitle(messages) {
|
|
15
87
|
const first = messages.find((m) => m.role === "user");
|
|
16
88
|
if (!first)
|
|
17
89
|
return "Untitled";
|
|
18
|
-
const
|
|
19
|
-
return
|
|
90
|
+
const text = textOfContent(first.content);
|
|
91
|
+
return text.slice(0, 60) || "Untitled";
|
|
20
92
|
}
|
|
21
|
-
|
|
93
|
+
/** Convert binary image parts to data URLs so JSON serialization stays usable. */
|
|
94
|
+
export function sanitizeForStorage(messages) {
|
|
95
|
+
return messages
|
|
96
|
+
.filter((msg) => !isSyntheticMessage(msg))
|
|
97
|
+
.map((msg) => {
|
|
98
|
+
if (!Array.isArray(msg.content))
|
|
99
|
+
return msg;
|
|
100
|
+
const content = msg.content.map((part) => {
|
|
101
|
+
if (part.type !== "image" || part.image == null || typeof part.image === "string")
|
|
102
|
+
return part;
|
|
103
|
+
let buf = null;
|
|
104
|
+
if (Buffer.isBuffer(part.image) || part.image instanceof Uint8Array) {
|
|
105
|
+
buf = Buffer.isBuffer(part.image) ? part.image : Buffer.from(part.image);
|
|
106
|
+
}
|
|
107
|
+
else if (typeof part.image === "object" && !(part.image instanceof URL)) {
|
|
108
|
+
const data = part.image.data;
|
|
109
|
+
if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
|
110
|
+
buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!buf)
|
|
114
|
+
return part;
|
|
115
|
+
const mime = part.mimeType ?? "image/png";
|
|
116
|
+
return { ...part, image: `data:${mime};base64,${buf.toString("base64")}` };
|
|
117
|
+
});
|
|
118
|
+
return { ...msg, content };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export function saveSession(messages, existingId, title, created) {
|
|
22
122
|
const dir = getSessionsDir();
|
|
23
123
|
mkdirSync(dir, { recursive: true });
|
|
24
|
-
const id = existingId
|
|
124
|
+
const id = existingId && SESSION_ID_RE.test(existingId) ? existingId : generateId();
|
|
25
125
|
const now = new Date().toISOString();
|
|
126
|
+
const existing = existingId ? readMeta(id) : null;
|
|
26
127
|
const data = {
|
|
27
128
|
meta: {
|
|
28
129
|
id,
|
|
29
|
-
title: title ?? deriveTitle(messages),
|
|
30
|
-
created:
|
|
130
|
+
title: title ?? existing?.title ?? deriveTitle(messages),
|
|
131
|
+
created: created ?? existing?.created ?? now,
|
|
31
132
|
updated: now,
|
|
32
133
|
messageCount: messages.length,
|
|
33
134
|
},
|
|
34
|
-
messages,
|
|
135
|
+
messages: sanitizeForStorage(messages),
|
|
35
136
|
};
|
|
36
137
|
writeFileSync(sessionPath(id), JSON.stringify(data, null, 2), "utf-8");
|
|
37
138
|
return id;
|
|
38
139
|
}
|
|
140
|
+
/** Read only the meta head to recover the original created/title. */
|
|
141
|
+
function readMeta(id) {
|
|
142
|
+
const file = sessionPath(id);
|
|
143
|
+
if (!existsSync(file))
|
|
144
|
+
return null;
|
|
145
|
+
try {
|
|
146
|
+
return parseMetaFromHead(readHead(file, META_READ_LIMIT));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
39
152
|
/** Save session with auto-generated title from LLM */
|
|
40
153
|
export async function saveSessionWithTitle(messages, model, existingId) {
|
|
41
154
|
let title = null;
|
|
@@ -46,6 +159,8 @@ export async function saveSessionWithTitle(messages, model, existingId) {
|
|
|
46
159
|
return saveSession(messages, existingId, title ?? undefined);
|
|
47
160
|
}
|
|
48
161
|
export function loadSession(id) {
|
|
162
|
+
if (!SESSION_ID_RE.test(id))
|
|
163
|
+
return null;
|
|
49
164
|
const file = sessionPath(id);
|
|
50
165
|
if (!existsSync(file))
|
|
51
166
|
return null;
|
|
@@ -64,8 +179,7 @@ export function listSessions() {
|
|
|
64
179
|
.filter((f) => f.endsWith(".json"))
|
|
65
180
|
.map((f) => {
|
|
66
181
|
try {
|
|
67
|
-
|
|
68
|
-
return data.meta;
|
|
182
|
+
return parseMetaFromHead(readHead(path.join(dir, f), META_READ_LIMIT));
|
|
69
183
|
}
|
|
70
184
|
catch {
|
|
71
185
|
return null;
|
|
@@ -75,9 +189,33 @@ export function listSessions() {
|
|
|
75
189
|
.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
76
190
|
}
|
|
77
191
|
export function deleteSession(id) {
|
|
192
|
+
if (!SESSION_ID_RE.test(id))
|
|
193
|
+
return false;
|
|
194
|
+
const file = sessionPath(id);
|
|
195
|
+
if (!existsSync(file))
|
|
196
|
+
return false;
|
|
197
|
+
try {
|
|
198
|
+
unlinkSync(file);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function renameSession(id, title) {
|
|
206
|
+
if (!SESSION_ID_RE.test(id))
|
|
207
|
+
return false;
|
|
78
208
|
const file = sessionPath(id);
|
|
79
209
|
if (!existsSync(file))
|
|
80
210
|
return false;
|
|
81
|
-
|
|
82
|
-
|
|
211
|
+
try {
|
|
212
|
+
const data = JSON.parse(readFileSync(file, "utf-8"));
|
|
213
|
+
data.meta.title = title;
|
|
214
|
+
data.meta.updated = new Date().toISOString();
|
|
215
|
+
writeFileSync(file, JSON.stringify(data, null, 2), "utf-8");
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
83
221
|
}
|