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.
Files changed (96) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +116 -3
  3. package/dist/cli/main.js +35 -8
  4. package/dist/cli/repl-commands.js +633 -0
  5. package/dist/cli/repl.js +110 -611
  6. package/dist/cli/setup.js +32 -12
  7. package/dist/config/config.js +46 -30
  8. package/dist/config/defaults.js +10 -1
  9. package/dist/config/security.js +15 -8
  10. package/dist/core/agent-moe.js +24 -12
  11. package/dist/core/agent.js +281 -47
  12. package/dist/core/bootstrap.js +52 -36
  13. package/dist/core/session-logger.js +35 -2
  14. package/dist/core/workspace.js +76 -0
  15. package/dist/i18n/en.json +79 -15
  16. package/dist/i18n/index.js +12 -9
  17. package/dist/i18n/ru.json +79 -15
  18. package/dist/index.js +13 -13
  19. package/dist/llm/openai-compat.js +39 -10
  20. package/dist/logger/app-logger.js +83 -16
  21. package/dist/logger/file-log.js +151 -0
  22. package/dist/main.js +537 -284
  23. package/dist/modules/browser/bridge-server.mjs +113 -105
  24. package/dist/modules/browser/session.js +108 -60
  25. package/dist/modules/certification/cli.js +176 -0
  26. package/dist/modules/certification/fact-checker.js +84 -0
  27. package/dist/modules/certification/loader.js +111 -0
  28. package/dist/modules/certification/manifest.js +50 -0
  29. package/dist/modules/certification/runner.js +162 -0
  30. package/dist/modules/certification/scenarios.js +124 -0
  31. package/dist/modules/certification/types.js +1 -0
  32. package/dist/modules/context/manager.js +119 -10
  33. package/dist/modules/execution/auditor.js +33 -39
  34. package/dist/modules/execution/index.js +8 -6
  35. package/dist/modules/execution/module.js +474 -32
  36. package/dist/modules/execution/moe-executor.js +97 -40
  37. package/dist/modules/execution/plan-coverage.js +68 -0
  38. package/dist/modules/execution/plan-persister.js +46 -0
  39. package/dist/modules/execution/plan-store.js +159 -0
  40. package/dist/modules/execution/planner.js +63 -13
  41. package/dist/modules/execution/stuck-detector.js +252 -39
  42. package/dist/modules/execution/tracker.js +21 -7
  43. package/dist/modules/execution/verifier.js +46 -17
  44. package/dist/modules/hallucination/confidence.js +7 -2
  45. package/dist/modules/hallucination/consistency.js +8 -42
  46. package/dist/modules/hallucination/detector.js +26 -21
  47. package/dist/modules/hallucination/factual.js +170 -150
  48. package/dist/modules/hallucination/index.js +5 -4
  49. package/dist/modules/hallucination/js-identifiers.js +72 -0
  50. package/dist/modules/hallucination/llm-judge.js +103 -0
  51. package/dist/modules/index.js +5 -5
  52. package/dist/modules/lsp/client.js +235 -0
  53. package/dist/modules/lsp/config.js +81 -0
  54. package/dist/modules/lsp/index.js +3 -0
  55. package/dist/modules/lsp/module.js +68 -0
  56. package/dist/modules/lsp/types.js +1 -0
  57. package/dist/modules/mcp/client.js +8 -2
  58. package/dist/modules/memory/store.js +4 -0
  59. package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
  60. package/dist/modules/processes/index.js +1 -2
  61. package/dist/modules/processes/registry.js +125 -35
  62. package/dist/modules/processes/runner.js +9 -110
  63. package/dist/modules/security/audit-log.js +30 -10
  64. package/dist/modules/security/command-validator.js +42 -16
  65. package/dist/modules/security/content-scanner.js +9 -8
  66. package/dist/modules/security/network-validator.js +2 -2
  67. package/dist/modules/security/path-validator.js +64 -10
  68. package/dist/modules/security/security-policies.js +221 -67
  69. package/dist/modules/security/session-encryption.js +42 -25
  70. package/dist/modules/session/manager.js +15 -10
  71. package/dist/modules/session/store.js +62 -8
  72. package/dist/modules/skills/index.js +2 -3
  73. package/dist/modules/skills/module.js +10 -23
  74. package/dist/tools/bash.js +287 -90
  75. package/dist/tools/create-dir.js +0 -1
  76. package/dist/tools/delete-file.js +0 -1
  77. package/dist/tools/edit-file.js +10 -8
  78. package/dist/tools/executor.js +57 -7
  79. package/dist/tools/grep-tool.js +51 -29
  80. package/dist/tools/index.js +55 -40
  81. package/dist/tools/load-skill.js +14 -18
  82. package/dist/tools/move-file.js +3 -2
  83. package/dist/tools/pipeline-run.js +1 -1
  84. package/dist/tools/read-file.js +15 -5
  85. package/dist/tools/search-history.js +42 -22
  86. package/dist/tools/subagent.js +21 -12
  87. package/dist/tools/web-browse.js +54 -25
  88. package/dist/tools/web-fetch.js +60 -34
  89. package/dist/tools/web-search.js +39 -20
  90. package/dist/tools/write-file.js +13 -10
  91. package/dist/ui/diff.js +9 -16
  92. package/dist/ui/renderer.js +69 -6
  93. package/package.json +48 -45
  94. package/dist/modules/context/history.js +0 -15
  95. package/dist/modules/processes/detect.js +0 -34
  96. package/dist/modules/skills/matcher.js +0 -27
