min-agent 0.2.1 → 0.4.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 (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
package/dist/sessions.js CHANGED
@@ -1,41 +1,188 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from "fs";
1
+ import { readFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, openSync, readSync, closeSync, statSync, } from "fs";
2
2
  import path from "path";
3
+ import { randomUUID } from "crypto";
4
+ import { atomicWriteFileSync } from "./tools/atomic-file.js";
3
5
  import { getConfigDir } from "./config.js";
4
6
  import { generateTitle } from "./title-gen.js";
7
+ import { isSyntheticMessage } from "./synthetic.js";
8
+ import { parseTaskState } from "./tools/todo.js";
5
9
  function getSessionsDir() {
6
10
  return path.join(getConfigDir(), "sessions");
7
11
  }
12
+ /**
13
+ * In-process per-session mutex serializing read-modify-write cycles
14
+ * (load → append → run → save) so concurrent requests on the same session
15
+ * cannot lose messages. Cross-process safety is NOT provided.
16
+ */
17
+ const sessionLocks = new Map();
18
+ export async function withSessionLock(sessionId, fn) {
19
+ if (!sessionId)
20
+ return fn();
21
+ const prev = sessionLocks.get(sessionId) ?? Promise.resolve();
22
+ let release;
23
+ const ours = new Promise((r) => {
24
+ release = r;
25
+ });
26
+ const chained = prev.then(() => ours);
27
+ sessionLocks.set(sessionId, chained);
28
+ await prev;
29
+ try {
30
+ return await fn();
31
+ }
32
+ finally {
33
+ release();
34
+ if (sessionLocks.get(sessionId) === chained)
35
+ sessionLocks.delete(sessionId);
36
+ }
37
+ }
38
+ /** Session ids are UUIDs; a strict whitelist blocks path traversal via `/v1/sessions/..%2F..%2Fconfig`. */
39
+ const SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/;
8
40
  function sessionPath(id) {
41
+ if (!SESSION_ID_RE.test(id))
42
+ throw new Error(`invalid session id: ${id}`);
9
43
  return path.join(getSessionsDir(), `${id}.json`);
10
44
  }
45
+ /** Session meta lives at the head of the JSON file, so only the first bytes are needed. */
46
+ const META_READ_LIMIT = 64 * 1024;
47
+ function readHead(file, limit) {
48
+ const fd = openSync(file, "r");
49
+ try {
50
+ const buf = Buffer.alloc(Math.min(statSync(file).size, limit));
51
+ if (buf.length === 0)
52
+ return "";
53
+ readSync(fd, buf, 0, buf.length, 0);
54
+ return buf.toString("utf-8");
55
+ }
56
+ finally {
57
+ closeSync(fd);
58
+ }
59
+ }
60
+ /** Find the index of the `}` that closes the object opened at `start`, or -1. */
61
+ function matchBrace(text, start) {
62
+ let depth = 0;
63
+ let inString = false;
64
+ let escaped = false;
65
+ for (let i = start; i < text.length; i++) {
66
+ const ch = text[i];
67
+ if (inString) {
68
+ if (escaped)
69
+ escaped = false;
70
+ else if (ch === "\\")
71
+ escaped = true;
72
+ else if (ch === '"')
73
+ inString = false;
74
+ continue;
75
+ }
76
+ if (ch === '"')
77
+ inString = true;
78
+ else if (ch === "{")
79
+ depth++;
80
+ else if (ch === "}") {
81
+ depth--;
82
+ if (depth === 0)
83
+ return i;
84
+ }
85
+ }
86
+ return -1;
87
+ }
88
+ /** Extract the meta object from a (possibly truncated) session JSON head. */
89
+ function parseMetaFromHead(head) {
90
+ const key = head.indexOf('"meta"');
91
+ if (key < 0)
92
+ return null;
93
+ const brace = head.indexOf("{", key);
94
+ if (brace < 0)
95
+ return null;
96
+ const end = matchBrace(head, brace);
97
+ if (end < 0)
98
+ return null;
99
+ try {
100
+ return JSON.parse(head.slice(brace, end + 1));
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
11
106
  function generateId() {
12
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
107
+ return randomUUID();
108
+ }
109
+ function textOfContent(content) {
110
+ if (typeof content === "string")
111
+ return content;
112
+ return content
113
+ .map((p) => (p.type === "text" ? p.text : ""))
114
+ .join(" ")
115
+ .trim();
13
116
  }
14
117
  function deriveTitle(messages) {
15
118
  const first = messages.find((m) => m.role === "user");
16
119
  if (!first)
17
120
  return "Untitled";
18
- const content = typeof first.content === "string" ? first.content : "";
19
- return content.slice(0, 60) || "Untitled";
121
+ const text = textOfContent(first.content);
122
+ return text.slice(0, 60) || "Untitled";
20
123
  }
21
- export function saveSession(messages, existingId, title) {
124
+ /** Convert binary image parts to data URLs so JSON serialization stays usable. */
125
+ export function sanitizeForStorage(messages) {
126
+ return messages
127
+ .filter((msg) => !isSyntheticMessage(msg))
128
+ .map((msg) => {
129
+ if (!Array.isArray(msg.content))
130
+ return msg;
131
+ const content = msg.content.map((part) => {
132
+ if (part.type !== "image" || part.image == null || typeof part.image === "string")
133
+ return part;
134
+ let buf = null;
135
+ if (Buffer.isBuffer(part.image) || part.image instanceof Uint8Array) {
136
+ buf = Buffer.isBuffer(part.image) ? part.image : Buffer.from(part.image);
137
+ }
138
+ else if (typeof part.image === "object" && !(part.image instanceof URL)) {
139
+ const data = part.image.data;
140
+ if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
141
+ buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
142
+ }
143
+ }
144
+ if (!buf)
145
+ return part;
146
+ const mime = part.mimeType ?? "image/png";
147
+ return { ...part, image: `data:${mime};base64,${buf.toString("base64")}` };
148
+ });
149
+ return { ...msg, content };
150
+ });
151
+ }
152
+ export function saveSession(messages, existingId, title, created, taskState) {
22
153
  const dir = getSessionsDir();
23
154
  mkdirSync(dir, { recursive: true });
24
- const id = existingId ?? generateId();
155
+ const id = existingId && SESSION_ID_RE.test(existingId) ? existingId : generateId();
25
156
  const now = new Date().toISOString();
157
+ const existingMeta = existingId ? readMeta(id) : null;
158
+ const preservedState = taskState === undefined && existingId ? loadSession(id)?.taskState : undefined;
159
+ const resolvedState = taskState ?? preservedState;
26
160
  const data = {
27
161
  meta: {
28
162
  id,
29
- title: title ?? deriveTitle(messages),
30
- created: existingId ? loadSession(id)?.meta.created ?? now : now,
163
+ title: title ?? existingMeta?.title ?? deriveTitle(messages),
164
+ created: created ?? existingMeta?.created ?? now,
31
165
  updated: now,
32
166
  messageCount: messages.length,
33
167
  },
34
- messages,
168
+ ...(resolvedState ? { taskState: resolvedState } : {}),
169
+ messages: sanitizeForStorage(messages),
35
170
  };
36
- writeFileSync(sessionPath(id), JSON.stringify(data, null, 2), "utf-8");
171
+ atomicWriteFileSync(sessionPath(id), JSON.stringify(data, null, 2));
37
172
  return id;
38
173
  }
174
+ /** Read only the meta head to recover the original created/title. */
175
+ function readMeta(id) {
176
+ const file = sessionPath(id);
177
+ if (!existsSync(file))
178
+ return null;
179
+ try {
180
+ return parseMetaFromHead(readHead(file, META_READ_LIMIT));
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
39
186
  /** Save session with auto-generated title from LLM */
40
187
  export async function saveSessionWithTitle(messages, model, existingId) {
41
188
  let title = null;
@@ -46,16 +193,28 @@ export async function saveSessionWithTitle(messages, model, existingId) {
46
193
  return saveSession(messages, existingId, title ?? undefined);
47
194
  }
48
195
  export function loadSession(id) {
196
+ if (!SESSION_ID_RE.test(id))
197
+ return null;
49
198
  const file = sessionPath(id);
50
199
  if (!existsSync(file))
51
200
  return null;
52
201
  try {
53
- return JSON.parse(readFileSync(file, "utf-8"));
202
+ const data = JSON.parse(readFileSync(file, "utf-8"));
203
+ const taskState = parseTaskState(data.taskState);
204
+ return taskState ? { ...data, taskState } : data;
54
205
  }
55
206
  catch {
56
207
  return null;
57
208
  }
58
209
  }
210
+ export function loadExecHistory(resumeSessionId) {
211
+ if (!resumeSessionId)
212
+ return { ok: true, messages: [], sessionId: undefined };
213
+ const session = loadSession(resumeSessionId);
214
+ if (!session)
215
+ return { ok: false, error: `会话不存在: ${resumeSessionId}` };
216
+ return { ok: true, messages: [...session.messages], sessionId: resumeSessionId, taskState: session.taskState };
217
+ }
59
218
  export function listSessions() {
60
219
  const dir = getSessionsDir();
61
220
  if (!existsSync(dir))
@@ -64,8 +223,7 @@ export function listSessions() {
64
223
  .filter((f) => f.endsWith(".json"))
65
224
  .map((f) => {
66
225
  try {
67
- const data = JSON.parse(readFileSync(path.join(dir, f), "utf-8"));
68
- return data.meta;
226
+ return parseMetaFromHead(readHead(path.join(dir, f), META_READ_LIMIT));
69
227
  }
70
228
  catch {
71
229
  return null;
@@ -75,9 +233,33 @@ export function listSessions() {
75
233
  .sort((a, b) => b.updated.localeCompare(a.updated));
76
234
  }
77
235
  export function deleteSession(id) {
236
+ if (!SESSION_ID_RE.test(id))
237
+ return false;
78
238
  const file = sessionPath(id);
79
239
  if (!existsSync(file))
80
240
  return false;
81
- unlinkSync(file);
82
- return true;
241
+ try {
242
+ unlinkSync(file);
243
+ return true;
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ }
249
+ export function renameSession(id, title) {
250
+ if (!SESSION_ID_RE.test(id))
251
+ return false;
252
+ const file = sessionPath(id);
253
+ if (!existsSync(file))
254
+ return false;
255
+ try {
256
+ const data = JSON.parse(readFileSync(file, "utf-8"));
257
+ data.meta.title = title;
258
+ data.meta.updated = new Date().toISOString();
259
+ atomicWriteFileSync(file, JSON.stringify(data, null, 2));
260
+ return true;
261
+ }
262
+ catch {
263
+ return false;
264
+ }
83
265
  }