micro-models-agent 0.39.0 → 0.40.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/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +116 -3
- package/dist/cli/main.js +35 -8
- package/dist/cli/repl-commands.js +633 -0
- package/dist/cli/repl.js +110 -611
- package/dist/cli/setup.js +32 -12
- package/dist/config/config.js +46 -30
- package/dist/config/defaults.js +10 -1
- package/dist/config/security.js +15 -8
- package/dist/core/agent-moe.js +24 -12
- package/dist/core/agent.js +281 -47
- package/dist/core/bootstrap.js +52 -36
- package/dist/core/session-logger.js +35 -2
- package/dist/core/workspace.js +76 -0
- package/dist/i18n/en.json +79 -15
- package/dist/i18n/index.js +12 -9
- package/dist/i18n/ru.json +79 -15
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +39 -10
- package/dist/logger/app-logger.js +83 -16
- package/dist/logger/file-log.js +151 -0
- package/dist/main.js +537 -284
- package/dist/modules/browser/bridge-server.mjs +113 -105
- package/dist/modules/browser/session.js +108 -60
- package/dist/modules/certification/cli.js +176 -0
- package/dist/modules/certification/fact-checker.js +84 -0
- package/dist/modules/certification/loader.js +111 -0
- package/dist/modules/certification/manifest.js +50 -0
- package/dist/modules/certification/runner.js +162 -0
- package/dist/modules/certification/scenarios.js +124 -0
- package/dist/modules/certification/types.js +1 -0
- package/dist/modules/context/manager.js +119 -10
- package/dist/modules/execution/auditor.js +33 -39
- package/dist/modules/execution/index.js +8 -6
- package/dist/modules/execution/module.js +474 -32
- package/dist/modules/execution/moe-executor.js +97 -40
- package/dist/modules/execution/plan-coverage.js +68 -0
- package/dist/modules/execution/plan-persister.js +46 -0
- package/dist/modules/execution/plan-store.js +159 -0
- package/dist/modules/execution/planner.js +63 -13
- package/dist/modules/execution/stuck-detector.js +252 -39
- package/dist/modules/execution/tracker.js +21 -7
- package/dist/modules/execution/verifier.js +46 -17
- package/dist/modules/hallucination/confidence.js +7 -2
- package/dist/modules/hallucination/consistency.js +8 -42
- package/dist/modules/hallucination/detector.js +26 -21
- package/dist/modules/hallucination/factual.js +170 -150
- package/dist/modules/hallucination/index.js +5 -4
- package/dist/modules/hallucination/js-identifiers.js +72 -0
- package/dist/modules/hallucination/llm-judge.js +103 -0
- package/dist/modules/index.js +5 -5
- package/dist/modules/lsp/client.js +235 -0
- package/dist/modules/lsp/config.js +81 -0
- package/dist/modules/lsp/index.js +3 -0
- package/dist/modules/lsp/module.js +68 -0
- package/dist/modules/lsp/types.js +1 -0
- package/dist/modules/mcp/client.js +8 -2
- package/dist/modules/memory/store.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
- package/dist/modules/processes/index.js +1 -2
- package/dist/modules/processes/registry.js +125 -35
- package/dist/modules/processes/runner.js +9 -110
- package/dist/modules/security/audit-log.js +30 -10
- package/dist/modules/security/command-validator.js +42 -16
- package/dist/modules/security/content-scanner.js +9 -8
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +64 -10
- package/dist/modules/security/security-policies.js +221 -67
- package/dist/modules/security/session-encryption.js +42 -25
- package/dist/modules/session/manager.js +15 -10
- package/dist/modules/session/store.js +62 -8
- package/dist/modules/skills/index.js +2 -3
- package/dist/modules/skills/module.js +10 -23
- package/dist/tools/bash.js +287 -90
- package/dist/tools/create-dir.js +0 -1
- package/dist/tools/delete-file.js +0 -1
- package/dist/tools/edit-file.js +10 -8
- package/dist/tools/executor.js +57 -7
- package/dist/tools/grep-tool.js +51 -29
- package/dist/tools/index.js +55 -40
- package/dist/tools/load-skill.js +14 -18
- package/dist/tools/move-file.js +3 -2
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +15 -5
- package/dist/tools/search-history.js +42 -22
- package/dist/tools/subagent.js +21 -12
- package/dist/tools/web-browse.js +54 -25
- package/dist/tools/web-fetch.js +60 -34
- package/dist/tools/web-search.js +39 -20
- package/dist/tools/write-file.js +13 -10
- package/dist/ui/diff.js +9 -16
- package/dist/ui/renderer.js +69 -6
- package/package.json +48 -45
- package/dist/modules/context/history.js +0 -15
- package/dist/modules/processes/detect.js +0 -34
- package/dist/modules/skills/matcher.js +0 -27
package/dist/tools/web-browse.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { t } from
|
|
2
|
-
import { isUrlAllowed, sanitizeUrl } from
|
|
3
|
-
import { logNetworkRequest, logSecurityBlock } from
|
|
4
|
-
import { getSessionSecurityConfig } from
|
|
5
|
-
import { MAX_PREVIEW_LINES } from
|
|
1
|
+
import { t } from "../i18n/index";
|
|
2
|
+
import { isUrlAllowed, sanitizeUrl, } from "../modules/security/network-validator";
|
|
3
|
+
import { logNetworkRequest, logSecurityBlock, } from "../modules/security/audit-log";
|
|
4
|
+
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
5
|
+
import { MAX_PREVIEW_LINES } from "./preview";
|
|
6
6
|
const MAX_CHARS = 3000;
|
|
7
7
|
export const webBrowseTool = {
|
|
8
|
-
name:
|
|
8
|
+
name: "web_browse",
|
|
9
9
|
description: `Fetch and read a web page. Returns the page content as plain text (max ${MAX_PREVIEW_LINES} lines / ${MAX_CHARS} chars).`,
|
|
10
|
-
tags: [
|
|
10
|
+
tags: ["research"],
|
|
11
11
|
parameters: {
|
|
12
|
-
type:
|
|
12
|
+
type: "object",
|
|
13
13
|
properties: {
|
|
14
|
-
url: { type:
|
|
14
|
+
url: { type: "string", description: "URL to fetch" },
|
|
15
15
|
},
|
|
16
|
-
required: [
|
|
16
|
+
required: ["url"],
|
|
17
17
|
},
|
|
18
18
|
handler: async (ctx, args) => {
|
|
19
|
-
const url = String(args.url ||
|
|
19
|
+
const url = String(args.url || "");
|
|
20
20
|
// Get session-specific security config
|
|
21
21
|
const securityConfig = ctx.sessionContext
|
|
22
22
|
? getSessionSecurityConfig(ctx.config, ctx.sessionContext).network
|
|
@@ -30,28 +30,57 @@ export const webBrowseTool = {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
try {
|
|
33
|
-
const response = await fetch(url, {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000),
|
|
35
|
+
});
|
|
34
36
|
const text = await response.text();
|
|
35
37
|
const stripped = text
|
|
36
|
-
.replace(/<script[\s\S]*?<\/script>/gi,
|
|
37
|
-
.replace(/<style[\s\S]*?<\/style>/gi,
|
|
38
|
-
.replace(/<[^>]+>/g,
|
|
39
|
-
.replace(/&[^;]+;/g,
|
|
40
|
-
.replace(/\s+/g,
|
|
38
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
39
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
40
|
+
.replace(/<[^>]+>/g, "")
|
|
41
|
+
.replace(/&[^;]+;/g, " ")
|
|
42
|
+
.replace(/\s+/g, " ")
|
|
41
43
|
.trim();
|
|
42
|
-
// Log successful network request
|
|
43
44
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const fullChars = stripped.length;
|
|
46
|
+
const fullLines = stripped.split("\n").length;
|
|
47
|
+
const maxChars = securityConfig?.maxResponseSize || MAX_CHARS;
|
|
48
|
+
let content;
|
|
49
|
+
if (stripped.length > maxChars) {
|
|
50
|
+
content = stripped.slice(0, maxChars) + t("file.truncated");
|
|
49
51
|
}
|
|
50
|
-
|
|
52
|
+
else {
|
|
53
|
+
content = stripped;
|
|
54
|
+
}
|
|
55
|
+
const contentLines = content.split("\n");
|
|
56
|
+
if (contentLines.length > MAX_PREVIEW_LINES) {
|
|
57
|
+
content =
|
|
58
|
+
contentLines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
59
|
+
`\n... (${contentLines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
60
|
+
}
|
|
61
|
+
const truncated = stripped.length > maxChars || fullLines > MAX_PREVIEW_LINES
|
|
62
|
+
? t("file.truncated").trim()
|
|
63
|
+
: "";
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
output: content || t("file.empty_page"),
|
|
67
|
+
display: t("tool.web_browse_result", {
|
|
68
|
+
url: sanitizeUrl(url),
|
|
69
|
+
chars: String(fullChars),
|
|
70
|
+
lines: String(fullLines),
|
|
71
|
+
truncated,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
51
74
|
}
|
|
52
75
|
catch (err) {
|
|
53
76
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), false, `Error: ${err.message}`);
|
|
54
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
output: t("error.fetch_url_failed", {
|
|
80
|
+
url: sanitizeUrl(url),
|
|
81
|
+
message: err.message,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
55
84
|
}
|
|
56
85
|
},
|
|
57
86
|
};
|
package/dist/tools/web-fetch.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { t } from
|
|
2
|
-
import { isUrlAllowed, sanitizeUrl } from
|
|
3
|
-
import { logNetworkRequest, logSecurityBlock } from
|
|
4
|
-
import { getSessionSecurityConfig } from
|
|
5
|
-
import { DEFAULT_SECURITY_CONFIG } from
|
|
6
|
-
import { MAX_PREVIEW_LINES } from
|
|
1
|
+
import { t } from "../i18n/index";
|
|
2
|
+
import { isUrlAllowed, sanitizeUrl, } from "../modules/security/network-validator";
|
|
3
|
+
import { logNetworkRequest, logSecurityBlock, } from "../modules/security/audit-log";
|
|
4
|
+
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
5
|
+
import { DEFAULT_SECURITY_CONFIG } from "../config/security";
|
|
6
|
+
import { MAX_PREVIEW_LINES } from "./preview";
|
|
7
7
|
const MAX_CHARS = 5000;
|
|
8
8
|
function stripHtml(html) {
|
|
9
9
|
return html
|
|
10
|
-
.replace(/<script[\s\S]*?<\/script>/gi,
|
|
11
|
-
.replace(/<style[\s\S]*?<\/style>/gi,
|
|
12
|
-
.replace(/<[^>]+>/g,
|
|
13
|
-
.replace(/&[^;]+;/g,
|
|
14
|
-
.replace(/\s+/g,
|
|
10
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
11
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
12
|
+
.replace(/<[^>]+>/g, "")
|
|
13
|
+
.replace(/&[^;]+;/g, " ")
|
|
14
|
+
.replace(/\s+/g, " ")
|
|
15
15
|
.trim();
|
|
16
16
|
}
|
|
17
17
|
export const webFetchTool = {
|
|
18
|
-
name:
|
|
19
|
-
description:
|
|
20
|
-
tags: [
|
|
18
|
+
name: "web_fetch",
|
|
19
|
+
description: "Fetch a URL and convert its content to markdown. Use for reading documentation, APIs, web pages.",
|
|
20
|
+
tags: ["research"],
|
|
21
21
|
parameters: {
|
|
22
|
-
type:
|
|
22
|
+
type: "object",
|
|
23
23
|
properties: {
|
|
24
|
-
url: { type:
|
|
24
|
+
url: { type: "string", description: "URL to fetch" },
|
|
25
25
|
},
|
|
26
|
-
required: [
|
|
26
|
+
required: ["url"],
|
|
27
27
|
},
|
|
28
28
|
handler: async (ctx, args) => {
|
|
29
29
|
const url = String(args.url);
|
|
@@ -41,32 +41,58 @@ export const webFetchTool = {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
|
-
const response = await fetch(url, {
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000),
|
|
46
|
+
});
|
|
45
47
|
if (!response.ok) {
|
|
46
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
output: t("error.http", {
|
|
51
|
+
status: response.status,
|
|
52
|
+
statusText: response.statusText,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
47
55
|
}
|
|
48
|
-
const contentType = response.headers.get(
|
|
56
|
+
const contentType = response.headers.get("content-type") || "";
|
|
49
57
|
const text = await response.text();
|
|
50
|
-
const cleaned = contentType.includes(
|
|
51
|
-
// Log successful network request
|
|
58
|
+
const cleaned = contentType.includes("html") ? stripHtml(text) : text;
|
|
52
59
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return { success: true, output: content };
|
|
60
|
+
const fullChars = cleaned.length;
|
|
61
|
+
const fullLines = cleaned.split("\n").length;
|
|
62
|
+
const maxChars = securityConfig?.maxResponseSize || MAX_CHARS;
|
|
63
|
+
let content;
|
|
64
|
+
if (cleaned.length > maxChars) {
|
|
65
|
+
content = cleaned.slice(0, maxChars) + t("file.truncated");
|
|
60
66
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return { success: true, output: lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)` };
|
|
67
|
+
else {
|
|
68
|
+
content = cleaned;
|
|
64
69
|
}
|
|
65
|
-
|
|
70
|
+
const contentLines = content.split("\n");
|
|
71
|
+
if (contentLines.length > MAX_PREVIEW_LINES) {
|
|
72
|
+
content =
|
|
73
|
+
contentLines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
74
|
+
`\n... (${contentLines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
75
|
+
}
|
|
76
|
+
const truncated = cleaned.length > maxChars || fullLines > MAX_PREVIEW_LINES
|
|
77
|
+
? t("file.truncated").trim()
|
|
78
|
+
: "";
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
output: content || t("file.empty_page"),
|
|
82
|
+
display: t("tool.web_fetch_result", {
|
|
83
|
+
url: sanitizeUrl(url),
|
|
84
|
+
chars: String(fullChars),
|
|
85
|
+
lines: String(fullLines),
|
|
86
|
+
truncated,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
66
89
|
}
|
|
67
90
|
catch (e) {
|
|
68
91
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), false, `Error: ${e.message}`);
|
|
69
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
output: t("error.fetch_failed", { message: e.message }),
|
|
95
|
+
};
|
|
70
96
|
}
|
|
71
97
|
},
|
|
72
98
|
};
|
package/dist/tools/web-search.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import { t } from
|
|
2
|
-
import { isUrlAllowed, sanitizeUrl } from
|
|
3
|
-
import { logNetworkRequest, logSecurityBlock } from
|
|
4
|
-
import { getSessionSecurityConfig } from
|
|
1
|
+
import { t } from "../i18n/index";
|
|
2
|
+
import { isUrlAllowed, sanitizeUrl, } from "../modules/security/network-validator";
|
|
3
|
+
import { logNetworkRequest, logSecurityBlock, } from "../modules/security/audit-log";
|
|
4
|
+
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
5
5
|
export const webSearchTool = {
|
|
6
|
-
name:
|
|
7
|
-
description:
|
|
8
|
-
tags: [
|
|
6
|
+
name: "web_search",
|
|
7
|
+
description: "Search the web for information. Returns search results with titles and snippets.",
|
|
8
|
+
tags: ["research"],
|
|
9
9
|
parameters: {
|
|
10
|
-
type:
|
|
10
|
+
type: "object",
|
|
11
11
|
properties: {
|
|
12
|
-
query: { type:
|
|
13
|
-
numResults: {
|
|
12
|
+
query: { type: "string", description: "Search query" },
|
|
13
|
+
numResults: {
|
|
14
|
+
type: "number",
|
|
15
|
+
description: "Number of results (default 5)",
|
|
16
|
+
},
|
|
14
17
|
},
|
|
15
|
-
required: [
|
|
18
|
+
required: ["query"],
|
|
16
19
|
},
|
|
17
20
|
handler: async (ctx, args) => {
|
|
18
|
-
const query = String(args.query ||
|
|
21
|
+
const query = String(args.query || "");
|
|
19
22
|
const numResults = Number(args.numResults) || 5;
|
|
20
23
|
// Build search URL
|
|
21
24
|
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
@@ -32,28 +35,44 @@ export const webSearchTool = {
|
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
37
|
try {
|
|
35
|
-
const response = await fetch(url, {
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
signal: AbortSignal.timeout(securityConfig?.requestTimeout || 10000),
|
|
40
|
+
});
|
|
36
41
|
const html = await response.text();
|
|
37
42
|
const results = [];
|
|
38
|
-
const snippetRegex = /<a[^>]+class="result__a"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
43
|
+
const snippetRegex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
39
44
|
let match;
|
|
40
45
|
let count = 0;
|
|
41
46
|
while ((match = snippetRegex.exec(html)) !== null && count < numResults) {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
47
|
+
const href = match[1].trim();
|
|
48
|
+
const title = match[2].replace(/<[^>]+>/g, "").trim();
|
|
49
|
+
const snippet = match[3].replace(/<[^>]+>/g, "").trim();
|
|
50
|
+
results.push(`${count + 1}. ${title}\n URL: ${href}\n ${snippet}`);
|
|
45
51
|
count++;
|
|
46
52
|
}
|
|
47
53
|
// Log successful network request
|
|
48
54
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Results: ${results.length}`);
|
|
49
55
|
if (results.length === 0) {
|
|
50
|
-
return { success: true, output: t(
|
|
56
|
+
return { success: true, output: t("tool.no_results", { query }) };
|
|
51
57
|
}
|
|
52
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
output: t("tool.search_results", {
|
|
61
|
+
query,
|
|
62
|
+
results: results.join("\n"),
|
|
63
|
+
}),
|
|
64
|
+
display: t("tool.web_search_result", {
|
|
65
|
+
query,
|
|
66
|
+
count: String(results.length),
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
53
69
|
}
|
|
54
70
|
catch (err) {
|
|
55
71
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), false, `Error: ${err.message}`);
|
|
56
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
output: t("error.search_failed", { message: err.message }),
|
|
75
|
+
};
|
|
57
76
|
}
|
|
58
77
|
},
|
|
59
78
|
};
|
package/dist/tools/write-file.js
CHANGED
|
@@ -47,15 +47,19 @@ export const writeFileTool = {
|
|
|
47
47
|
output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
// Check content for dangerous patterns
|
|
50
|
+
// Check content for dangerous patterns — only when security is enabled.
|
|
51
|
+
// contentScan has its own `enabled` flag, but it must NOT fire while the
|
|
52
|
+
// global security switch is off (false positives on ordinary CLI code).
|
|
51
53
|
const content = String(args.content);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (securityConfig?.enabled && securityConfig?.contentScan?.enabled) {
|
|
55
|
+
const scanResult = scanContent(content, path, securityConfig.contentScan);
|
|
56
|
+
if (!scanResult.allowed) {
|
|
57
|
+
logSecurityBlock(ctx.sessionId, "file_write", scanResult.reason || "Content contains dangerous patterns", path);
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
output: `[SECURITY BLOCKED] ${scanResult.reason}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
const dir = dirname(resolved);
|
|
61
65
|
if (!existsSync(dir)) {
|
|
@@ -73,8 +77,7 @@ export const writeFileTool = {
|
|
|
73
77
|
// Increment file operations counter
|
|
74
78
|
ctx.fileOperationsCount = currentCount + 1;
|
|
75
79
|
// Log successful file write
|
|
76
|
-
logFileWrite(ctx.sessionId, path, true, `File ${fileExists ?
|
|
77
|
-
ctx.trackCreatedPath?.(path);
|
|
80
|
+
logFileWrite(ctx.sessionId, path, true, `File ${fileExists ? "updated" : "created"}`);
|
|
78
81
|
return { success: true, output: t("file.written", { path }), diff };
|
|
79
82
|
},
|
|
80
83
|
};
|
package/dist/ui/diff.js
CHANGED
|
@@ -110,10 +110,10 @@ function formatLine(line, maxNumWidth) {
|
|
|
110
110
|
const num = line.type === "remove" ? line.oldNum : line.newNum;
|
|
111
111
|
const numStr = num !== null ? String(num).padStart(maxNumWidth) : " ".repeat(maxNumWidth);
|
|
112
112
|
if (line.type === "remove") {
|
|
113
|
-
return
|
|
113
|
+
return `${numStr} ${pc.red("-")} ${line.content}`;
|
|
114
114
|
}
|
|
115
115
|
else if (line.type === "add") {
|
|
116
|
-
return
|
|
116
|
+
return `${numStr} ${pc.green("+")} ${line.content}`;
|
|
117
117
|
}
|
|
118
118
|
else if (line.content === "...") {
|
|
119
119
|
return pc.dim(` ${" ".repeat(maxNumWidth)}...`);
|
|
@@ -127,17 +127,13 @@ export function generateDiff(oldContent, newContent) {
|
|
|
127
127
|
return "";
|
|
128
128
|
const oldLines = oldContent.split("\n");
|
|
129
129
|
const newLines = newContent.split("\n");
|
|
130
|
-
if (oldLines.length === 1 &&
|
|
131
|
-
oldLines[0] === "" &&
|
|
132
|
-
newLines.length === 1 &&
|
|
133
|
-
newLines[0] === "") {
|
|
134
|
-
return "";
|
|
135
|
-
}
|
|
136
130
|
const diff = buildDiff(oldLines, newLines);
|
|
137
131
|
if (diff.length === 0)
|
|
138
132
|
return "";
|
|
139
|
-
const
|
|
140
|
-
const
|
|
133
|
+
const oldNums = diff.filter((l) => l.oldNum !== null).map((l) => l.oldNum);
|
|
134
|
+
const newNums = diff.filter((l) => l.newNum !== null).map((l) => l.newNum);
|
|
135
|
+
const maxOldNum = oldNums.length > 0 ? Math.max(...oldNums) : 0;
|
|
136
|
+
const maxNewNum = newNums.length > 0 ? Math.max(...newNums) : 0;
|
|
141
137
|
const maxNumWidth = Math.max(String(maxOldNum).length, String(maxNewNum).length, 3);
|
|
142
138
|
let lines = diff.map((l) => formatLine(l, maxNumWidth));
|
|
143
139
|
if (lines.length > MAX_DIFF_LINES) {
|
|
@@ -153,7 +149,7 @@ export function generateNewFileDiff(content) {
|
|
|
153
149
|
const diffLines = [];
|
|
154
150
|
for (let i = 0; i < lines.length; i++) {
|
|
155
151
|
const numStr = String(i + 1).padStart(maxNumWidth);
|
|
156
|
-
diffLines.push(
|
|
152
|
+
diffLines.push(`${numStr} ${pc.green("+")} ${lines[i]}`);
|
|
157
153
|
}
|
|
158
154
|
if (diffLines.length > MAX_DIFF_LINES) {
|
|
159
155
|
const truncated = diffLines.slice(0, MAX_DIFF_LINES);
|
|
@@ -168,7 +164,7 @@ export function generateDeleteDiff(content) {
|
|
|
168
164
|
const diffLines = [];
|
|
169
165
|
for (let i = 0; i < lines.length; i++) {
|
|
170
166
|
const numStr = String(i + 1).padStart(maxNumWidth);
|
|
171
|
-
diffLines.push(
|
|
167
|
+
diffLines.push(`${numStr} ${pc.red("-")} ${lines[i]}`);
|
|
172
168
|
}
|
|
173
169
|
if (diffLines.length > MAX_DIFF_LINES) {
|
|
174
170
|
const truncated = diffLines.slice(0, MAX_DIFF_LINES);
|
|
@@ -178,8 +174,5 @@ export function generateDeleteDiff(content) {
|
|
|
178
174
|
return diffLines.join("\n");
|
|
179
175
|
}
|
|
180
176
|
export function generateMoveDiff(fromPath, toPath) {
|
|
181
|
-
return [
|
|
182
|
-
pc.red(` - ${fromPath}`),
|
|
183
|
-
pc.green(` + ${toPath}`),
|
|
184
|
-
].join("\n");
|
|
177
|
+
return [pc.red(` - ${fromPath}`), pc.green(` + ${toPath}`)].join("\n");
|
|
185
178
|
}
|
package/dist/ui/renderer.js
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { pc } from "./colors";
|
|
2
2
|
import { FormattingStream } from "./md-formatter";
|
|
3
3
|
import { Spinner } from "./spinner";
|
|
4
|
-
import { box } from "./box";
|
|
4
|
+
import { box, divider } from "./box";
|
|
5
5
|
import { getTerminalWidth } from "./table";
|
|
6
6
|
import { t } from "../i18n/index";
|
|
7
|
+
const GUTTER = " ";
|
|
8
|
+
/** opencode-like leading marker per tool category. */
|
|
9
|
+
export function toolMarker(tool) {
|
|
10
|
+
switch (tool) {
|
|
11
|
+
case "write_file":
|
|
12
|
+
case "edit_file":
|
|
13
|
+
case "create_dir":
|
|
14
|
+
case "move_file":
|
|
15
|
+
case "delete_file":
|
|
16
|
+
return "←";
|
|
17
|
+
case "read_file":
|
|
18
|
+
case "list_dir":
|
|
19
|
+
case "file_info":
|
|
20
|
+
return "→";
|
|
21
|
+
case "glob":
|
|
22
|
+
case "grep":
|
|
23
|
+
return "✱";
|
|
24
|
+
case "bash":
|
|
25
|
+
return "$";
|
|
26
|
+
default:
|
|
27
|
+
return "⚙";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
7
30
|
export function isRichTerminal() {
|
|
8
31
|
return Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
9
32
|
}
|
|
@@ -38,12 +61,14 @@ export class Renderer {
|
|
|
38
61
|
out;
|
|
39
62
|
err;
|
|
40
63
|
width;
|
|
64
|
+
toolStyle;
|
|
41
65
|
card = null;
|
|
42
66
|
constructor(opts = {}) {
|
|
43
67
|
this.rich = opts.rich ?? isRichTerminal();
|
|
44
68
|
this.out = opts.out ?? process.stdout;
|
|
45
69
|
this.err = opts.err ?? process.stderr;
|
|
46
70
|
this.width = opts.width ?? getTerminalWidth();
|
|
71
|
+
this.toolStyle = opts.toolStyle ?? "inline";
|
|
47
72
|
this.spinner = new Spinner({
|
|
48
73
|
enabled: this.rich && (opts.spinner ?? true),
|
|
49
74
|
stream: this.err,
|
|
@@ -61,12 +86,25 @@ export class Renderer {
|
|
|
61
86
|
meta(chunk) {
|
|
62
87
|
this.spinner.stop();
|
|
63
88
|
if (this.card) {
|
|
64
|
-
this.
|
|
89
|
+
if (this.toolStyle === "inline") {
|
|
90
|
+
this.writeInlineBody(chunk);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.card.body.push(chunk);
|
|
94
|
+
}
|
|
65
95
|
}
|
|
66
96
|
else {
|
|
67
97
|
this.out.write(chunk);
|
|
68
98
|
}
|
|
69
99
|
}
|
|
100
|
+
/** Stream tool body lines with the gutter prefix, skipping blank lines. */
|
|
101
|
+
writeInlineBody(chunk) {
|
|
102
|
+
for (const line of chunk.split("\n")) {
|
|
103
|
+
if (line.trim() === "")
|
|
104
|
+
continue;
|
|
105
|
+
this.out.write(`${GUTTER}${line}\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
70
108
|
/** Dimmed reasoning stream (already colored by the agent). */
|
|
71
109
|
reasoning(chunk) {
|
|
72
110
|
this.spinner.stop();
|
|
@@ -89,15 +127,41 @@ export class Renderer {
|
|
|
89
127
|
return;
|
|
90
128
|
}
|
|
91
129
|
this.card = { tool, args, body: [], start: Date.now() };
|
|
130
|
+
if (this.toolStyle === "inline") {
|
|
131
|
+
const marker = toolMarker(tool);
|
|
132
|
+
this.out.write(`\n${pc.dim(marker)} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
92
135
|
this.spinner.start(`${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}`);
|
|
93
136
|
}
|
|
94
|
-
toolEnd(_tool, duration, error) {
|
|
137
|
+
toolEnd(_tool, duration, error, ctxDelta) {
|
|
95
138
|
this.spinner.stop();
|
|
96
|
-
if (!this.rich)
|
|
139
|
+
if (!this.rich) {
|
|
140
|
+
if (ctxDelta !== undefined && ctxDelta !== 0) {
|
|
141
|
+
const deltaStr = ctxDelta > 0
|
|
142
|
+
? pc.green(`+${ctxDelta}`)
|
|
143
|
+
: pc.yellow(`${ctxDelta} ↓`);
|
|
144
|
+
this.out.write(`${pc.dim("ctx")} ${deltaStr}\n`);
|
|
145
|
+
}
|
|
97
146
|
return;
|
|
147
|
+
}
|
|
98
148
|
if (!this.card)
|
|
99
149
|
return;
|
|
100
150
|
const { tool, args, body } = this.card;
|
|
151
|
+
const marker = error ? pc.red("✗") : pc.green("✓");
|
|
152
|
+
let footer = `${marker} ${pc.dim(`${duration}ms`)}`;
|
|
153
|
+
if (ctxDelta !== undefined && ctxDelta !== 0) {
|
|
154
|
+
const deltaStr = ctxDelta > 0
|
|
155
|
+
? pc.green(`+${ctxDelta}`)
|
|
156
|
+
: pc.yellow(`${ctxDelta} ↓`);
|
|
157
|
+
footer += ` ${pc.dim("ctx")} ${deltaStr}`;
|
|
158
|
+
}
|
|
159
|
+
if (this.toolStyle === "inline") {
|
|
160
|
+
this.out.write(`${GUTTER}${footer}\n`);
|
|
161
|
+
this.out.write(`${divider(this.width)}\n`);
|
|
162
|
+
this.card = null;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
101
165
|
const lines = [];
|
|
102
166
|
const summary = summarizeArgs(args);
|
|
103
167
|
if (summary)
|
|
@@ -108,8 +172,7 @@ export class Renderer {
|
|
|
108
172
|
lines.push(line);
|
|
109
173
|
}
|
|
110
174
|
}
|
|
111
|
-
|
|
112
|
-
lines.push(`${marker} ${pc.dim(`${duration}ms`)}`);
|
|
175
|
+
lines.push(footer);
|
|
113
176
|
const title = `${marker} ${friendlyTool(tool)}`;
|
|
114
177
|
for (const line of box(lines, { title, width: this.width })) {
|
|
115
178
|
this.out.write(`${line}\n`);
|
package/package.json
CHANGED
|
@@ -1,45 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "micro-models-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"mma": "bin/mma.mjs"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist/",
|
|
11
|
-
"bin/"
|
|
12
|
-
],
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=20"
|
|
15
|
-
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"mma": "bun run src/cli/main.ts",
|
|
18
|
-
"build": "bun run build:tsc && bun run build:copy-assets",
|
|
19
|
-
"build:tsc": "tsc -p tsconfig.build.json",
|
|
20
|
-
"build:copy-assets": "bun run scripts/copy-assets.ts",
|
|
21
|
-
"build:prod": "bun run 'build:bundle' && bun run 'build:copy-assets'",
|
|
22
|
-
"build:clean": "cmd /c \"if exist dist rmdir /s /q dist\"",
|
|
23
|
-
"build:bundle": "bun build ./src/cli/main.ts --outfile ./dist/main.js --target node --format esm --external playwright",
|
|
24
|
-
"dev": "bun --watch src/cli/main.ts",
|
|
25
|
-
"typecheck": "tsc --noEmit",
|
|
26
|
-
"test": "bun test",
|
|
27
|
-
"test:watch": "bun test --watch",
|
|
28
|
-
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "micro-models-agent",
|
|
3
|
+
"version": "0.40.0",
|
|
4
|
+
"description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mma": "bin/mma.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"bin/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"mma": "bun run src/cli/main.ts",
|
|
18
|
+
"build": "bun run build:tsc && bun run build:copy-assets",
|
|
19
|
+
"build:tsc": "tsc -p tsconfig.build.json",
|
|
20
|
+
"build:copy-assets": "bun run scripts/copy-assets.ts",
|
|
21
|
+
"build:prod": "bun run 'build:bundle' && bun run 'build:copy-assets'",
|
|
22
|
+
"build:clean": "cmd /c \"if exist dist rmdir /s /q dist\"",
|
|
23
|
+
"build:bundle": "bun build ./src/cli/main.ts --outfile ./dist/main.js --target node --format esm --external playwright",
|
|
24
|
+
"dev": "bun --watch src/cli/main.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"test:watch": "bun test --watch",
|
|
28
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
29
|
+
"format": "prettier --write \"src/**/*.{ts,js,mjs,json}\" \"tests/**/*.{ts,js,json}\" \"scripts/**/*.ts\" \"*.{ts,json}\"",
|
|
30
|
+
"format:check": "prettier --check \"src/**/*.{ts,js,mjs,json}\" \"tests/**/*.{ts,js,json}\" \"scripts/**/*.ts\" \"*.{ts,json}\""
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^12.0.0",
|
|
34
|
+
"js-tiktoken": "^1.0.0",
|
|
35
|
+
"jsonrepair": "^3.15.0",
|
|
36
|
+
"picocolors": "^1.1.1",
|
|
37
|
+
"playwright": "^1.62.0",
|
|
38
|
+
"string-width": "^8.2.2",
|
|
39
|
+
"yaml": "^2.9.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "^1.3.14",
|
|
43
|
+
"@types/node": "^22.20.1",
|
|
44
|
+
"prettier": "^3.9.6",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.1.10"
|
|
47
|
+
}
|
|
48
|
+
}
|