micro-models-agent 0.7.9 → 0.8.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 (150) hide show
  1. package/dist/cli/commands.js +173 -0
  2. package/dist/cli/completer.js +168 -0
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/main.js +95 -0
  5. package/dist/cli/repl.js +762 -0
  6. package/dist/cli/security-commands.js +166 -0
  7. package/dist/cli/setup.js +214 -0
  8. package/dist/config/config.js +123 -0
  9. package/dist/config/defaults.js +91 -0
  10. package/dist/config/experts.js +15 -0
  11. package/dist/config/index.js +3 -0
  12. package/dist/config/security.js +187 -0
  13. package/dist/config/types.js +1 -0
  14. package/dist/core/agent.js +626 -0
  15. package/dist/core/bootstrap.js +307 -0
  16. package/dist/core/index.js +2 -0
  17. package/dist/core/prompt-builder.js +55 -0
  18. package/dist/core/types.js +1 -0
  19. package/dist/i18n/en.json +405 -0
  20. package/dist/i18n/index.js +43 -0
  21. package/dist/i18n/ru.json +405 -0
  22. package/dist/index.js +22 -0
  23. package/dist/llm/index.js +4 -0
  24. package/dist/llm/model-loader.js +78 -0
  25. package/dist/llm/openai-compat.js +277 -0
  26. package/dist/llm/orchestrator.js +194 -0
  27. package/dist/llm/provider.js +2 -0
  28. package/dist/llm/response.js +39 -0
  29. package/dist/llm/token-counter.js +37 -0
  30. package/dist/llm/types.js +1 -0
  31. package/dist/logger/app-logger.js +76 -0
  32. package/dist/logger/index.js +1 -0
  33. package/dist/migration/backup.js +45 -0
  34. package/dist/migration/detect.js +50 -0
  35. package/dist/migration/index.js +2 -0
  36. package/dist/modules/browser/actions.js +46 -0
  37. package/dist/modules/browser/cookie-store.js +24 -0
  38. package/dist/modules/browser/index.js +5 -0
  39. package/dist/modules/browser/module.js +28 -0
  40. package/dist/modules/browser/session.js +287 -0
  41. package/dist/modules/browser/snapshot.js +114 -0
  42. package/dist/modules/browser/types.js +9 -0
  43. package/dist/modules/context/history.js +15 -0
  44. package/dist/modules/context/index.js +1 -0
  45. package/dist/modules/context/manager.js +179 -0
  46. package/dist/modules/execution/auditor.js +72 -0
  47. package/dist/modules/execution/index.js +6 -0
  48. package/dist/modules/execution/module.js +334 -0
  49. package/dist/modules/execution/moe-executor.js +196 -0
  50. package/dist/modules/execution/plan-validator.js +153 -0
  51. package/dist/modules/execution/planner.js +35 -0
  52. package/dist/modules/execution/stuck-detector.js +113 -0
  53. package/dist/modules/execution/tracker.js +53 -0
  54. package/dist/modules/execution/types.js +1 -0
  55. package/dist/modules/execution/verifier.js +149 -0
  56. package/dist/modules/hallucination/confidence.js +47 -0
  57. package/dist/modules/hallucination/consistency.js +32 -0
  58. package/dist/modules/hallucination/detector.js +41 -0
  59. package/dist/modules/hallucination/factual.js +128 -0
  60. package/dist/modules/hallucination/index.js +4 -0
  61. package/dist/modules/index.js +5 -0
  62. package/dist/modules/indexer/cache.js +38 -0
  63. package/dist/modules/indexer/index.js +3 -0
  64. package/dist/modules/indexer/module.js +192 -0
  65. package/dist/modules/indexer/walker.js +101 -0
  66. package/dist/modules/mcp/client.js +393 -0
  67. package/dist/modules/mcp/index.js +3 -0
  68. package/dist/modules/mcp/module.js +146 -0
  69. package/dist/modules/mcp/registry.js +15 -0
  70. package/dist/modules/memory/index.js +1 -0
  71. package/dist/modules/memory/search.js +26 -0
  72. package/dist/modules/memory/store.js +38 -0
  73. package/dist/modules/pipelines/engine.js +60 -0
  74. package/dist/modules/pipelines/index.js +3 -0
  75. package/dist/modules/pipelines/parser.js +53 -0
  76. package/dist/modules/pipelines/template.js +14 -0
  77. package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
  78. package/dist/modules/plugins/builtin/notify.js +8 -0
  79. package/dist/modules/plugins/index.js +1 -0
  80. package/dist/modules/plugins/loader.js +28 -0
  81. package/dist/modules/plugins/manager.js +161 -0
  82. package/dist/modules/plugins/types.js +1 -0
  83. package/dist/modules/registry.js +45 -0
  84. package/dist/modules/security/audit-log.js +108 -0
  85. package/dist/modules/security/audit-notifier.js +292 -0
  86. package/dist/modules/security/command-validator.js +91 -0
  87. package/dist/modules/security/content-scanner.js +52 -0
  88. package/dist/modules/security/data-sanitizer.js +97 -0
  89. package/dist/modules/security/encryption.js +218 -0
  90. package/dist/modules/security/index.js +14 -0
  91. package/dist/modules/security/network-validator.js +79 -0
  92. package/dist/modules/security/path-validator.js +155 -0
  93. package/dist/modules/security/rate-limiter.js +119 -0
  94. package/dist/modules/security/security-policies.js +393 -0
  95. package/dist/modules/security/session-encryption.js +193 -0
  96. package/dist/modules/security/session-isolation.js +95 -0
  97. package/dist/modules/session/index.js +3 -0
  98. package/dist/modules/session/manager.js +167 -0
  99. package/dist/modules/session/module.js +28 -0
  100. package/dist/modules/session/store.js +174 -0
  101. package/dist/modules/session/types.js +1 -0
  102. package/dist/modules/skills/index.js +3 -0
  103. package/dist/modules/skills/loader.js +72 -0
  104. package/dist/modules/skills/matcher.js +27 -0
  105. package/dist/modules/skills/module.js +180 -0
  106. package/dist/modules/types.js +1 -0
  107. package/dist/modules/updater/checker.js +32 -0
  108. package/dist/modules/updater/index.js +1 -0
  109. package/dist/modules/user-profile/compressor.js +16 -0
  110. package/dist/modules/user-profile/index.js +1 -0
  111. package/dist/modules/user-profile/profile.js +68 -0
  112. package/dist/tools/approve.js +32 -0
  113. package/dist/tools/bash.js +77 -0
  114. package/dist/tools/browser.js +97 -0
  115. package/dist/tools/create-dir.js +57 -0
  116. package/dist/tools/delete-file.js +64 -0
  117. package/dist/tools/edit-file.js +78 -0
  118. package/dist/tools/executor.js +83 -0
  119. package/dist/tools/file-info.js +46 -0
  120. package/dist/tools/filter-tools.js +10 -0
  121. package/dist/tools/glob-tool.js +19 -0
  122. package/dist/tools/grep-tool.js +51 -0
  123. package/dist/tools/index.js +44 -0
  124. package/dist/tools/list-dir.js +40 -0
  125. package/dist/tools/load-skill.js +48 -0
  126. package/dist/tools/mcp-call.js +68 -0
  127. package/dist/tools/move-file.js +84 -0
  128. package/dist/tools/pipeline-run.js +39 -0
  129. package/dist/tools/question.js +142 -0
  130. package/dist/tools/read-file.js +65 -0
  131. package/dist/tools/registry.js +36 -0
  132. package/dist/tools/scope-check.js +30 -0
  133. package/dist/tools/search-history.js +64 -0
  134. package/dist/tools/subagent.js +130 -0
  135. package/dist/tools/types.js +1 -0
  136. package/dist/tools/user-input.js +123 -0
  137. package/dist/tools/web-browse.js +51 -0
  138. package/dist/tools/web-fetch.js +62 -0
  139. package/dist/tools/web-search.js +59 -0
  140. package/dist/tools/write-file.js +80 -0
  141. package/dist/ui/box.js +81 -0
  142. package/dist/ui/colors.js +4 -0
  143. package/dist/ui/diff.js +185 -0
  144. package/dist/ui/index.js +6 -0
  145. package/dist/ui/md-formatter.js +212 -0
  146. package/dist/ui/output.js +13 -0
  147. package/dist/ui/renderer.js +141 -0
  148. package/dist/ui/spinner.js +70 -0
  149. package/dist/ui/table.js +144 -0
  150. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ import { pc } from "./colors";
