blun-king-cli 9.1.571 → 9.1.573

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 CHANGED
@@ -1,5 +1,30 @@
1
+ ## 9.1.573
2
+
3
+ - Keep the input field visible and focused after the startup goal choice,
4
+ including Escape and a failed goal-resume request. Preserve unsent drafts.
5
+ - Show a compact with/without-goal choice instead of the entire objective.
6
+ - Ctrl+S leaves that choice without approving the goal and reaches the normal
7
+ queue shortcut. It also handles pending input while the session is idle.
8
+ - Preserve existing Escape behavior: queued work is cleared before a subsequent
9
+ Escape cancels the active turn. No automatic deletion of session history.
10
+
11
+ The release does not reconstruct acknowledgements lost by an older process
12
+ before upgrading. Genuinely pending messages remain attached to their session.
13
+
1
14
  # Changelog
2
15
 
16
+ ## 9.1.572
17
+
18
+ - Research evidence rejects source URLs containing recognized credential parameter
19
+ names in queries or parameter-shaped fragments, including nested names and
20
+ fragment routes. Rejection applies to failed sources as well as usable sources.
21
+ - Previously saved reports with these URLs are withheld during recall with the
22
+ existing `partial` / `report-invalid` result. Original snapshots are not modified
23
+ or deleted, and source addresses are never silently stripped or rewritten.
24
+ - Ordinary documentation queries and anchors retain their exact bytes. This is
25
+ a credential-name boundary, not general secret detection or a history cleanup.
26
+
27
+
3
28
  ## 9.1.571
4
29
 
5
30
  - Bind Telegram queue ranges to their owning session. A new session no longer automatically consumes work left pending by an older session; resuming a known session restores only its own pending ranges.
package/blun.mjs CHANGED
@@ -417373,6 +417373,10 @@ var ChoicePickerComponent = class extends Container {
417373
417373
  }
