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.
Files changed (81) hide show
  1. package/README.md +146 -18
  2. package/dist/agent.js +293 -408
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli.js +397 -139
  5. package/dist/clipboard.js +59 -23
  6. package/dist/code-mode.js +3 -3
  7. package/dist/compaction.js +182 -81
  8. package/dist/config.js +186 -35
  9. package/dist/confirm.js +55 -6
  10. package/dist/context-window.js +67 -54
  11. package/dist/doom-loop.js +19 -12
  12. package/dist/http.js +119 -0
  13. package/dist/instructions.js +51 -33
  14. package/dist/logger.js +66 -0
  15. package/dist/markdown.js +3 -44
  16. package/dist/mcp.js +547 -100
  17. package/dist/memory.js +48 -6
  18. package/dist/output.js +36 -27
  19. package/dist/paste-handler.js +3 -3
  20. package/dist/plugins.js +33 -6
  21. package/dist/pricing.js +119 -0
  22. package/dist/provider.js +17 -15
  23. package/dist/serve.js +658 -369
  24. package/dist/sessions.js +151 -13
  25. package/dist/skills.js +466 -76
  26. package/dist/synthetic.js +7 -0
  27. package/dist/title-gen.js +2 -1
  28. package/dist/tool-display.js +173 -0
  29. package/dist/tool-output.js +54 -45
  30. package/dist/tools/apply_patch.js +191 -0
  31. package/dist/tools/backend.js +61 -0
  32. package/dist/tools/bash.js +147 -70
  33. package/dist/tools/code_search.js +6 -5
  34. package/dist/tools/edit.js +23 -7
  35. package/dist/tools/explore.js +80 -12
  36. package/dist/tools/glob.js +3 -3
  37. package/dist/tools/grep.js +146 -14
  38. package/dist/tools/index.js +7 -7
  39. package/dist/tools/question.js +4 -22
  40. package/dist/tools/read.js +71 -11
  41. package/dist/tools/task.js +33 -20
  42. package/dist/tools/todo.js +83 -73
  43. package/dist/tools/web_fetch.js +150 -46
  44. package/dist/tools/web_search.js +706 -28
  45. package/dist/tools/write.js +13 -7
  46. package/dist/tui/App.js +40 -6
  47. package/dist/tui/ConfirmBar.js +24 -3
  48. package/dist/tui/InputBar.js +390 -45
  49. package/dist/tui/MessageList.js +533 -20
  50. package/dist/tui/ModelPicker.js +108 -0
  51. package/dist/tui/QuestionBar.js +104 -0
  52. package/dist/tui/StatusBar.js +19 -11
  53. package/dist/tui/agent-runner.js +103 -0
  54. package/dist/tui/caret-pos.js +134 -0
  55. package/dist/tui/caret.js +69 -0
  56. package/dist/tui/diff-view.js +61 -0
  57. package/dist/tui/drag-state.js +44 -0
  58. package/dist/tui/index.js +153 -24
  59. package/dist/tui/input-history.js +44 -0
  60. package/dist/tui/layout.js +17 -0
  61. package/dist/tui/mouse.js +46 -0
  62. package/dist/tui/selection.js +134 -0
  63. package/dist/tui/slash-commands.js +90 -0
  64. package/dist/tui/slash-handler.js +370 -0
  65. package/dist/tui/text-width.js +91 -0
  66. package/dist/tui/theme.js +12 -0
  67. package/dist/tui/undo-stack.js +14 -0
  68. package/dist/tui/use-sgr-mouse.js +27 -0
  69. package/dist/tui-chat.js +111 -331
  70. package/dist/updater.js +57 -0
  71. package/docs/API.md +160 -14
  72. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  73. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  74. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  75. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  76. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  77. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  78. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  79. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  80. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  81. 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 Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
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 content = typeof first.content === "string" ? first.content : "";
19
- return content.slice(0, 60) || "Untitled";
90
+ const text = textOfContent(first.content);
91
+ return text.slice(0, 60) || "Untitled";
20
92
  }
21
- export function saveSession(messages, existingId, title) {
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 ?? generateId();
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: existingId ? loadSession(id)?.meta.created ?? now : now,
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
- const data = JSON.parse(readFileSync(path.join(dir, f), "utf-8"));
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
- unlinkSync(file);
82
- return true;
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
  }