2
+ const CONTEXT_LINES = 3;
3
+ const MAX_DIFF_LINES = 100;
4
+ function computeLCS(oldLines, newLines) {
5
+ const m = oldLines.length;
6
+ const n = newLines.length;
7
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
8
+ for (let i = 1; i <= m; i++) {
9
+ for (let j = 1; j <= n; j++) {
10
+ if (oldLines[i - 1] === newLines[j - 1]) {
11
+ dp[i][j] = dp[i - 1][j - 1] + 1;
12
+ }
13
+ else {
14
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
15
+ }
16
+ }
17
+ }
18
+ return dp;
19
+ }
20
+ function buildDiff(oldLines, newLines) {
21
+ const dp = computeLCS(oldLines, newLines);
22
+ const result = [];
23
+ let i = oldLines.length;
24
+ let j = newLines.length;
25
+ const raw = [];
26
+ while (i > 0 && j > 0) {
27
+ if (oldLines[i - 1] === newLines[j - 1]) {
28
+ raw.unshift({
29
+ type: "context",
30
+ oldNum: i,
31
+ newNum: j,
32
+ content: oldLines[i - 1],
33
+ });
34
+ i--;
35
+ j--;
36
+ }
37
+ else if (dp[i - 1][j] > dp[i][j - 1]) {
38
+ raw.unshift({
39
+ type: "remove",
40
+ oldNum: i,
41
+ newNum: 0,
42
+ content: oldLines[i - 1],
43
+ });
44
+ i--;
45
+ }
46
+ else {
47
+ raw.unshift({
48
+ type: "add",
49
+ oldNum: 0,
50
+ newNum: j,
51
+ content: newLines[j - 1],
52
+ });
53
+ j--;
54
+ }
55
+ }
56
+ while (i > 0) {
57
+ raw.unshift({
58
+ type: "remove",
59
+ oldNum: i,
60
+ newNum: 0,
61
+ content: oldLines[i - 1],
62
+ });
63
+ i--;
64
+ }
65
+ while (j > 0) {
66
+ raw.unshift({
67
+ type: "add",
68
+ oldNum: 0,
69
+ newNum: j,
70
+ content: newLines[j - 1],
71
+ });
72
+ j--;
73
+ }
74
+ const changed = new Set();
75
+ for (let k = 0; k < raw.length; k++) {
76
+ if (raw[k].type !== "context") {
77
+ changed.add(k);
78
+ }
79
+ }
80
+ const included = new Set();
81
+ for (const idx of changed) {
82
+ for (let k = Math.max(0, idx - CONTEXT_LINES); k <= Math.min(raw.length - 1, idx + CONTEXT_LINES); k++) {
83
+ included.add(k);
84
+ }
85
+ }
86
+ let lastIncluded = -1;
87
+ for (let k = 0; k < raw.length; k++) {
88
+ if (included.has(k)) {
89
+ if (lastIncluded >= 0 && k > lastIncluded + 1) {
90
+ result.push({
91
+ type: "context",
92
+ oldNum: null,
93
+ newNum: null,
94
+ content: "...",
95
+ });
96
+ }
97
+ const line = raw[k];
98
+ result.push({
99
+ type: line.type,
100
+ oldNum: line.type === "add" ? null : line.oldNum,
101
+ newNum: line.type === "remove" ? null : line.newNum,
102
+ content: line.content,
103
+ });
104
+ lastIncluded = k;
105
+ }
106
+ }
107
+ return result;
108
+ }
109
+ function formatLine(line, maxNumWidth) {
110
+ const num = line.type === "remove" ? line.oldNum : line.newNum;
111
+ const numStr = num !== null ? String(num).padStart(maxNumWidth) : " ".repeat(maxNumWidth);
112
+ if (line.type === "remove") {
113
+ return pc.red(`${pc.bgRed(`${numStr} - ${line.content}`)}`);
114
+ }
115
+ else if (line.type === "add") {
116
+ return pc.green(`${pc.bgGreen(`${numStr} + ${line.content}`)}`);
117
+ }
118
+ else if (line.content === "...") {
119
+ return pc.dim(` ${" ".repeat(maxNumWidth)}...`);
120
+ }
121
+ else {
122
+ return ` ${numStr} ${line.content}`;
123
+ }
124
+ }
125
+ export function generateDiff(oldContent, newContent) {
126
+ if (oldContent === newContent)
127
+ return "";
128
+ const oldLines = oldContent.split("\n");
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
+ const diff = buildDiff(oldLines, newLines);
137
+ if (diff.length === 0)
138
+ 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));
141
+ const maxNumWidth = Math.max(String(maxOldNum).length, String(maxNewNum).length, 3);
142
+ let lines = diff.map((l) => formatLine(l, maxNumWidth));
143
+ if (lines.length > MAX_DIFF_LINES) {
144
+ const truncated = lines.slice(0, MAX_DIFF_LINES);
145
+ truncated.push(pc.dim(` ... (${lines.length - MAX_DIFF_LINES} more lines)`));
146
+ lines = truncated;
147
+ }
148
+ return lines.join("\n");
149
+ }
150
+ export function generateNewFileDiff(content) {
151
+ const lines = content.split("\n");
152
+ const maxNumWidth = Math.max(String(lines.length).length, 3);
153
+ const diffLines = [];
154
+ for (let i = 0; i < lines.length; i++) {
155
+ const numStr = String(i + 1).padStart(maxNumWidth);
156
+ diffLines.push(pc.green(`${pc.bgGreen(`${numStr} + ${lines[i]}`)}`));
157
+ }
158
+ if (diffLines.length > MAX_DIFF_LINES) {
159
+ const truncated = diffLines.slice(0, MAX_DIFF_LINES);
160
+ truncated.push(pc.dim(` ... (${diffLines.length - MAX_DIFF_LINES} more lines)`));
161
+ return truncated.join("\n");
162
+ }
163
+ return diffLines.join("\n");
164
+ }
165
+ export function generateDeleteDiff(content) {
166
+ const lines = content.split("\n");
167
+ const maxNumWidth = Math.max(String(lines.length).length, 3);
168
+ const diffLines = [];
169
+ for (let i = 0; i < lines.length; i++) {
170
+ const numStr = String(i + 1).padStart(maxNumWidth);
171
+ diffLines.push(pc.red(`${pc.bgRed(`${numStr} - ${lines[i]}`)}`));
172
+ }
173
+ if (diffLines.length > MAX_DIFF_LINES) {
174
+ const truncated = diffLines.slice(0, MAX_DIFF_LINES);
175
+ truncated.push(pc.dim(` ... (${diffLines.length - MAX_DIFF_LINES} more lines)`));
176
+ return truncated.join("\n");
177
+ }
178
+ return diffLines.join("\n");
179
+ }
180
+ export function generateMoveDiff(fromPath, toPath) {
181
+ return [
182
+ pc.red(` - ${fromPath}`),
183
+ pc.green(` + ${toPath}`),
184
+ ].join("\n");
185
+ }
@@ -0,0 +1,6 @@
1
+ export { formatMarkdown, formatError, formatSuccess, formatWarning, FormattingStream } from './md-formatter';
2
+ export { writeOutput, writeError, writeSuccess, writeWarning } from './output';
3
+ export { formatTable, splitRow, isSeparatorRow, getTerminalWidth } from './table';
4
+ export { box, divider, wrapText } from './box';
5
+ export { Spinner, isSpinnerSupported } from './spinner';
6
+ export { Renderer, isRichTerminal, summarizeArgs } from './renderer';
@@ -0,0 +1,212 @@
1
+ import { pc } from './colors';
2
+ import { t } from '../i18n/index';
3
+ import { formatTable, getTerminalWidth } from './table';
4
+ const KW = /\b(import|export|from|const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|class|extends|implements|interface|type|enum|namespace|module|declare|abstract|async|await|yield|throw|try|catch|finally|typeof|instanceof|in|of|as|is|keyof|readonly|static|private|public|protected|get|set|constructor|this|super|void|never|any|unknown|boolean|string|number|symbol|object|true|false|null|undefined|default)\b/g;
5
+ const STRING = /('[^']*'|"[^"]*"|`[^`]*`)/g;
6
+ const COMMENT = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/|#[^\n]*)/g;
7
+ const NUMBER = /\b(\d+\.?\d*)\b/g;
8
+ const TAG = /<\/?([A-Z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9-]*)/g;
9
+ const ATTR = /\b([a-zA-Z-]+=)"([^"]*)"/g;
10
+ function highlight(code, lang) {
11
+ if (lang === 'html' || lang === 'xml' || lang === 'svg') {
12
+ code = code.replace(COMMENT, (m) => pc.dim(pc.green(m)));
13
+ code = code.replace(ATTR, (_m, attr, val) => pc.yellow(attr) + '=' + pc.green('"' + val + '"'));
14
+ code = code.replace(TAG, (_m, tag) => '<' + pc.cyan(tag));
15
+ }
16
+ else if (lang === 'css' || lang === 'scss' || lang === 'less') {
17
+ code = code.replace(COMMENT, (m) => pc.dim(pc.green(m)));
18
+ code = code.replace(STRING, (m) => pc.green(m));
19
+ code = code.replace(NUMBER, (m) => pc.yellow(m));
20
+ }
21
+ else if (lang === 'bash' || lang === 'sh' || lang === 'shell' || lang === 'zsh') {
22
+ code = code.replace(COMMENT, (m) => pc.dim(pc.green(m)));
23
+ code = code.replace(STRING, (m) => pc.green(m));
24
+ }
25
+ else if (lang === 'json') {
26
+ code = code.replace(STRING, (m) => pc.green(m));
27
+ code = code.replace(NUMBER, (m) => pc.yellow(m));
28
+ }
29
+ else {
30
+ // js, ts, jsx, tsx, and fallback
31
+ code = code.replace(COMMENT, (m) => pc.dim(pc.green(m)));
32
+ code = code.replace(STRING, (m) => pc.green(m));
33
+ code = code.replace(KW, (m) => pc.magenta(m));
34
+ code = code.replace(NUMBER, (m) => pc.yellow(m));
35
+ }
36
+ return code;
37
+ }
38
+ function isTableRow(line) {
39
+ const trimmed = line.trim();
40
+ if (!trimmed.startsWith('|'))
41
+ return false;
42
+ const pipes = trimmed.match(/\|/g);
43
+ return (pipes?.length ?? 0) >= 2;
44
+ }
45
+ function isHorizontalRule(line) {
46
+ return /^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim());
47
+ }
48
+ export class FormattingStream {
49
+ buffer = '';
50
+ inCodeBlock = false;
51
+ codeLang = '';
52
+ codeLines = [];
53
+ tableBuffer = [];
54
+ onWrite;
55
+ width;
56
+ constructor(onWrite, width) {
57
+ this.onWrite = onWrite;
58
+ this.width = width ?? getTerminalWidth();
59
+ }
60
+ write(chunk) {
61
+ this.buffer += chunk;
62
+ let idx;
63
+ while ((idx = this.buffer.indexOf('\n')) !== -1) {
64
+ const line = this.buffer.slice(0, idx);
65
+ this.buffer = this.buffer.slice(idx + 1);
66
+ this.processLine(line);
67
+ }
68
+ }
69
+ flush() {
70
+ if (this.inCodeBlock && this.codeLines.length > 0) {
71
+ this.emitCodeBlock();
72
+ }
73
+ if (this.tableBuffer.length > 0) {
74
+ this.flushTable();
75
+ }
76
+ if (this.buffer.length > 0) {
77
+ const line = this.buffer;
78
+ this.buffer = '';
79
+ if (line.trim() === '```' || line.trim().startsWith('```')) {
80
+ if (this.codeLines.length > 0) {
81
+ this.emitCodeBlock();
82
+ }
83
+ return;
84
+ }
85
+ this.onWrite(this.formatLine(line));
86
+ }
87
+ }
88
+ processLine(line) {
89
+ if (line.trim() === '```' || line.trim().startsWith('```')) {
90
+ if (this.inCodeBlock) {
91
+ this.emitCodeBlock();
92
+ }
93
+ else {
94
+ this.inCodeBlock = true;
95
+ this.codeLang = line.trim().slice(3).trim();
96
+ this.codeLines = [];
97
+ }
98
+ return;
99
+ }
100
+ if (this.inCodeBlock) {
101
+ this.codeLines.push(line);
102
+ return;
103
+ }
104
+ if (isTableRow(line)) {
105
+ this.tableBuffer.push(line);
106
+ return;
107
+ }
108
+ if (this.tableBuffer.length > 0) {
109
+ this.flushTable();
110
+ }
111
+ this.onWrite(this.formatLine(line));
112
+ }
113
+ flushTable() {
114
+ const lines = this.tableBuffer;
115
+ this.tableBuffer = [];
116
+ const hasSeparator = lines.length >= 2 && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[1].trim());
117
+ if (hasSeparator) {
118
+ for (const l of formatTable(lines, { maxWidth: this.width })) {
119
+ this.onWrite(l);
120
+ }
121
+ }
122
+ else {
123
+ for (const l of lines) {
124
+ this.onWrite(this.formatLine(l));
125
+ }
126
+ }
127
+ }
128
+ emitCodeBlock() {
129
+ this.inCodeBlock = false;
130
+ if (this.codeLines.length > 0) {
131
+ const code = this.codeLines.join('\n');
132
+ const highlighted = highlight(code, this.codeLang);
133
+ const lang = this.codeLang ? ` ${this.codeLang} ` : ' ';
134
+ const top = pc.dim(`┌─${lang}${'─'.repeat(Math.max(0, this.width - 3 - lang.length))}┐`);
135
+ this.onWrite(top);
136
+ for (const l of highlighted.split('\n')) {
137
+ this.onWrite(pc.dim('│ ') + l);
138
+ }
139
+ this.onWrite(pc.dim(`└${'─'.repeat(Math.max(0, this.width - 1))}┘`));
140
+ }
141
+ this.codeLines = [];
142
+ this.codeLang = '';
143
+ }
144
+ formatLine(line) {
145
+ let result = line;
146
+ const heading = result.match(/^(#{1,6})\s+(.+)$/);
147
+ if (heading) {
148
+ const level = heading[1].length;
149
+ const content = this.formatInline(heading[2]);
150
+ if (level === 1)
151
+ return pc.cyan(pc.bold(pc.underline(content)));
152
+ return pc.cyan(pc.bold(content));
153
+ }
154
+ if (isHorizontalRule(result)) {
155
+ return pc.dim('─'.repeat(Math.max(0, Math.min(this.width, 60))));
156
+ }
157
+ const quote = result.match(/^(>+)\s?(.*)$/);
158
+ if (quote) {
159
+ const depth = Math.min(quote[1].length, 4);
160
+ const marker = pc.dim('▍'.repeat(depth));
161
+ return `${marker} ${this.formatInline(quote[2])}`;
162
+ }
163
+ const checkbox = result.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
164
+ if (checkbox) {
165
+ const checked = checkbox[1].toLowerCase() === 'x';
166
+ const mark = checked ? pc.green('✓') : pc.dim('☐');
167
+ return ` ${mark} ${this.formatInline(checkbox[2])}`;
168
+ }
169
+ const ordered = result.match(/^(\d+)[.)]\s+(.+)$/);
170
+ if (ordered) {
171
+ return ` ${pc.yellow(ordered[1])}. ${this.formatInline(ordered[2])}`;
172
+ }
173
+ const bullet = result.match(/^[-*]\s+(.+)$/);
174
+ if (bullet) {
175
+ return ` ${pc.dim('•')} ${this.formatInline(bullet[1])}`;
176
+ }
177
+ return this.formatInline(result);
178
+ }
179
+ formatInline(text) {
180
+ const codeSpans = [];
181
+ let result = text.replace(/`([^`]+)`/g, (_m, c) => {
182
+ codeSpans.push(pc.yellow(c));
183
+ return `\u0000${codeSpans.length - 1}\u0000`;
184
+ });
185
+ result = result.replace(/\*\*(.+?)\*\*/g, (_, s) => pc.bold(s));
186
+ result = result.replace(/~~(.+?)~~/g, (_, s) => pc.strikethrough(s));
187
+ result = result.replace(/\*([^*]+)\*/g, (_, s) => pc.italic(s));
188
+ result = result.replace(/(^|\s)_([^_\n]+)_(?=\s|$)/g, (_m, pre, s) => pre + pc.italic(s));
189
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
190
+ const short = url.length > 80 ? url.slice(0, 77) + '…' : url;
191
+ return `${pc.cyan(label)} ${pc.dim(`(${short})`)}`;
192
+ });
193
+ result = result.replace(/\u0000(\d+)\u0000/g, (_m, i) => codeSpans[Number(i)]);
194
+ return result;
195
+ }
196
+ }
197
+ export function formatMarkdown(text) {
198
+ const lines = [];
199
+ const stream = new FormattingStream((l) => lines.push(l));
200
+ stream.write(text);
201
+ stream.flush();
202
+ return lines.join('\n');
203
+ }
204
+ export function formatError(text) {
205
+ return pc.red(pc.bold(t('ui.error_prefix')) + text);
206
+ }
207
+ export function formatSuccess(text) {
208
+ return pc.green(pc.bold(t('ui.success_prefix')) + text);
209
+ }
210
+ export function formatWarning(text) {
211
+ return pc.yellow(pc.bold(t('ui.warning_prefix')) + text);
212
+ }
@@ -0,0 +1,13 @@
1
+ import { formatMarkdown, formatError, formatSuccess, formatWarning } from './md-formatter';
2
+ export function writeOutput(text) {
3
+ process.stdout.write(formatMarkdown(text) + '\n');
4
+ }
5
+ export function writeError(text) {
6
+ process.stderr.write(formatError(text) + '\n');
7
+ }
8
+ export function writeSuccess(text) {
9
+ process.stdout.write(formatSuccess(text) + '\n');
10
+ }
11
+ export function writeWarning(text) {
12
+ process.stdout.write(formatWarning(text) + '\n');
13
+ }
@@ -0,0 +1,141 @@
1
+ import { pc } from "./colors";
2
+ import { FormattingStream } from "./md-formatter";
3
+ import { Spinner } from "./spinner";
4
+ import { box } from "./box";
5
+ import { getTerminalWidth } from "./table";
6
+ import { t } from "../i18n/index";
7
+ export function isRichTerminal() {
8
+ return Boolean(process.stdout.isTTY) && !process.env.CI;
9
+ }
10
+ /** Short human-friendly summary of tool arguments. */
11
+ export function summarizeArgs(args) {
12
+ const preferred = ["path", "file", "query", "url", "command", "name"];
13
+ for (const key of preferred) {
14
+ const value = args[key];
15
+ if (typeof value === "string" && value) {
16
+ return value.length > 60 ? `${value.slice(0, 57)}…` : value;
17
+ }
18
+ }
19
+ const serialized = JSON.stringify(args);
20
+ if (!serialized || serialized === "{}")
21
+ return "";
22
+ return serialized.length > 60 ? `${serialized.slice(0, 57)}…` : serialized;
23
+ }
24
+ function friendlyTool(tool) {
25
+ const key = `tool.friendly.${tool}`;
26
+ const value = t(key);
27
+ return value !== key ? value : tool;
28
+ }
29
+ /**
30
+ * Streams agent events to the terminal. Rich mode (TTY) adds a spinner
31
+ * and boxed tool cards; plain mode falls back to simple prefixed lines.
32
+ * Colors come from `colors.ts`, disabled outside a TTY, in CI, or on NO_COLOR.
33
+ */
34
+ export class Renderer {
35
+ rich;
36
+ spinner;
37
+ fmt;
38
+ out;
39
+ err;
40
+ width;
41
+ card = null;
42
+ constructor(opts = {}) {
43
+ this.rich = opts.rich ?? isRichTerminal();
44
+ this.out = opts.out ?? process.stdout;
45
+ this.err = opts.err ?? process.stderr;
46
+ this.width = opts.width ?? getTerminalWidth();
47
+ this.spinner = new Spinner({
48
+ enabled: this.rich && (opts.spinner ?? true),
49
+ stream: this.err,
50
+ width: this.width,
51
+ });
52
+ this.fmt = new FormattingStream((line) => this.out.write(`${line}\n`), this.width);
53
+ }
54
+ /** Final answer streamed from the model (markdown-formatted). */
55
+ text(chunk) {
56
+ this.endCard();
57
+ this.spinner.stop();
58
+ this.fmt.write(chunk);
59
+ }
60
+ /** Arbitrary status/meta text (tool output, reasoning, MoE status). */
61
+ meta(chunk) {
62
+ this.spinner.stop();
63
+ if (this.card) {
64
+ this.card.body.push(chunk);
65
+ }
66
+ else {
67
+ this.out.write(chunk);
68
+ }
69
+ }
70
+ /** Dimmed reasoning stream (already colored by the agent). */
71
+ reasoning(chunk) {
72
+ this.spinner.stop();
73
+ this.out.write(pc.dim(chunk));
74
+ }
75
+ /** Model is loading / generating — show the thinking spinner. */
76
+ thinkingStart() {
77
+ this.spinner.start(t("ui.thinking"));
78
+ }
79
+ /** LLM call finished; clear the thinking spinner. */
80
+ thinkingEnd() {
81
+ this.spinner.stop();
82
+ }
83
+ toolStart(tool, args) {
84
+ this.endCard();
85
+ this.spinner.stop();
86
+ const summary = summarizeArgs(args);
87
+ if (!this.rich) {
88
+ this.out.write(`\n${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}\n`);
89
+ return;
90
+ }
91
+ this.card = { tool, args, body: [], start: Date.now() };
92
+ this.spinner.start(`${pc.dim("⚙")} ${friendlyTool(tool)}${summary ? ` ${pc.dim(summary)}` : ""}`);
93
+ }
94
+ toolEnd(_tool, duration, error) {
95
+ this.spinner.stop();
96
+ if (!this.rich)
97
+ return;
98
+ if (!this.card)
99
+ return;
100
+ const { tool, args, body } = this.card;
101
+ const lines = [];
102
+ const summary = summarizeArgs(args);
103
+ if (summary)
104
+ lines.push(pc.dim(summary));
105
+ for (const chunk of body) {
106
+ for (const line of chunk.split("\n")) {
107
+ if (line.trim() !== "")
108
+ lines.push(line);
109
+ }
110
+ }
111
+ const marker = error ? pc.red("✗") : pc.green("✓");
112
+ lines.push(`${marker} ${pc.dim(`${duration}ms`)}`);
113
+ const title = `${marker} ${friendlyTool(tool)}`;
114
+ for (const line of box(lines, { title, width: this.width })) {
115
+ this.out.write(`${line}\n`);
116
+ }
117
+ this.card = null;
118
+ }
119
+ /** Raw pre-formatted text (diff blocks, warnings). */
120
+ raw(text) {
121
+ this.endCard();
122
+ this.spinner.stop();
123
+ this.out.write(text);
124
+ }
125
+ error(text) {
126
+ this.endCard();
127
+ this.spinner.stop();
128
+ this.err.write(`${pc.red(text)}\n`);
129
+ }
130
+ flush() {
131
+ this.endCard();
132
+ this.spinner.stop();
133
+ this.fmt.flush();
134
+ }
135
+ endCard() {
136
+ if (!this.card)
137
+ return;
138
+ const { tool, start } = this.card;
139
+ this.toolEnd(tool, Date.now() - start, true);
140
+ }
141
+ }
@@ -0,0 +1,70 @@
1
+ import { pc } from "./colors";
2
+ import stringWidth from "string-width";
3
+ import { getTerminalWidth } from "./table";
4
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5
+ export function isSpinnerSupported(stream) {
6
+ const s = stream ?? process.stderr;
7
+ return Boolean(s.isTTY) && !process.env.CI;
8
+ }
9
+ /**
10
+ * Zero-dependency ANSI spinner. Writes to stderr using a single
11
+ * overwritten line (`\r` + text + clear-to-end). Disabled automatically
12
+ * when the target stream is not a TTY.
13
+ */
14
+ export class Spinner {
15
+ timer = null;
16
+ frame = 0;
17
+ message = "";
18
+ enabled;
19
+ stream;
20
+ intervalMs;
21
+ width;
22
+ constructor(opts = {}) {
23
+ this.stream = opts.stream ?? process.stderr;
24
+ this.enabled = opts.enabled ?? isSpinnerSupported(this.stream);
25
+ this.intervalMs = opts.intervalMs ?? 80;
26
+ this.width = opts.width ?? getTerminalWidth();
27
+ }
28
+ get running() {
29
+ return this.timer !== null;
30
+ }
31
+ start(message) {
32
+ if (!this.enabled)
33
+ return;
34
+ this.stop();
35
+ this.message = this.truncate(message);
36
+ this.frame = 0;
37
+ this.timer = setInterval(() => this.tick(), this.intervalMs);
38
+ this.tick();
39
+ }
40
+ setMessage(message) {
41
+ this.message = this.truncate(message);
42
+ if (this.timer)
43
+ this.tick();
44
+ }
45
+ stop() {
46
+ if (this.timer) {
47
+ clearInterval(this.timer);
48
+ this.timer = null;
49
+ this.stream.write("\r\x1b[K");
50
+ }
51
+ }
52
+ truncate(text) {
53
+ if (stringWidth(text) <= this.width - 2)
54
+ return text;
55
+ let acc = "";
56
+ for (const ch of text) {
57
+ if (stringWidth(acc + ch) > this.width - 3)
58
+ break;
59
+ acc += ch;
60
+ }
61
+ return acc + "…";
62
+ }
63
+ tick() {
64
+ if (!this.timer)
65
+ return;
66
+ const frame = FRAMES[this.frame % FRAMES.length];
67
+ this.frame++;
68
+ this.stream.write("\r" + pc.cyan(frame) + " " + this.message + "\x1b[K");
69
+ }
70
+ }