417374
417374
  handleInput(data) {
417375
417375
  if (isKeyRelease(data)) return;
417376
+ if (matchesKey(normalizeCapsLockedCtrl(data), Key.ctrl("s")) && this.opts.onCtrlS !== void 0) {
417377
+ this.opts.onCtrlS();
417378
+ return;
417379
+ }
417376
417380
  if (matchesKey(data, Key.ctrl("c")) && this.opts.onCtrlC !== void 0) {
417377
417381
  this.opts.onCtrlC();
417378
417382
  return;
@@ -508611,7 +508615,7 @@ var EditorKeyboardController = class {
508611
508615
  };
508612
508616
  editor.onCtrlS = () => {
508613
508617
  const draft = (editor.getExpandedText?.() ?? editor.getText()).trim();
508614
- if (!host.streamingUI.hasActiveTurn() && host.state.appState.streamingPhase === "idle" && !draft.trimStart().startsWith("/") && !host.state.queuedMessages.some((item) => host.canRetryLocalPrompt?.(item.localPromptRecovery))) return;
508618
+ if (!host.streamingUI.hasActiveTurn() && host.state.appState.streamingPhase === "idle" && draft.length === 0 && host.state.queuedMessages.length === 0) return;
508615
508619
  host.flushQueuedMessages(draft, editor.inputMode);
508616
508620
  host.updateQueueDisplay();
508617
508621
  host.state.ui.requestRender();
@@ -519807,13 +519811,14 @@ function startupResumeGoalPromptCopy() {
519807
519811
  withGoal: uiText("startupResumeGoal.resume")
519808
519812
  };
519809
519813
  }
519810
- function promptStartupResumeGoal(host, objective, copy) {
519814
+ function promptStartupResumeGoal(host, objective, copy, onFlushQueue) {
519811
519815
  return new Promise((resolve) => {
519812
519816
  let done = false;
519813
519817
  const finish = (choice) => {
519814
519818
  if (done) return;
519815
519819
  done = true;
519816
- host.dismissEditorReplacement();
519820
+ if (choice === "exit") host.dismissEditorReplacement();
519821
+ else host.restoreEditor();
519817
519822
  resolve(choice);
519818
519823
  };
519819
519824
  host.mountEditorReplacement(new ChoicePickerComponent({
@@ -519824,8 +519829,7 @@ function promptStartupResumeGoal(host, objective, copy) {
519824
519829
  label: copy.withoutGoal
519825
519830
  }, {
519826
519831
  value: "resume",
519827
- label: copy.withGoal,
519828
- description: objective
519832
+ label: copy.withGoal
519829
519833
  }],
519830
519834
  onSelect: (value) => {
519831
519835
  finish(value);
@@ -519838,6 +519842,11 @@ function promptStartupResumeGoal(host, objective, copy) {
519838
519842
  },
519839
519843
  onCtrlD: () => {
519840
519844
  finish("exit");
519845
+ },
519846
+ onCtrlS: () => {
519847
+ if (done) return;
519848
+ finish("without");
519849
+ onFlushQueue?.();
519841
519850
  }
519842
519851
  }));
519843
519852
  });
@@ -521194,13 +521203,15 @@ const telegramBoundary = this.captureTelegramQueueBoundary();
521194
521203
  this.syncGoalTimeTrigger();
521195
521204
  if (goal.status !== "paused" && goal.status !== "blocked") return;
521196
521205
  this.startupGoalPromptedSessionId = sessionId;
521197
- const choice = await promptStartupResumeGoal(this, goal.objective, startupResumeGoalPromptCopy());
521198
- if (this.aborted || this.session?.id !== sessionId) return;
521199
- if (choice === "exit") {
521200
- await this.stop(0);
521201
- return;
521202
- }
521203
- if (choice === "resume") await handleGoalCommand(this, "resume");
521206
+ await this.withTelegramQueuePaused(async () => {
521207
+ const choice = await promptStartupResumeGoal(this, goal.objective, startupResumeGoalPromptCopy(), () => { this.state.editor.handleInput("\u0013"); });
521208
+ if (this.aborted || this.session?.id !== sessionId) return;
521209
+ if (choice === "exit") {
521210
+ await this.stop(0);
521211
+ return;
521212
+ }
521213
+ if (choice === "resume") await handleGoalCommand(this, "resume");
521214
+ });
521204
521215
  }
521205
521216
  async stop(exitCode) {
521206
521217
  if (this.isShuttingDown) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.571",
3
+ "version": "9.1.573",
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": {
@@ -80,3 +80,25 @@ unread global corpus. `limited:true` also flags result-count or output truncatio
80
80
  continuation is for the read budget, not pagination of every lexical hit. Narrow
81
81
  the query when necessary. Unavailable or partial results must remain visible as
82
82
  limitations; they do not prevent ordinary authorized research.
83
+
84
+ ## Source URL Credential Boundary
85
+
86
+ All source statuses reject URL userinfo and recognized credential parameter names
87
+ in queries and parameter-shaped fragments, including fragment routes. Names are
88
+ decoded with URLSearchParams, compared case-insensitively, and checked within
89
+ bracket/dot nesting. Examples include access_token, refresh_token, api_key,
90
+ client_secret, authorization, session_id and signed-URL credential/signature fields.
91
+
92
+ The source address is never silently stripped or rewritten. Normal queries such
93
+ as `q=access_token`, bare `key` / `code` parameters, and ordinary documentation
94
+ anchors are not classified as credentials. This is not a general secret detector:
95
+ custom keys, opaque path values, nested encoded URLs and report text still require
96
+ the explicit review described above. Passing validation does not prove public
97
+ reachability, permission to share, or absence of secrets.
98
+
99
+ New invalid reports return `saved:false` / `report-invalid` before collection
100
+ creation. Reading an older invalid report withholds that snapshot and reports
101
+ `partial` / `report-invalid`, while leaving its original bytes on disk. Other valid
102
+ snapshots may still be returned. This is not automatic cleanup or credential
103
+ revocation. Do not remove history or rewrite URLs to make validation pass.
104
+
@@ -5,6 +5,25 @@ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/;
5
5
  const HASH = /^[a-f0-9]{64}$/;
6
6
  const STATES = new Set(['usable', 'failed', 'challenge', 'irrelevant', 'stale']);
7
7
  const VERDICTS = new Set(['supported', 'unsupported', 'uncertain']);
8
+ const CREDENTIAL_PARAMETER = /^(?:(?:access|refresh|id|oauth)[_-]?token|token|api[_-]?key|client[_-]?secret|password|passwd|authorization|bearer|jwt|session[_-]?(?:id|token)|secret|signature|sig|x-(?:amz|goog)-(?:signature|credential|security-token))$/i;
9
+ function validateSourceUrl(value) {
10
+ let url;
11
+ try { url = new URL(value); } catch { assert.fail('source URL'); }
12
+ assert(['http:', 'https:'].includes(url.protocol) && !url.username && !url.password, 'source URL');
13
+ const parameters = [url.searchParams];
14
+ // OAuth-style fragments can hold credentials; ordinary documentation anchors stay intact.
15
+ const fragment = url.hash.slice(1);
16
+ if (fragment.includes('=')) {
17
+ parameters.push(new URLSearchParams(fragment));
18
+ const queryStart = fragment.indexOf('?');
19
+ if (queryStart !== -1) parameters.push(new URLSearchParams(fragment.slice(queryStart + 1)));
20
+ }
21
+ for (const params of parameters) {
22
+ for (const key of params.keys()) {
23
+ assert(!key.split(/[\[\].]+/).some(part => CREDENTIAL_PARAMETER.test(part)), 'source URL');
24
+ }
25
+ }
26
+ }
8
27
  function boundedArray(value, maximum, label) {
9
28
  assert(Array.isArray(value) && value.length <= maximum, label);
10
29
  return value;
@@ -38,8 +57,7 @@ function score(report) {
38
57
  const sourceMap = new Map();
39
58
  const sourceCounts = Object.fromEntries([...STATES].map(status => [status, 0]));
40
59
  for (const source of sources) {
41
- const url = new URL(source.url);
42
- assert(['http:', 'https:'].includes(url.protocol) && !url.username && !url.password, 'source URL');
60
+ validateSourceUrl(source.url);
43
61
  assert(STATES.has(source.status), 'source status');
44
62
  assert(source.retrievedAt === null || timestamp(source.retrievedAt), 'source time');
45
63
  assert(source.contentSha256 === null || HASH.test(source.contentSha256), 'source hash');