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.
@@ -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": break;
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.284",
3
+ "version": "9.1.286",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {