micro-models-agent 0.28.17 → 0.29.1

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 (94) hide show
  1. package/dist/cli/commands.js +3 -116
  2. package/dist/cli/main.js +8 -35
  3. package/dist/cli/repl.js +611 -110
  4. package/dist/cli/setup.js +12 -32
  5. package/dist/config/config.js +30 -46
  6. package/dist/config/defaults.js +1 -10
  7. package/dist/config/security.js +8 -15
  8. package/dist/core/agent-moe.js +12 -24
  9. package/dist/core/agent.js +47 -281
  10. package/dist/core/bootstrap.js +36 -52
  11. package/dist/core/session-logger.js +2 -35
  12. package/dist/i18n/en.json +15 -79
  13. package/dist/i18n/index.js +9 -12
  14. package/dist/i18n/ru.json +15 -79
  15. package/dist/index.js +13 -13
  16. package/dist/llm/openai-compat.js +10 -39
  17. package/dist/logger/app-logger.js +16 -83
  18. package/dist/main.js +625 -243
  19. package/dist/modules/browser/session.js +60 -108
  20. package/dist/modules/context/history.js +15 -0
  21. package/dist/modules/context/manager.js +10 -119
  22. package/dist/modules/execution/auditor.js +39 -33
  23. package/dist/modules/execution/index.js +6 -8
  24. package/dist/modules/execution/module.js +32 -474
  25. package/dist/modules/execution/moe-executor.js +40 -97
  26. package/dist/modules/execution/planner.js +13 -63
  27. package/dist/modules/execution/stuck-detector.js +39 -252
  28. package/dist/modules/execution/tracker.js +7 -21
  29. package/dist/modules/execution/verifier.js +17 -46
  30. package/dist/modules/hallucination/confidence.js +2 -7
  31. package/dist/modules/hallucination/consistency.js +42 -8
  32. package/dist/modules/hallucination/detector.js +21 -26
  33. package/dist/modules/hallucination/factual.js +150 -170
  34. package/dist/modules/hallucination/index.js +4 -5
  35. package/dist/modules/index.js +5 -5
  36. package/dist/modules/mcp/client.js +2 -8
  37. package/dist/modules/memory/store.js +0 -4
  38. package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
  39. package/dist/modules/processes/detect.js +34 -0
  40. package/dist/modules/processes/index.js +2 -1
  41. package/dist/modules/processes/registry.js +35 -125
  42. package/dist/modules/processes/runner.js +110 -9
  43. package/dist/modules/security/audit-log.js +10 -30
  44. package/dist/modules/security/command-validator.js +16 -42
  45. package/dist/modules/security/content-scanner.js +8 -9
  46. package/dist/modules/security/network-validator.js +2 -2
  47. package/dist/modules/security/path-validator.js +10 -64
  48. package/dist/modules/security/security-policies.js +67 -221
  49. package/dist/modules/security/session-encryption.js +25 -42
  50. package/dist/modules/session/manager.js +10 -15
  51. package/dist/modules/session/store.js +8 -62
  52. package/dist/modules/skills/index.js +3 -2
  53. package/dist/modules/skills/matcher.js +27 -0
  54. package/dist/modules/skills/module.js +23 -10
  55. package/dist/tools/bash.js +90 -287
  56. package/dist/tools/create-dir.js +1 -0
  57. package/dist/tools/delete-file.js +1 -0
  58. package/dist/tools/edit-file.js +8 -10
  59. package/dist/tools/executor.js +7 -57
  60. package/dist/tools/grep-tool.js +29 -51
  61. package/dist/tools/index.js +40 -55
  62. package/dist/tools/load-skill.js +18 -14
  63. package/dist/tools/move-file.js +2 -3
  64. package/dist/tools/pipeline-run.js +1 -1
  65. package/dist/tools/read-file.js +5 -15
  66. package/dist/tools/search-history.js +22 -42
  67. package/dist/tools/subagent.js +12 -21
  68. package/dist/tools/web-browse.js +25 -54
  69. package/dist/tools/web-fetch.js +34 -60
  70. package/dist/tools/web-search.js +20 -39
  71. package/dist/tools/write-file.js +10 -13
  72. package/dist/ui/diff.js +16 -9
  73. package/dist/ui/renderer.js +6 -69
  74. package/package.json +1 -1
  75. package/dist/cli/repl-commands.js +0 -633
  76. package/dist/core/workspace.js +0 -76
  77. package/dist/logger/file-log.js +0 -151
  78. package/dist/modules/certification/cli.js +0 -176
  79. package/dist/modules/certification/fact-checker.js +0 -84
  80. package/dist/modules/certification/loader.js +0 -111
  81. package/dist/modules/certification/manifest.js +0 -50
  82. package/dist/modules/certification/runner.js +0 -162
  83. package/dist/modules/certification/scenarios.js +0 -124
  84. package/dist/modules/certification/types.js +0 -1
  85. package/dist/modules/execution/plan-coverage.js +0 -68
  86. package/dist/modules/execution/plan-persister.js +0 -46
  87. package/dist/modules/execution/plan-store.js +0 -159
  88. package/dist/modules/hallucination/js-identifiers.js +0 -72
  89. package/dist/modules/hallucination/llm-judge.js +0 -103
  90. package/dist/modules/lsp/client.js +0 -235
  91. package/dist/modules/lsp/config.js +0 -81
  92. package/dist/modules/lsp/index.js +0 -3
  93. package/dist/modules/lsp/module.js +0 -68
  94. package/dist/modules/lsp/types.js +0 -1
