blun-king-cli 9.1.284 → 9.1.286
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/bin/input-draft-persistence.cjs +77 -0
- package/bin/model-retry-progress-policy.cjs +46 -0
- package/blun.mjs +76 -1
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs/promises');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const MAX_INPUT_DRAFT_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_INPUT_DRAFT_FILE_BYTES = MAX_INPUT_DRAFT_BYTES + 1024;
|
|
9
|
+
const SCHEMA_VERSION = 1;
|
|
10
|
+
const VALID_MODES = new Set(['bash', 'prompt']);
|
|
11
|
+
|
|
12
|
+
function normalizeInputDraft(value) {
|
|
13
|
+
if (!value || typeof value !== 'object') return null;
|
|
14
|
+
if (!VALID_MODES.has(value.mode) || typeof value.text !== 'string') return null;
|
|
15
|
+
if (value.text.length === 0) return null;
|
|
16
|
+
if (Buffer.byteLength(value.text, 'utf8') > MAX_INPUT_DRAFT_BYTES) return null;
|
|
17
|
+
return { mode: value.mode, text: value.text };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function clearInputDraft(filePath) {
|
|
21
|
+
await fs.rm(path.resolve(filePath), { force: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readInputDraft(filePath) {
|
|
25
|
+
const target = path.resolve(filePath);
|
|
26
|
+
try {
|
|
27
|
+
const stat = await fs.lstat(target);
|
|
28
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_INPUT_DRAFT_FILE_BYTES) return null;
|
|
29
|
+
const record = JSON.parse(await fs.readFile(target, 'utf8'));
|
|
30
|
+
if (record?.schemaVersion !== SCHEMA_VERSION) return null;
|
|
31
|
+
return normalizeInputDraft(record);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function writeInputDraft(filePath, value) {
|
|
38
|
+
const target = path.resolve(filePath);
|
|
39
|
+
const text = typeof value?.text === 'string' ? value.text : '';
|
|
40
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_INPUT_DRAFT_BYTES) {
|
|
41
|
+
await clearInputDraft(target);
|
|
42
|
+
throw new Error('draft_too_large');
|
|
43
|
+
}
|
|
44
|
+
if (text.length === 0) {
|
|
45
|
+
await clearInputDraft(target);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const draft = normalizeInputDraft(value);
|
|
49
|
+
if (draft === null) throw new Error('draft_invalid');
|
|
50
|
+
|
|
51
|
+
const directory = path.dirname(target);
|
|
52
|
+
const temporary = path.join(
|
|
53
|
+
directory,
|
|
54
|
+
`.${path.basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`,
|
|
55
|
+
);
|
|
56
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
57
|
+
try {
|
|
58
|
+
await fs.writeFile(
|
|
59
|
+
temporary,
|
|
60
|
+
`${JSON.stringify({ schemaVersion: SCHEMA_VERSION, ...draft })}\n`,
|
|
61
|
+
{ encoding: 'utf8', flag: 'wx', mode: 0o600 },
|
|
62
|
+
);
|
|
63
|
+
await fs.rename(temporary, target);
|
|
64
|
+
await fs.chmod(target, 0o600).catch(() => {});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
await fs.rm(temporary, { force: true }).catch(() => {});
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
MAX_INPUT_DRAFT_BYTES,
|
|
73
|
+
clearInputDraft,
|
|
74
|
+
normalizeInputDraft,
|
|
75
|
+
readInputDraft,
|
|
76
|
+
writeInputDraft,
|
|
77
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_MODEL_RETRY_HINT_CHARS = 32;
|
|
4
|
+
const MAX_MODEL_RETRY_ATTEMPTS = 99;
|
|
5
|
+
const MAX_MODEL_RETRY_DELAY_MS = 600_000;
|
|
6
|
+
|
|
7
|
+
function boundedInteger(value, minimum, maximum, fallback) {
|
|
8
|
+
if (!Number.isFinite(value)) return fallback;
|
|
9
|
+
return Math.min(maximum, Math.max(minimum, Math.trunc(value)));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatRetryDelay(delayMs) {
|
|
13
|
+
const bounded = boundedInteger(delayMs, 0, MAX_MODEL_RETRY_DELAY_MS, 0);
|
|
14
|
+
if (bounded === 0) return '';
|
|
15
|
+
if (bounded < 1_000) return `${bounded}ms`;
|
|
16
|
+
return `${Math.ceil(bounded / 100) / 10}s`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatModelRetryProgress(event = {}) {
|
|
20
|
+
const maxAttempts = boundedInteger(
|
|
21
|
+
event.maxAttempts,
|
|
22
|
+
1,
|
|
23
|
+
MAX_MODEL_RETRY_ATTEMPTS,
|
|
24
|
+
1,
|
|
25
|
+
);
|
|
26
|
+
const failedAttempt = boundedInteger(
|
|
27
|
+
event.failedAttempt,
|
|
28
|
+
0,
|
|
29
|
+
maxAttempts - 1,
|
|
30
|
+
0,
|
|
31
|
+
);
|
|
32
|
+
const nextAttempt = boundedInteger(
|
|
33
|
+
event.nextAttempt,
|
|
34
|
+
1,
|
|
35
|
+
maxAttempts,
|
|
36
|
+
Math.min(maxAttempts, failedAttempt + 1),
|
|
37
|
+
);
|
|
38
|
+
const delay = formatRetryDelay(event.delayMs);
|
|
39
|
+
const hint = `Retry ${nextAttempt}/${maxAttempts}${delay ? ` | ${delay}` : ''}`;
|
|
40
|
+
return hint.slice(0, MAX_MODEL_RETRY_HINT_CHARS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
MAX_MODEL_RETRY_HINT_CHARS,
|
|
45
|
+
formatModelRetryProgress,
|
|
46
|
+
};
|
package/blun.mjs
CHANGED
|
@@ -328722,6 +328722,7 @@ const BLUN_UPDATE_STATE_FILE_NAME = "latest.json";
|
|
|
328722
328722
|
const BLUN_UPDATE_INSTALL_STATE_FILE_NAME = "install.json";
|
|
328723
328723
|
const BLUN_UPDATE_ROLLOUT_LOG_FILE_NAME = "rollout.log";
|
|
328724
328724
|
const BLUN_INPUT_HISTORY_DIR_NAME = "user-history";
|
|
328725
|
+
const BLUN_INPUT_DRAFT_DIR_NAME = "input-drafts";
|
|
328725
328726
|
const BLUN_BANNER_DIR_NAME = "banner";
|
|
328726
328727
|
const BLUN_BANNER_STATE_FILE_NAME = "state.json";
|
|
328727
328728
|
const BLUN_STARTUP_DIR_NAME = "startup";
|
|
@@ -340176,6 +340177,10 @@ function getInputHistoryFile(workDir) {
|
|
|
340176
340177
|
const hash = createHash("md5").update(workDir, "utf-8").digest("hex");
|
|
340177
340178
|
return join(getDataDir(), BLUN_INPUT_HISTORY_DIR_NAME, `${hash}.jsonl`);
|
|
340178
340179
|
}
|
|
340180
|
+
function getInputDraftFile(workDir, profile) {
|
|
340181
|
+
const hash = createHash("sha256").update(`${profile}\0${workDir}`, "utf-8").digest("hex");
|
|
340182
|
+
return join(getDataDir(), BLUN_INPUT_DRAFT_DIR_NAME, `${hash}.json`);
|
|
340183
|
+
}
|
|
340179
340184
|
//#endregion
|
|
340180
340185
|
//#region src/cli/build-info.ts
|
|
340181
340186
|
function optionalBuildString(value) {
|
|
@@ -496904,6 +496909,7 @@ async function appendJsonlLine(filePath, lineSchema, value) {
|
|
|
496904
496909
|
//#region src/utils/history/input-history.ts
|
|
496905
496910
|
init_zod$1();
|
|
496906
496911
|
const InputHistoryEntrySchema = object({ content: string() });
|
|
496912
|
+
var { readInputDraft, writeInputDraft } = createRequire(import.meta.url)("./bin/input-draft-persistence.cjs");
|
|
496907
496913
|
async function loadInputHistory(file) {
|
|
496908
496914
|
return readJsonlFile(file, InputHistoryEntrySchema);
|
|
496909
496915
|
}
|
|
@@ -504234,6 +504240,7 @@ var EditorKeyboardController = class {
|
|
|
504234
504240
|
if (!editor.wasGhostAcceptance()) this.inlineSuggest?.notifyActivity();
|
|
504235
504241
|
if (this.pendingExit) this.clearPendingExit();
|
|
504236
504242
|
host.updateEditorBorderHighlight(text);
|
|
504243
|
+
host.scheduleInputDraftPersist(text, editor.inputMode);
|
|
504237
504244
|
};
|
|
504238
504245
|
let browseMode = null;
|
|
504239
504246
|
editor.setHistoryFilter((entry) => {
|
|
@@ -504360,6 +504367,7 @@ var EditorKeyboardController = class {
|
|
|
504360
504367
|
};
|
|
504361
504368
|
editor.onInputModeChange = (mode) => {
|
|
504362
504369
|
host.handleInputModeChange(mode);
|
|
504370
|
+
host.scheduleInputDraftPersist(editor.getText(), mode);
|
|
504363
504371
|
};
|
|
504364
504372
|
editor.onOpenExternalEditor = () => {
|
|
504365
504373
|
host.track("shortcut_editor");
|
|
@@ -507172,6 +507180,7 @@ function normalizeJson(value) {
|
|
|
507172
507180
|
}
|
|
507173
507181
|
//#endregion
|
|
507174
507182
|
//#region src/tui/controllers/session-event-handler.ts
|
|
507183
|
+
var { formatModelRetryProgress } = createRequire(import.meta.url)("./bin/model-retry-progress-policy.cjs");
|
|
507175
507184
|
const MANAGED_TELEGRAM_MCP_SERVER = "plugin-telegram:telegram";
|
|
507176
507185
|
function isManagedTelegramMissingTokenFailure(server) {
|
|
507177
507186
|
return server.name === MANAGED_TELEGRAM_MCP_SERVER && server.status === "failed" && server.error?.includes("BLUN_TELEGRAM_BOT_TOKEN required") === true;
|
|
@@ -507212,7 +507221,9 @@ var SessionEventHandler = class {
|
|
|
507212
507221
|
queuedGoalPromotionInFlight = false;
|
|
507213
507222
|
queuedGoalPromotionTimer;
|
|
507214
507223
|
lastCompactionInstruction;
|
|
507224
|
+
modelRetryHint;
|
|
507215
507225
|
resetRuntimeState() {
|
|
507226
|
+
this.clearModelRetryHint();
|
|
507216
507227
|
this.turnWatchdog.stop();
|
|
507217
507228
|
this.backgroundTasks.clear();
|
|
507218
507229
|
this.backgroundTaskTranscriptedTerminal.clear();
|
|
@@ -507312,7 +507323,9 @@ var SessionEventHandler = class {
|
|
|
507312
507323
|
this.handleStepCompleted(event);
|
|
507313
507324
|
this.host.releaseChannelQueueAtSafePoint();
|
|
507314
507325
|
break;
|
|
507315
|
-
case "turn.step.retrying":
|
|
507326
|
+
case "turn.step.retrying":
|
|
507327
|
+
this.handleStepRetrying(event);
|
|
507328
|
+
break;
|
|
507316
507329
|
case "tool.progress":
|
|
507317
507330
|
this.handleToolProgress(event);
|
|
507318
507331
|
break;
|
|
@@ -507401,6 +507414,7 @@ var SessionEventHandler = class {
|
|
|
507401
507414
|
this.mcpServerStatusSpinners.clear();
|
|
507402
507415
|
}
|
|
507403
507416
|
handleTurnBegin(event) {
|
|
507417
|
+
this.clearModelRetryHint();
|
|
507404
507418
|
this.turnWatchdog.start();
|
|
507405
507419
|
const sessionId = this.host.session?.id;
|
|
507406
507420
|
if (sessionId !== void 0) bindPersonalMemoryRememberIntentTurn(sessionId, event.turnId);
|
|
@@ -507443,6 +507457,7 @@ var SessionEventHandler = class {
|
|
|
507443
507457
|
}, 0);
|
|
507444
507458
|
}
|
|
507445
507459
|
handleTurnEnd(event, sendQueued) {
|
|
507460
|
+
this.clearModelRetryHint();
|
|
507446
507461
|
this.turnWatchdog.stop();
|
|
507447
507462
|
this.host.restoreQueuedSteerAtTurnEnd(event.turnId);
|
|
507448
507463
|
const sessionId = this.host.session?.id;
|
|
@@ -507469,6 +507484,7 @@ var SessionEventHandler = class {
|
|
|
507469
507484
|
}
|
|
507470
507485
|
handleStepBegin(event) {
|
|
507471
507486
|
this.turnWatchdog.recordProgress();
|
|
507487
|
+
this.clearModelRetryHint();
|
|
507472
507488
|
this.host.commitQueuedSteerAtStepStart(event.turnId);
|
|
507473
507489
|
this.host.streamingUI.flushNow();
|
|
507474
507490
|
this.host.streamingUI.setStep(event.step);
|
|
@@ -507485,6 +507501,7 @@ var SessionEventHandler = class {
|
|
|
507485
507501
|
});
|
|
507486
507502
|
}
|
|
507487
507503
|
handleStepCompleted(event) {
|
|
507504
|
+
this.clearModelRetryHint();
|
|
507488
507505
|
this.host.streamingUI.flushNow();
|
|
507489
507506
|
if (event.usage !== void 0) this.currentTurnTokenCount = (this.currentTurnTokenCount ?? 0) + totalTokenUsage(event.usage);
|
|
507490
507507
|
this.maybeShowDebugTiming(event);
|
|
@@ -507527,6 +507544,7 @@ var SessionEventHandler = class {
|
|
|
507527
507544
|
this.subAgentEventHandler.markActiveAgentSwarmsCancelled();
|
|
507528
507545
|
}
|
|
507529
507546
|
handleStepInterrupted(event) {
|
|
507547
|
+
this.clearModelRetryHint();
|
|
507530
507548
|
this.host.streamingUI.flushNow();
|
|
507531
507549
|
this.host.streamingUI.resetToolUi();
|
|
507532
507550
|
this.host.streamingUI.finalizeLiveTextBuffers("idle");
|
|
@@ -507539,10 +507557,26 @@ var SessionEventHandler = class {
|
|
|
507539
507557
|
}
|
|
507540
507558
|
this.host.showError(reason === "max_steps" ? uiText("sessionEvent.interrupted.maxSteps") : uiText("sessionEvent.interrupted.reason", { reason }));
|
|
507541
507559
|
}
|
|
507560
|
+
handleStepRetrying(event) {
|
|
507561
|
+
this.turnWatchdog.recordProgress();
|
|
507562
|
+
const hint = formatModelRetryProgress(event);
|
|
507563
|
+
this.modelRetryHint = hint;
|
|
507564
|
+
this.host.state.footer.setTransientHint(hint);
|
|
507565
|
+
this.host.state.ui.requestRender();
|
|
507566
|
+
}
|
|
507567
|
+
clearModelRetryHint() {
|
|
507568
|
+
if (this.modelRetryHint === void 0) return;
|
|
507569
|
+
if (this.host.state.footer.getTransientHint() === this.modelRetryHint) {
|
|
507570
|
+
this.host.state.footer.setTransientHint(null);
|
|
507571
|
+
this.host.state.ui.requestRender();
|
|
507572
|
+
}
|
|
507573
|
+
this.modelRetryHint = void 0;
|
|
507574
|
+
}
|
|
507542
507575
|
handleThinkingDelta(event) {
|
|
507543
507576
|
const { state, streamingUI } = this.host;
|
|
507544
507577
|
if (event.delta.length === 0 && !streamingUI.hasThinkingDraft()) return;
|
|
507545
507578
|
if (event.delta.length > 0) this.turnWatchdog.recordProgress();
|
|
507579
|
+
if (event.delta.length > 0) this.clearModelRetryHint();
|
|
507546
507580
|
streamingUI.appendThinkingDelta(event.delta);
|
|
507547
507581
|
this.host.patchLivePane({ mode: "idle" });
|
|
507548
507582
|
if (state.appState.streamingPhase !== "thinking") this.host.setAppState({
|
|
@@ -507556,6 +507590,7 @@ var SessionEventHandler = class {
|
|
|
507556
507590
|
if (streamingUI.hasThinkingDraft()) streamingUI.flushThinkingToTranscript("idle");
|
|
507557
507591
|
if (event.delta.trim().length > 0) {
|
|
507558
507592
|
this.turnWatchdog.recordProgress();
|
|
507593
|
+
this.clearModelRetryHint();
|
|
507559
507594
|
this.currentTurnHasAssistantText = true;
|
|
507560
507595
|
this.pendingModelBlockedFallback = void 0;
|
|
507561
507596
|
}
|
|
@@ -507593,6 +507628,7 @@ var SessionEventHandler = class {
|
|
|
507593
507628
|
});
|
|
507594
507629
|
}
|
|
507595
507630
|
handleToolCall(event) {
|
|
507631
|
+
this.clearModelRetryHint();
|
|
507596
507632
|
this.turnWatchdog.recordToolCall(event.toolCallId, event.name, event.args);
|
|
507597
507633
|
const { streamingUI } = this.host;
|
|
507598
507634
|
streamingUI.flushNow();
|
|
@@ -515552,6 +515588,9 @@ var BlunTUI = class {
|
|
|
515552
515588
|
mediaActivityTickTimer;
|
|
515553
515589
|
mediaActivityExpanded = false;
|
|
515554
515590
|
lastHistoryContent;
|
|
515591
|
+
inputDraftTimer;
|
|
515592
|
+
pendingInputDraft;
|
|
515593
|
+
inputDraftWrite = Promise.resolve();
|
|
515555
515594
|
shellOutputStreams = /* @__PURE__ */ new Map();
|
|
515556
515595
|
streamingUI;
|
|
515557
515596
|
authFlow;
|
|
@@ -515798,6 +515837,7 @@ var BlunTUI = class {
|
|
|
515798
515837
|
this.loadPersistedInputHistory();
|
|
515799
515838
|
this.state.editorContainer.clear();
|
|
515800
515839
|
this.state.editorContainer.addChild(this.state.editor);
|
|
515840
|
+
await this.loadPersistedInputDraft();
|
|
515801
515841
|
this.state.ui.setFocus(this.state.editor);
|
|
515802
515842
|
return shouldReplayHistory;
|
|
515803
515843
|
}
|
|
@@ -516069,6 +516109,7 @@ var BlunTUI = class {
|
|
|
516069
516109
|
async stop(exitCode) {
|
|
516070
516110
|
if (this.isShuttingDown) return;
|
|
516071
516111
|
this.isShuttingDown = true;
|
|
516112
|
+
await this.flushInputDraft();
|
|
516072
516113
|
if (exitCode === 0 && process.connected) try {
|
|
516073
516114
|
process.send({ type: RUNTIME_EXIT_INTENT_MESSAGE });
|
|
516074
516115
|
} catch {}
|
|
@@ -516952,6 +516993,40 @@ var BlunTUI = class {
|
|
|
516952
516993
|
this.managedImageReaderAvailable = false;
|
|
516953
516994
|
}
|
|
516954
516995
|
}
|
|
516996
|
+
inputDraftFile() {
|
|
516997
|
+
return getInputDraftFile(
|
|
516998
|
+
this.state.appState.workDir,
|
|
516999
|
+
process.env["BLUN_PROFILE"] ?? "default"
|
|
517000
|
+
);
|
|
517001
|
+
}
|
|
517002
|
+
async loadPersistedInputDraft() {
|
|
517003
|
+
const draft = await readInputDraft(this.inputDraftFile());
|
|
517004
|
+
if (draft === null || this.state.editor.getText().length > 0) return;
|
|
517005
|
+
this.state.editor.setInputMode(draft.mode);
|
|
517006
|
+
this.state.editor.setText(draft.text);
|
|
517007
|
+
this.updateEditorBorderHighlight(draft.text);
|
|
517008
|
+
}
|
|
517009
|
+
scheduleInputDraftPersist(text, mode) {
|
|
517010
|
+
this.pendingInputDraft = { text, mode };
|
|
517011
|
+
if (this.inputDraftTimer !== void 0) clearTimeout(this.inputDraftTimer);
|
|
517012
|
+
this.inputDraftTimer = setTimeout(() => {
|
|
517013
|
+
this.inputDraftTimer = void 0;
|
|
517014
|
+
void this.flushInputDraft();
|
|
517015
|
+
}, 300);
|
|
517016
|
+
}
|
|
517017
|
+
async flushInputDraft() {
|
|
517018
|
+
if (this.inputDraftTimer !== void 0) {
|
|
517019
|
+
clearTimeout(this.inputDraftTimer);
|
|
517020
|
+
this.inputDraftTimer = void 0;
|
|
517021
|
+
}
|
|
517022
|
+
const draft = this.pendingInputDraft;
|
|
517023
|
+
this.pendingInputDraft = void 0;
|
|
517024
|
+
if (draft === void 0) return this.inputDraftWrite;
|
|
517025
|
+
this.inputDraftWrite = this.inputDraftWrite
|
|
517026
|
+
.then(() => writeInputDraft(this.inputDraftFile(), draft))
|
|
517027
|
+
.catch(() => {});
|
|
517028
|
+
return this.inputDraftWrite;
|
|
517029
|
+
}
|
|
516955
517030
|
async loadPersistedInputHistory() {
|
|
516956
517031
|
try {
|
|
516957
517032
|
const entries = await loadInputHistory(getInputHistoryFile(this.state.appState.workDir));
|