micro-models-agent 2.11.0 → 2.12.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 (347) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/js/tabs.js +22 -3
  3. package/dist/cli/cache-line.js +1 -0
  4. package/dist/cli/command-suggest.js +5 -4
  5. package/dist/cli/commands.js +231 -122
  6. package/dist/cli/completer.js +132 -31
  7. package/dist/cli/index.js +1 -0
  8. package/dist/cli/json-payload.js +4 -4
  9. package/dist/cli/main.js +166 -61
  10. package/dist/cli/onboarding-commands.js +126 -0
  11. package/dist/cli/plugin-commands.js +2 -1
  12. package/dist/cli/relaunch.js +16 -13
  13. package/dist/cli/repl-commands-lsp.js +75 -0
  14. package/dist/cli/repl-commands-skills.js +105 -0
  15. package/dist/cli/repl-commands.js +589 -266
  16. package/dist/cli/repl.js +436 -177
  17. package/dist/cli/run-result.js +5 -6
  18. package/dist/cli/security-commands.js +37 -28
  19. package/dist/cli/setup-order.js +4 -55
  20. package/dist/cli/setup-prompt.js +3 -2
  21. package/dist/cli/setup.js +27 -107
  22. package/dist/cli/web-command.js +98 -0
  23. package/dist/config/budget.js +5 -5
  24. package/dist/config/config.js +103 -96
  25. package/dist/config/defaults.js +38 -8
  26. package/dist/config/domains.js +24 -27
  27. package/dist/config/env.js +46 -0
  28. package/dist/config/experts.js +2 -0
  29. package/dist/config/index.js +1 -0
  30. package/dist/config/paths.js +56 -0
  31. package/dist/config/redact.js +44 -0
  32. package/dist/config/registry-extra.js +334 -0
  33. package/dist/config/registry.js +584 -0
  34. package/dist/config/security.js +35 -36
  35. package/dist/config/timeouts.js +74 -0
  36. package/dist/config/utils.js +16 -15
  37. package/dist/core/agent/artifact-progress.js +59 -0
  38. package/dist/core/agent/artifact-resolve.js +79 -0
  39. package/dist/core/agent/audit-gate.js +7 -3
  40. package/dist/core/agent/compaction.js +2 -2
  41. package/dist/core/agent/constants.js +33 -4
  42. package/dist/core/agent/context-log.js +454 -0
  43. package/dist/core/agent/context-renderer.js +5 -0
  44. package/dist/core/agent/critic-gate.js +132 -0
  45. package/dist/core/agent/hallucination-gate.js +148 -38
  46. package/dist/core/agent/loop-state.js +15 -0
  47. package/dist/core/agent/prefix-monitor.js +1 -1
  48. package/dist/core/agent/reasoning-resolver.js +10 -1
  49. package/dist/core/agent/token-tracker.js +7 -4
  50. package/dist/core/agent/tool-batch.js +93 -18
  51. package/dist/core/agent/tool-images.js +18 -0
  52. package/dist/core/agent/tool-output.js +65 -11
  53. package/dist/core/agent/tool-pairing.js +60 -0
  54. package/dist/core/agent-moe.js +25 -24
  55. package/dist/core/agent.js +429 -108
  56. package/dist/core/bootstrap/agent-phase.js +66 -0
  57. package/dist/core/bootstrap/config-phase.js +86 -0
  58. package/dist/core/bootstrap/modules-phase.js +269 -0
  59. package/dist/core/bootstrap/provider-phase.js +183 -0
  60. package/dist/core/bootstrap/session-phase.js +204 -0
  61. package/dist/core/bootstrap/state.js +94 -0
  62. package/dist/core/bootstrap/workspace-phase.js +115 -0
  63. package/dist/core/bootstrap.js +92 -660
  64. package/dist/core/crash-handler.js +17 -7
  65. package/dist/core/environment.js +37 -12
  66. package/dist/core/host-plan.js +43 -0
  67. package/dist/core/host.js +23 -0
  68. package/dist/core/index.js +1 -0
  69. package/dist/core/prompt-builder.js +17 -1
  70. package/dist/core/prompt-overflow.js +42 -43
  71. package/dist/core/session-logger.js +122 -30
  72. package/dist/core/system-prompt.js +117 -0
  73. package/dist/core/version.js +6 -4
  74. package/dist/core/web-host.js +34 -0
  75. package/dist/core/workspace.js +19 -15
  76. package/dist/i18n/index.js +37 -3
  77. package/dist/index.js +7 -3
  78. package/dist/llm/image-utils.js +55 -64
  79. package/dist/llm/llm-errors.js +9 -0
  80. package/dist/llm/model-loader.js +10 -32
  81. package/dist/llm/models-dev.js +121 -0
  82. package/dist/llm/openai-compat.js +146 -73
  83. package/dist/llm/orchestrator.js +33 -11
  84. package/dist/llm/provider-budget.js +2 -2
  85. package/dist/llm/provider-context.js +233 -0
  86. package/dist/llm/provider.js +1 -1
  87. package/dist/llm/response.js +5 -0
  88. package/dist/llm/token-counter.js +27 -11
  89. package/dist/logger/app-logger.js +48 -22
  90. package/dist/logger/file-log.js +28 -19
  91. package/dist/logger/index.js +1 -0
  92. package/dist/main.js +273 -47
  93. package/dist/migration/backup.js +14 -3
  94. package/dist/migration/detect.js +12 -3
  95. package/dist/migration/index.js +1 -0
  96. package/dist/modules/artifacts/store.js +9 -8
  97. package/dist/modules/browser/actions.js +6 -0
  98. package/dist/modules/browser/bridge-client.js +14 -12
  99. package/dist/modules/browser/bridge-path.js +4 -4
  100. package/dist/modules/browser/console-buffer.js +37 -0
  101. package/dist/modules/browser/cookie-store.js +5 -0
  102. package/dist/modules/browser/driver-types.js +1 -0
  103. package/dist/modules/browser/driver.js +19 -11
  104. package/dist/modules/browser/index.js +3 -1
  105. package/dist/modules/browser/module.js +2 -1
  106. package/dist/modules/browser/session.js +10 -30
  107. package/dist/modules/browser/snapshot.js +19 -13
  108. package/dist/modules/browser/types.js +1 -0
  109. package/dist/modules/certification/cli.js +28 -20
  110. package/dist/modules/certification/fact-checker.js +9 -1
  111. package/dist/modules/certification/loader.js +9 -2
  112. package/dist/modules/certification/manifest.js +83 -21
  113. package/dist/modules/certification/runner.js +23 -13
  114. package/dist/modules/certification/scenarios.js +20 -20
  115. package/dist/modules/certification/syntax-scenarios.js +1 -2
  116. package/dist/modules/context/chunk-query.js +11 -9
  117. package/dist/modules/context/fact-extractor.js +37 -39
  118. package/dist/modules/context/index.js +1 -0
  119. package/dist/modules/context/manager.js +106 -80
  120. package/dist/modules/critic/client.js +176 -0
  121. package/dist/modules/critic/evidence.js +81 -0
  122. package/dist/modules/critic/provider.js +45 -0
  123. package/dist/modules/critic/tools.js +78 -0
  124. package/dist/modules/critic/types.js +1 -0
  125. package/dist/modules/execution/audit-runners.js +28 -34
  126. package/dist/modules/execution/auditor.js +53 -49
  127. package/dist/modules/execution/execution-plugin.js +142 -102
  128. package/dist/modules/execution/index.js +1 -1
  129. package/dist/modules/execution/input-from.js +37 -13
  130. package/dist/modules/execution/module.js +368 -191
  131. package/dist/modules/execution/moe-executor.js +53 -26
  132. package/dist/modules/execution/plan-actions.js +320 -202
  133. package/dist/modules/execution/plan-checks.js +35 -0
  134. package/dist/modules/execution/plan-coverage.js +36 -33
  135. package/dist/modules/execution/plan-steps.js +11 -3
  136. package/dist/modules/execution/plan-store.js +83 -17
  137. package/dist/modules/execution/plan-tool.js +89 -35
  138. package/dist/modules/execution/plan-validator.js +30 -10
  139. package/dist/modules/execution/planner.js +22 -13
  140. package/dist/modules/execution/stuck-detector.js +196 -93
  141. package/dist/modules/execution/tracker.js +22 -8
  142. package/dist/modules/execution/verifier.js +69 -48
  143. package/dist/modules/execution/windows-commands.js +12 -12
  144. package/dist/modules/hallucination/artifact-claim.js +93 -0
  145. package/dist/modules/hallucination/confidence.js +13 -11
  146. package/dist/modules/hallucination/consistency.js +10 -2
  147. package/dist/modules/hallucination/detector.js +10 -5
  148. package/dist/modules/hallucination/factual.js +43 -26
  149. package/dist/modules/hallucination/index.js +1 -0
  150. package/dist/modules/hallucination/js-identifiers.js +44 -45
  151. package/dist/modules/hallucination/llm-judge.js +17 -12
  152. package/dist/modules/hallucination/success-claim.js +59 -0
  153. package/dist/modules/indexer/cache.js +52 -4
  154. package/dist/modules/indexer/index.js +1 -0
  155. package/dist/modules/indexer/map-command.js +2 -4
  156. package/dist/modules/indexer/map-select.js +16 -16
  157. package/dist/modules/indexer/module.js +47 -12
  158. package/dist/modules/indexer/project-profile.js +8 -7
  159. package/dist/modules/indexer/symbols.js +15 -15
  160. package/dist/modules/indexer/walker.js +159 -29
  161. package/dist/modules/lsp/check-tool.js +7 -5
  162. package/dist/modules/lsp/client.js +39 -39
  163. package/dist/modules/lsp/command.js +17 -24
  164. package/dist/modules/lsp/config.js +12 -11
  165. package/dist/modules/lsp/index.js +1 -0
  166. package/dist/modules/lsp/module.js +48 -46
  167. package/dist/modules/lsp/probe.js +13 -14
  168. package/dist/modules/lsp/project-root.js +14 -17
  169. package/dist/modules/lsp/startup-check.js +18 -16
  170. package/dist/modules/mcp/client.js +59 -62
  171. package/dist/modules/mcp/index.js +1 -0
  172. package/dist/modules/mcp/module.js +66 -37
  173. package/dist/modules/mcp/registry.js +5 -0
  174. package/dist/modules/memory/index.js +1 -0
  175. package/dist/modules/memory/module.js +12 -8
  176. package/dist/modules/memory/search.js +4 -1
  177. package/dist/modules/memory/store.js +40 -2
  178. package/dist/modules/onboarding/init.js +47 -0
  179. package/dist/modules/onboarding/meet.js +30 -0
  180. package/dist/modules/onboarding/mode.js +30 -0
  181. package/dist/modules/onboarding/prompts.js +43 -0
  182. package/dist/modules/pipelines/engine.js +10 -0
  183. package/dist/modules/pipelines/index.js +1 -0
  184. package/dist/modules/pipelines/parser.js +4 -1
  185. package/dist/modules/pipelines/template.js +4 -0
  186. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  187. package/dist/modules/plugins/builtin/notify.js +2 -0
  188. package/dist/modules/plugins/index.js +1 -0
  189. package/dist/modules/plugins/loader.js +18 -3
  190. package/dist/modules/plugins/manager.js +37 -17
  191. package/dist/modules/pricing/index.js +10 -11
  192. package/dist/modules/pricing/prices.js +12 -12
  193. package/dist/modules/processes/index.js +1 -0
  194. package/dist/modules/processes/kill-tree.js +27 -31
  195. package/dist/modules/processes/registry.js +44 -25
  196. package/dist/modules/processes/runner.js +4 -4
  197. package/dist/modules/providers/capabilities.js +28 -0
  198. package/dist/modules/providers/create.js +3 -4
  199. package/dist/modules/providers/factory.js +7 -7
  200. package/dist/modules/providers/fallback.js +12 -12
  201. package/dist/modules/providers/health.js +4 -4
  202. package/dist/modules/providers/manager.js +50 -19
  203. package/dist/modules/providers/naming.js +15 -0
  204. package/dist/modules/providers/preflight.js +71 -0
  205. package/dist/modules/providers/presets.js +39 -8
  206. package/dist/modules/providers/registry.js +7 -2
  207. package/dist/modules/reasoning/policy.js +8 -6
  208. package/dist/modules/reasoning/probe.js +30 -25
  209. package/dist/modules/security/audit-log.js +19 -36
  210. package/dist/modules/security/audit-notifier.js +35 -75
  211. package/dist/modules/security/command-validator.js +99 -51
  212. package/dist/modules/security/content-scanner.js +8 -8
  213. package/dist/modules/security/data-sanitizer.js +9 -16
  214. package/dist/modules/security/encryption.js +32 -52
  215. package/dist/modules/security/index.js +2 -1
  216. package/dist/modules/security/network-validator.js +120 -43
  217. package/dist/modules/security/path-validator.js +61 -46
  218. package/dist/modules/security/protected-paths.js +73 -0
  219. package/dist/modules/security/rate-limiter.js +17 -49
  220. package/dist/modules/security/security-policies.js +25 -57
  221. package/dist/modules/security/session-encryption.js +32 -71
  222. package/dist/modules/security/session-isolation.js +15 -28
  223. package/dist/modules/session/auto-name.js +63 -0
  224. package/dist/modules/session/export.js +98 -0
  225. package/dist/modules/session/index.js +1 -0
  226. package/dist/modules/session/manager.js +79 -28
  227. package/dist/modules/session/module.js +4 -3
  228. package/dist/modules/session/store.js +57 -20
  229. package/dist/modules/setup/answers.js +35 -0
  230. package/dist/modules/setup/apply.js +79 -0
  231. package/dist/modules/setup/discovery.js +72 -0
  232. package/dist/modules/skills/index.js +1 -0
  233. package/dist/modules/skills/loader.js +9 -1
  234. package/dist/modules/skills/module.js +11 -0
  235. package/dist/modules/updater/changelog-reader.js +15 -16
  236. package/dist/modules/updater/checker.js +9 -4
  237. package/dist/modules/updater/dev-detect.js +7 -6
  238. package/dist/modules/updater/index.js +1 -0
  239. package/dist/modules/updater/module.js +17 -15
  240. package/dist/modules/user-profile/compressor.js +2 -0
  241. package/dist/modules/user-profile/index.js +1 -0
  242. package/dist/modules/user-profile/profile.js +10 -1
  243. package/dist/modules/webui/answer-api.js +29 -0
  244. package/dist/modules/webui/assets.js +50 -0
  245. package/dist/modules/webui/body.js +21 -0
  246. package/dist/modules/webui/commands-settings.js +209 -0
  247. package/dist/modules/webui/commands-shared.js +60 -0
  248. package/dist/modules/webui/commands.js +174 -0
  249. package/dist/modules/webui/config-api.js +134 -0
  250. package/dist/modules/webui/file-api.js +202 -0
  251. package/dist/modules/webui/forward.js +159 -0
  252. package/dist/modules/webui/history-api.js +112 -0
  253. package/dist/modules/webui/host-state.js +60 -0
  254. package/dist/modules/webui/i18n-api.js +9 -0
  255. package/dist/modules/webui/limits.js +6 -0
  256. package/dist/modules/webui/log-forward.js +20 -0
  257. package/dist/modules/webui/memory-commands.js +64 -0
  258. package/dist/modules/webui/model-cache.js +18 -0
  259. package/dist/modules/webui/module.js +148 -0
  260. package/dist/modules/webui/persist.js +24 -0
  261. package/dist/modules/webui/prompt-io.js +97 -0
  262. package/dist/modules/webui/providers-api.js +337 -0
  263. package/dist/modules/webui/providers-view.js +56 -0
  264. package/dist/modules/webui/reasoning-view.js +15 -0
  265. package/dist/modules/webui/run-agent.js +81 -0
  266. package/dist/modules/webui/server.js +284 -0
  267. package/dist/modules/webui/sessions-api.js +178 -0
  268. package/dist/modules/webui/settings-api.js +561 -0
  269. package/dist/modules/webui/setup-api.js +201 -0
  270. package/dist/modules/webui/state.js +74 -0
  271. package/dist/modules/webui/util.js +261 -0
  272. package/dist/output/bus.js +7 -2
  273. package/dist/output/channel.js +80 -28
  274. package/dist/output/format.js +2 -1
  275. package/dist/output/index.js +1 -0
  276. package/dist/output/json-sink.js +5 -3
  277. package/dist/output/machine.js +4 -3
  278. package/dist/output/session-sink.js +4 -4
  279. package/dist/tools/approve.js +4 -0
  280. package/dist/tools/attach-image.js +5 -4
  281. package/dist/tools/auto-fixer.js +26 -44
  282. package/dist/tools/auto-tags.js +83 -0
  283. package/dist/tools/bash.js +193 -116
  284. package/dist/tools/browser.js +76 -31
  285. package/dist/tools/chunk-query.js +14 -6
  286. package/dist/tools/create-dir.js +9 -5
  287. package/dist/tools/delete-file.js +9 -5
  288. package/dist/tools/download-file.js +32 -7
  289. package/dist/tools/edit-file.js +52 -14
  290. package/dist/tools/enable-tools.js +6 -6
  291. package/dist/tools/executor.js +132 -27
  292. package/dist/tools/file-info.js +3 -1
  293. package/dist/tools/filter-tools.js +3 -3
  294. package/dist/tools/glob-tool.js +4 -1
  295. package/dist/tools/grep-tool.js +13 -10
  296. package/dist/tools/hidden-tools-block.js +6 -6
  297. package/dist/tools/index.js +16 -8
  298. package/dist/tools/list-dir.js +4 -2
  299. package/dist/tools/load-skill.js +7 -0
  300. package/dist/tools/mcp-call.js +15 -4
  301. package/dist/tools/meet-done.js +14 -0
  302. package/dist/tools/move-file.js +14 -7
  303. package/dist/tools/path-utils.js +29 -29
  304. package/dist/tools/pipeline-run.js +29 -7
  305. package/dist/tools/preview.js +1 -1
  306. package/dist/tools/process-ids.js +31 -0
  307. package/dist/tools/process-kill.js +9 -16
  308. package/dist/tools/process-list.js +1 -0
  309. package/dist/tools/process-log.js +11 -16
  310. package/dist/tools/process-wait.js +63 -0
  311. package/dist/tools/prompt-io.js +4 -3
  312. package/dist/tools/question.js +172 -25
  313. package/dist/tools/read-file.js +55 -34
  314. package/dist/tools/recall.js +14 -8
  315. package/dist/tools/registry.js +6 -5
  316. package/dist/tools/remember.js +8 -4
  317. package/dist/tools/scope-request.js +11 -6
  318. package/dist/tools/search-history.js +5 -2
  319. package/dist/tools/session-info.js +2 -0
  320. package/dist/tools/set-thinking.js +8 -4
  321. package/dist/tools/subagent.js +59 -39
  322. package/dist/tools/syntax-validator.js +292 -90
  323. package/dist/tools/user-input.js +22 -18
  324. package/dist/tools/web-browse.js +9 -3
  325. package/dist/tools/web-fetch.js +12 -23
  326. package/dist/tools/web-search.js +7 -5
  327. package/dist/tools/write-file.js +45 -15
  328. package/dist/ui/action-log.js +13 -0
  329. package/dist/ui/box.js +4 -3
  330. package/dist/ui/colors.js +2 -0
  331. package/dist/ui/diff.js +6 -5
  332. package/dist/ui/index.js +1 -0
  333. package/dist/ui/line-editor.js +230 -122
  334. package/dist/ui/line-math.js +10 -2
  335. package/dist/ui/md-formatter.js +57 -37
  336. package/dist/ui/output.js +8 -3
  337. package/dist/ui/plan-view.js +21 -10
  338. package/dist/ui/renderer.js +393 -67
  339. package/dist/ui/spinner.js +16 -5
  340. package/dist/ui/table.js +15 -17
  341. package/dist/ui/theme.js +143 -0
  342. package/dist/utils/child-run.js +20 -0
  343. package/dist/utils/error.js +1 -1
  344. package/dist/utils/index.js +2 -0
  345. package/dist/utils/retry.js +2 -2
  346. package/dist/utils/truncate.js +1 -1
  347. package/package.json +1 -1