@@ -1,22 +1,22 @@
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';
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: 'web_browse',
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: ['research'],
10
+ tags: ["research"],
11
11
  parameters: {
12
- type: 'object',
12
+ type: "object",
13
13
  properties: {
14
- url: { type: 'string', description: 'URL to fetch' },
14
+ url: { type: "string", description: "URL to fetch" },
15
15
  },
16
- required: ['url'],
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, { signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000) });
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 maxLen = securityConfig?.maxResponseSize || MAX_CHARS;
45
- let content = stripped.length > maxLen ? stripped.slice(0, maxLen) + t('file.truncated') : stripped;
46
- const lines = content.split('\n');
47
- if (lines.length > MAX_PREVIEW_LINES) {
48
- content = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
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
- return { success: true, output: content || t('file.empty_page') };
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 { success: false, output: t('error.fetch_url_failed', { url: sanitizeUrl(url), message: err.message }) };
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
  };
@@ -1,29 +1,29 @@
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';
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: 'web_fetch',
19
- description: 'Fetch a URL and convert its content to markdown. Use for reading documentation, APIs, web pages.',
20
- tags: ['research'],
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: 'object',
22
+ type: "object",
23
23
  properties: {
24
- url: { type: 'string', description: 'URL to fetch' },
24
+ url: { type: "string", description: "URL to fetch" },
25
25
  },
26
- required: ['url'],
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, { signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000) });
44
+ const response = await fetch(url, {
45
+ signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000),
46
+ });
45
47
  if (!response.ok) {
46
- return { success: false, output: t('error.http', { status: response.status, statusText: response.statusText }) };
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('content-type') || '';
56
+ const contentType = response.headers.get("content-type") || "";
49
57
  const text = await response.text();
50
- const cleaned = contentType.includes('html') ? stripHtml(text) : text;
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
- if (cleaned.length > (securityConfig?.maxResponseSize || MAX_CHARS)) {
54
- let content = cleaned.slice(0, securityConfig?.maxResponseSize || MAX_CHARS) + t('file.truncated');
55
- const lines = content.split('\n');
56
- if (lines.length > MAX_PREVIEW_LINES) {
57
- content = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
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
- const lines = cleaned.split('\n');
62
- if (lines.length > MAX_PREVIEW_LINES) {
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
- return { success: true, output: cleaned || t('file.empty_page') };
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 { success: false, output: t('error.fetch_failed', { message: e.message }) };
92
+ return {
93
+ success: false,
94
+ output: t("error.fetch_failed", { message: e.message }),
95
+ };
70
96
  }
71
97
  },
72
98
  };
@@ -1,21 +1,24 @@
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';
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: 'web_search',
7
- description: 'Search the web for information. Returns search results with titles and snippets.',
8
- tags: ['research'],
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: 'object',
10
+ type: "object",
11
11
  properties: {
12
- query: { type: 'string', description: 'Search query' },
13
- numResults: { type: 'number', description: 'Number of results (default 5)' },
12
+ query: { type: "string", description: "Search query" },
13
+ numResults: {
14
+ type: "number",
15
+ description: "Number of results (default 5)",
16
+ },
14
17
  },
15
- required: ['query'],
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, { signal: AbortSignal.timeout(securityConfig?.requestTimeout || 10000) });
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 title = match[1].replace(/<[^>]+>/g, '').trim();
43
- const snippet = match[2].replace(/<[^>]+>/g, '').trim();
44
- results.push(`${title}: ${snippet}`);
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('tool.no_results', { query }) };
56
+ return { success: true, output: t("tool.no_results", { query }) };
51
57
  }
52
- return { success: true, output: t('tool.search_results', { query, results: results.join('\n') }) };
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 { success: false, output: t('error.search_failed', { message: err.message }) };
72
+ return {
73
+ success: false,
74
+ output: t("error.search_failed", { message: err.message }),
75
+ };
57
76
  }
58
77
  },
59
78
  };
@@ -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
- const scanResult = scanContent(content, path, ctx.config.security?.contentScan);
53
- if (!scanResult.allowed) {
54
- logSecurityBlock(ctx.sessionId, "file_write", scanResult.reason || "Content contains dangerous patterns", path);
55
- return {
56
- success: false,
57
- output: `[SECURITY BLOCKED] ${scanResult.reason}`,
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 ? 'updated' : 'created'}`);
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 pc.red(`${pc.bgRed(`${numStr} - ${line.content}`)}`);
113
+ return `${numStr} ${pc.red("-")} ${line.content}`;
114
114
  }
115
115
  else if (line.type === "add") {
116
- return pc.green(`${pc.bgGreen(`${numStr} + ${line.content}`)}`);
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 maxOldNum = Math.max(...diff.filter((l) => l.oldNum !== null).map((l) => l.oldNum));
140
- const maxNewNum = Math.max(...diff.filter((l) => l.newNum !== null).map((l) => l.newNum));
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(pc.green(`${pc.bgGreen(`${numStr} + ${lines[i]}`)}`));
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(pc.red(`${pc.bgRed(`${numStr} - ${lines[i]}`)}`));
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
  }
@@ -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.card.body.push(chunk);
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
- const marker = error ? pc.red("✗") : pc.green("✓");
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.39.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
- "dependencies": {
31
- "commander": "^12.0.0",
32
- "js-tiktoken": "^1.0.0",
33
- "jsonrepair": "^3.15.0",
34
- "picocolors": "^1.1.1",
35
- "playwright": "^1.62.0",
36
- "string-width": "^8.2.2",
37
- "yaml": "^2.9.0"
38
- },
39
- "devDependencies": {
40
- "@types/bun": "^1.3.14",
41
- "@types/node": "^22.20.1",
42
- "typescript": "^5.9.3",
43
- "vitest": "^4.1.10"
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
+ }