micro-models-agent 0.63.3 → 1.1.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 (185) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +7 -4
  41. package/dist/i18n/ru.json +7 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1606 -800
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/driver.js +46 -4
  59. package/dist/modules/certification/cli.js +85 -42
  60. package/dist/modules/certification/loader.js +15 -1
  61. package/dist/modules/certification/manifest.js +126 -15
  62. package/dist/modules/certification/runner.js +4 -26
  63. package/dist/modules/certification/scenarios.js +184 -5
  64. package/dist/modules/certification/syntax-scenarios.js +51 -0
  65. package/dist/modules/context/chunk-query.js +25 -5
  66. package/dist/modules/context/fact-extractor.js +6 -2
  67. package/dist/modules/context/manager.js +23 -7
  68. package/dist/modules/execution/audit-runners.js +7 -1
  69. package/dist/modules/execution/auditor.js +3 -3
  70. package/dist/modules/execution/execution-plugin.js +22 -15
  71. package/dist/modules/execution/input-from.js +46 -0
  72. package/dist/modules/execution/module.js +107 -18
  73. package/dist/modules/execution/moe-executor.js +166 -54
  74. package/dist/modules/execution/plan-actions.js +524 -0
  75. package/dist/modules/execution/plan-steps.js +23 -0
  76. package/dist/modules/execution/plan-store.js +15 -3
  77. package/dist/modules/execution/plan-tool.js +6 -488
  78. package/dist/modules/execution/plan-validator.js +24 -0
  79. package/dist/modules/execution/stuck-detector.js +3 -18
  80. package/dist/modules/execution/tracker.js +14 -5
  81. package/dist/modules/execution/transient-error.js +30 -0
  82. package/dist/modules/execution/verifier.js +94 -7
  83. package/dist/modules/execution/windows-commands.js +11 -0
  84. package/dist/modules/hallucination/confidence.js +36 -23
  85. package/dist/modules/hallucination/consistency.js +3 -0
  86. package/dist/modules/hallucination/detector.js +8 -3
  87. package/dist/modules/hallucination/factual.js +26 -7
  88. package/dist/modules/hallucination/llm-judge.js +12 -2
  89. package/dist/modules/indexer/map-command.js +35 -0
  90. package/dist/modules/indexer/map-select.js +87 -0
  91. package/dist/modules/indexer/module.js +34 -22
  92. package/dist/modules/indexer/symbols.js +189 -0
  93. package/dist/modules/indexer/walker.js +96 -42
  94. package/dist/modules/lsp/check-tool.js +2 -1
  95. package/dist/modules/lsp/client.js +49 -32
  96. package/dist/modules/lsp/config.js +55 -2
  97. package/dist/modules/lsp/module.js +38 -5
  98. package/dist/modules/lsp/probe.js +4 -3
  99. package/dist/modules/lsp/project-root.js +41 -1
  100. package/dist/modules/lsp/startup-check.js +12 -4
  101. package/dist/modules/mcp/client.js +153 -104
  102. package/dist/modules/mcp/module.js +165 -41
  103. package/dist/modules/memory/module.js +4 -3
  104. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  105. package/dist/modules/plugins/manager.js +47 -84
  106. package/dist/modules/pricing/index.js +17 -7
  107. package/dist/modules/pricing/prices.js +30 -12
  108. package/dist/modules/processes/index.js +1 -0
  109. package/dist/modules/processes/kill-tree.js +56 -0
  110. package/dist/modules/processes/registry.js +2 -54
  111. package/dist/modules/providers/cache.js +23 -0
  112. package/dist/modules/providers/factory.js +28 -0
  113. package/dist/modules/providers/fallback.js +7 -5
  114. package/dist/modules/providers/health.js +2 -1
  115. package/dist/modules/providers/index.js +1 -0
  116. package/dist/modules/providers/manager.js +17 -2
  117. package/dist/modules/providers/presets.js +79 -6
  118. package/dist/modules/reasoning/policy.js +40 -0
  119. package/dist/modules/reasoning/probe.js +111 -0
  120. package/dist/modules/security/audit-notifier.js +42 -27
  121. package/dist/modules/security/command-validator.js +25 -20
  122. package/dist/modules/security/encryption.js +6 -12
  123. package/dist/modules/security/network-validator.js +76 -5
  124. package/dist/modules/security/path-validator.js +77 -34
  125. package/dist/modules/security/rate-limiter.js +11 -0
  126. package/dist/modules/security/security-policies.js +1 -1
  127. package/dist/modules/security/session-encryption.js +13 -2
  128. package/dist/modules/security/session-isolation.js +2 -9
  129. package/dist/modules/session/manager.js +11 -0
  130. package/dist/modules/session/module.js +11 -3
  131. package/dist/modules/session/store.js +41 -5
  132. package/dist/modules/skills/loader.js +7 -1
  133. package/dist/modules/skills/module.js +2 -1
  134. package/dist/modules/updater/changelog-reader.js +94 -0
  135. package/dist/modules/updater/dev-detect.js +17 -0
  136. package/dist/modules/updater/index.js +1 -0
  137. package/dist/modules/updater/module.js +14 -3
  138. package/dist/output/bus.js +32 -0
  139. package/dist/output/channel.js +233 -0
  140. package/dist/output/format.js +14 -0
  141. package/dist/output/index.js +7 -0
  142. package/dist/output/json-sink.js +22 -0
  143. package/dist/output/machine.js +8 -0
  144. package/dist/output/session-sink.js +27 -0
  145. package/dist/output/types.js +1 -0
  146. package/dist/tools/approve.js +6 -2
  147. package/dist/tools/attach-image.js +11 -11
  148. package/dist/tools/auto-fixer.js +198 -0
  149. package/dist/tools/bash.js +142 -89
  150. package/dist/tools/chunk-query.js +10 -6
  151. package/dist/tools/download-file.js +1 -1
  152. package/dist/tools/edit-file.js +20 -2
  153. package/dist/tools/executor.js +54 -9
  154. package/dist/tools/glob-tool.js +7 -0
  155. package/dist/tools/grep-tool.js +15 -1
  156. package/dist/tools/index.js +3 -1
  157. package/dist/tools/list-dir.js +3 -1
  158. package/dist/tools/load-skill.js +2 -1
  159. package/dist/tools/mcp-call.js +1 -1
  160. package/dist/tools/move-file.js +5 -4
  161. package/dist/tools/path-utils.js +7 -0
  162. package/dist/tools/pipeline-run.js +1 -1
  163. package/dist/tools/prompt-io.js +28 -0
  164. package/dist/tools/question.js +12 -12
  165. package/dist/tools/scope-request.js +91 -0
  166. package/dist/tools/session-info.js +44 -0
  167. package/dist/tools/set-thinking.js +71 -0
  168. package/dist/tools/subagent.js +50 -9
  169. package/dist/tools/syntax-validator.js +177 -0
  170. package/dist/tools/user-input.js +16 -9
  171. package/dist/tools/write-file.js +17 -1
  172. package/dist/ui/diff.js +10 -0
  173. package/dist/ui/line-editor.js +179 -26
  174. package/dist/ui/line-math.js +20 -3
  175. package/dist/ui/md-formatter.js +100 -10
  176. package/dist/ui/output.js +5 -4
  177. package/dist/ui/plan-view.js +2 -7
  178. package/dist/ui/renderer.js +89 -85
  179. package/dist/ui/spinner.js +14 -4
  180. package/dist/utils/error.js +4 -0
  181. package/dist/utils/index.js +4 -0
  182. package/dist/utils/retry.js +17 -0
  183. package/dist/utils/sleep.js +23 -0
  184. package/dist/utils/truncate.js +9 -0
  185. package/package.json +1 -1
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Language registry and symbol extraction for the project index.
3
+ *
4
+ * Adding a language = one entry in `RULES` + one or more extensions in
5
+ * `EXT_TO_LANGUAGE`. Each rule declares whether the language is source code
6
+ * (used for map ranking) and how to pull public/exported symbol names out of
7
+ * a file. Extraction is regex-based on purpose (no parser dependency): it is
8
+ * cheap, runs on every indexed file, and only needs enough structure to make
9
+ * the project map useful — not a full AST.
10
+ */
11
+ /** Cap per file so a single generated file cannot blow up the map. */
12
+ export const MAX_SYMBOLS_PER_FILE = 40;
13
+ /** Unique preserved-order names. */
14
+ function dedupe(names) {
15
+ const seen = new Set();
16
+ const out = [];
17
+ for (const name of names) {
18
+ if (name && !seen.has(name)) {
19
+ seen.add(name);
20
+ out.push(name);
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+ /** Collect captured group 1 from a list of global (multiline) regexes. */
26
+ function collect(content, patterns) {
27
+ const names = [];
28
+ for (const re of patterns) {
29
+ re.lastIndex = 0;
30
+ let match;
31
+ while ((match = re.exec(content)) !== null) {
32
+ if (match[1])
33
+ names.push(match[1]);
34
+ if (match.index === re.lastIndex)
35
+ re.lastIndex++;
36
+ }
37
+ }
38
+ return names;
39
+ }
40
+ /** Names from `export { a, b as c }` / `export type { X }` clauses. */
41
+ function collectExportClauses(content) {
42
+ const names = [];
43
+ const re = /export\s+(?:type\s+)?\{([^}]*)\}/g;
44
+ let match;
45
+ while ((match = re.exec(content)) !== null) {
46
+ for (const part of match[1].split(",")) {
47
+ const cleaned = part.trim().replace(/^type\s+/, "");
48
+ if (!cleaned || cleaned.startsWith("//"))
49
+ continue;
50
+ const alias = cleaned.split(/\s+as\s+/);
51
+ const exported = (alias.length > 1 ? alias[1] : alias[0]).trim();
52
+ if (/^\w+$/.test(exported))
53
+ names.push(exported);
54
+ }
55
+ }
56
+ return names;
57
+ }
58
+ const TS_JS_PATTERNS = [
59
+ /^export\s+(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/gm,
60
+ /^export\s+default\s+(?:class|function)\s+(\w+)/gm,
61
+ ];
62
+ function tsLike(content) {
63
+ return dedupe([...collect(content, TS_JS_PATTERNS), ...collectExportClauses(content)]);
64
+ }
65
+ const PYTHON_PATTERNS = [/^(?:async\s+)?def\s+(\w+)/gm, /^class\s+(\w+)/gm];
66
+ const RUST_PATTERNS = [
67
+ /^\s*pub(?:\([^)]*\))?\s+(?:async\s+)?(?:unsafe\s+)?(?:fn|struct|enum|trait|const|static|type|mod)\s+(\w+)/gm,
68
+ /macro_rules!\s+(\w+)/g,
69
+ ];
70
+ const GO_PATTERNS = [
71
+ /^func\s+(\w+)\s*[([]/gm,
72
+ /^func\s+\([^)]*\)\s+(\w+)\s*[([]/gm,
73
+ /^type\s+(\w+)/gm,
74
+ /^(?:var|const)\s+(\w+)/gm,
75
+ ];
76
+ const JAVA_PATTERNS = [
77
+ /^\s*(?:(?:public|protected|private|final|abstract|static|sealed|non-sealed|strictfp)\s+)*(?:class|interface|enum|record)\s+(\w+)/gm,
78
+ ];
79
+ const KOTLIN_PATTERNS = [
80
+ /^\s*(?:(?:public|internal|private|protected|open|abstract|sealed|data|enum|annotation|value|inline)\s+)*(?:class|interface|object)\s+(\w+)/gm,
81
+ /^\s*(?:(?:public|internal|private|protected|open|override|suspend|inline|operator|infix|tailrec|external)\s+)*fun\s+(\w+)/gm,
82
+ ];
83
+ const CSHARP_PATTERNS = [
84
+ /^\s*(?:(?:public|internal|private|protected|static|sealed|abstract|partial|readonly|unsafe|new|record)\s+)*(?:class|interface|struct|enum|record)\s+(\w+)/gm,
85
+ ];
86
+ const RUBY_PATTERNS = [/^\s*(?:def|class|module)\s+([A-Za-z_]\w*[?!]?)/gm];
87
+ const PHP_PATTERNS = [/\b(?:function|class|interface|trait|enum)\s+(\w+)/g];
88
+ const SWIFT_PATTERNS = [/\b(?:func|class|struct|enum|protocol|extension)\s+(\w+)/g];
89
+ const SCALA_PATTERNS = [
90
+ /^\s*(?:(?:private|protected|final|abstract|sealed|implicit|lazy|override)\s+)*(?:def|class|object|trait)\s+(\w+)/gm,
91
+ ];
92
+ const C_CPP_PATTERNS = [
93
+ /^\s*(?:typedef\s+)?(?:class|struct|enum|union|namespace)\s+(\w+)/gm,
94
+ /\}\s*(\w+)\s*;/g,
95
+ ];
96
+ const SHELL_PATTERNS = [/^\s*(?:function\s+)?(\w+)\s*\(\s*\)\s*\{/gm];
97
+ const LUA_PATTERNS = [/^\s*(?:local\s+)?function\s+([\w.:]+)/gm];
98
+ const NO_SYMBOLS = () => [];
99
+ const RULES = {
100
+ typescript: { code: true, extract: tsLike },
101
+ "typescript-react": { code: true, extract: tsLike },
102
+ javascript: { code: true, extract: tsLike },
103
+ "javascript-react": { code: true, extract: tsLike },
104
+ python: { code: true, extract: (c) => collect(c, PYTHON_PATTERNS) },
105
+ rust: { code: true, extract: (c) => collect(c, RUST_PATTERNS) },
106
+ go: { code: true, extract: (c) => collect(c, GO_PATTERNS) },
107
+ java: { code: true, extract: (c) => collect(c, JAVA_PATTERNS) },
108
+ kotlin: { code: true, extract: (c) => collect(c, KOTLIN_PATTERNS) },
109
+ csharp: { code: true, extract: (c) => collect(c, CSHARP_PATTERNS) },
110
+ ruby: { code: true, extract: (c) => collect(c, RUBY_PATTERNS) },
111
+ php: { code: true, extract: (c) => collect(c, PHP_PATTERNS) },
112
+ swift: { code: true, extract: (c) => collect(c, SWIFT_PATTERNS) },
113
+ scala: { code: true, extract: (c) => collect(c, SCALA_PATTERNS) },
114
+ c: { code: true, extract: (c) => collect(c, C_CPP_PATTERNS) },
115
+ cpp: { code: true, extract: (c) => collect(c, C_CPP_PATTERNS) },
116
+ shell: { code: true, extract: (c) => collect(c, SHELL_PATTERNS) },
117
+ lua: { code: true, extract: (c) => collect(c, LUA_PATTERNS) },
118
+ json: { code: false, extract: NO_SYMBOLS },
119
+ markdown: { code: false, extract: NO_SYMBOLS },
120
+ yaml: { code: false, extract: NO_SYMBOLS },
121
+ toml: { code: false, extract: NO_SYMBOLS },
122
+ xml: { code: false, extract: NO_SYMBOLS },
123
+ html: { code: false, extract: NO_SYMBOLS },
124
+ css: { code: false, extract: NO_SYMBOLS },
125
+ };
126
+ const EXT_TO_LANGUAGE = {
127
+ ".ts": "typescript",
128
+ ".mts": "typescript",
129
+ ".cts": "typescript",
130
+ ".tsx": "typescript-react",
131
+ ".js": "javascript",
132
+ ".mjs": "javascript",
133
+ ".cjs": "javascript",
134
+ ".jsx": "javascript-react",
135
+ ".py": "python",
136
+ ".pyi": "python",
137
+ ".rs": "rust",
138
+ ".go": "go",
139
+ ".java": "java",
140
+ ".kt": "kotlin",
141
+ ".kts": "kotlin",
142
+ ".cs": "csharp",
143
+ ".rb": "ruby",
144
+ ".php": "php",
145
+ ".swift": "swift",
146
+ ".scala": "scala",
147
+ ".c": "c",
148
+ ".h": "c",
149
+ ".cpp": "cpp",
150
+ ".cc": "cpp",
151
+ ".cxx": "cpp",
152
+ ".hpp": "cpp",
153
+ ".hxx": "cpp",
154
+ ".sh": "shell",
155
+ ".bash": "shell",
156
+ ".zsh": "shell",
157
+ ".lua": "lua",
158
+ ".json": "json",
159
+ ".md": "markdown",
160
+ ".markdown": "markdown",
161
+ ".yaml": "yaml",
162
+ ".yml": "yaml",
163
+ ".toml": "toml",
164
+ ".xml": "xml",
165
+ ".html": "html",
166
+ ".htm": "html",
167
+ ".css": "css",
168
+ ".scss": "css",
169
+ ".less": "css",
170
+ };
171
+ /** Language id for a file extension (lowercase, with dot), or null if not indexed. */
172
+ export function languageIdForExt(ext) {
173
+ return EXT_TO_LANGUAGE[ext.toLowerCase()] ?? null;
174
+ }
175
+ /** True when the language is source code (affects project-map ranking). */
176
+ export function isCodeLanguage(languageId) {
177
+ return RULES[languageId]?.code ?? false;
178
+ }
179
+ /** Extract public/exported symbol names from file content. */
180
+ export function extractSymbols(content, languageId) {
181
+ const rule = RULES[languageId];
182
+ if (!rule)
183
+ return [];
184
+ return dedupe(rule.extract(content)).slice(0, MAX_SYMBOLS_PER_FILE);
185
+ }
186
+ /** All indexed extensions (for docs/tests). */
187
+ export function indexedExtensions() {
188
+ return Object.keys(EXT_TO_LANGUAGE).sort();
189
+ }
@@ -1,19 +1,72 @@
1
- import { readdirSync, readFileSync, statSync, existsSync, watch } from "fs";
1
+ import { readdirSync, readFileSync, statSync, lstatSync, existsSync, watch } from "fs";
2
2
  import { join, relative, extname } from "path";
3
- const LANGUAGES = {
4
- ".ts": "typescript",
5
- ".tsx": "typescript-react",
6
- ".js": "javascript",
7
- ".jsx": "javascript-react",
8
- ".json": "json",
9
- ".md": "markdown",
10
- ".yaml": "yaml",
11
- ".yml": "yaml",
12
- ".py": "python",
13
- ".rs": "rust",
14
- ".go": "go",
15
- };
16
- const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", "build", ".mma", "coverage"]);
3
+ import { Logger } from "../../logger/app-logger";
4
+ import { languageIdForExt, extractSymbols } from "./symbols";
5
+ import { t } from "../../i18n/index";
6
+ const logger = new Logger("warn", "indexer");
7
+ /**
8
+ * Directories that never hold project source but would flood the index:
9
+ * VCS/build output, package caches and virtual environments across
10
+ * ecosystems (Node, Python, Rust, Go/PHP/Ruby, JVM, .NET, Apple, IDE).
11
+ * Stored lowercase — matching is case-insensitive (Windows/macOS).
12
+ */
13
+ const IGNORE_DIRS = new Set([
14
+ // VCS / MMA
15
+ ".git",
16
+ ".hg",
17
+ ".svn",
18
+ ".mma",
19
+ // Node / JS bundlers
20
+ "node_modules",
21
+ "dist",
22
+ "build",
23
+ "coverage",
24
+ ".next",
25
+ ".nuxt",
26
+ ".svelte-kit",
27
+ ".turbo",
28
+ ".parcel-cache",
29
+ ".cache",
30
+ // Python
31
+ "venv",
32
+ ".venv",
33
+ "env",
34
+ "virtualenv",
35
+ "__pycache__",
36
+ ".pytest_cache",
37
+ ".mypy_cache",
38
+ ".ruff_cache",
39
+ ".tox",
40
+ "site-packages",
41
+ // Rust
42
+ "target",
43
+ // Go / PHP / Ruby
44
+ "vendor",
45
+ ".bundle",
46
+ // JVM
47
+ ".gradle",
48
+ "out",
49
+ // .NET
50
+ "obj",
51
+ // Apple
52
+ "pods",
53
+ "deriveddata",
54
+ // IDE / editor
55
+ ".idea",
56
+ ".vscode",
57
+ ]);
58
+ /** True when a directory name is ignored (case-insensitive). */
59
+ export function isIgnoredDirName(name) {
60
+ return IGNORE_DIRS.has(name.toLowerCase());
61
+ }
62
+ /**
63
+ * True when any path SEGMENT is an ignored directory. Segment-based (not a
64
+ * substring) so short names like `env`/`out`/`obj` do not black out
65
+ * `environment.ts` or `about/`.
66
+ */
67
+ function isIgnoredPath(filename) {
68
+ return filename.split(/[\\/]/).some((segment) => isIgnoredDirName(segment));
69
+ }
17
70
  export class Indexer {
18
71
  baseDir;
19
72
  MAX_FILES = 1000;
@@ -23,7 +76,7 @@ export class Indexer {
23
76
  }
24
77
  watch(callback) {
25
78
  this.watcher = watch(this.baseDir, { recursive: true }, (event, filename) => {
26
- if (filename && !Array.from(IGNORE_DIRS).some((dir) => filename.includes(dir))) {
79
+ if (filename && !isIgnoredPath(filename)) {
27
80
  callback(event, filename);
28
81
  }
29
82
  });
@@ -53,40 +106,41 @@ export class Indexer {
53
106
  return;
54
107
  const fullPath = join(dir, entry);
55
108
  const relPath = relative(this.baseDir, fullPath);
56
- const stat = statSync(fullPath);
57
- if (stat.isDirectory()) {
58
- if (!IGNORE_DIRS.has(entry)) {
59
- walkDir(fullPath);
109
+ // Per-entry isolation: a file deleted (or unreadable) between readdir
110
+ // and stat must not kill the whole walk. Symlinks are skipped — they
111
+ // can point outside the project or create directory cycles.
112
+ try {
113
+ const lst = lstatSync(fullPath, { throwIfNoEntry: false });
114
+ if (!lst || lst.isSymbolicLink())
115
+ continue;
116
+ const stat = statSync(fullPath);
117
+ if (stat.isDirectory()) {
118
+ if (!isIgnoredDirName(entry)) {
119
+ walkDir(fullPath);
120
+ }
60
121
  }
61
- }
62
- else if (stat.isFile()) {
63
- const ext = extname(entry).toLowerCase();
64
- const language = LANGUAGES[ext];
65
- if (language) {
66
- const content = readFileSync(fullPath, "utf-8");
67
- const exports = this.extractExports(content, language);
68
- files.push({ path: relPath, language, exports, size: stat.size });
69
- totalSize += stat.size;
70
- count++;
122
+ else if (stat.isFile()) {
123
+ const ext = extname(entry).toLowerCase();
124
+ const language = languageIdForExt(ext);
125
+ if (language) {
126
+ const content = readFileSync(fullPath, "utf-8");
127
+ const exports = extractSymbols(content, language);
128
+ files.push({ path: relPath, language, exports, size: stat.size });
129
+ totalSize += stat.size;
130
+ count++;
131
+ }
71
132
  }
72
133
  }
134
+ catch (e) {
135
+ // Entry vanished or is unreadable — skip it, keep indexing the rest.
136
+ logger.warn(t("env.index_skip_unreadable", { path: relPath }), { error: String(e) });
137
+ continue;
138
+ }
73
139
  }
74
140
  };
75
141
  walkDir(this.baseDir);
76
142
  return { files, totalSize };
77
143
  }
78
- extractExports(content, language) {
79
- if (language === "typescript" || language === "javascript") {
80
- const exports = [];
81
- const exportRegex = /export\s+(?:const|function|class|interface|type|enum|default\s+(?:class|function))\s+(\w+)/g;
82
- let match;
83
- while ((match = exportRegex.exec(content)) !== null) {
84
- exports.push(match[1]);
85
- }
86
- return exports;
87
- }
88
- return [];
89
- }
90
144
  summarize(result) {
91
145
  const byLang = {};
92
146
  for (const f of result.files) {
@@ -1,6 +1,7 @@
1
1
  import { readdir, stat } from "fs/promises";
2
2
  import { resolve, relative } from "path";
3
3
  import { getServerForFile } from "./config";
4
+ import { toForwardSlash } from "../../tools/path-utils";
4
5
  /** lsp_check directory mode: walk depth and per-call file cap. */
5
6
  export const MAX_LSP_CHECK_FILES = 15;
6
7
  const MAX_LSP_WALK_DEPTH = 4;
@@ -48,7 +49,7 @@ export function formatCheckDiagnostics(items, baseDir) {
48
49
  return items
49
50
  .slice(0, 40)
50
51
  .map(({ file, diag }) => {
51
- const rel = relative(baseDir, file).replace(/\\/g, "/");
52
+ const rel = toForwardSlash(relative(baseDir, file));
52
53
  const line = diag.range.start.line + 1;
53
54
  const col = diag.range.start.character + 1;
54
55
  const sev = severityLabels[diag.severity] ?? "unknown";
@@ -3,6 +3,8 @@ import { resolve } from "path";
3
3
  import { platform } from "os";
4
4
  import { resolveSpawnCommand, buildWinCommandLine } from "./command";
5
5
  import { killTree } from "../processes/registry";
6
+ import { toForwardSlash } from "../../tools/path-utils";
7
+ import { errMsg } from "../../utils";
6
8
  /**
7
9
  * Logger adapter that writes LSP diagnostics to app.jsonl only — never to
8
10
  * console/terminal. LSP warnings are noisy (timeout on first run, npx
@@ -36,7 +38,7 @@ export class LspClient {
36
38
  process = null;
37
39
  requestId = 0;
38
40
  pending = new Map();
39
- buffer = "";
41
+ buffer = Buffer.alloc(0);
40
42
  contentLength = -1;
41
43
  diagnostics = [];
42
44
  diagnosticsResolve = null;
@@ -103,7 +105,7 @@ export class LspClient {
103
105
  await this.sendRequest("initialize", initParams, timeout);
104
106
  }
105
107
  catch (e) {
106
- const msg = e instanceof Error ? e.message : String(e);
108
+ const msg = errMsg(e);
107
109
  this.logger?.warn(`LSP initialize failed (attempt 1): ${msg}`, {
108
110
  command: config.command,
109
111
  projectRoot,
@@ -117,7 +119,7 @@ export class LspClient {
117
119
  await this.sendRequest("initialize", initParams, timeout);
118
120
  }
119
121
  catch (retryErr) {
120
- const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
122
+ const retryMsg = errMsg(retryErr);
121
123
  this.logger?.error(`LSP initialize failed (attempt 2, giving up): ${retryMsg}`, {
122
124
  command: config.command,
123
125
  projectRoot,
@@ -167,6 +169,11 @@ export class LspClient {
167
169
  stdio: ["pipe", "pipe", "pipe"],
168
170
  env: { ...process.env, ...config.env },
169
171
  cwd: projectRoot,
172
+ // POSIX: make the child a process-group leader so killTree()'s
173
+ // `kill(-pid)` reaches the whole chain (npx → node → tsserver).
174
+ // Without it the group kill fails with ESRCH and grandchildren
175
+ // survive as orphans. Windows relies on taskkill /T instead.
176
+ detached: !isWin,
170
177
  };
171
178
  if (isWin) {
172
179
  // A `.cmd` shim can't be spawned directly under Node (`spawn npx EINVAL`) — run via cmd.exe.
@@ -187,14 +194,14 @@ export class LspClient {
187
194
  setTimeout(() => {
188
195
  if (!this.initialized && this.process) {
189
196
  const msg = "LSP server start timeout";
190
- this.logger?.error(msg, { command: config.command, projectRoot, timeout: config.timeout ?? 10000 });
197
+ this.logger?.error(msg, { command: config.command, projectRoot, timeout: config.timeout ?? DEFAULT_TIMEOUT });
191
198
  reject(new Error(msg));
192
199
  }
193
- }, config.timeout ?? 10000);
200
+ }, config.timeout ?? DEFAULT_TIMEOUT);
194
201
  });
195
202
  }
196
203
  handleData(chunk) {
197
- this.buffer += chunk.toString();
204
+ this.buffer = Buffer.concat([this.buffer, chunk]);
198
205
  this.parseMessages();
199
206
  }
200
207
  parseMessages() {
@@ -203,33 +210,36 @@ export class LspClient {
203
210
  const headerEnd = this.buffer.indexOf("\r\n\r\n");
204
211
  if (headerEnd === -1)
205
212
  return;
206
- const header = this.buffer.slice(0, headerEnd);
213
+ // Headers are ASCII — decode just the header slice.
214
+ const header = this.buffer.subarray(0, headerEnd).toString("utf8");
207
215
  const match = header.match(/Content-Length: (\d+)/i);
208
216
  if (!match) {
209
- this.buffer = this.buffer.slice(headerEnd + 4);
210
- this.contentLength = -1;
217
+ this.buffer = this.buffer.subarray(headerEnd + 4);
211
218
  continue;
212
219
  }
213
220
  this.contentLength = parseInt(match[1], 10);
214
221
  // Guard against malicious/buggy servers sending absurd Content-Length
215
222
  if (this.contentLength > 10 * 1024 * 1024) {
216
- this.buffer = this.buffer.slice(headerEnd + 4);
223
+ this.buffer = this.buffer.subarray(headerEnd + 4);
217
224
  this.contentLength = -1;
218
225
  continue;
219
226
  }
220
- this.buffer = this.buffer.slice(headerEnd + 4);
227
+ this.buffer = this.buffer.subarray(headerEnd + 4);
221
228
  }
229
+ // Content-Length counts BYTES, not JS characters. Slicing a decoded
230
+ // string by character count corrupts frames containing multi-byte
231
+ // UTF-8 (e.g. Cyrillic diagnostics) — keep the remainder as raw bytes.
222
232
  if (this.buffer.length < this.contentLength)
223
233
  return;
224
- const body = this.buffer.slice(0, this.contentLength);
225
- this.buffer = this.buffer.slice(this.contentLength);
234
+ const body = this.buffer.subarray(0, this.contentLength).toString("utf8");
235
+ this.buffer = this.buffer.subarray(this.contentLength);
226
236
  this.contentLength = -1;
227
237
  try {
228
238
  const msg = JSON.parse(body);
229
239
  this.handleMessage(msg);
230
240
  }
231
241
  catch (e) {
232
- this.logger?.debug(`LSP JSON parse error: ${e instanceof Error ? e.message : String(e)}`);
242
+ this.logger?.debug(`LSP JSON parse error: ${errMsg(e)}`);
233
243
  }
234
244
  }
235
245
  }
@@ -290,7 +300,7 @@ export class LspClient {
290
300
  this.process.stdin.write(header + message);
291
301
  }
292
302
  catch (e) {
293
- this.logger?.debug(`LSP write error: ${e instanceof Error ? e.message : String(e)}`);
303
+ this.logger?.debug(`LSP write error: ${errMsg(e)}`);
294
304
  }
295
305
  }
296
306
  async shutdown() {
@@ -304,14 +314,14 @@ export class LspClient {
304
314
  await this.sendRequest("shutdown", null, 3000);
305
315
  }
306
316
  catch (e) {
307
- this.logger?.debug(`LSP shutdown request failed: ${e instanceof Error ? e.message : String(e)}`);
317
+ this.logger?.debug(`LSP shutdown request failed: ${errMsg(e)}`);
308
318
  }
309
319
  this.sendNotification("exit", null);
310
320
  await new Promise((r) => setTimeout(r, 200));
311
321
  }
312
322
  }
313
323
  catch (e) {
314
- this.logger?.debug(`LSP shutdown error: ${e instanceof Error ? e.message : String(e)}`);
324
+ this.logger?.debug(`LSP shutdown error: ${errMsg(e)}`);
315
325
  }
316
326
  finally {
317
327
  if (this.process) {
@@ -322,12 +332,12 @@ export class LspClient {
322
332
  killTree(this.process);
323
333
  }
324
334
  catch (e) {
325
- this.logger?.debug(`LSP killTree error: ${e instanceof Error ? e.message : String(e)}`);
335
+ this.logger?.debug(`LSP killTree error: ${errMsg(e)}`);
326
336
  }
327
337
  this.process = null;
328
338
  }
329
339
  this.initialized = false;
330
- this.buffer = "";
340
+ this.buffer = Buffer.alloc(0);
331
341
  this.contentLength = -1;
332
342
  if (this.diagnosticsTimer) {
333
343
  clearTimeout(this.diagnosticsTimer);
@@ -336,7 +346,7 @@ export class LspClient {
336
346
  }
337
347
  }
338
348
  pathToUri(filePath) {
339
- const normalized = filePath.replace(/\\/g, "/");
349
+ const normalized = toForwardSlash(filePath);
340
350
  return /^[a-zA-Z]:/.test(normalized) ? `file:///${normalized}` : `file://${normalized}`;
341
351
  }
342
352
  languageFromPath(filePath) {
@@ -364,23 +374,30 @@ export class LspClient {
364
374
  return map[ext ?? ""] ?? "plaintext";
365
375
  }
366
376
  /**
367
- * Extract the actual binary name from npx args.
368
- * For `npx --yes --package typescript-language-server --package typescript@5 typescript-language-server --stdio`
369
- * returns `typescript-language-server` (the last non-flag arg before `--stdio`).
377
+ * Extract the actual binary name from npx args — only for the strict layout
378
+ * `[--yes] [--package X]... <binary> --stdio`. Servers with trailing server
379
+ * arguments (e.g. angular's `ngserver --stdio --tsProbeLocations …`) return
380
+ * null and always run through npx, so their flags are never mistaken for the
381
+ * binary or stripped.
370
382
  */
371
383
  extractBinaryFromNpxArgs(args) {
372
- // Find the binary: last arg that doesn't start with -- and isn't a package version
373
- for (let i = args.length - 1; i >= 0; i--) {
374
- const arg = args[i];
375
- if (arg === "--stdio")
384
+ const stdioIdx = args.lastIndexOf("--stdio");
385
+ if (stdioIdx !== args.length - 1 || stdioIdx < 1)
386
+ return null;
387
+ const binary = args[stdioIdx - 1];
388
+ if (!binary || binary.startsWith("-") || this.isPackageVersion(binary))
389
+ return null;
390
+ for (let i = 0; i < stdioIdx - 1; i++) {
391
+ const a = args[i];
392
+ if (a === "--yes")
376
393
  continue;
377
- if (arg.startsWith("--"))
378
- return null; // unexpected format
379
- if (this.isPackageVersion(arg))
394
+ if (a === "--package") {
395
+ i++;
380
396
  continue;
381
- return arg;
397
+ }
398
+ return null;
382
399
  }
383
- return null;
400
+ return binary;
384
401
  }
385
402
  /** Check if a string looks like a package version specifier (e.g. "typescript@5"). */
386
403
  isPackageVersion(s) {
@@ -1,3 +1,5 @@
1
+ import { existsSync } from "fs";
2
+ import { dirname, join } from "path";
1
3
  /**
2
4
  * LSP server defaults.
3
5
  *
@@ -101,6 +103,25 @@ export const DEFAULT_LSP_CONFIG = {
101
103
  autoInstall: true,
102
104
  workspaceMarkers: ["package.json"],
103
105
  },
106
+ angular: {
107
+ command: "npx",
108
+ args: [
109
+ "--yes",
110
+ "--package",
111
+ "@angular/language-server",
112
+ "--package",
113
+ "typescript@5",
114
+ "ngserver",
115
+ "--stdio",
116
+ "--tsProbeLocations",
117
+ "./node_modules",
118
+ "--ngProbeLocations",
119
+ "./node_modules",
120
+ ],
121
+ timeout: 60000,
122
+ autoInstall: true,
123
+ workspaceMarkers: ["angular.json"],
124
+ },
104
125
  },
105
126
  };
106
127
  const LANGUAGE_MAP = {
@@ -127,9 +148,41 @@ export function getLanguageForFile(filePath) {
127
148
  return null;
128
149
  return LANGUAGE_MAP[ext] ?? null;
129
150
  }
130
- export function getServerForFile(filePath, config) {
151
+ const ANGULAR_MARKER = "angular.json";
152
+ const ANGULAR_LOOKUP_DEPTH = 10;
153
+ /**
154
+ * True when the file lives inside an Angular workspace (an `angular.json`
155
+ * marker exists in one of its ancestor directories, bounded lookup).
156
+ */
157
+ function isAngularProject(filePath) {
158
+ let dir = dirname(filePath);
159
+ for (let i = 0; i < ANGULAR_LOOKUP_DEPTH; i++) {
160
+ if (existsSync(join(dir, ANGULAR_MARKER)))
161
+ return true;
162
+ const parent = dirname(dir);
163
+ if (parent === dir)
164
+ return false;
165
+ dir = parent;
166
+ }
167
+ return false;
168
+ }
169
+ /**
170
+ * Config key of the LSP server for a file. `.html` routes to the `angular`
171
+ * server inside Angular workspaces (templates with bindings need the Angular
172
+ * language service) and to the generic `html` server everywhere else.
173
+ */
174
+ export function getServerKeyForFile(filePath, config) {
131
175
  const lang = getLanguageForFile(filePath);
132
176
  if (!lang)
133
177
  return null;
134
- return config.servers[lang] ?? null;
178
+ if (lang === "html" && config.servers.angular && isAngularProject(filePath)) {
179
+ return "angular";
180
+ }
181
+ return lang;
182
+ }
183
+ export function getServerForFile(filePath, config) {
184
+ const key = getServerKeyForFile(filePath, config);
185
+ if (!key)
186
+ return null;
187
+ return config.servers[key] ?? null;
135
188
  }