@@ -0,0 +1,233 @@
1
+ import { lookupModelsDevContext } from "./models-dev";
2
+ /**
3
+ * Спрашивает нативный API бэкенда (LM Studio `/api/v1/models`) о длине
4
+ * контекста загруженной модели И заявленных capabilities. Длина — из живого
5
+ * инстанса (`loaded_instances[].config.context_length`), фолбэк —
6
+ * `max_context_length`. Capabilities читаются из `capabilities` (`vision`,
7
+ * `trained_for_tool_use`), если есть — это единый источник правды вместо
8
+ * гадания по имени.
9
+ *
10
+ * Null, если эндпоинт недоступен (не LM Studio), модели нет/снята или запрос
11
+ * упал — вызывающий деградирует молча.
12
+ */
13
+ export async function probeModel(baseUrl, model, timeoutMs = 3000) {
14
+ const base = baseUrl.replace(/\/$/, "").replace(/\/v1\/?$/, "");
15
+ try {
16
+ const response = await fetch(`${base}/api/v1/models`, {
17
+ signal: AbortSignal.timeout(timeoutMs),
18
+ });
19
+ if (!response.ok)
20
+ return null;
21
+ const data = (await response.json());
22
+ const models = data.models ?? [];
23
+ const found = models.find((m) => m.key === model || m.name === model || (m.key ? model.includes(m.key) : false));
24
+ if (!found)
25
+ return null;
26
+ const loadedInstance = (found.loaded_instances ?? [])[0];
27
+ const contextLength = loadedInstance?.config?.context_length ?? found.max_context_length;
28
+ if (typeof contextLength !== "number" || contextLength <= 0)
29
+ return null;
30
+ const raw = found.capabilities;
31
+ let capabilities;
32
+ if (raw && typeof raw.vision === "boolean") {
33
+ capabilities = { vision: raw.vision };
34
+ if (typeof raw.trained_for_tool_use === "boolean") {
35
+ capabilities.tools = raw.trained_for_tool_use;
36
+ }
37
+ }
38
+ return { model: found.key ?? found.name ?? model, contextLength, capabilities };
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /**
45
+ * Тонкая context-only проекция {@link probeModel}. Для вызывающих, кому нужно
46
+ * только окно контекста (баннер, диагностика context-probe).
47
+ */
48
+ export async function getLoadedContextLength(baseUrl, model, timeoutMs = 3000) {
49
+ const probe = await probeModel(baseUrl, model, timeoutMs);
50
+ return probe ? { model: probe.model, actual: probe.contextLength } : null;
51
+ }
52
+ /** Числовые context-подобные поля, что может нести запись `/models`. */
53
+ const CONTEXT_KEYS = [
54
+ "context_length",
55
+ "context_window",
56
+ "max_context_length",
57
+ "max_context_window",
58
+ "max_model_len",
59
+ "maxModelLen",
60
+ "n_ctx",
61
+ "num_ctx",
62
+ "contextLength",
63
+ "contextWindow",
64
+ ];
65
+ function asPositiveInt(v) {
66
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
67
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
68
+ }
69
+ /**
70
+ * Первое положительное среди известных context-полей объекта. OpenRouter
71
+ * кладёт per-endpoint значение в `top_provider.context_length`, поэтому один
72
+ * уровень вложенности тоже покрыт.
73
+ */
74
+ function pickContextField(obj) {
75
+ for (const k of CONTEXT_KEYS) {
76
+ const n = asPositiveInt(obj[k]);
77
+ if (n !== null)
78
+ return n;
79
+ }
80
+ const top = obj.top_provider ?? obj.topProvider;
81
+ if (top && typeof top === "object") {
82
+ for (const k of CONTEXT_KEYS) {
83
+ const n = asPositiveInt(top[k]);
84
+ if (n !== null)
85
+ return n;
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ function matchesModel(entry, model) {
91
+ for (const k of ["id", "name", "model", "key"]) {
92
+ const v = entry[k];
93
+ if (typeof v === "string" && v && (v === model || model.includes(v) || v.includes(model))) {
94
+ return true;
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+ async function fetchJson(url, timeoutMs, init) {
100
+ try {
101
+ const response = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
102
+ if (!response.ok)
103
+ return null;
104
+ return (await response.json());
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /** Context-поля живого инстанса из Ollama `/api/ps`, если есть. */
111
+ async function probeOllamaPs(origin, model, timeoutMs) {
112
+ const ps = await fetchJson(`${origin}/api/ps`, timeoutMs);
113
+ const running = ps?.models;
114
+ if (!Array.isArray(running))
115
+ return null;
116
+ const found = running.find((m) => m && typeof m === "object" && matchesModel(m, model));
117
+ if (!found)
118
+ return null;
119
+ return pickContextField(found);
120
+ }
121
+ function contextFromModelInfo(bag) {
122
+ for (const [k, v] of Object.entries(bag)) {
123
+ if (k.toLowerCase().includes("context_length") || k.toLowerCase().endsWith("n_ctx")) {
124
+ const n = asPositiveInt(v);
125
+ if (n !== null)
126
+ return n;
127
+ }
128
+ }
129
+ return pickContextField(bag);
130
+ }
131
+ /** Ollama `/api/show`: детали модели, пробуем полное и короткое имя. */
132
+ async function probeOllamaShow(origin, model, timeoutMs) {
133
+ const short = model.split(":")[0];
134
+ for (const name of [model, short]) {
135
+ const show = await fetchJson(`${origin}/api/show`, timeoutMs, {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify({ name }),
139
+ });
140
+ const info = show;
141
+ const bag = info?.model_info ?? info?.modelInfo;
142
+ if (bag && typeof bag === "object") {
143
+ const n = contextFromModelInfo(bag);
144
+ if (n !== null)
145
+ return n;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /** Нативный Ollama: живые инстансы (`/api/ps`) + детали (`/api/show`). */
151
+ async function probeOllama(origin, model, timeoutMs) {
152
+ // `/api/show` сообщает контекст модели (напр. `llama.context_length` в
153
+ // `model_info`), даже когда `/api/ps` размерных полей не несёт.
154
+ return ((await probeOllamaPs(origin, model, timeoutMs)) ??
155
+ (await probeOllamaShow(origin, model, timeoutMs)));
156
+ }
157
+ /** Сервер llama.cpp: одномодельный `/props` несёт загруженный `n_ctx`. */
158
+ async function probeLlamaCpp(origin, timeoutMs) {
159
+ const props = await fetchJson(`${origin}/props`, timeoutMs);
160
+ if (!props || typeof props !== "object")
161
+ return null;
162
+ const bag = props;
163
+ const direct = pickContextField(bag);
164
+ if (direct !== null)
165
+ return direct;
166
+ const gen = bag.default_generation_settings;
167
+ if (gen && typeof gen === "object") {
168
+ return pickContextField(gen);
169
+ }
170
+ return null;
171
+ }
172
+ /**
173
+ * OpenAI-совместимый `/models` с расширенными метаданными. Серверы с голыми id
174
+ * (чистый OpenAI, Zen/Go) context-полей не несут → null.
175
+ */
176
+ async function probeOpenAICompatModels(baseUrl, model, apiKey, timeoutMs) {
177
+ const url = `${baseUrl.replace(/\/+$/, "")}/models`;
178
+ const headers = { "Content-Type": "application/json" };
179
+ if (apiKey && apiKey !== "not-needed")
180
+ headers.Authorization = `Bearer ${apiKey}`;
181
+ const data = await fetchJson(url, timeoutMs, { headers });
182
+ const list = data?.data;
183
+ if (!Array.isArray(list))
184
+ return null;
185
+ const found = list.find((m) => !!m && typeof m === "object" && matchesModel(m, model));
186
+ if (!found)
187
+ return null;
188
+ return pickContextField(found);
189
+ }
190
+ /**
191
+ * Лукап каталога models.dev — последний шанс для OpenCode Zen/Go. Их `/models`
192
+ * отдаёт голые id, а models.dev (открытая БД, что использует сам opencode)
193
+ * держит модели провайдера `opencode` под теми же id с `limit.context`.
194
+ * Гейт на Zen-URL/тип: локальные бэкенды ради этого в сеть не ходят.
195
+ */
196
+ async function probeModelsDev(baseUrl, model, providerType) {
197
+ const isZen = providerType === "opencode-zen" ||
198
+ providerType === "opencode-go" ||
199
+ /opencode\.ai\/zen/.test(baseUrl);
200
+ if (!isZen)
201
+ return null;
202
+ return lookupModelsDevContext(model);
203
+ }
204
+ /**
205
+ * Пробует все источники по порядку, возвращает первый давший окно.
206
+ * @param baseUrl базовый URL провайдера; @param model имя модели;
207
+ * @param opts ключ/таймаут/тип; @returns окно контекста либо null (fail-open).
208
+ */
209
+ export async function probeProviderContext(baseUrl, model, opts = {}) {
210
+ const timeoutMs = opts.timeoutMs ?? 3000;
211
+ const origin = baseUrl.replace(/\/$/, "").replace(/\/v1\/?$/, "");
212
+ try {
213
+ const lm = await probeModel(baseUrl, model, timeoutMs);
214
+ if (lm)
215
+ return { model: lm.model, actual: lm.contextLength };
216
+ const ollama = await probeOllama(origin, model, timeoutMs);
217
+ if (ollama !== null)
218
+ return { model, actual: ollama };
219
+ const llama = await probeLlamaCpp(origin, timeoutMs);
220
+ if (llama !== null)
221
+ return { model, actual: llama };
222
+ const compat = await probeOpenAICompatModels(baseUrl, model, opts.apiKey, timeoutMs);
223
+ if (compat !== null)
224
+ return { model, actual: compat };
225
+ const catalog = await probeModelsDev(baseUrl, model, opts.providerType);
226
+ if (catalog !== null)
227
+ return { model, actual: catalog };
228
+ }
229
+ catch {
230
+ // Fail-open: любая ошибка пробы оставляет настроенный contextWindow.
231
+ }
232
+ return null;
233
+ }
@@ -1,4 +1,4 @@
1
- /** Helper: extract plain text from message content (for token counting, logging, etc.) */
1
+ /** Хелпер: плоский текст из контента (подсчёт токенов, логи и т.п.). */
2
2
  export function getMessageText(content) {
3
3
  if (typeof content === "string")
4
4
  return content;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Сворачивает чанки стрима в ответ (текст склеивается, reasoning копится,
3
+ * аргументы тулов JSON-парсятся с фолбэком {}).
4
+ * @param chunks чанки стрима; @returns распарсенный ответ.
5
+ */
1
6
  export function parseChunks(chunks) {
2
7
  let text = "";
3
8
  let reasoning;
@@ -1,14 +1,13 @@
1
- // src/llm/token-counter.ts
1
+ /** Подсчёт токенов: точный (tiktoken) и дешёвая эвристика для блоков. */
2
2
  import { encodingForModel, getEncoding } from "js-tiktoken";
3
3
  import { getMessageText } from "./provider";
4
4
  /**
5
- * Script-aware token ESTIMATE (cheap, no full tokenization). Cheap enough for
6
- * every hand-built PromptBlock; used at block-build time where the exact
7
- * TokenCounter may not be in scope. Measured on cl100k_base: Latin AND
8
- * Cyrillic both encode at ~3.3-5 chars/token (the old flat len/4 was actually
9
- * close for Cyrillic — the "cyr ≈ /2" assumption was wrong); CJK/Hangul are
10
- * the real outliers at ~1-1.5 tokens/char, so they get /1.5. Within ±30% of
11
- * tiktoken for Russian text.
5
+ * Скриптозависимая ОЦЕНКА токенов (дешёвая, без полной токенизации). Достаточно
6
+ * дешёвая для каждого PromptBlock; на этапе сборки точного TokenCounter может
7
+ * не быть в scope. Замерено на cl100k_base: латиница И кириллица — ~3.3–5
8
+ * символов/токен (старый плоский len/4 для кириллицы был почти верен —
9
+ * гипотеза «кириллица ≈ /2» ошибочна); CJK/хангыль — выбросы ~1–1.5 токена/
10
+ * символ, им /1.5. В пределах ±30% от tiktoken на русском.
12
11
  */
13
12
  export function estimateTokens(text) {
14
13
  if (!text)
@@ -29,6 +28,7 @@ export function estimateTokens(text) {
29
28
  }
30
29
  return Math.ceil(cjk / 1.5 + rest / 4);
31
30
  }
31
+ /** Точный счётчик токенов поверх tiktoken (энкодинг по модели). */
32
32
  export class TokenCounter {
33
33
  encoder;
34
34
  constructor(model = "gpt-4o") {
@@ -36,31 +36,47 @@ export class TokenCounter {
36
36
  this.encoder = encodingForModel(model);
37
37
  }
38
38
  catch {
39
- // Fallback to cl100k_base for unknown models
39
+ // Фолбэк на cl100k_base для неизвестных моделей
40
40
  this.encoder = getEncoding("cl100k_base");
41
41
  }
42
42
  }
43
+ /**
44
+ * Считает токены текста.
45
+ * @param text текст; @returns число токенов.
46
+ */
43
47
  count(text) {
44
48
  if (!text)
45
49
  return 0;
46
50
  return this.encoder.encode(text).length;
47
51
  }
52
+ /**
53
+ * Считает токены сообщений с оверхедом ролей/границ (~4/сообщение).
54
+ * @param messages сообщения чата; @returns суммарные токены.
55
+ */
48
56
  countMessages(messages) {
49
57
  let total = 0;
50
58
  for (const msg of messages) {
51
- // ~4 tokens per message overhead (role, boundaries)
59
+ // ~4 токена оверхеда на сообщение (роль, границы)
52
60
  total += 4;
53
61
  const text = getMessageText(msg.content);
54
62
  total += this.count(text);
55
63
  if (msg.role === "tool")
56
64
  total += 2; // tool_call_id
57
65
  }
58
- total += 2; // assistant priming
66
+ total += 2; // прайминг ассистента
59
67
  return total;
60
68
  }
69
+ /**
70
+ * Кодирует текст в токены.
71
+ * @param text текст; @returns массив id токенов.
72
+ */
61
73
  encode(text) {
62
74
  return this.encoder.encode(text);
63
75
  }
76
+ /**
77
+ * Декодирует токены в текст.
78
+ * @param tokens id токенов; @returns текст.
79
+ */
64
80
  decode(tokens) {
65
81
  return this.encoder.decode(tokens);
66
82
  }
@@ -1,9 +1,11 @@
1
+ // Логер: шина вывода + файловые логи (.log, app.jsonl), чистка секретов.
1
2
  import { appendFileSync, mkdirSync, existsSync } from "fs";
2
3
  import { join } from "path";
3
4
  import { sanitizeLogMessage } from "../modules/security/data-sanitizer";
4
5
  import { FileLogWriter } from "./file-log";
5
6
  import { defaultOutputBus } from "../output";
6
7
  const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
8
+ /** Центральный логер: консоль через шину, файлы, JSONL, тегированные хелперы. */
7
9
  export class Logger {
8
10
  level;
9
11
  prefix;
@@ -36,36 +38,47 @@ export class Logger {
36
38
  clearSessionDir() {
37
39
  this.sessionDir = null;
38
40
  }
39
- /** Open a per-session `.log` file (legacy v1 behavior). */
41
+ /** Открывает посессионный `.log`-файл (поведение legacy v1). */
40
42
  initSessionLog(sessionId) {
41
43
  this.fileLog.initSessionLog(sessionId);
42
44
  }
43
- /** Fall back to daily log files when no session is active. */
45
+ /** Возврат к дневным лог-файлам, когда нет активной сессии. */
44
46
  closeSessionLog() {
45
47
  this.fileLog.closeSessionLog();
46
48
  }
49
+ /** Путь к текущему лог-файлу (сессионный или дневной). */
47
50
  getLogPath() {
48
51
  return this.fileLog.getLogPath();
49
52
  }
53
+ /** Удаляет старые логи по возрасту/количеству. */
50
54
  cleanupOldLogs(maxDays, maxFiles) {
51
55
  this.fileLog.cleanupOldLogs(maxDays, maxFiles);
52
56
  }
53
- // --- Tagged helpers (legacy v1 API) ---
57
+ // Тегированные хелперы (API legacy v1).
58
+ /** Пишет запрос к LLM в файловый лог. */
54
59
  logLLMRequest(model, messagesCount, promptPreview, caller) {
55
60
  this.fileLog.logLLMRequest(model, messagesCount, sanitizeLogMessage(promptPreview), caller);
56
61
  }
62
+ /** Пишет ответ LLM (или ошибку) в файловый лог. */
57
63
  logLLMResponse(model, responseLength, genTimeMs, error, caller) {
58
64
  this.fileLog.logLLMResponse(model, responseLength, genTimeMs, error, caller);
59
65
  }
66
+ /** Пишет вызов тула и превью результата в файловый лог. */
60
67
  logToolCall(tool, preview, result) {
61
68
  this.fileLog.logToolCall(tool, sanitizeLogMessage(preview), result !== undefined ? sanitizeLogMessage(result) : undefined);
62
69
  }
70
+ /** Пишет вывод тула с кодом выхода в файловый лог. */
63
71
  logToolOutput(tool, output, exitCode) {
64
72
  this.fileLog.logToolOutput(tool, sanitizeLogMessage(output), exitCode);
65
73
  }
74
+ /** Пишет реплику REPL (user/assistant/system) в файловый лог. */
66
75
  logREPL(tag, content) {
67
76
  this.fileLog.logREPL(tag, sanitizeLogMessage(content));
68
77
  }
78
+ /**
79
+ * Дочерний логер с расширенным префиксом; наследует уровень, sink и каталоги.
80
+ * @param prefix суффикс префикса. @returns новый Logger.
81
+ */
69
82
  child(prefix) {
70
83
  const childLogger = new Logger(this.level, this.prefix ? `${this.prefix}:${prefix}` : prefix, this.sink);
71
84
  if (this.logDir)
@@ -86,42 +99,55 @@ export class Logger {
86
99
  error(msg, meta) {
87
100
  this.log("error", msg, meta);
88
101
  }
89
- log(level, msg, meta) {
102
+ /**
103
+ * Внутренняя диагностика петли (ретраи галлюцинаций, аудит/компакция,
104
+ * вердикты критика, …). Персистентность та же, что у warn/info, но событие
105
+ * шины несёт `data.diagnostic = true`, и чат дропает его, пока
106
+ * `ui.verbosity !== "verbose"`.
107
+ */
108
+ diagnostic(level, msg, meta) {
109
+ this.log(level, msg, meta, true);
110
+ }
111
+ log(level, msg, meta, diagnostic) {
90
112
  if (LEVELS[level] < LEVELS[this.level])
91
113
  return;
92
- // Sanitize log message to remove sensitive data
114
+ // Чистим сообщение — вырезаем секреты.
93
115
  const sanitizedMsg = sanitizeLogMessage(msg);
94
116
  const sanitizedMeta = meta ? this.sanitizeMeta(meta) : undefined;
95
117
  const ts = new Date().toISOString();
96
118
  const metaStr = sanitizedMeta ? ` ${JSON.stringify(sanitizedMeta)}` : "";
97
119
  const line = `${sanitizedMsg}${metaStr}`;
98
120
  try {
99
- this.sink.emit({ level, source: this.prefix || "mma", kind: "log", text: line });
121
+ this.sink.emit({
122
+ level,
123
+ source: this.prefix || "mma",
124
+ kind: "log",
125
+ text: line,
126
+ data: diagnostic ? { diagnostic: true } : undefined,
127
+ });
100
128
  }
101
129
  catch {
102
130
  /* terminal output is best-effort; file logging below must still run */
103
131
  }
104
- // Legacy v1 behavior: also append to the tagged `.log` file (session or daily).
132
+ // Поведение legacy v1: дубль в тегированный `.log` (сессионный или дневной).
105
133
  this.fileLog.log(level.toUpperCase(), this.prefix || "MMA", `${ts} — ${sanitizedMsg}${metaStr}`);
106
134
  const logTarget = this.sessionDir ?? this.logDir;
107
135
  if (logTarget) {
108
136
  try {
109
- appendFileSync(join(logTarget, "app.jsonl"), JSON.stringify({
137
+ appendFileSync(join(logTarget, "app.jsonl"), `${JSON.stringify({
110
138
  level,
111
139
  ts,
112
140
  prefix: this.prefix,
113
141
  msg: sanitizedMsg,
114
142
  meta: sanitizedMeta ?? null,
115
- }) + "\n", "utf-8");
143
+ })}\n`, "utf-8");
116
144
  }
117
145
  catch {
118
146
  /* file logging is best-effort */
119
147
  }
120
148
  }
121
149
  }
122
- /**
123
- * Sanitize metadata object to remove sensitive data
124
- */
150
+ /** Рекурсивно чистит секреты в объекте метаданных. */
125
151
  sanitizeMeta(meta) {
126
152
  const sanitized = {};
127
153
  for (const [key, value] of Object.entries(meta)) {
@@ -138,30 +164,30 @@ export class Logger {
138
164
  return sanitized;
139
165
  }
140
166
  /**
141
- * Write a structured record to app.jsonl only (no console / .log output).
142
- * Used for machine-readable reports (environment, etc.) that would be too
143
- * noisy on the terminal but are valuable for cross-device diagnosis.
167
+ * Структурная запись только в app.jsonl (без консоли и .log).
168
+ * Для машиночитаемых отчётов (окружение и т.п.), слишком шумных для терминала,
169
+ * но ценных для кросс-девайс диагностики.
144
170
  */
145
171
  logStructured(type, data) {
146
172
  const logTarget = this.sessionDir ?? this.logDir;
147
173
  if (!logTarget)
148
174
  return;
149
175
  try {
150
- appendFileSync(join(logTarget, "app.jsonl"), JSON.stringify({
176
+ appendFileSync(join(logTarget, "app.jsonl"), `${JSON.stringify({
151
177
  level: "info",
152
178
  ts: new Date().toISOString(),
153
179
  type,
154
180
  meta: this.sanitizeMeta(data),
155
- }) + "\n", "utf-8");
181
+ })}\n`, "utf-8");
156
182
  }
157
183
  catch {
158
184
  /* file logging is best-effort */
159
185
  }
160
186
  }
161
187
  /**
162
- * Write a log entry to app.jsonl only — no console output, no .log file.
163
- * Use this for noisy subsystems (LSP, probes) whose warnings should be
164
- * diagnosable from session files but not pollute the user's terminal.
188
+ * Запись только в app.jsonl — без консоли и .log. Для шумных подсистем
189
+ * (LSP, пробники): предупреждения диагностируются по файлам сессии,
190
+ * но не мусорят в терминале.
165
191
  */
166
192
  logSilent(level, msg, meta) {
167
193
  const logTarget = this.sessionDir ?? this.logDir;
@@ -171,13 +197,13 @@ export class Logger {
171
197
  const sanitizedMsg = sanitizeLogMessage(msg);
172
198
  const sanitizedMeta = meta ? this.sanitizeMeta(meta) : undefined;
173
199
  try {
174
- appendFileSync(join(logTarget, "app.jsonl"), JSON.stringify({
200
+ appendFileSync(join(logTarget, "app.jsonl"), `${JSON.stringify({
175
201
  level,
176
202
  ts,
177
203
  prefix: this.prefix,
178
204
  msg: sanitizedMsg,
179
205
  meta: sanitizedMeta ?? null,
180
- }) + "\n", "utf-8");
206
+ })}\n`, "utf-8");
181
207
  }
182
208
  catch {
183
209
  /* file logging is best-effort */
@@ -1,3 +1,4 @@
1
+ // Файловый логер legacy v1: тегированные строки, ротация, чистка старых логов.
1
2
  import { appendFileSync, mkdirSync, existsSync, readdirSync, renameSync, statSync, unlinkSync, } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  const MAX_LOG_SIZE = 5 * 1024 * 1024;
@@ -5,27 +6,28 @@ const MAX_LOG_FILES = 30;
5
6
  const DEFAULT_MAX_DAYS = 30;
6
7
  const TOOL_OUTPUT_LIMIT = 2000;
7
8
  /**
8
- * Tagged file logger (legacy MMA v1 behavior).
9
+ * Тегированный файловый логер (поведение legacy MMA v1).
9
10
  *
10
- * Writes human-readable log lines to `~/.mma/logs/`:
11
- * - `<sessionId>.log` while a session is active
12
- * - `YYYY-MM-DD.log` for everything outside a session
11
+ * Пишет читаемые строки в `~/.mma/logs/`:
12
+ * - `<sessionId>.log`, пока сессия активна;
13
+ * - `YYYY-MM-DD.log` для всего вне сессии.
13
14
  *
14
- * Features restored from v1:
15
- * - size-based rotation (5 MB → file.log.1, file.log.2, ...)
16
- * - cleanup of old logs (age + max file count)
17
- * - tagged helpers: LLM request/response, tool calls, REPL
15
+ * Фичи из v1:
16
+ * - ротация по размеру (5 МБ → file.log.1, file.log.2, …);
17
+ * - чистка старых логов (возраст + лимит файлов);
18
+ * - тегированные хелперы: запрос/ответ LLM, вызовы тулов, REPL.
18
19
  */
19
20
  export class FileLogWriter {
20
21
  logDir = null;
21
22
  sessionLogPath = null;
23
+ /** Задаёт каталог логов (создаёт при отсутствии). */
22
24
  setLogDir(dir) {
23
25
  this.logDir = dir;
24
26
  if (!existsSync(dir)) {
25
27
  mkdirSync(dir, { recursive: true });
26
28
  }
27
29
  }
28
- /** Open a per-session log file. Triggers cleanup of old logs. */
30
+ /** Открывает посессионный лог-файл. Триггерит чистку старых логов. */
29
31
  initSessionLog(sessionId) {
30
32
  if (!this.logDir)
31
33
  return;
@@ -35,10 +37,11 @@ export class FileLogWriter {
35
37
  this.sessionLogPath = join(this.logDir, `${sessionId}.log`);
36
38
  this.cleanupOldLogs();
37
39
  }
38
- /** Fall back to daily log files when no session is active. */
40
+ /** Возврат к дневным лог-файлам, когда нет активной сессии. */
39
41
  closeSessionLog() {
40
42
  this.sessionLogPath = null;
41
43
  }
44
+ /** Путь к текущему лог-файлу (сессионный или дневной). */
42
45
  getLogPath() {
43
46
  if (this.sessionLogPath)
44
47
  return this.sessionLogPath;
@@ -47,7 +50,7 @@ export class FileLogWriter {
47
50
  const date = new Date().toISOString().slice(0, 10);
48
51
  return join(this.logDir, `${date}.log`);
49
52
  }
50
- /** Rotate the file if it exceeds the size limit. */
53
+ /** Ротирует файл при превышении лимита размера. */
51
54
  rotateIfNeeded(filePath) {
52
55
  if (!existsSync(filePath))
53
56
  return;
@@ -59,6 +62,7 @@ export class FileLogWriter {
59
62
  idx++;
60
63
  renameSync(filePath, `${filePath}.${idx}`);
61
64
  }
65
+ /** Пишет одну строку. Файловый лог — best-effort, ошибки глотаются. */
62
66
  log(level, tag, message) {
63
67
  const fp = this.getLogPath();
64
68
  if (!fp)
@@ -75,10 +79,12 @@ export class FileLogWriter {
75
79
  /* file logging is best-effort */
76
80
  }
77
81
  }
82
+ /** Пишет запрос к LLM (модель, число сообщений, превью промпта). */
78
83
  logLLMRequest(model, messagesCount, promptPreview, caller) {
79
84
  const tag = caller ? `LLM/${caller}` : "LLM";
80
85
  this.log("INFO", tag, `→ ${model} | ${messagesCount} messages | "${promptPreview.slice(0, 100)}"`);
81
86
  }
87
+ /** Пишет ответ LLM (длина, время) или ошибку. */
82
88
  logLLMResponse(model, responseLength, genTimeMs, error, caller) {
83
89
  const tag = caller ? `LLM/${caller}` : "LLM";
84
90
  if (error) {
@@ -88,16 +94,18 @@ export class FileLogWriter {
88
94
  this.log("INFO", tag, `← ${model} | ${responseLength} chars | ${genTimeMs}ms`);
89
95
  }
90
96
  }
97
+ /** Пишет вызов тула с обрезанными превью аргументов и результата. */
91
98
  logToolCall(tool, preview, result) {
92
- const previewTrimmed = preview.length > 200 ? preview.slice(0, 200) + "..." : preview;
99
+ const previewTrimmed = preview.length > 200 ? `${preview.slice(0, 200)}...` : preview;
93
100
  if (result !== undefined) {
94
- const resultTrimmed = result.length > 200 ? result.slice(0, 200) + "..." : result;
101
+ const resultTrimmed = result.length > 200 ? `${result.slice(0, 200)}...` : result;
95
102
  this.log("INFO", "TOOL", `${tool} | ${previewTrimmed} → ${resultTrimmed}`);
96
103
  }
97
104
  else {
98
105
  this.log("INFO", "TOOL", `${tool} | ${previewTrimmed}`);
99
106
  }
100
107
  }
108
+ /** Пишет вывод тула с кодом выхода (тело режется лимитом). */
101
109
  logToolOutput(tool, output, exitCode) {
102
110
  const trimmed = output.length > TOOL_OUTPUT_LIMIT
103
111
  ? output.slice(0, TOOL_OUTPUT_LIMIT) +
@@ -105,14 +113,15 @@ export class FileLogWriter {
105
113
  : output;
106
114
  this.log("DEBUG", "TOOL_OUTPUT", `${tool} exit:${exitCode}\n${trimmed}`);
107
115
  }
116
+ /** Пишет реплику REPL (тело режется до 400 символов). */
108
117
  logREPL(tag, content) {
109
- const trimmed = content.length > 400 ? content.slice(0, 400) + "..." : content;
118
+ const trimmed = content.length > 400 ? `${content.slice(0, 400)}...` : content;
110
119
  this.log("INFO", `REPL/${tag}`, trimmed);
111
120
  }
112
121
  /**
113
- * Clean up old log files.
114
- * - Deletes files older than maxDays
115
- * - Keeps at most maxFiles most recent .log files
122
+ * Чистит старые лог-файлы.
123
+ * - удаляет старше maxDays;
124
+ * - оставляет не более maxFiles самых свежих .log-файлов.
116
125
  */
117
126
  cleanupOldLogs(maxDays = DEFAULT_MAX_DAYS, maxFiles = MAX_LOG_FILES) {
118
127
  if (!this.logDir || !existsSync(this.logDir))
@@ -126,7 +135,7 @@ export class FileLogWriter {
126
135
  mtime: statSync(join(this.logDir, f)).mtimeMs,
127
136
  }))
128
137
  .sort((a, b) => b.mtime - a.mtime);
129
- // 1. Delete by age.
138
+ // 1. Удаление по возрасту.
130
139
  for (const f of list()) {
131
140
  if (now - f.mtime > maxDays * 24 * 60 * 60 * 1000) {
132
141
  try {
@@ -137,7 +146,7 @@ export class FileLogWriter {
137
146
  }
138
147
  }
139
148
  }
140
- // 2. Delete excess (fresh but more than maxFiles).
149
+ // 2. Удаление излишков (свежие, но сверх maxFiles).
141
150
  const remaining = list();
142
151
  for (let i = maxFiles; i < remaining.length; i++) {
143
152
  try {
@@ -1 +1,2 @@
1
+ // Логер приложения: единая точка логирования (терминал, файлы, JSONL).
1
2
  export { Logger } from "./app-logger";