@@ -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,57 +30,28 @@ export const webBrowseTool = {
30
30
  };
31
31
  }
32
32
  try {
33
- const response = await fetch(url, {
34
- signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000),
35
- });
33
+ const response = await fetch(url, { signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000) });
36
34
  const text = await response.text();
37
35
  const stripped = text
38
- .replace(/<script[\s\S]*?<\/script>/gi, "")
39
- .replace(/<style[\s\S]*?<\/style>/gi, "")
40
- .replace(/<[^>]+>/g, "")
41
- .replace(/&[^;]+;/g, " ")
42
- .replace(/\s+/g, " ")
36
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
37
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
38
+ .replace(/<[^>]+>/g, '')
39
+ .replace(/&[^;]+;/g, ' ')
40
+ .replace(/\s+/g, ' ')
43
41
  .trim();
42
+ // Log successful network request
44
43
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
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");
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)`;
51
49
  }
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
- };
50
+ return { success: true, output: content || t('file.empty_page') };
74
51
  }
75
52
  catch (err) {
76
53
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), false, `Error: ${err.message}`);
77
- return {
78
- success: false,
79
- output: t("error.fetch_url_failed", {
80
- url: sanitizeUrl(url),
81
- message: err.message,
82
- }),
83
- };
54
+ return { success: false, output: t('error.fetch_url_failed', { url: sanitizeUrl(url), message: err.message }) };
84
55
  }
85
56
  },
86
57
  };
@@ -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,58 +41,32 @@ export const webFetchTool = {
41
41
  };
42
42
  }
43
43
  try {
44
- const response = await fetch(url, {
45
- signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000),
46
- });
44
+ const response = await fetch(url, { signal: AbortSignal.timeout(securityConfig?.requestTimeout || 15000) });
47
45
  if (!response.ok) {
48
- return {
49
- success: false,
50
- output: t("error.http", {
51
- status: response.status,
52
- statusText: response.statusText,
53
- }),
54
- };
46
+ return { success: false, output: t('error.http', { status: response.status, statusText: response.statusText }) };
55
47
  }
56
- const contentType = response.headers.get("content-type") || "";
48
+ const contentType = response.headers.get('content-type') || '';
57
49
  const text = await response.text();
58
- const cleaned = contentType.includes("html") ? stripHtml(text) : text;
50
+ const cleaned = contentType.includes('html') ? stripHtml(text) : text;
51
+ // Log successful network request
59
52
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
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");
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 };
66
60
  }
67
- else {
68
- content = cleaned;
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)` };
69
64
  }
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
- };
65
+ return { success: true, output: cleaned || t('file.empty_page') };
89
66
  }
90
67
  catch (e) {
91
68
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), false, `Error: ${e.message}`);
92
- return {
93
- success: false,
94
- output: t("error.fetch_failed", { message: e.message }),
95
- };
69
+ return { success: false, output: t('error.fetch_failed', { message: e.message }) };
96
70
  }
97
71
  },
98
72
  };
@@ -1,24 +1,21 @@
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: {
14
- type: "number",
15
- description: "Number of results (default 5)",
16
- },
12
+ query: { type: 'string', description: 'Search query' },
13
+ numResults: { type: 'number', description: 'Number of results (default 5)' },
17
14
  },
