micro-models-agent 0.63.0 → 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.
- package/CHANGELOG.md +174 -0
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +8 -4
- package/dist/i18n/ru.json +8 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1755 -841
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/bridge-server.mjs +37 -4
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- 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), {
|
|
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", {
|
|
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
|
-
|
|
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", {
|
|
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
|
-
|
|
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
|
|
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,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) =>
|
|
28
|
-
error: (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
|
-
|
|
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 {};
|
package/dist/tools/approve.js
CHANGED
|
@@ -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
|
|
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
|
}
|