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.
- package/README.md +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- 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/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -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/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* fetch wrapper for model requests.
|
|
4
|
+
*
|
|
5
|
+
* Providers can stall in two ways that a plain fetch never surfaces:
|
|
6
|
+
* 1. the response never starts (no headers / first byte),
|
|
7
|
+
* 2. the SSE stream opens and then goes quiet forever.
|
|
8
|
+
* Both would hang the agent loop indefinitely, so each has its own timer. On
|
|
9
|
+
* timeout the request is aborted with a marked error the loop recognises as a
|
|
10
|
+
* transient provider failure (see isProviderStallError).
|
|
11
|
+
*
|
|
12
|
+
* The wrapper also carries optional request/response tracing (MIN_AGENT_TRACE),
|
|
13
|
+
* which is the only way to see what a misbehaving gateway actually returned.
|
|
14
|
+
*/
|
|
15
|
+
/** Marker embedded in stall errors so the agent loop can classify them. */
|
|
16
|
+
export const PROVIDER_STALL_MARKER = "min-agent: provider stalled";
|
|
17
|
+
export const DEFAULT_FIRST_BYTE_TIMEOUT_MS = 180_000;
|
|
18
|
+
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 90_000;
|
|
19
|
+
function stallError(kind, ms) {
|
|
20
|
+
const err = new Error(`${PROVIDER_STALL_MARKER}: no ${kind} for ${ms}ms`);
|
|
21
|
+
err.name = "ProviderStallError";
|
|
22
|
+
return err;
|
|
23
|
+
}
|
|
24
|
+
/** Walk the error chain so a stall marker survives provider SDK wrapping. */
|
|
25
|
+
export function describeError(err, depth = 4) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
let current = err;
|
|
28
|
+
for (let i = 0; i < depth && current != null; i++) {
|
|
29
|
+
if (current instanceof Error) {
|
|
30
|
+
parts.push(current.message);
|
|
31
|
+
current = current.cause;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
parts.push(String(current));
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
return parts.join(" | ");
|
|
38
|
+
}
|
|
39
|
+
/** True when the failure is a provider stall we injected (safe to retry). */
|
|
40
|
+
export function isProviderStallError(err) {
|
|
41
|
+
return describeError(err).includes(PROVIDER_STALL_MARKER);
|
|
42
|
+
}
|
|
43
|
+
function traceRequestLine(url, init) {
|
|
44
|
+
const body = typeof init?.body === "string" ? init.body : "";
|
|
45
|
+
let summary = `bytes=${body.length}`;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(body || "{}");
|
|
48
|
+
const tools = parsed.tools?.map((t) => t.function?.name ?? "?") ?? [];
|
|
49
|
+
summary += ` model=${parsed.model ?? "?"} messages=${parsed.messages?.length ?? 0} tools=${tools.length} stream=${parsed.stream ?? false}`;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* not JSON — bytes only */
|
|
53
|
+
}
|
|
54
|
+
return `trace request ${url} ${summary}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Wrap a response body so the idle timer resets on every chunk and fires when
|
|
58
|
+
* the provider goes quiet mid-stream.
|
|
59
|
+
*/
|
|
60
|
+
function guardBody(response, idleTimeoutMs, abort) {
|
|
61
|
+
if (!response.body || idleTimeoutMs <= 0)
|
|
62
|
+
return response;
|
|
63
|
+
const source = response.body;
|
|
64
|
+
const reader = source.getReader();
|
|
65
|
+
let timer;
|
|
66
|
+
const clear = () => {
|
|
67
|
+
if (timer)
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
timer = undefined;
|
|
70
|
+
};
|
|
71
|
+
const guarded = new ReadableStream({
|
|
72
|
+
async pull(controller) {
|
|
73
|
+
const arm = () => {
|
|
74
|
+
clear();
|
|
75
|
+
timer = setTimeout(() => abort(stallError("stream", idleTimeoutMs)), idleTimeoutMs);
|
|
76
|
+
};
|
|
77
|
+
arm();
|
|
78
|
+
try {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
clear();
|
|
81
|
+
if (done) {
|
|
82
|
+
controller.close();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
controller.enqueue(value);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
clear();
|
|
89
|
+
controller.error(err);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
cancel(reason) {
|
|
93
|
+
clear();
|
|
94
|
+
return reader.cancel(reason);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return new Response(guarded, {
|
|
98
|
+
status: response.status,
|
|
99
|
+
statusText: response.statusText,
|
|
100
|
+
headers: response.headers,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function createTimeoutFetch(options = {}) {
|
|
104
|
+
const firstByteTimeoutMs = options.firstByteTimeoutMs ?? DEFAULT_FIRST_BYTE_TIMEOUT_MS;
|
|
105
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
106
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
107
|
+
const now = options.now ?? Date.now;
|
|
108
|
+
const trace = options.trace ?? false;
|
|
109
|
+
return async (input, init) => {
|
|
110
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
let stall;
|
|
113
|
+
const abortWith = (err) => {
|
|
114
|
+
stall = err;
|
|
115
|
+
log("warn", `${err.message} (${url})`);
|
|
116
|
+
controller.abort(err);
|
|
117
|
+
};
|
|
118
|
+
const external = init?.signal;
|
|
119
|
+
const forward = () => controller.abort(external?.reason);
|
|
120
|
+
if (external) {
|
|
121
|
+
if (external.aborted)
|
|
122
|
+
forward();
|
|
123
|
+
else
|
|
124
|
+
external.addEventListener("abort", forward, { once: true });
|
|
125
|
+
}
|
|
126
|
+
let firstByteTimer;
|
|
127
|
+
if (firstByteTimeoutMs > 0) {
|
|
128
|
+
firstByteTimer = setTimeout(() => abortWith(stallError("first byte", firstByteTimeoutMs)), firstByteTimeoutMs);
|
|
129
|
+
}
|
|
130
|
+
if (trace)
|
|
131
|
+
log("info", traceRequestLine(url, init));
|
|
132
|
+
const started = now();
|
|
133
|
+
try {
|
|
134
|
+
const response = await doFetch(input, { ...init, signal: controller.signal });
|
|
135
|
+
if (firstByteTimer)
|
|
136
|
+
clearTimeout(firstByteTimer);
|
|
137
|
+
if (trace)
|
|
138
|
+
log("info", `trace response ${url} status=${response.status} ttfb=${now() - started}ms`);
|
|
139
|
+
return guardBody(response, idleTimeoutMs, abortWith);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (firstByteTimer)
|
|
143
|
+
clearTimeout(firstByteTimer);
|
|
144
|
+
if (stall)
|
|
145
|
+
throw stall;
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
external?.removeEventListener("abort", forward);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const APPROVAL_TIMEOUT_MS = 300_000;
|
|
2
|
+
export function createApprovalGate(timeoutMs = APPROVAL_TIMEOUT_MS) {
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
const clear = (id) => {
|
|
5
|
+
const item = pending.get(id);
|
|
6
|
+
if (!item)
|
|
7
|
+
return undefined;
|
|
8
|
+
clearTimeout(item.timer);
|
|
9
|
+
pending.delete(id);
|
|
10
|
+
return item;
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
waitConfirm(id) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
pending.delete(id);
|
|
17
|
+
resolve(false);
|
|
18
|
+
}, timeoutMs);
|
|
19
|
+
pending.set(id, { kind: "confirm", resolve, timer });
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
waitQuestion(id) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
pending.delete(id);
|
|
26
|
+
resolve(null);
|
|
27
|
+
}, timeoutMs);
|
|
28
|
+
pending.set(id, { kind: "question", resolve, timer });
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
resolveConfirm(id, accepted) {
|
|
32
|
+
const item = pending.get(id);
|
|
33
|
+
if (!item || item.kind !== "confirm")
|
|
34
|
+
return false;
|
|
35
|
+
clear(id);
|
|
36
|
+
item.resolve(accepted);
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
resolveQuestion(id, answer) {
|
|
40
|
+
const item = pending.get(id);
|
|
41
|
+
if (!item || item.kind !== "question")
|
|
42
|
+
return false;
|
|
43
|
+
clear(id);
|
|
44
|
+
item.resolve(answer);
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
cancel(id) {
|
|
48
|
+
const item = clear(id);
|
|
49
|
+
if (!item)
|
|
50
|
+
return;
|
|
51
|
+
if (item.kind === "confirm")
|
|
52
|
+
item.resolve(false);
|
|
53
|
+
else
|
|
54
|
+
item.resolve(null);
|
|
55
|
+
},
|
|
56
|
+
has(id) {
|
|
57
|
+
return pending.has(id);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { lookup } from "dns/promises";
|
|
2
|
+
import { isIP } from "net";
|
|
3
|
+
function isPrivateIpv4(parts) {
|
|
4
|
+
if (parts.length !== 4 || parts.some((p) => Number.isNaN(p)))
|
|
5
|
+
return true;
|
|
6
|
+
const [a, b] = parts;
|
|
7
|
+
if (a === 0 || a === 10 || a === 127)
|
|
8
|
+
return true;
|
|
9
|
+
if (a === 169 && b === 254)
|
|
10
|
+
return true;
|
|
11
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
12
|
+
return true;
|
|
13
|
+
if (a === 192 && b === 168)
|
|
14
|
+
return true;
|
|
15
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
16
|
+
return true;
|
|
17
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
18
|
+
return true;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decode the tail of an IPv4-mapped IPv6 address (`::ffff:7f00:1` or
|
|
23
|
+
* `::ffff:127.0.0.1`) into IPv4 octets, or return null.
|
|
24
|
+
*/
|
|
25
|
+
function mappedIpv4Bytes(ip) {
|
|
26
|
+
const tail = ip.slice(7);
|
|
27
|
+
if (tail.includes(".")) {
|
|
28
|
+
return tail.split(".").map(Number);
|
|
29
|
+
}
|
|
30
|
+
const groups = tail.split(":");
|
|
31
|
+
if (groups.length !== 2)
|
|
32
|
+
return null;
|
|
33
|
+
const hi = parseInt(groups[0], 16);
|
|
34
|
+
const lo = parseInt(groups[1], 16);
|
|
35
|
+
if (Number.isNaN(hi) || Number.isNaN(lo))
|
|
36
|
+
return null;
|
|
37
|
+
return [hi >> 8, hi & 0xff, lo >> 8, lo & 0xff];
|
|
38
|
+
}
|
|
39
|
+
/** Check whether an IP address belongs to a private / reserved / loopback range. */
|
|
40
|
+
export function isPrivateAddress(ip) {
|
|
41
|
+
if (ip.startsWith("::ffff:")) {
|
|
42
|
+
const bytes = mappedIpv4Bytes(ip);
|
|
43
|
+
if (bytes)
|
|
44
|
+
return isPrivateIpv4(bytes);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (isIP(ip) === 6) {
|
|
48
|
+
const lower = ip.toLowerCase();
|
|
49
|
+
if (lower === "::1" || lower.startsWith("::"))
|
|
50
|
+
return true;
|
|
51
|
+
if (lower.startsWith("fc") || lower.startsWith("fd"))
|
|
52
|
+
return true;
|
|
53
|
+
if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb"))
|
|
54
|
+
return true;
|
|
55
|
+
if (lower.startsWith("2001:db8"))
|
|
56
|
+
return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return isPrivateIpv4(ip.split(".").map(Number));
|
|
60
|
+
}
|
|
61
|
+
/** Returns an error message if the URL is unsafe to fetch (SSRF protection), or null if safe. */
|
|
62
|
+
export async function assertSafeUrl(raw) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(raw);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return "Invalid URL";
|
|
69
|
+
}
|
|
70
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
71
|
+
return "Only http/https URLs are allowed";
|
|
72
|
+
}
|
|
73
|
+
const host = parsed.hostname;
|
|
74
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
75
|
+
return `Blocked: local hostname "${host}"`;
|
|
76
|
+
}
|
|
77
|
+
if (isIP(host)) {
|
|
78
|
+
return isPrivateAddress(host) ? `Blocked: private/internal IP "${host}"` : null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const addresses = await lookup(host, { all: true });
|
|
82
|
+
for (const addr of addresses) {
|
|
83
|
+
if (isPrivateAddress(addr.address)) {
|
|
84
|
+
return `Blocked: "${host}" resolves to private address "${addr.address}"`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// DNS failure — let fetch surface the real error
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read a response body up to maxBytes. Bytes beyond the limit are dropped and
|
|
95
|
+
* the read is cancelled early so huge responses never fill memory.
|
|
96
|
+
*/
|
|
97
|
+
export async function readBodyLimited(response, maxBytes) {
|
|
98
|
+
if (!response.body) {
|
|
99
|
+
return { text: "", truncated: false };
|
|
100
|
+
}
|
|
101
|
+
const reader = response.body.getReader();
|
|
102
|
+
const chunks = [];
|
|
103
|
+
let total = 0;
|
|
104
|
+
let truncated = false;
|
|
105
|
+
for (;;) {
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
if (done)
|
|
108
|
+
break;
|
|
109
|
+
total += value.byteLength;
|
|
110
|
+
if (total > maxBytes) {
|
|
111
|
+
chunks.push(Buffer.from(value.subarray(0, value.byteLength - (total - maxBytes))));
|
|
112
|
+
truncated = true;
|
|
113
|
+
await reader.cancel();
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
chunks.push(Buffer.from(value));
|
|
117
|
+
}
|
|
118
|
+
return { text: Buffer.concat(chunks).toString("utf-8"), truncated };
|
|
119
|
+
}
|
package/dist/instructions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
1
2
|
import { readFileSync, existsSync } from "fs";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import os from "os";
|
|
@@ -16,6 +17,21 @@ import { getRulesFile, loadConfig } from "./config.js";
|
|
|
16
17
|
* When the agent reads a file, nearby AGENTS.md/RULES.md are auto-loaded.
|
|
17
18
|
*/
|
|
18
19
|
const PROJECT_FILES = ["AGENTS.md", "RULES.md", "CLAUDE.md"];
|
|
20
|
+
function isWithinDir(root, p) {
|
|
21
|
+
const rel = path.relative(root, p);
|
|
22
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
23
|
+
}
|
|
24
|
+
function walkUpDirs(startDir, minDir) {
|
|
25
|
+
const dirs = [];
|
|
26
|
+
let current = path.resolve(startDir);
|
|
27
|
+
while (current !== path.dirname(minDir) && isWithinDir(minDir, current)) {
|
|
28
|
+
dirs.push(current);
|
|
29
|
+
if (current === minDir)
|
|
30
|
+
break;
|
|
31
|
+
current = path.dirname(current);
|
|
32
|
+
}
|
|
33
|
+
return dirs;
|
|
34
|
+
}
|
|
19
35
|
/** Tracks which instruction files have already been loaded to avoid duplicates */
|
|
20
36
|
export class InstructionTracker {
|
|
21
37
|
loaded = new Set();
|
|
@@ -25,6 +41,9 @@ export class InstructionTracker {
|
|
|
25
41
|
markLoaded(filepath) {
|
|
26
42
|
this.loaded.add(path.resolve(filepath));
|
|
27
43
|
}
|
|
44
|
+
reset() {
|
|
45
|
+
this.loaded.clear();
|
|
46
|
+
}
|
|
28
47
|
/**
|
|
29
48
|
* Context-aware: when a file is read, walk up from its directory
|
|
30
49
|
* looking for AGENTS.md/RULES.md that haven't been loaded yet.
|
|
@@ -33,10 +52,9 @@ export class InstructionTracker {
|
|
|
33
52
|
resolveForFile(filepath) {
|
|
34
53
|
const results = [];
|
|
35
54
|
const root = process.cwd();
|
|
36
|
-
|
|
37
|
-
while (current.startsWith(root) && current !== path.dirname(root)) {
|
|
55
|
+
for (const dir of walkUpDirs(path.dirname(path.resolve(filepath)), root)) {
|
|
38
56
|
for (const file of PROJECT_FILES) {
|
|
39
|
-
const candidate = path.join(
|
|
57
|
+
const candidate = path.join(dir, file);
|
|
40
58
|
if (existsSync(candidate) && !this.isLoaded(candidate)) {
|
|
41
59
|
try {
|
|
42
60
|
const content = readFileSync(candidate, "utf-8").trim();
|
|
@@ -50,32 +68,41 @@ export class InstructionTracker {
|
|
|
50
68
|
}
|
|
51
69
|
if (results.length > 0)
|
|
52
70
|
break;
|
|
53
|
-
current = path.dirname(current);
|
|
54
71
|
}
|
|
55
72
|
return results;
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
|
-
|
|
75
|
+
const instructionStore = new AsyncLocalStorage();
|
|
76
|
+
const fallbackInstructionTracker = new InstructionTracker();
|
|
77
|
+
/** Per-run tracker so concurrent HTTP chats do not share loaded-file state. */
|
|
78
|
+
export async function runWithInstructionTracker(fn) {
|
|
79
|
+
return instructionStore.run(new InstructionTracker(), fn);
|
|
80
|
+
}
|
|
81
|
+
export function getActiveInstructionTracker() {
|
|
82
|
+
return instructionStore.getStore() ?? fallbackInstructionTracker;
|
|
83
|
+
}
|
|
84
|
+
/** After compaction, nearby AGENTS.md must be allowed to re-inject. */
|
|
85
|
+
export function resetActiveInstructionTracker() {
|
|
86
|
+
const current = instructionStore.getStore();
|
|
87
|
+
if (current)
|
|
88
|
+
current.reset();
|
|
89
|
+
else
|
|
90
|
+
fallbackInstructionTracker.reset();
|
|
91
|
+
}
|
|
92
|
+
export function findProjectInstructions() {
|
|
59
93
|
const results = [];
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
// Check .min-agent/AGENTS.md in project
|
|
64
|
-
const dotDir = path.join(current, ".min-agent", "AGENTS.md");
|
|
94
|
+
const root = path.parse(process.cwd()).root;
|
|
95
|
+
for (const dir of walkUpDirs(process.cwd(), root)) {
|
|
96
|
+
const dotDir = path.join(dir, ".min-agent", "AGENTS.md");
|
|
65
97
|
if (existsSync(dotDir)) {
|
|
66
98
|
results.push(dotDir);
|
|
67
99
|
break;
|
|
68
100
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
results.push(filepath);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (results.length > 0)
|
|
101
|
+
const found = PROJECT_FILES.find((file) => existsSync(path.join(dir, file)));
|
|
102
|
+
if (found) {
|
|
103
|
+
results.push(path.join(dir, found));
|
|
77
104
|
break;
|
|
78
|
-
|
|
105
|
+
}
|
|
79
106
|
}
|
|
80
107
|
return results;
|
|
81
108
|
}
|
|
@@ -103,23 +130,35 @@ function resolveConfigInstructions() {
|
|
|
103
130
|
}
|
|
104
131
|
return results;
|
|
105
132
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
const REMOTE_CACHE_TTL_MS = 60_000;
|
|
134
|
+
const remoteCache = new Map();
|
|
135
|
+
async function fetchRemoteInstruction(url) {
|
|
136
|
+
const cached = remoteCache.get(url);
|
|
137
|
+
if (cached && Date.now() - cached.fetchedAt < REMOTE_CACHE_TTL_MS)
|
|
138
|
+
return cached.text;
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
const text = await response.text();
|
|
143
|
+
if (text.trim()) {
|
|
144
|
+
remoteCache.set(url, { fetchedAt: Date.now(), text });
|
|
145
|
+
return text;
|
|
118
146
|
}
|
|
119
147
|
}
|
|
120
|
-
catch { }
|
|
121
148
|
}
|
|
122
|
-
|
|
149
|
+
catch { }
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
export async function fetchRemoteInstructions(urls) {
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
const instructions = urls ?? config.instructions ?? [];
|
|
155
|
+
const results = await Promise.all(instructions
|
|
156
|
+
.filter((i) => i.startsWith("http://") || i.startsWith("https://"))
|
|
157
|
+
.map(async (url) => {
|
|
158
|
+
const text = await fetchRemoteInstruction(url);
|
|
159
|
+
return text ? `Instructions from: ${url}\n${text}` : "";
|
|
160
|
+
}));
|
|
161
|
+
return results.filter(Boolean);
|
|
123
162
|
}
|
|
124
163
|
export async function loadInstructions() {
|
|
125
164
|
const parts = [];
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getConfigDir } from "./config.js";
|
|
4
|
+
const KEEP_DAYS = 7;
|
|
5
|
+
function logsDir() {
|
|
6
|
+
return path.join(getConfigDir(), "logs");
|
|
7
|
+
}
|
|
8
|
+
function logFilePath(date) {
|
|
9
|
+
const y = date.getFullYear();
|
|
10
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
11
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
12
|
+
return path.join(logsDir(), `min-agent.${y}-${m}-${d}.log`);
|
|
13
|
+
}
|
|
14
|
+
export function initLogger() {
|
|
15
|
+
try {
|
|
16
|
+
mkdirSync(logsDir(), { recursive: true });
|
|
17
|
+
const cutoff = Date.now() - KEEP_DAYS * 24 * 60 * 60 * 1000;
|
|
18
|
+
for (const f of readdirSync(logsDir())) {
|
|
19
|
+
const m = f.match(/^min-agent\.(\d{4})-(\d{2})-(\d{2})\.log$/);
|
|
20
|
+
if (!m)
|
|
21
|
+
continue;
|
|
22
|
+
const ts = new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00`).getTime();
|
|
23
|
+
if (ts < cutoff) {
|
|
24
|
+
try {
|
|
25
|
+
unlinkSync(path.join(logsDir(), f));
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
33
|
+
function writeLog(level, msg) {
|
|
34
|
+
try {
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
|
|
37
|
+
writeFileSync(logFilePath(now), `[${time}] [${level}]${runTag()} ${scrubSecrets(msg)}\n`, { flag: "a" });
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run context stamped onto every log line, so interleaved turns (and the passes
|
|
43
|
+
* inside one turn) can be told apart when reading the file afterwards.
|
|
44
|
+
*/
|
|
45
|
+
let runContext = null;
|
|
46
|
+
function runTag() {
|
|
47
|
+
if (!runContext)
|
|
48
|
+
return "";
|
|
49
|
+
return ` [run=${runContext.runId} pass=${runContext.pass}]`;
|
|
50
|
+
}
|
|
51
|
+
/** Start a new run scope and return its id. */
|
|
52
|
+
export function startRunLog() {
|
|
53
|
+
const runId = Math.random().toString(36).slice(2, 8);
|
|
54
|
+
runContext = { runId, pass: 0 };
|
|
55
|
+
return runId;
|
|
56
|
+
}
|
|
57
|
+
/** Mark the beginning of a model pass inside the current run. */
|
|
58
|
+
export function nextRunPass() {
|
|
59
|
+
if (!runContext)
|
|
60
|
+
startRunLog();
|
|
61
|
+
runContext.pass++;
|
|
62
|
+
return runContext.pass;
|
|
63
|
+
}
|
|
64
|
+
export function endRunLog() {
|
|
65
|
+
runContext = null;
|
|
66
|
+
}
|
|
67
|
+
export function currentRunId() {
|
|
68
|
+
return runContext?.runId ?? null;
|
|
69
|
+
}
|
|
70
|
+
const SECRET_PATTERNS = [
|
|
71
|
+
/\b(Bearer\s+)[A-Za-z0-9._~+/-]{8,}/g,
|
|
72
|
+
/\b(api[_-]?key|apikey|token|secret|password|passwd)\s*[:=]\s*["']?[A-Za-z0-9._~+/-]{6,}["']?/gi,
|
|
73
|
+
/\b(sk-[A-Za-z0-9_-]{6,})/g,
|
|
74
|
+
/\b(gh[pousr]_[A-Za-z0-9]{20,})/g,
|
|
75
|
+
];
|
|
76
|
+
/** Mask obvious secrets before anything hits the log file. */
|
|
77
|
+
function scrubSecrets(msg) {
|
|
78
|
+
let out = msg;
|
|
79
|
+
for (const re of SECRET_PATTERNS)
|
|
80
|
+
out = out.replace(re, "$1***");
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
export function log(level, msg) {
|
|
84
|
+
writeLog(level, msg);
|
|
85
|
+
}
|
|
86
|
+
function summarize(v, max = 200) {
|
|
87
|
+
const s = typeof v === "string" ? v : (JSON.stringify(v) ?? String(v));
|
|
88
|
+
return s.length > max ? s.slice(0, max) + "…" : s;
|
|
89
|
+
}
|
|
90
|
+
export function logToolCall(name, input) {
|
|
91
|
+
writeLog("info", `tool_call ${name} ${summarize(input)}`);
|
|
92
|
+
}
|
|
93
|
+
export function logToolResult(name, output) {
|
|
94
|
+
writeLog("info", `tool_result ${name} ${summarize(output)}`);
|
|
95
|
+
}
|