18
- required: ["query"],
15
+ required: ['query'],
19
16
  },
20
17
  handler: async (ctx, args) => {
21
- const query = String(args.query || "");
18
+ const query = String(args.query || '');
22
19
  const numResults = Number(args.numResults) || 5;
23
20
  // Build search URL
24
21
  const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
@@ -35,44 +32,28 @@ export const webSearchTool = {
35
32
  };
36
33
  }
37
34
  try {
38
- const response = await fetch(url, {
39
- signal: AbortSignal.timeout(securityConfig?.requestTimeout || 10000),
40
- });
35
+ const response = await fetch(url, { signal: AbortSignal.timeout(securityConfig?.requestTimeout || 10000) });
41
36
  const html = await response.text();
42
37
  const results = [];
43
- const snippetRegex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
38
+ const snippetRegex = /<a[^>]+class="result__a"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
44
39
  let match;
45
40
  let count = 0;
46
41
  while ((match = snippetRegex.exec(html)) !== null && count < numResults) {
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}`);
42
+ const title = match[1].replace(/<[^>]+>/g, '').trim();
43
+ const snippet = match[2].replace(/<[^>]+>/g, '').trim();
44
+ results.push(`${title}: ${snippet}`);
51
45
  count++;
52
46
  }
53
47
  // Log successful network request
54
48
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Results: ${results.length}`);
55
49
  if (results.length === 0) {
56
- return { success: true, output: t("tool.no_results", { query }) };
50
+ return { success: true, output: t('tool.no_results', { query }) };
57
51
  }
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
- };
52
+ return { success: true, output: t('tool.search_results', { query, results: results.join('\n') }) };
69
53
  }
70
54
  catch (err) {
71
55
  logNetworkRequest(ctx.sessionId, sanitizeUrl(url), false, `Error: ${err.message}`);
72
- return {
73
- success: false,
74
- output: t("error.search_failed", { message: err.message }),
75
- };
56
+ return { success: false, output: t('error.search_failed', { message: err.message }) };
76
57
  }
77
58
  },
78
59
  };
@@ -47,19 +47,15 @@ export const writeFileTool = {
47
47
  output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
48
48
  };
49
49
  }
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).
50
+ // Check content for dangerous patterns
53
51
  const content = String(args.content);
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
- }
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
+ };
63
59
  }
64
60
  const dir = dirname(resolved);
65
61
  if (!existsSync(dir)) {
@@ -77,7 +73,8 @@ export const writeFileTool = {
77
73
  // Increment file operations counter
78
74
  ctx.fileOperationsCount = currentCount + 1;
79
75
  // Log successful file write
80
- logFileWrite(ctx.sessionId, path, true, `File ${fileExists ? "updated" : "created"}`);
76
+ logFileWrite(ctx.sessionId, path, true, `File ${fileExists ? 'updated' : 'created'}`);
77
+ ctx.trackCreatedPath?.(path);
81
78
  return { success: true, output: t("file.written", { path }), diff };
82
79
  },
83
80
  };
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 `${numStr} ${pc.red("-")} ${line.content}`;
113
+ return pc.red(`${pc.bgRed(`${numStr} - ${line.content}`)}`);
114
114
  }
115
115
  else if (line.type === "add") {
116
- return `${numStr} ${pc.green("+")} ${line.content}`;
116
+ return pc.green(`${pc.bgGreen(`${numStr} + ${line.content}`)}`);
117
117
  }
118
118
  else if (line.content === "...") {
119
119
  return pc.dim(` ${" ".repeat(maxNumWidth)}...`);
@@ -127,13 +127,17 @@ 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
+ }
130
136
  const diff = buildDiff(oldLines, newLines);
131
137
  if (diff.length === 0)
132
138
  return "";
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;
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));
137
141
  const maxNumWidth = Math.max(String(maxOldNum).length, String(maxNewNum).length, 3);
138
142
  let lines = diff.map((l) => formatLine(l, maxNumWidth));
