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.
- package/CHANGELOG.md +148 -1
- 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 +7 -4
- package/dist/i18n/ru.json +7 -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 +1606 -800
- 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/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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// src/core/prompt-overflow.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { PromptBuilder } from "./prompt-builder";
|
|
6
|
+
import { estimateTokens } from "../llm/token-counter";
|
|
7
|
+
import { t } from "../i18n/index";
|
|
8
|
+
/** How many tokens the model-facing hint block costs (upper bound for allocation). */
|
|
9
|
+
export const HINT_BLOCK_TOKENS = 60;
|
|
10
|
+
/** Rough chars-per-token factor used for hard truncation fallback. */
|
|
11
|
+
const CHARS_PER_TOKEN = 4;
|
|
12
|
+
const KIND_ORDER = ["instructions", "project-map"];
|
|
13
|
+
const KIND_LABEL = {
|
|
14
|
+
instructions: "project instructions (AGENTS.md)",
|
|
15
|
+
"project-map": "project map",
|
|
16
|
+
};
|
|
17
|
+
const KIND_SOURCE_FILE = {
|
|
18
|
+
instructions: "AGENTS.md",
|
|
19
|
+
"project-map": "",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Static dry-run of the system-prompt budget: which kind-carrying blocks do
|
|
23
|
+
* not fit. Used by bootstrap for the startup banner and by Agent before the
|
|
24
|
+
* first LLM call (the real resolution).
|
|
25
|
+
*/
|
|
26
|
+
export function dryRunOverflow(allBlocks, systemBudget) {
|
|
27
|
+
const builder = new PromptBuilder(systemBudget);
|
|
28
|
+
builder.addBlocks(allBlocks);
|
|
29
|
+
const result = builder.build();
|
|
30
|
+
const includedTokens = result.blocks
|
|
31
|
+
.filter((b) => b.included)
|
|
32
|
+
.reduce((sum, b) => sum + b.tokens, 0);
|
|
33
|
+
const overflow = result.excludedBlocks.filter((b) => Boolean(b.kind));
|
|
34
|
+
return { includedTokens, overflow };
|
|
35
|
+
}
|
|
36
|
+
function cacheKey(kind, content, maxTokens) {
|
|
37
|
+
return createHash("sha256").update(`${kind}|${maxTokens}|${content}`).digest("hex").slice(0, 24);
|
|
38
|
+
}
|
|
39
|
+
function readCache(cacheDir, key) {
|
|
40
|
+
try {
|
|
41
|
+
const path = join(cacheDir, `${key}.md`);
|
|
42
|
+
if (!existsSync(path))
|
|
43
|
+
return null;
|
|
44
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
45
|
+
if (!raw)
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
if ((parsed.mode === "summary" || parsed.mode === "truncate") && typeof parsed.text === "string" && parsed.text) {
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Legacy plain-text entry — treat as a summary hit
|
|
55
|
+
return { mode: "summary", text: raw };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Corrupted cache — fall through to re-summarize
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function writeCache(cacheDir, key, entry) {
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
67
|
+
writeFileSync(join(cacheDir, `${key}.md`), JSON.stringify(entry), "utf-8");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Best-effort — cache failure must not crash the agent
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Collect a full text response from the streaming provider API. */
|
|
74
|
+
async function collectText(provider, prompt, maxTokens) {
|
|
75
|
+
let text = "";
|
|
76
|
+
// Reasoning must be off: a thinking model would burn the completion cap on
|
|
77
|
+
// <think> before emitting any summary content (observed on qwen3.5-9b).
|
|
78
|
+
// 2x headroom over the target: token estimates are heuristics, and the
|
|
79
|
+
// clamp below enforces the real cap no matter what the model returns.
|
|
80
|
+
for await (const chunk of provider.chat([{ role: "user", content: prompt }], undefined, undefined, {
|
|
81
|
+
maxTokens: maxTokens * 2,
|
|
82
|
+
reasoningEffort: "none",
|
|
83
|
+
})) {
|
|
84
|
+
if (chunk.type === "text" && chunk.content)
|
|
85
|
+
text += chunk.content;
|
|
86
|
+
}
|
|
87
|
+
return text.trim();
|
|
88
|
+
}
|
|
89
|
+
function truncateToTokens(content, maxTokens) {
|
|
90
|
+
const maxChars = Math.max(200, maxTokens * CHARS_PER_TOKEN);
|
|
91
|
+
if (content.length <= maxChars)
|
|
92
|
+
return content;
|
|
93
|
+
let cut = content.slice(0, maxChars);
|
|
94
|
+
const lastBreak = cut.lastIndexOf("\n");
|
|
95
|
+
if (lastBreak > maxChars * 0.5)
|
|
96
|
+
cut = cut.slice(0, lastBreak);
|
|
97
|
+
return `${cut.trimEnd()}\n\n[...truncated — read the full file for details...]`;
|
|
98
|
+
}
|
|
99
|
+
/** One-line first-line label for warnings/logs. */
|
|
100
|
+
function blockLabel(content) {
|
|
101
|
+
const first = content.split("\n")[0].trim();
|
|
102
|
+
return first.length > 60 ? `${first.slice(0, 57)}...` : first;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Summarize (or, failing that, truncate) prompt blocks that exceeded the
|
|
106
|
+
* system-prompt budget so the project instructions / project map still reach
|
|
107
|
+
* the model. Summaries are cached on disk keyed by kind + content + budget.
|
|
108
|
+
* Blocks are resolved in KIND_ORDER priority; the first gets the full
|
|
109
|
+
* remaining budget, later blocks get whatever is left.
|
|
110
|
+
*/
|
|
111
|
+
export async function resolvePromptOverflow(opts) {
|
|
112
|
+
const { overflow, includedTokens, systemBudget, provider, cacheDir, logger } = opts;
|
|
113
|
+
const sorted = [...overflow].sort((a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind));
|
|
114
|
+
const replacements = [];
|
|
115
|
+
const warnings = [];
|
|
116
|
+
let used = includedTokens;
|
|
117
|
+
let unresolved = [];
|
|
118
|
+
// The bootstrap dry-run only warns that blocks "will be summarized before the
|
|
119
|
+
// first run". Without a notice here the first prompt appears to hang while
|
|
120
|
+
// the model rewrites AGENTS.md / the project map — surface it.
|
|
121
|
+
if (sorted.length > 0) {
|
|
122
|
+
logger?.warn(t("prompt.overflow.compressing", { count: String(sorted.length) }));
|
|
123
|
+
}
|
|
124
|
+
for (const block of sorted) {
|
|
125
|
+
const kind = block.kind;
|
|
126
|
+
const maxTokens = systemBudget - used - HINT_BLOCK_TOKENS;
|
|
127
|
+
if (maxTokens < 100) {
|
|
128
|
+
unresolved.push(kind);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let mode = "summary";
|
|
132
|
+
let text = "";
|
|
133
|
+
const key = cacheKey(kind, block.content, maxTokens);
|
|
134
|
+
const cached = readCache(cacheDir, key);
|
|
135
|
+
if (cached) {
|
|
136
|
+
text = cached.text;
|
|
137
|
+
mode = cached.mode;
|
|
138
|
+
logger?.debug(`Prompt overflow: cache hit for ${kind} (${key}, ${mode})`);
|
|
139
|
+
}
|
|
140
|
+
else if (provider) {
|
|
141
|
+
const targetChars = Math.floor(maxTokens * (CHARS_PER_TOKEN - 1));
|
|
142
|
+
logger?.warn(t("prompt.overflow.summarizing", {
|
|
143
|
+
label: blockLabel(block.content),
|
|
144
|
+
original: String(block.estimatedTokens),
|
|
145
|
+
budget: String(maxTokens),
|
|
146
|
+
}));
|
|
147
|
+
try {
|
|
148
|
+
const prompt = [
|
|
149
|
+
`Summarize the following ${KIND_LABEL[kind]} document.`,
|
|
150
|
+
// 3 chars/token (not 4): non-Latin text (Cyrillic etc.) is ~2-3 chars
|
|
151
|
+
// per token, so a 4x target lands the summary over the clamp.
|
|
152
|
+
`Target length: at most ${targetChars} characters (~${maxTokens} tokens). Be strict about it.`,
|
|
153
|
+
`Preserve concrete facts: file paths, commands, rules, naming conventions, workflow steps.`,
|
|
154
|
+
`Keep the structure (headings / bullet lists). Output ONLY the summary text.`,
|
|
155
|
+
``,
|
|
156
|
+
`--- DOCUMENT START ---`,
|
|
157
|
+
block.content,
|
|
158
|
+
`--- DOCUMENT END ---`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
text = await collectText(provider, prompt, maxTokens);
|
|
161
|
+
// Small models routinely ignore length instructions (observed: the 9B
|
|
162
|
+
// model produced ~4700 chars against a 3170-char target). One
|
|
163
|
+
// self-compression retry on the overshoot — the shorter input makes
|
|
164
|
+
// compliance much likelier — before falling back to the hard clamp.
|
|
165
|
+
let retries = 0;
|
|
166
|
+
while (text && estimateTokens(text) > maxTokens && retries < 1) {
|
|
167
|
+
retries++;
|
|
168
|
+
logger?.debug(`Prompt overflow: ${kind} summary overshot (${estimateTokens(text)} > ${maxTokens} tok), compressing (retry ${retries})`);
|
|
169
|
+
text = await collectText(provider, [
|
|
170
|
+
`Compress the following text to at most ${targetChars} characters.`,
|
|
171
|
+
`Keep the structure (headings / bullet lists) and all concrete facts: file paths, commands, rules.`,
|
|
172
|
+
`Output ONLY the compressed text.`,
|
|
173
|
+
``,
|
|
174
|
+
`--- TEXT START ---`,
|
|
175
|
+
text,
|
|
176
|
+
`--- TEXT END ---`,
|
|
177
|
+
].join("\n"), maxTokens);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
logger?.warn(t("prompt.overflow.summarize_failed", {
|
|
182
|
+
label: blockLabel(block.content),
|
|
183
|
+
error: String(err?.message ?? err),
|
|
184
|
+
}));
|
|
185
|
+
text = "";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!text) {
|
|
189
|
+
// No provider or summarization failed — hard truncation fallback.
|
|
190
|
+
mode = "truncate";
|
|
191
|
+
logger?.warn(t("prompt.overflow.truncating", {
|
|
192
|
+
label: blockLabel(block.content),
|
|
193
|
+
original: String(block.estimatedTokens),
|
|
194
|
+
}));
|
|
195
|
+
text = truncateToTokens(block.content, maxTokens);
|
|
196
|
+
}
|
|
197
|
+
// LLMs overshoot: enforce the cap no matter what produced the text.
|
|
198
|
+
if (estimateTokens(text) > maxTokens) {
|
|
199
|
+
if (mode === "summary") {
|
|
200
|
+
mode = "truncate";
|
|
201
|
+
}
|
|
202
|
+
text = truncateToTokens(text, maxTokens);
|
|
203
|
+
}
|
|
204
|
+
const resolvedTokens = estimateTokens(text);
|
|
205
|
+
used += resolvedTokens;
|
|
206
|
+
writeCache(cacheDir, key, { mode, text });
|
|
207
|
+
replacements.push({
|
|
208
|
+
content: text,
|
|
209
|
+
priority: "high",
|
|
210
|
+
essential: false,
|
|
211
|
+
estimatedTokens: resolvedTokens,
|
|
212
|
+
kind,
|
|
213
|
+
});
|
|
214
|
+
warnings.push({
|
|
215
|
+
kind,
|
|
216
|
+
label: blockLabel(block.content),
|
|
217
|
+
originalTokens: block.estimatedTokens,
|
|
218
|
+
resolvedTokens,
|
|
219
|
+
mode,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const hintBlock = replacements.length > 0
|
|
223
|
+
? {
|
|
224
|
+
content: [
|
|
225
|
+
`[Instructions note] Some project blocks exceeded the system-prompt budget and were compressed:`,
|
|
226
|
+
...warnings.map((w) => `- ${KIND_LABEL[w.kind]}: ${w.originalTokens} tokens → ${w.resolvedTokens} tokens (${w.mode})`),
|
|
227
|
+
...(unresolved.length > 0
|
|
228
|
+
? unresolved.map((k) => `- ${KIND_LABEL[k]}: did not fit at all — read it yourself if needed`)
|
|
229
|
+
: []),
|
|
230
|
+
...warnings
|
|
231
|
+
.filter((w) => KIND_SOURCE_FILE[w.kind])
|
|
232
|
+
.map((w) => `Read the full ${KIND_SOURCE_FILE[w.kind]} with read_file if details are missing.`),
|
|
233
|
+
].join("\n"),
|
|
234
|
+
priority: "high",
|
|
235
|
+
essential: true,
|
|
236
|
+
estimatedTokens: HINT_BLOCK_TOKENS,
|
|
237
|
+
}
|
|
238
|
+
: null;
|
|
239
|
+
return { replacements, hintBlock, warnings };
|
|
240
|
+
}
|
|
241
|
+
export function resolveContextWindowSource(config) {
|
|
242
|
+
const entries = config.provider.entries ?? [];
|
|
243
|
+
const activeLabel = config.provider.active;
|
|
244
|
+
const activeEntry = entries.find((e) => e.label && e.label === activeLabel) ??
|
|
245
|
+
(entries.length === 1 ? entries[0] : undefined);
|
|
246
|
+
if (activeEntry?.contextWindow) {
|
|
247
|
+
return { source: "entry", label: activeEntry.label, value: activeEntry.contextWindow };
|
|
248
|
+
}
|
|
249
|
+
return { source: "global", value: config.contextWindow };
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Human-facing hint on how to raise the context window, aware of whether the
|
|
253
|
+
* active value comes from the global config (`mma context <N>`) or from a
|
|
254
|
+
* provider entry override (must be edited in the provider domain file).
|
|
255
|
+
*/
|
|
256
|
+
export function contextWindowHint(config, configDir, recommended) {
|
|
257
|
+
const src = resolveContextWindowSource(config);
|
|
258
|
+
if (src.source === "global") {
|
|
259
|
+
return `run "mma context ${recommended}"`;
|
|
260
|
+
}
|
|
261
|
+
return `edit "${join(configDir, "config", "provider.json")}" → provider.entries[label="${src.label}"].contextWindow = ${recommended}`;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Recommend the next standard context size whose system budget (10%)
|
|
265
|
+
* accommodates `neededSystemTokens`. Falls back to the largest known size.
|
|
266
|
+
*/
|
|
267
|
+
export function recommendContextSize(neededSystemTokens) {
|
|
268
|
+
const sizes = [8192, 16384, 32768, 65536, 131072, 262144];
|
|
269
|
+
return (sizes.find((s) => Math.floor(s * 0.1) >= neededSystemTokens) ?? sizes[sizes.length - 1]);
|
|
270
|
+
}
|
|
271
|
+
/** Context window needed so that `systemFraction` of it covers `neededTokens`. */
|
|
272
|
+
export function requiredContextWindow(neededTokens, systemFraction) {
|
|
273
|
+
const fraction = systemFraction > 0 ? systemFraction : 0.1;
|
|
274
|
+
return Math.ceil(neededTokens / fraction);
|
|
275
|
+
}
|
|
276
|
+
/** System-prompt share of the CURRENT window needed to cover `neededTokens`. */
|
|
277
|
+
export function requiredSystemFraction(neededTokens, contextWindow) {
|
|
278
|
+
if (contextWindow <= 0)
|
|
279
|
+
return 1;
|
|
280
|
+
return Math.min(1, Math.ceil((neededTokens / contextWindow) * 100) / 100);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Localized, concrete fix for an oversized system prompt. Instead of only
|
|
284
|
+
* snapping to a coarse standard size (which makes a 32K project jump straight
|
|
285
|
+
* to 131072), it states the raw requirement and both levers: a bigger window
|
|
286
|
+
* or a bigger share of the current window.
|
|
287
|
+
*/
|
|
288
|
+
export function overflowHint(config, configDir, neededTokens) {
|
|
289
|
+
const fraction = config.contextBudget?.systemPrompt ?? 0.1;
|
|
290
|
+
const budget = Math.floor(config.contextWindow * fraction);
|
|
291
|
+
const recommended = recommendContextSize(neededTokens);
|
|
292
|
+
const requiredWindow = requiredContextWindow(neededTokens, fraction);
|
|
293
|
+
const requiredFraction = requiredSystemFraction(neededTokens, config.contextWindow);
|
|
294
|
+
const how = configDir
|
|
295
|
+
? contextWindowHint(config, configDir, recommended)
|
|
296
|
+
: `increase contextWindow (e.g. to ${recommended})`;
|
|
297
|
+
return [
|
|
298
|
+
t("prompt.overflow.hint_needed", {
|
|
299
|
+
needed: neededTokens,
|
|
300
|
+
window: config.contextWindow,
|
|
301
|
+
fraction,
|
|
302
|
+
budget,
|
|
303
|
+
}),
|
|
304
|
+
t("prompt.overflow.hint_window", { required: requiredWindow, recommended, how }),
|
|
305
|
+
t("prompt.overflow.hint_fraction", { fraction: requiredFraction }),
|
|
306
|
+
].join(" ");
|
|
307
|
+
}
|
|
@@ -88,7 +88,7 @@ export class SessionLogger {
|
|
|
88
88
|
const caller = this.getCaller?.();
|
|
89
89
|
this.session?.appendMessage({
|
|
90
90
|
role: "assistant",
|
|
91
|
-
content
|
|
91
|
+
content,
|
|
92
92
|
timestamp: new Date().toISOString(),
|
|
93
93
|
provider: caller?.provider,
|
|
94
94
|
model: caller?.model,
|
|
@@ -117,7 +117,7 @@ export class SessionLogger {
|
|
|
117
117
|
const caller = this.getCaller?.();
|
|
118
118
|
this.session?.appendMessage({
|
|
119
119
|
role: "tool",
|
|
120
|
-
content: sanitizeLogMessage(result.output
|
|
120
|
+
content: sanitizeLogMessage(result.output),
|
|
121
121
|
name: call.name,
|
|
122
122
|
timestamp: new Date().toISOString(),
|
|
123
123
|
provider: caller?.provider,
|
|
@@ -191,6 +191,11 @@ export class SessionLogger {
|
|
|
191
191
|
totalTokens: usage.totalTokens,
|
|
192
192
|
source: usage.source,
|
|
193
193
|
durationMs: usage.durationMs,
|
|
194
|
+
cacheStable: usage.prefix ? Number(usage.prefix.stableRatio.toFixed(4)) : undefined,
|
|
195
|
+
cacheCause: usage.prefix?.cause,
|
|
196
|
+
cachedTokens: usage.cache?.cachedTokens,
|
|
197
|
+
cacheWriteTokens: usage.cache?.cacheWriteTokens,
|
|
198
|
+
cacheSource: usage.cache?.source,
|
|
194
199
|
});
|
|
195
200
|
}
|
|
196
201
|
logError(message) {
|
|
@@ -248,4 +253,31 @@ export class SessionLogger {
|
|
|
248
253
|
skills: data.skills,
|
|
249
254
|
});
|
|
250
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Log a reasoning control event (level set, policy evaluation).
|
|
258
|
+
*/
|
|
259
|
+
logReasoningControl(data) {
|
|
260
|
+
this.session?.appendLog({
|
|
261
|
+
ts: new Date().toISOString(),
|
|
262
|
+
type: "reasoning_control",
|
|
263
|
+
content: `reasoning level: ${data.level} (source: ${data.source ?? "policy"})`,
|
|
264
|
+
iteration: data.iteration,
|
|
265
|
+
level: data.level,
|
|
266
|
+
strategy: data.strategy,
|
|
267
|
+
probePassed: data.probePassed,
|
|
268
|
+
source: data.source,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Log a MoE observability event (moe_plan / moe_subtask / moe_replan /
|
|
273
|
+
* moe_verify). Emitted by runWithMoE and MoEExecutor (Plan 4.2).
|
|
274
|
+
*/
|
|
275
|
+
logMoE(event, data) {
|
|
276
|
+
this.session?.appendLog({
|
|
277
|
+
ts: new Date().toISOString(),
|
|
278
|
+
type: event,
|
|
279
|
+
content: data.detail ?? data.explanation ?? event,
|
|
280
|
+
...data,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
251
283
|
}
|
package/dist/i18n/en.json
CHANGED
|
@@ -340,6 +340,7 @@
|
|
|
340
340
|
"cli.show_details": "Show session details",
|
|
341
341
|
"cli.delete_session": "Delete a session",
|
|
342
342
|
"cli.first_run": "First run detected. Running setup wizard...",
|
|
343
|
+
"cli.setup_saved_restart": "Settings saved. Restarting with the new configuration...",
|
|
343
344
|
"cli.unknown_cmd": "Unknown command: {name}.",
|
|
344
345
|
"cli.unknown_command": "Unknown command \"{input}\". Did you mean \"{suggestion}\"?",
|
|
345
346
|
"cli.help_hint": "Type /help for available commands.",
|
|
@@ -441,6 +442,8 @@
|
|
|
441
442
|
"repl.skill_usage": "Usage: /skill [list|loaded|load|unload|search]",
|
|
442
443
|
"repl.agent": "Agent: ",
|
|
443
444
|
"repl.you": "You: ",
|
|
445
|
+
"repl.you_reasoning": "You [{level}]: ",
|
|
446
|
+
"repl.you_reasoning_cont": "You [{level}]… ",
|
|
444
447
|
"repl.interrupt": "Interrupted (Esc)",
|
|
445
448
|
"repl.title": "MMA REPL v{version}",
|
|
446
449
|
"repl.model": "Model:",
|
|
@@ -509,6 +512,7 @@
|
|
|
509
512
|
"setup.title": "\n MMA Setup Wizard v2\n",
|
|
510
513
|
"setup.select_provider": "\n Select provider type:",
|
|
511
514
|
"setup.select_provider_num": "\n Select provider (1-{max})",
|
|
515
|
+
"setup.select_language_num": "\n Select language (1-{max})",
|
|
512
516
|
"setup.scanning": " Scanning local LLM servers...",
|
|
513
517
|
"setup.found_servers": " Found {count} server(s):",
|
|
514
518
|
"setup.no_servers": " No local servers found. Enter URL manually.",
|
|
@@ -533,8 +537,6 @@
|
|
|
533
537
|
"setup.scanning_spinner": "Scanning local LLM servers…",
|
|
534
538
|
"setup.fetching_spinner": "Fetching model list…",
|
|
535
539
|
"setup.testing_spinner": "Testing chat with \"{model}\"…",
|
|
536
|
-
"setup.summary_setting": "Setting",
|
|
537
|
-
"setup.summary_value": "Value",
|
|
538
540
|
"setup.security_header": "\n --- Security ---",
|
|
539
541
|
"setup.security_status_off": " Security: OFF (default — all commands allowed)",
|
|
540
542
|
"setup.security_status_on": " Security: ON (balanced policy by default)",
|
|
@@ -585,6 +587,7 @@
|
|
|
585
587
|
"exec.error_search_failed": "Web search returned nothing for \"{query}\".",
|
|
586
588
|
"exec.error_search_no_query": "Error output is not meaningful — skipping the web search.",
|
|
587
589
|
"exec.npm_exec_hint": "\"could not determine executable to run\" — no \"bin\" for that package/script. Use \"npm run <script>\" (script must exist in package.json) or \"bunx <pkg>\" for a package that declares a bin.",
|
|
590
|
+
"exec.interactive_hint": "the command needed interactive input but the shell has no TTY (prompt aborted). Re-run it non-interactively — pass every required flag/argument (e.g. `bun create vite <name> --template react-ts` instead of a bare interactive scaffold).",
|
|
588
591
|
"exec.hidden_tool_hint": "\"{tool}\" is not a shell command — it is an MMA tool that is currently hidden. Call the enable_tools tool with tags [\"shell\"] to unlock it; it becomes available on the next iteration.",
|
|
589
592
|
"exec.task_reminder": "Task: {task}. Continue making progress — do not repeat failed actions.",
|
|
590
593
|
"hall.max_retries_exhausted": "Model returned empty or insufficient responses after multiple retries",
|
|
@@ -762,9 +765,9 @@
|
|
|
762
765
|
"prompt.overflow.hint_needed": "the prompt needs ~{needed} system tokens (current budget: {window} × {fraction} = {budget})",
|
|
763
766
|
"prompt.overflow.hint_window": "raise contextWindow to at least {required} (standard size {recommended}) — {how}",
|
|
764
767
|
"prompt.overflow.hint_fraction": "or keep the window and raise contextBudget.systemPrompt to ~{fraction} (run: mma context --system {fraction})",
|
|
765
|
-
"prompt.overflow.exceeded": "Prompt overflow:
|
|
768
|
+
"prompt.overflow.exceeded": "Prompt overflow: the system prompt needs ~{needed} tok (budget {budget} tok); \"{label}\" ({original} tok) was {mode} to {resolved} tok. To fit everything fully, {hint}.",
|
|
766
769
|
"prompt.overflow.failed": "Prompt overflow resolution failed: {error} — oversized blocks will be dropped.",
|
|
767
|
-
"prompt.overflow.startup": "Startup check: {
|
|
770
|
+
"prompt.overflow.startup": "Startup check: the system prompt needs ~{needed} tok (budget: {budget} tok); {block} ({original} tok) will be summarized/truncated before the first run. To include everything fully, {hint}.",
|
|
768
771
|
"tool.session_info.no_active": "No active session",
|
|
769
772
|
"tool.session_info.result": "Session: \"{name}\" ({id})\nModel: {model}\nProvider: {provider}\nContext window: {contextWindow} tokens\nMessages: {messages}\nCreated: {createdAt}\nUpdated: {updatedAt}",
|
|
770
773
|
"tool.session_info.context": "Context used: {used} / {budget} tokens ({percent}%)",
|
package/dist/i18n/ru.json
CHANGED
|
@@ -334,6 +334,7 @@
|
|
|
334
334
|
"cli.show_details": "Показать детали сессии",
|
|
335
335
|
"cli.delete_session": "Удалить сессию",
|
|
336
336
|
"cli.first_run": "Первый запуск. Запускаем мастер настройки...",
|
|
337
|
+
"cli.setup_saved_restart": "Настройки сохранены. Перезапуск с новой конфигурацией...",
|
|
337
338
|
"cli.unknown_cmd": "Неизвестная команда: {name}.",
|
|
338
339
|
"cli.unknown_command": "Неизвестная команда \"{input}\". Возможно, вы имели в виду \"{suggestion}\"?",
|
|
339
340
|
"cli.help_hint": "Введите /help для списка команд.",
|
|
@@ -435,6 +436,8 @@
|
|
|
435
436
|
"repl.skill_usage": "Использование: /skill [list|loaded|load|unload|search]",
|
|
436
437
|
"repl.agent": "Агент: ",
|
|
437
438
|
"repl.you": "Вы: ",
|
|
439
|
+
"repl.you_reasoning": "Вы [{level}]: ",
|
|
440
|
+
"repl.you_reasoning_cont": "Вы [{level}]… ",
|
|
438
441
|
"repl.interrupt": "Прервано (Esc)",
|
|
439
442
|
"repl.title": "MMA REPL v{version}",
|
|
440
443
|
"repl.model": "Модель:",
|
|
@@ -507,6 +510,7 @@
|
|
|
507
510
|
"setup.title": "\n Мастер настройки MMA v2\n",
|
|
508
511
|
"setup.select_provider": "\n Выберите тип провайдера:",
|
|
509
512
|
"setup.select_provider_num": "\n Выберите провайдер (1-{max})",
|
|
513
|
+
"setup.select_language_num": "\n Выберите язык (1-{max})",
|
|
510
514
|
"setup.scanning": " Сканирование локальных LLM-серверов...",
|
|
511
515
|
"setup.found_servers": " Найдено серверов: {count}",
|
|
512
516
|
"setup.no_servers": " Локальные серверы не найдены. Введите URL вручную.",
|
|
@@ -531,8 +535,6 @@
|
|
|
531
535
|
"setup.scanning_spinner": "Сканирование локальных LLM-серверов…",
|
|
532
536
|
"setup.fetching_spinner": "Загрузка списка моделей…",
|
|
533
537
|
"setup.testing_spinner": "Проверка чата с \"{model}\"…",
|
|
534
|
-
"setup.summary_setting": "Параметр",
|
|
535
|
-
"setup.summary_value": "Значение",
|
|
536
538
|
"setup.security_header": "\n --- Безопасность ---",
|
|
537
539
|
"setup.security_status_off": " Безопасность: ВЫКЛ (по умолчанию — все команды разрешены)",
|
|
538
540
|
"setup.security_status_on": " Безопасность: ВКЛ (по умолчанию — политика balanced)",
|
|
@@ -583,6 +585,7 @@
|
|
|
583
585
|
"exec.error_search_failed": "Поиск в интернете для \"{query}\" ничего не дал.",
|
|
584
586
|
"exec.error_search_no_query": "Текст ошибки незначимый — поиск в интернете пропущен.",
|
|
585
587
|
"exec.npm_exec_hint": "\"could not determine executable to run\" — у пакета/скрипта нет \"bin\". Используй \"npm run <script>\" (скрипт должен быть в package.json) или \"bunx <pkg>\" для пакета с объявленным bin.",
|
|
588
|
+
"exec.interactive_hint": "команде нужен интерактивный ввод, но у шелла нет TTY (запрос отменён). Запусти её неинтерактивно — передай все нужные флаги/аргументы (например, `bun create vite <name> --template react-ts` вместо голого интерактивного скаффолда).",
|
|
586
589
|
"exec.hidden_tool_hint": "\"{tool}\" — не команда оболочки, это инструмент MMA, который сейчас скрыт. Вызови тул enable_tools с tags [\"shell\"], чтобы включить его; он станет доступен на следующей итерации.",
|
|
587
590
|
"exec.task_reminder": "Задача: {task}. Продолжай работу — не повторяй неудачные действия.",
|
|
588
591
|
"hall.max_retries_exhausted": "Модель вернула пустой или недостаточный ответ после нескольких попыток",
|
|
@@ -762,9 +765,9 @@
|
|
|
762
765
|
"prompt.overflow.hint_needed": "промпту нужно ~{needed} токенов системного бюджета (сейчас: {window} × {fraction} = {budget})",
|
|
763
766
|
"prompt.overflow.hint_window": "подними contextWindow минимум до {required} (стандартный размер {recommended}) — {how}",
|
|
764
767
|
"prompt.overflow.hint_fraction": "или оставь окно и подними contextBudget.systemPrompt до ~{fraction} (выполни: mma context --system {fraction})",
|
|
765
|
-
"prompt.overflow.exceeded": "Промпт переполнен:
|
|
768
|
+
"prompt.overflow.exceeded": "Промпт переполнен: системному промпту нужно ~{needed} токенов (бюджет {budget} токенов); \"{label}\" ({original} токенов) было {mode} до {resolved} токенов. Чтобы включить всё полностью, {hint}.",
|
|
766
769
|
"prompt.overflow.failed": "Не удалось разрешить переполнение промпта: {error} — слишком большие блоки будут отброшены.",
|
|
767
|
-
"prompt.overflow.startup": "Проверка при старте: {
|
|
770
|
+
"prompt.overflow.startup": "Проверка при старте: системному промпту нужно ~{needed} токенов (бюджет: {budget} токенов); {block} ({original} токенов) будет суммаризован/обрезан перед первым запуском. Чтобы включить всё полностью, {hint}.",
|
|
768
771
|
"tool.session_info.no_active": "Нет активной сессии",
|
|
769
772
|
"tool.session_info.result": "Сессия: \"{name}\" ({id})\nМодель: {model}\nПровайдер: {provider}\nКонтекстное окно: {contextWindow} токенов\nСообщений: {messages}\nСоздана: {createdAt}\nОбновлена: {updatedAt}",
|
|
770
773
|
"tool.session_info.context": "Использовано контекста: {used} / {budget} токенов ({percent}%)",
|
package/dist/index.js
CHANGED
|
@@ -7,16 +7,20 @@ export { MigrationDetector, BackupManager } from "./migration/index";
|
|
|
7
7
|
import { MigrationDetector } from "./migration/detect";
|
|
8
8
|
import { BackupManager } from "./migration/backup";
|
|
9
9
|
import { t } from "./i18n/index";
|
|
10
|
+
import { defaultOutputBus, getDefaultChannel } from "./output";
|
|
10
11
|
import { homedir } from "os";
|
|
11
12
|
import { join } from "path";
|
|
12
13
|
export function checkMigration(configDir) {
|
|
13
14
|
const dir = configDir || join(homedir(), ".mma");
|
|
14
15
|
const detector = new MigrationDetector(dir);
|
|
15
16
|
if (detector.needsMigration()) {
|
|
17
|
+
// Ensure a terminal subscriber exists before emitting to the bus; library
|
|
18
|
+
// consumers have no other eager channel creation.
|
|
19
|
+
getDefaultChannel();
|
|
16
20
|
const backup = new BackupManager(dir);
|
|
17
21
|
backup.backupConfig();
|
|
18
22
|
backup.backupAll();
|
|
19
23
|
const summary = backup.getBackupSummary();
|
|
20
|
-
|
|
24
|
+
defaultOutputBus.log("info", "mma", t("migration.summary", { summary }));
|
|
21
25
|
}
|
|
22
26
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Безопасно приводит значение к конечному числу; иначе undefined. */
|
|
2
|
+
function num(value) {
|
|
3
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
4
|
+
}
|
|
5
|
+
/** Объект ли это (для защиты от null/строк в ответе). */
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Разбирает usage-объект провайдера в нормализованный `CacheUsage` согласно
|
|
11
|
+
* формату отчёта. Возвращает undefined, когда провайдер не сообщает о кеше —
|
|
12
|
+
* вызывающий не должен выдавать отсутствие данных за «cache miss».
|
|
13
|
+
*
|
|
14
|
+
* `estimatedPromptTokens` нужен только для формата `ollama`, который не
|
|
15
|
+
* отдаёт cache-полей: кешированное вычисляется как разница оценки и
|
|
16
|
+
* `prompt_eval_count`.
|
|
17
|
+
*/
|
|
18
|
+
export function parseCacheUsage(usage, format, estimatedPromptTokens) {
|
|
19
|
+
if (!isRecord(usage))
|
|
20
|
+
return undefined;
|
|
21
|
+
switch (format) {
|
|
22
|
+
case "openai": {
|
|
23
|
+
const details = usage.prompt_tokens_details;
|
|
24
|
+
if (!isRecord(details))
|
|
25
|
+
return undefined;
|
|
26
|
+
const cached = num(details.cached_tokens);
|
|
27
|
+
if (cached === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const prompt = num(usage.prompt_tokens) ?? cached;
|
|
30
|
+
const write = num(details.cache_write_tokens) ?? 0;
|
|
31
|
+
return {
|
|
32
|
+
cachedTokens: cached,
|
|
33
|
+
cacheWriteTokens: write,
|
|
34
|
+
uncachedTokens: Math.max(0, prompt - cached),
|
|
35
|
+
source: "api",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
case "deepseek": {
|
|
39
|
+
const hit = num(usage.prompt_cache_hit_tokens);
|
|
40
|
+
const miss = num(usage.prompt_cache_miss_tokens);
|
|
41
|
+
if (hit === undefined && miss === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
return {
|
|
44
|
+
cachedTokens: hit ?? 0,
|
|
45
|
+
cacheWriteTokens: 0,
|
|
46
|
+
uncachedTokens: miss ?? 0,
|
|
47
|
+
source: "api",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
case "anthropic": {
|
|
51
|
+
const read = num(usage.cache_read_input_tokens);
|
|
52
|
+
const creation = num(usage.cache_creation_input_tokens);
|
|
53
|
+
if (read === undefined && creation === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
return {
|
|
56
|
+
cachedTokens: read ?? 0,
|
|
57
|
+
cacheWriteTokens: creation ?? 0,
|
|
58
|
+
uncachedTokens: num(usage.input_tokens) ?? 0,
|
|
59
|
+
source: "api",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case "ollama": {
|
|
63
|
+
const evaluated = num(usage.prompt_eval_count);
|
|
64
|
+
if (evaluated === undefined || estimatedPromptTokens === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
return {
|
|
67
|
+
cachedTokens: Math.max(0, estimatedPromptTokens - evaluated),
|
|
68
|
+
cacheWriteTokens: 0,
|
|
69
|
+
uncachedTokens: evaluated,
|
|
70
|
+
source: "derived",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/llm/image-utils.js
CHANGED
|
@@ -41,27 +41,31 @@ async function readClipboardFallback() {
|
|
|
41
41
|
const { execSync } = await import("child_process");
|
|
42
42
|
const { readFileSync, unlinkSync } = await import("fs");
|
|
43
43
|
const { join } = await import("path");
|
|
44
|
+
if (platform() !== "linux")
|
|
45
|
+
return null; // macOS/Windows use Bun.Image
|
|
44
46
|
const tmpPath = join(process.env.TEMP || process.env.TMP || "/tmp", `mma-clip-${Date.now()}.png`);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
else {
|
|
52
|
-
return null; // macOS/Windows should use Bun.Image
|
|
53
|
-
}
|
|
54
|
-
const buf = readFileSync(tmpPath);
|
|
55
|
-
unlinkSync(tmpPath);
|
|
56
|
-
return buf.length > 0 ? buf : null;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
47
|
+
// Try Wayland (wl-paste) first, then X11 (xclip)
|
|
48
|
+
const commands = [
|
|
49
|
+
`wl-paste --type image/png > "${tmpPath}" 2>/dev/null`,
|
|
50
|
+
`xclip -selection clipboard -t image/png -o > "${tmpPath}" 2>/dev/null`,
|
|
51
|
+
];
|
|
52
|
+
for (const cmd of commands) {
|
|
59
53
|
try {
|
|
54
|
+
execSync(cmd, { timeout: 5000 });
|
|
55
|
+
const buf = readFileSync(tmpPath);
|
|
60
56
|
unlinkSync(tmpPath);
|
|
57
|
+
if (buf.length > 0)
|
|
58
|
+
return buf;
|
|
61
59
|
}
|
|
62
|
-
catch {
|
|
63
|
-
|
|
60
|
+
catch {
|
|
61
|
+
// try next command
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
unlinkSync(tmpPath);
|
|
64
66
|
}
|
|
67
|
+
catch { }
|
|
68
|
+
return null;
|
|
65
69
|
}
|
|
66
70
|
/**
|
|
67
71
|
* Load an image from a file path, resize via Bun.Image, return as JPEG data URL.
|