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
@@ -57,7 +57,10 @@ export class SessionStore {
57
57
  mkdirSync(dir, { recursive: true, mode: 0o700 });
58
58
  const content = JSON.stringify(meta, null, 2);
59
59
  if (this.encryptor) {
60
- writeFileSync(this.metaPath(id), this.encryptor.encryptFileContent(content), { encoding: "utf-8", mode: 0o600 });
60
+ writeFileSync(this.metaPath(id), this.encryptor.encryptFileContent(content), {
61
+ encoding: "utf-8",
62
+ mode: 0o600,
63
+ });
61
64
  }
62
65
  else {
63
66
  writeFileSync(this.metaPath(id), content, { encoding: "utf-8", mode: 0o600 });
@@ -81,17 +84,39 @@ export class SessionStore {
81
84
  return null;
82
85
  }
83
86
  }
87
+ /** Read meta straight from disk, bypassing _metaCache — used where counter
88
+ * accuracy matters more than read speed (see appendMessage). */
89
+ readMetaFromDisk(id) {
90
+ const path = this.metaPath(id);
91
+ if (!existsSync(path))
92
+ return null;
93
+ try {
94
+ const raw = readFileSync(path, "utf-8");
95
+ const content = this.encryptor ? this.encryptor.decryptFileContent(raw) : raw;
96
+ return JSON.parse(content);
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
84
102
  appendMessage(id, msg) {
85
103
  const dir = this.sessionDir(id);
86
104
  mkdirSync(dir, { recursive: true, mode: 0o700 });
87
105
  const line = JSON.stringify(msg);
88
106
  if (this.encryptor?.isEnabled()) {
89
- appendFileSync(this.historyPath(id), this.encryptor.encryptFileContent(line) + "\n", { encoding: "utf-8", mode: 0o600 });
107
+ appendFileSync(this.historyPath(id), this.encryptor.encryptFileContent(line) + "\n", {
108
+ encoding: "utf-8",
109
+ mode: 0o600,
110
+ });
90
111
  }
91
112
  else {
92
113
  appendFileSync(this.historyPath(id), line + "\n", { encoding: "utf-8", mode: 0o600 });
93
114
  }
94
- const meta = this.loadMeta(id);
115
+ // Re-read meta from disk (not the cache) right before the increment so
116
+ // counters appended by ANOTHER process are not clobbered by a stale
117
+ // in-memory snapshot (last-write-wins). Full cross-process locking is out
118
+ // of scope; this narrows the race window.
119
+ const meta = this.readMetaFromDisk(id) ?? this._metaCache.get(id) ?? null;
95
120
  if (meta) {
96
121
  meta.messageCount = (meta.messageCount || 0) + 1;
97
122
  meta.updatedAt = msg.timestamp || new Date().toISOString();
@@ -138,7 +163,10 @@ export class SessionStore {
138
163
  mkdirSync(dir, { recursive: true, mode: 0o700 });
139
164
  const line = JSON.stringify(entry);
140
165
  if (this.encryptor?.isEnabled()) {
141
- appendFileSync(this.sessionLogPath(id), this.encryptor.encryptFileContent(line) + "\n", { encoding: "utf-8", mode: 0o600 });
166
+ appendFileSync(this.sessionLogPath(id), this.encryptor.encryptFileContent(line) + "\n", {
167
+ encoding: "utf-8",
168
+ mode: 0o600,
169
+ });
142
170
  }
143
171
  else {
144
172
  appendFileSync(this.sessionLogPath(id), line + "\n", { encoding: "utf-8", mode: 0o600 });
@@ -204,7 +232,7 @@ export class SessionStore {
204
232
  rotateOldSessions() {
205
233
  const thirtyDaysAgo = new Date();
206
234
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
207
- const sessions = this.listSessions();
235
+ const sessions = this.listSessions().slice();
208
236
  for (const session of sessions) {
209
237
  const updatedAt = new Date(session.updatedAt);
210
238
  if (updatedAt < thirtyDaysAgo) {
@@ -215,6 +243,14 @@ export class SessionStore {
215
243
  const gzPath = join(this.baseDir, `${session.id}.jsonl.gz`);
216
244
  writeFileSync(gzPath, compressed);
217
245
  rmSync(historyPath);
246
+ // Mark the meta so the session stops looking "live" in lists —
247
+ // otherwise a gzipped session still advertises messageCount > 0.
248
+ const meta = this.loadMeta(session.id);
249
+ if (meta) {
250
+ meta.messageCount = 0;
251
+ meta.rotated = true;
252
+ this.saveMeta(session.id, meta);
253
+ }
218
254
  }
219
255
  }
220
256
  }
@@ -12,7 +12,13 @@ export class SkillsLoader {
12
12
  const entries = readdirSync(dirPath);
13
13
  for (const entry of entries) {
14
14
  const fullPath = join(dirPath, entry);
15
- const stat = statSync(fullPath);
15
+ let stat;
16
+ try {
17
+ stat = statSync(fullPath);
18
+ }
19
+ catch {
20
+ continue;
21
+ }
16
22
  if (stat.isDirectory()) {
17
23
  this.scanDir(fullPath, skills);
18
24
  continue;
@@ -1,4 +1,5 @@
1
1
  import { t } from "../../i18n/index";
2
+ import { estimateTokens } from "../../llm/token-counter";
2
3
  export class SkillsModule {
3
4
  name = "skills";
4
5
  availableSkills;
@@ -124,6 +125,6 @@ export class SkillsModule {
124
125
  };
125
126
  }
126
127
  estimateTokens(text) {
127
- return Math.ceil(text.length / 4);
128
+ return estimateTokens(text);
128
129
  }
129
130
  }
@@ -0,0 +1,94 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ /**
4
+ * Read the bundled CHANGELOG.md from the installed npm package.
5
+ * Resolves relative to this file's location (dist/modules/updater/) going up
6
+ * to the package root.
7
+ */
8
+ export function readChangelog(packageName) {
9
+ try {
10
+ // Walk up from this file to find package.json
11
+ let dir = dirname(import.meta.url);
12
+ // Convert file:// URL to path on POSIX, or just use the path directly
13
+ if (dir.startsWith("file://")) {
14
+ dir = decodeURIComponent(dir.slice(7));
15
+ }
16
+ // Find package root (contains package.json with matching name)
17
+ for (let i = 0; i < 10; i++) {
18
+ const pkgPath = join(dir, "package.json");
19
+ if (existsSync(pkgPath)) {
20
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
21
+ if (pkg.name === packageName) {
22
+ const changelogPath = join(dir, "CHANGELOG.md");
23
+ if (existsSync(changelogPath)) {
24
+ return readFileSync(changelogPath, "utf-8");
25
+ }
26
+ return null;
27
+ }
28
+ }
29
+ dir = dirname(dir);
30
+ }
31
+ return null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * Extract changelog entries between two versions (exclusive old, inclusive new).
39
+ * Returns the raw markdown sections.
40
+ *
41
+ * parseChangelog(changelog, "0.55.1", "0.56.1") =>
42
+ * "## [0.56.1] - ...\n...\n## [0.56.0] - ...\n..."
43
+ */
44
+ export function extractChangelogRange(changelog, fromVersion, toVersion) {
45
+ const lines = changelog.split("\n");
46
+ const sections = [];
47
+ let capturing = false;
48
+ let foundFrom = false;
49
+ for (const line of lines) {
50
+ const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
51
+ if (match) {
52
+ const ver = match[1];
53
+ if (ver === fromVersion) {
54
+ // Stop before this section (exclusive).
55
+ foundFrom = true;
56
+ break;
57
+ }
58
+ if (capturing) {
59
+ // New section header — separate with blank line.
60
+ sections.push("");
61
+ }
62
+ capturing = true;
63
+ }
64
+ if (capturing) {
65
+ sections.push(line);
66
+ }
67
+ }
68
+ // If the "from" version was never found, return null (no valid range).
69
+ if (!foundFrom)
70
+ return null;
71
+ return sections.length > 0 ? sections.join("\n").trim() : null;
72
+ }
73
+ /**
74
+ * Get the latest changelog entry (most recent version).
75
+ */
76
+ export function extractLatestChangelog(changelog) {
77
+ const lines = changelog.split("\n");
78
+ const sections = [];
79
+ let capturing = false;
80
+ let found = false;
81
+ for (const line of lines) {
82
+ const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
83
+ if (match) {
84
+ if (found)
85
+ break; // Stop at second section header.
86
+ capturing = true;
87
+ found = true;
88
+ }
89
+ if (capturing) {
90
+ sections.push(line);
91
+ }
92
+ }
93
+ return sections.length > 0 ? sections.join("\n").trim() : null;
94
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * True when the running entry is a TypeScript source file — i.e. a dev run
3
+ * (`bun run src/cli/main.ts`, `bun --watch src/cli/main.ts`, tsx/ts-node).
4
+ * The published package is loaded as bundled `dist/main.js` via `bin/mma.mjs`,
5
+ * so this is false for real installs.
6
+ *
7
+ * Dev runs MUST NOT self-update: a checkout's package.json version can
8
+ * legitimately trail npm, and silently `npm install -g`-ing over the user's
9
+ * global install from a dev working tree is destructive and surprising.
10
+ */
11
+ export function isDevEntryPath(path) {
12
+ const clean = path.split(/[?#]/)[0];
13
+ return (clean.endsWith(".ts") ||
14
+ clean.endsWith(".tsx") ||
15
+ clean.endsWith(".mts") ||
16
+ clean.endsWith(".cts"));
17
+ }
@@ -1,2 +1,3 @@
1
1
  export { Updater } from "./checker";
2
2
  export { UpdaterModule } from "./module";
3
+ export { isDevEntryPath } from "./dev-detect";
@@ -1,6 +1,8 @@
1
1
  import { Updater } from "./checker";
2
+ import { extractLatestChangelog, readChangelog } from "./changelog-reader";
2
3
  import { t } from "../../i18n/index";
3
4
  import { pc } from "../../ui/colors";
5
+ import { defaultOutputBus } from "../../output";
4
6
  /**
5
7
  * Self-update module. Fires an ASYNC (non-blocking) check at startup so the
6
8
  * agent never waits on the network. When `autoInstall` is enabled, a newer
@@ -24,8 +26,8 @@ export class UpdaterModule {
24
26
  this.updater = new Updater(currentVersion, packageName, installRunner);
25
27
  this.logger = logger ?? {
26
28
  info: () => { },
27
- warn: (m) => console.warn(pc.yellow(m)),
28
- error: (m) => console.error(pc.red(m)),
29
+ warn: (m) => defaultOutputBus.log("warn", "updater", m),
30
+ error: (m) => defaultOutputBus.log("error", "updater", m),
29
31
  };
30
32
  }
31
33
  /** Test hook: prevent the module from actually running npm install. */
@@ -96,7 +98,16 @@ export class UpdaterModule {
96
98
  }));
97
99
  const install = await this.updater.install(result.latest);
98
100
  if (install.success) {
99
- this.logger.info(t("updater.installed", { current: result.current, latest: result.latest }));
101
+ const changelog = readChangelog("micro-models-agent");
102
+ const latestEntry = changelog ? extractLatestChangelog(changelog) : null;
103
+ const changelogBlock = latestEntry
104
+ ? pc.cyan(`\n${t("cli.changelog_title", { version: result.latest })}\n`) + latestEntry
105
+ : "";
106
+ this.logger.info(t("updater.installed", {
107
+ current: result.current,
108
+ latest: result.latest,
109
+ changelog: changelogBlock,
110
+ }));
100
111
  }
101
112
  else {
102
113
  this.logger.warn(t("updater.install_failed", {
@@ -0,0 +1,32 @@
1
+ export class OutputBus {
2
+ seq = 0;
3
+ subs = new Set();
4
+ subscribe(fn) {
5
+ this.subs.add(fn);
6
+ return () => this.subs.delete(fn);
7
+ }
8
+ emit(ev) {
9
+ const full = { ...ev, id: ++this.seq, ts: Date.now() };
10
+ for (const fn of [...this.subs]) {
11
+ try {
12
+ fn(full);
13
+ }
14
+ catch {
15
+ /* a subscriber must not break the bus */
16
+ }
17
+ }
18
+ return full;
19
+ }
20
+ log(level, source, text, data) {
21
+ return this.emit({ level, source, kind: "log", text, data });
22
+ }
23
+ /**
24
+ * Test-isolation / rare-use hook: drops all subscribers and restarts the id sequence.
25
+ * Do not call this in production code paths.
26
+ */
27
+ reset() {
28
+ this.subs.clear();
29
+ this.seq = 0;
30
+ }
31
+ }
32
+ export const defaultOutputBus = new OutputBus();
@@ -0,0 +1,233 @@
1
+ import { defaultOutputBus } from "./bus";
2
+ import { formatEventLine } from "./format";
3
+ export class OutputChannel {
4
+ bus;
5
+ out;
6
+ err;
7
+ color;
8
+ mode = "idle";
9
+ modeBeforeQuiet = "idle";
10
+ modeBeforeModal = "idle";
11
+ modalDepth = 0;
12
+ sink = null;
13
+ queue = [];
14
+ pending = null;
15
+ partialOpen = false;
16
+ /**
17
+ * Log/block stdout events captured while a prompt is active. They are
18
+ * flushed once per microtask so a synchronous burst (e.g. a 20-line command
19
+ * result) produces a single `printAbove` + one frame render instead of one
20
+ * per line (which erased/redrew the frame and fired a DSR query each time).
21
+ */
22
+ blockLines = null;
23
+ blockFlushScheduled = false;
24
+ unsub;
25
+ constructor(bus = defaultOutputBus, opts = {}) {
26
+ this.bus = bus;
27
+ this.out = opts.out ?? process.stdout;
28
+ this.err = opts.err ?? process.stderr;
29
+ const outTTY = this.out.isTTY === true;
30
+ this.color = opts.color ?? (!process.env.NO_COLOR && !process.env.CI && outTTY);
31
+ this.unsub = this.bus.subscribe((ev) => this.onEvent(ev));
32
+ }
33
+ dispose() {
34
+ this.flushBlocks();
35
+ this.unsub();
36
+ this.queue = [];
37
+ this.pending = null;
38
+ this.partialOpen = false;
39
+ this.sink = null;
40
+ this.mode = "idle";
41
+ }
42
+ attachPrompt(sink) {
43
+ this.sink = sink;
44
+ }
45
+ getMode() {
46
+ return this.mode;
47
+ }
48
+ beginStreaming() {
49
+ // A streaming transition must never discard the quiet state: `--json`
50
+ // sets quiet for the whole run and streamed runs must stay silent.
51
+ if (this.mode === "quiet")
52
+ return;
53
+ this.flushBlocks();
54
+ this.flushPendingAsBlock();
55
+ this.mode = "streaming";
56
+ }
57
+ endStreaming() {
58
+ // Symmetric with beginStreaming: quiet owns the mode until setQuiet(false).
59
+ if (this.mode === "quiet")
60
+ return;
61
+ this.closePartial();
62
+ this.mode = "idle";
63
+ }
64
+ beginModal() {
65
+ this.flushBlocks();
66
+ this.flushPendingAsBlock();
67
+ if (this.mode === "quiet")
68
+ return; // setQuiet owns quiet
69
+ // Nested modals (e.g. a wizard command inside another exclusive scope)
70
+ // must not overwrite the mode saved by the outermost beginModal.
71
+ if (this.modalDepth === 0)
72
+ this.modeBeforeModal = this.mode;
73
+ this.modalDepth++;
74
+ this.mode = "modal";
75
+ }
76
+ endModal() {
77
+ if (this.modalDepth > 0)
78
+ this.modalDepth--;
79
+ if (this.mode === "modal" && this.modalDepth === 0)
80
+ this.mode = this.modeBeforeModal;
81
+ const q = this.queue;
82
+ this.queue = [];
83
+ for (const ev of q)
84
+ this.onEvent(ev);
85
+ }
86
+ setQuiet(quiet) {
87
+ this.flushBlocks();
88
+ if (quiet) {
89
+ if (this.mode !== "quiet") {
90
+ this.modeBeforeQuiet = this.mode;
91
+ this.mode = "quiet";
92
+ }
93
+ }
94
+ else if (this.mode === "quiet") {
95
+ this.mode = this.modeBeforeQuiet;
96
+ }
97
+ }
98
+ writeRaw(text, stream = "stdout") {
99
+ // Raw output must never jump ahead of queued block lines.
100
+ this.flushBlocks();
101
+ // modal intentionally drops direct writes: the Renderer never runs during a modal prompt.
102
+ if (this.mode === "modal" || this.mode === "quiet")
103
+ return;
104
+ if (this.mode === "idle" && this.sink?.isActive() && stream === "stdout") {
105
+ const prev = this.pending?.stream === "stdout" ? this.pending.text : "";
106
+ this.pending = { stream, text: prev + text };
107
+ this.drainPending();
108
+ return;
109
+ }
110
+ if (stream === "stdout")
111
+ this.partialOpen = !text.endsWith("\n");
112
+ (stream === "stderr" ? this.err : this.out).write(text);
113
+ }
114
+ writeLine(text, stream = "stdout") {
115
+ this.bus.emit({ level: "info", source: "", kind: "block", text, stream });
116
+ }
117
+ clearScreen() {
118
+ this.flushBlocks();
119
+ // A buffered raw partial line would otherwise re-appear after the clear.
120
+ this.flushPendingAsBlock();
121
+ this.partialOpen = false;
122
+ if (this.mode === "quiet")
123
+ return;
124
+ if (this.sink?.isActive() && this.sink.clearScreen) {
125
+ this.sink.clearScreen();
126
+ return;
127
+ }
128
+ this.out.write("\x1b[2J\x1b[H");
129
+ }
130
+ writeOverlay(text) {
131
+ // modal intentionally drops overlays: the Renderer never runs during a modal prompt.
132
+ // Task 6's Spinner must stop before any log so an overlay never shares a row with a log line.
133
+ if (this.mode === "modal" || this.mode === "quiet")
134
+ return;
135
+ if (this.mode === "idle" && this.sink?.isActive())
136
+ return;
137
+ this.err.write(text);
138
+ }
139
+ onEvent(ev) {
140
+ if (this.mode === "quiet")
141
+ return;
142
+ if (this.mode === "modal") {
143
+ this.queue.push(ev);
144
+ return;
145
+ }
146
+ if (ev.kind === "raw") {
147
+ this.writeRaw(ev.text, ev.stream);
148
+ return;
149
+ }
150
+ const stream = ev.stream ?? "stdout";
151
+ const line = ev.kind === "block" ? ev.text : formatEventLine(ev, this.color);
152
+ if (this.mode === "idle" && this.sink?.isActive()) {
153
+ // Batch a synchronous burst into one printAbove/render (see blockLines).
154
+ // Both streams route above the prompt, otherwise a stderr log writes
155
+ // directly over the active input frame and skips flushBlocks ordering.
156
+ if (this.blockLines === null)
157
+ this.blockLines = [];
158
+ this.blockLines.push(line);
159
+ if (!this.blockFlushScheduled) {
160
+ this.blockFlushScheduled = true;
161
+ queueMicrotask(() => this.flushBlocks());
162
+ }
163
+ return;
164
+ }
165
+ // Preserve FIFO ordering: buffered blocks must not be overtaken by an
166
+ // event written straight to a raw stream (e.g. a stderr log mid-stream).
167
+ this.flushBlocks();
168
+ if (this.mode === "streaming")
169
+ this.closePartial();
170
+ (stream === "stderr" ? this.err : this.out).write(`${line}\n`);
171
+ }
172
+ flushBlocks() {
173
+ this.blockFlushScheduled = false;
174
+ const lines = this.blockLines;
175
+ this.blockLines = null;
176
+ if (!lines || lines.length === 0)
177
+ return;
178
+ const text = lines.join("\n");
179
+ if (this.sink?.isActive()) {
180
+ this.sink.printAbove(text);
181
+ }
182
+ else {
183
+ this.out.write(`${text}\n`);
184
+ }
185
+ }
186
+ closePartial() {
187
+ if (this.partialOpen) {
188
+ this.out.write("\n");
189
+ this.partialOpen = false;
190
+ }
191
+ }
192
+ drainPending() {
193
+ if (!this.pending)
194
+ return;
195
+ const { stream, text } = this.pending;
196
+ const idx = text.lastIndexOf("\n");
197
+ if (idx < 0)
198
+ return;
199
+ const head = text.slice(0, idx + 1);
200
+ this.pending = { stream, text: text.slice(idx + 1) };
201
+ const lines = head.replace(/\n$/, "").split("\n");
202
+ if (this.sink?.isActive())
203
+ this.sink.printAbove(lines.join("\n"));
204
+ else
205
+ this.out.write(head);
206
+ }
207
+ flushPendingAsBlock() {
208
+ this.flushBlocks();
209
+ if (!this.pending)
210
+ return;
211
+ const text = this.pending.text;
212
+ const { stream } = this.pending;
213
+ this.pending = null;
214
+ if (this.sink?.isActive()) {
215
+ this.sink.printAbove(text);
216
+ this.partialOpen = false;
217
+ }
218
+ else {
219
+ (stream === "stderr" ? this.err : this.out).write(text);
220
+ this.partialOpen = stream === "stdout" ? !text.endsWith("\n") : this.partialOpen;
221
+ }
222
+ }
223
+ }
224
+ let defaultChannel = null;
225
+ export function getDefaultChannel() {
226
+ if (!defaultChannel)
227
+ defaultChannel = new OutputChannel(defaultOutputBus);
228
+ return defaultChannel;
229
+ }
230
+ export function resetDefaultChannel() {
231
+ defaultChannel?.dispose();
232
+ defaultChannel = null;
233
+ }
@@ -0,0 +1,14 @@
1
+ import { createColors } from "picocolors";
2
+ const colored = createColors(true);
3
+ const LEVEL_COLORS = {
4
+ debug: (s) => colored.dim(s),
5
+ info: (s) => s,
6
+ warn: (s) => colored.yellow(s),
7
+ error: (s) => colored.red(s),
8
+ };
9
+ /** Reproduces the legacy Logger format: `[LEVEL] [source] ISO — text`. */
10
+ export function formatEventLine(ev, color) {
11
+ const prefix = ev.source ? ` [${ev.source}]` : "";
12
+ const line = `[${ev.level.toUpperCase()}]${prefix} ${new Date(ev.ts).toISOString()} — ${ev.text}`;
13
+ return color ? LEVEL_COLORS[ev.level](line) : line;
14
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./types";
2
+ export { OutputBus, defaultOutputBus } from "./bus";
3
+ export { formatEventLine } from "./format";
4
+ export { JsonOutputSink } from "./json-sink";
5
+ export { SessionOutputSink } from "./session-sink";
6
+ export { OutputChannel, getDefaultChannel, resetDefaultChannel, } from "./channel";
7
+ export { writeMachineJson, writeNonTtyLine } from "./machine";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Collects bus events so a machine-readable run (`--json`) can include the
3
+ * warn/error diagnostics that the human channel suppresses via `setQuiet`.
4
+ * Info/debug events are ignored: they are progress noise, not diagnostics.
5
+ */
6
+ export class JsonOutputSink {
7
+ events = [];
8
+ unsub;
9
+ constructor(bus) {
10
+ this.unsub = bus.subscribe((ev) => {
11
+ this.events.push(ev);
12
+ });
13
+ }
14
+ diagnostics() {
15
+ return this.events
16
+ .filter((e) => e.level === "warn" || e.level === "error")
17
+ .map((e) => ({ level: e.level, source: e.source, text: e.text }));
18
+ }
19
+ dispose() {
20
+ this.unsub();
21
+ }
22
+ }
@@ -0,0 +1,8 @@
1
+ /** Machine-readable output (CLI JSON contracts). The only place with raw stdout. */
2
+ export function writeMachineJson(value) {
3
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
4
+ }
5
+ /** Raw stdout write for the non-TTY REPL fallback (documented single-writer exception). */
6
+ export function writeNonTtyLine(text) {
7
+ process.stdout.write(text + "\n");
8
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Persists explicitly marked output events (`data.persist === true`) into the
3
+ * session log. Most output is transient terminal progress and is deliberately
4
+ * NOT persisted; the primary file path for durable records stays
5
+ * `SessionLogger.logSilent` / `logStructured`.
6
+ */
7
+ export class SessionOutputSink {
8
+ session;
9
+ unsub;
10
+ constructor(bus, session) {
11
+ this.session = session;
12
+ this.unsub = bus.subscribe((ev) => {
13
+ if (ev.data?.persist !== true)
14
+ return;
15
+ this.session.appendLog({
16
+ ts: new Date(ev.ts).toISOString(),
17
+ type: "output",
18
+ level: ev.level,
19
+ source: ev.source,
20
+ content: ev.text,
21
+ });
22
+ });
23
+ }
24
+ dispose() {
25
+ this.unsub();
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -21,10 +21,14 @@ export const approveTool = {
21
21
  return { success: false, output: t("tool.interactive_disabled") };
22
22
  }
23
23
  const action = String(args.action || "");
24
- const indexes = await askChoice(t("tool.approve_prompt", { action }), [
24
+ const question = t("tool.approve_prompt", { action });
25
+ const options = [
25
26
  { label: t("tool.approve_yes"), description: t("tool.approve_yes_desc") },
26
27
  { label: t("tool.approve_no"), description: t("tool.approve_no_desc") },
27
- ]);
28
+ ];
29
+ const indexes = ctx.promptIO
30
+ ? await ctx.promptIO.askChoice(question, options, { allowCustom: false })
31
+ : await askChoice(question, options);
28
32
  if (indexes[0] === 0) {
29
33
  return { success: true, output: t("tool.approved") };
30
34
  }