139
143
  if (lines.length > MAX_DIFF_LINES) {
@@ -149,7 +153,7 @@ export function generateNewFileDiff(content) {
149
153
  const diffLines = [];
150
154
  for (let i = 0; i < lines.length; i++) {
151
155
  const numStr = String(i + 1).padStart(maxNumWidth);
152
- diffLines.push(`${numStr} ${pc.green("+")} ${lines[i]}`);
156
+ diffLines.push(pc.green(`${pc.bgGreen(`${numStr} + ${lines[i]}`)}`));
153
157
  }
154
158
  if (diffLines.length > MAX_DIFF_LINES) {
155
159
  const truncated = diffLines.slice(0, MAX_DIFF_LINES);
@@ -164,7 +168,7 @@ export function generateDeleteDiff(content) {
164
168
  const diffLines = [];
165
169
  for (let i = 0; i < lines.length; i++) {
166
170
  const numStr = String(i + 1).padStart(maxNumWidth);
167
- diffLines.push(`${numStr} ${pc.red("-")} ${lines[i]}`);
171
+ diffLines.push(pc.red(`${pc.bgRed(`${numStr} - ${lines[i]}`)}`));
168
172
  }
169
173
  if (diffLines.length > MAX_DIFF_LINES) {
170
174
  const truncated = diffLines.slice(0, MAX_DIFF_LINES);
@@ -174,5 +178,8 @@ export function generateDeleteDiff(content) {
174
178
  return diffLines.join("\n");
175
179
  }
176
180
  export function generateMoveDiff(fromPath, toPath) {
177
- return [pc.red(` - ${fromPath}`), pc.green(` + ${toPath}`)].join("\n");
181
+ return [
182
+ pc.red(` - ${fromPath}`),
183
+ pc.green(` + ${toPath}`),
184
+ ].join("\n");
178
185
  }
@@ -1,32 +1,9 @@
1
1
  import { pc } from "./colors";
2
2
  import { FormattingStream } from "./md-formatter";
3
3
  import { Spinner } from "./spinner";
4
- import { box, divider } from "./box";
4
+ import { box } 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
- }
30
7
  export function isRichTerminal() {
31
8
  return Boolean(process.stdout.isTTY) && !process.env.CI;
32
9
  }
@@ -61,14 +38,12 @@ export class Renderer {
61
38
  out;
62
39
  err;
63
40
  width;
64
- toolStyle;
65
41
  card = null;
66
42
  constructor(opts = {}) {
67
43
  this.rich = opts.rich ?? isRichTerminal();
68
44
  this.out = opts.out ?? process.stdout;
69
45
  this.err = opts.err ?? process.stderr;
70
46
  this.width = opts.width ?? getTerminalWidth();
71
- this.toolStyle = opts.toolStyle ?? "inline";
72
47
  this.spinner = new Spinner({
73
48
  enabled: this.rich && (opts.spinner ?? true),
74
49
  stream: this.err,
@@ -86,25 +61,12 @@ export class Renderer {
86
61
  meta(chunk) {
87
62
  this.spinner.stop();
88
63
  if (this.card) {
89
- if (this.toolStyle === "inline") {
90
- this.writeInlineBody(chunk);
91
- }
92
- else {
93
- this.card.body.push(chunk);
94
- }
64
+ this.card.body.push(chunk);
95
65
  }
96
66
  else {
97
67
  this.out.write(chunk);
98
68
  }
99
69
  }
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
- }
108
70
  /** Dimmed reasoning stream (already colored by the agent). */
109
71
  reasoning(chunk) {
110
72
  this.spinner.stop();
@@ -127,41 +89,15 @@ export class Renderer {
127
89
  return;
128
90
  }
129
91
  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
- }
135
92
  this.spinner.start(`${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}`);
136
93
  }
137
- toolEnd(_tool, duration, error, ctxDelta) {
94
+ toolEnd(_tool, duration, error) {
138
95
  this.spinner.stop();
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
- }
96
+ if (!this.rich)
146
97
  return;
147
- }
148
98
  if (!this.card)
149
99
  return;
150
100
  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
- }
165
101
  const lines = [];
166
102
  const summary = summarizeArgs(args);
167
103
  if (summary)
@@ -172,7 +108,8 @@ export class Renderer {
172
108
  lines.push(line);
173
109
  }
174
110
  }
175
- lines.push(footer);
111
+ const marker = error ? pc.red("✗") : pc.green("✓");
112
+ lines.push(`${marker} ${pc.dim(`${duration}ms`)}`);
176
113
  const title = `${marker} ${friendlyTool(tool)}`;
177
114
  for (const line of box(lines, { title, width: this.width })) {
178
115
  this.out.write(`${line}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.28.17",
3
+ "version": "0.29.1",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {