blun-king-cli 9.1.527 → 9.1.536

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LIESMICH.txt +36 -1
  3. package/README.md +35 -1
  4. package/bin/agent-resume-snapshot.cjs +31 -0
  5. package/bin/assistant-message-offload-policy.cjs +21 -2
  6. package/bin/codebase-search-runtime.cjs +23 -0
  7. package/bin/empty-response-retry-policy.cjs +29 -0
  8. package/bin/fredrik-glm-provider.cjs +256 -0
  9. package/bin/history-offload-pressure-policy.cjs +33 -0
  10. package/bin/programmatic-context-isolation.cjs +25 -0
  11. package/bin/programmatic-tool-runtime.mjs +301 -0
  12. package/bin/skill-activation-performance-policy.cjs +9 -0
  13. package/bin/structured-subagent-output.cjs +252 -0
  14. package/bin/telegram-direct-focus-policy.cjs +25 -1
  15. package/bin/todo-list-turn-policy.cjs +111 -1
  16. package/bin/tool-result-offload-policy.cjs +29 -0
  17. package/bin/turn-thinking-policy.cjs +6 -15
  18. package/bin/turn-tool-performance-policy.cjs +5 -4
  19. package/bin/user-message-offload-policy.cjs +10 -1
  20. package/blun.mjs +656 -127
  21. package/codebase-index/README.md +70 -0
  22. package/codebase-index/codebase_index.py +358 -0
  23. package/fredrik-glm-profile.toml.example +26 -0
  24. package/package.json +24 -3
  25. package/scripts/check-active-work-steer-regression.js +46 -0
  26. package/scripts/check-codebase-search-packaging-regression.js +92 -0
  27. package/scripts/check-current-turn-read-pin-mutation-regression.js +72 -0
  28. package/scripts/check-current-turn-read-pin-regression.js +94 -0
  29. package/scripts/check-deepseek-native-max-regression.js +49 -0
  30. package/scripts/check-empty-response-effort-downgrade-regression.js +48 -0
  31. package/scripts/check-fredrik-glm-mutation-regression.js +18 -0
  32. package/scripts/check-fredrik-glm-regression.js +169 -0
  33. package/scripts/check-history-pressure-offload-regression.js +77 -0
  34. package/scripts/check-programmatic-context-isolation-regression.js +193 -0
  35. package/scripts/check-programmatic-tool-regression.js +294 -0
  36. package/scripts/check-resume-replay-regression.js +2 -0
  37. package/scripts/check-startup-swarm-command-regression.js +24 -0
  38. package/scripts/check-structured-subagent-output-regression.js +331 -0
  39. package/scripts/check-telegram-direct-work-resume-regression.js +53 -0
  40. package/scripts/check-todo-progress-regression.js +416 -0
  41. package/scripts/check-tool-schema-capacity-regression.js +40 -0
  42. package/scripts/programmatic-tool-runtime.test.mjs +365 -0
  43. package/scripts/structured-subagent-output.test.cjs +170 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.1.536 - 2026-09-01
4
+
5
+ - Keeps productive work alive across private Telegram DMs: direct work commands no longer stop at the reply boundary, and buffered DMs inject one bounded return step unless the user explicitly pauses or stops.
6
+ - Preserves the active work turn when short status steers arrive.
7
+ - Downgrades exhausted empty thinking retries instead of repeating the same effort.
8
+ - Keeps more deferred tool schemas available and delays message offload until real history pressure.
9
+ - Includes the verified TodoList progress policy and a one-line God Mode plus Swarm startup path.
10
+
11
+ ## 9.1.535 - 2026-09-01
12
+
13
+ - Keeps the current real user instruction complete for its entire active turn instead of aging it out after a fixed number of history messages.
14
+ - Pins all `Read` results produced during that turn so task files and source slices cannot be replaced by historical previews while the agent is still using them.
15
+ - Preserves the existing bounded offload behavior for completed older turns and ignores injected system reminders when locating the active user turn.
16
+ - Adds a packaged regression derived from the F35 live case where a complete 59-line task Read was archived after only four later history messages.
17
+
18
+ ## 9.1.534 - 2026-09-01
19
+
20
+ - Separates DeepSeek V4's native `max` reasoning effort from the explicit BLUN Swarm action, so selecting Max no longer enables agents or injects the swarm context.
21
+ - Keeps `/effort blun-swarm` as the deliberate session-only agent mode while showing native `max` as `Max` in the effort selector and session header.
22
+ - Adds a mutation-backed packaged-bundle regression for the native Max path and preserved explicit Swarm path.
23
+
24
+ ## 9.1.533 - 2026-09-01
25
+
26
+ - Adds a profile-gated, session-local model switch for Fredrik between King, GLM 5.3 Flash, and GLM 5.3 without changing the global default or any other King profile.
27
+ - Pins the external provider to the Z.AI Coding Plan endpoint, reads its credential only from a validated profile-relative file, and sends always-on thinking with the explicit `low`, `high`, or `max` effort selected in the TUI.
28
+ - Preserves the selected alias across normal prompts and resume while keeping new sessions on King, and rejects provider, alias, endpoint, effort, secret-path, and catalog widening mutations.
29
+ - Adds focused provider, payload, picker, profile-isolation, secret-format, and mutation regressions to the package gate.
30
+
31
+ ## 9.1.532 - 2026-09-01
32
+
33
+ - Makes automatic TodoList maintenance compare canonical visible state instead of treating every accepted rewrite as progress, while keeping truthful unchanged refreshes executable and the normal recovery tool catalogue available.
34
+ - Preserves bounded work evidence across unchanged refreshes, asks for a concrete evidence-backed transition after two repeats, emits one non-blocking warning after three, and backs later maintenance off from 8 to at most 64 work calls.
35
+ - Caps new and ordinary visible lists at 32 items, safely migrates oversized legacy lists only through explicit `done` pruning, and preserves bounded maintenance state across resume checkpoints.
36
+ - Adds executable bundle, runtime, checkpoint, compatibility, and seven-mutation regressions derived from read-only fleet measurements of Fredrik, Werner, and Manfred.
37
+
38
+ ## 9.1.531 - 2026-09-01
39
+
40
+ - Keeps nested `ProgrammaticTool` calls and results fully observable in the Wire while excluding their raw payloads from the next model context.
41
+ - Applies the same isolation marker during replay so resume cannot reintroduce nested payloads into context.
42
+ - Adds a mutation-backed regression for event marking, record preservation, live context exclusion, and resume reuse.
43
+
44
+ ## 9.1.530 - 2026-09-01
45
+
46
+ - Packages the existing local semantic `CodebaseSearch` runtime and documentation instead of resolving to a source-tree-only path that does not exist after npm installation.
47
+ - Resolves the bundled script relative to `blun.mjs`, preserves an explicit absolute `CODEBASE_INDEX_SCRIPT` override, and fails closed for missing or relative overrides.
48
+ - Adds an executable package, resolver, Python-syntax, compatibility, and mutation regression without changing indexing, querying, permission, or model behavior.
49
+
50
+ ## 9.1.529 - 2026-09-01
51
+
52
+ - Adds optional `response_format` support for foreground `Agent` calls, returning one canonical validated JSON envelope without changing the existing free-form result path.
53
+ - Compiles a bounded object JSON Schema through BLUN's existing AJV validator before child creation, keeps the schema reminder inside that child, and grants no additional tools or permissions.
54
+ - Allows exactly one cancellation-aware repair turn for invalid output, preserves the schema across automatic continuation, and rejects structured background or swarm execution until their persistence contracts are implemented separately.
55
+ - Adds executable helper, bundled-agent, repair, cancellation, compatibility, and mutation regressions.
56
+
57
+ ## 9.1.528 - 2026-09-01
58
+
59
+ - Adds a bounded QuickJS `ProgrammaticTool`, adapted from the verified DeepAgents interpreter pattern, so King can coordinate several sequential tool calls without returning every intermediate value to the model context.
60
+ - Keeps every nested call inside BLUN's existing schema validation, preparation, authorization, permission, scheduler, event, and result pipeline; recursive interpreter calls and undeclared tools fail closed.
61
+ - Separates the 120-second wall limit for real tools from the three-second guest-compute budget, propagates cancellation and turn-stop results, and exposes no host filesystem, network, process, package loader, clock, or dynamic evaluation API.
62
+ - Adds 18 runtime tests plus executable bundle, pipeline, mutation, resource-limit, teardown, and host-isolation regressions.
63
+
3
64
  ## 9.1.527 - 2026-09-01
4
65
 
5
66
  - Adds DeepAgents' `/copy` workflow to copy the latest completed, non-empty assistant response through BLUN's existing cross-platform clipboard path.
package/LIESMICH.txt CHANGED
@@ -9,7 +9,19 @@ Installation
9
9
  ------------
10
10
  Die geprüfte Version exakt global installieren:
11
11
 
12
- npm install -g blun-king-cli@9.1.527
12
+ npm install -g blun-king-cli@9.1.536
13
+
14
+ Todo-Fortschritt in 9.1.532
15
+ ---------------------------
16
+ Die automatische Todo-Pflege unterscheidet jetzt zwischen einer echten
17
+ Statusaenderung und einer unveraenderten Wiederholung. Unveraenderte,
18
+ wahrheitsgemaesse Listen bleiben ausfuehrbar und sperren weder Datei- noch
19
+ Shell-Werkzeuge. Nach mehrfach belegter Arbeit ohne Listenfortschritt fordert
20
+ King einen konkreten, evidenzgestuetzten naechsten Zustand an und vergroessert
21
+ den Wartungsabstand schrittweise bis auf 64 Werkzeugaufrufe. Neue und
22
+ gewoehnliche sichtbare Listen sind auf 32 Punkte begrenzt; aeltere groessere
23
+ Listen koennen ohne Datenverlust nur durch ausdruecklich als done markierte
24
+ Punkte schrumpfen. Dieser Wartungsstand bleibt im Resume-Checkpoint erhalten.
13
25
 
14
26
  AgentSpine 0.11.4
15
27
  -----------------
@@ -553,6 +565,29 @@ Folgeturns hinweg gespeichert. Solange er offen ist, bleibt `GetMedia`
553
565
  verfügbar; King prüft den tatsächlichen Status, statt fälschlich zu behaupten,
554
566
  die Medienerzeugung sei nicht verfügbar.
555
567
 
568
+ Begrenzte programmatische Werkzeugaufrufe
569
+ -----------------------------------------
570
+
571
+ Ab BLUN King 9.1.528 kann King bei passenden mehrstufigen Aufgaben mehrere
572
+ aufeinanderfolgende Werkzeugaufrufe in einem isolierten `ProgrammaticTool`
573
+ koordinieren. Zwischenergebnisse bleiben dabei im begrenzten QuickJS-Lauf,
574
+ statt nach jedem Aufruf den Modellkontext zu vergrößern. Das Werkzeug wird wie
575
+ andere nicht zum Kern gehörende Werkzeuge nur bei Bedarf als Schema geladen.
576
+
577
+ Jeder aufrufbare Werkzeugname muss im Auftrag ausdrücklich freigegeben sein.
578
+ Jeder verschachtelte Aufruf durchläuft weiterhin die vorhandene
579
+ Schema-Validierung, Vorbereitungs- und Autorisierungs-Hooks,
580
+ Freigabeentscheidung, Ablaufplanung sowie `tool.call` und `tool.result`.
581
+ Rekursive Interpreteraufrufe werden abgewiesen. Ein verschachteltes
582
+ `stopTurn`-Ergebnis beendet auch den äußeren Zug.
583
+
584
+ Der Gast besitzt keinen direkten Zugriff auf Dateisystem, Netzwerk, Prozesse,
585
+ Paketlader, Uhr oder dynamische Codeauswertung. Speicher, Stack, reine
586
+ Rechenzeit, gesamte Laufzeit, Code, Zustand, Argumente, Ergebnisse und Zahl der
587
+ Werkzeugaufrufe sind begrenzt. Langsame echte Werkzeuge verbrauchen dabei nicht
588
+ das getrennte Rechenzeitbudget des Gastes; Abbruch und Zeitgrenze werden bis in
589
+ laufende verschachtelte Aufrufe weitergegeben.
590
+
556
591
  Passende Werkzeuge ohne Such-Zwischenschritt
557
592
  ---------------------------------------------
558
593
 
package/README.md CHANGED
@@ -9,9 +9,21 @@ Voraussetzung ist Node.js 24.15 oder neuer. Die geprüfte Version wird exakt
9
9
  installiert:
10
10
 
11
11
  ```powershell
12
- npm install -g blun-king-cli@9.1.527
12
+ npm install -g blun-king-cli@9.1.536
13
13
  ```
14
14
 
15
+ ## Todo-Fortschritt in 9.1.532
16
+
17
+ Die automatische Todo-Pflege unterscheidet jetzt zwischen einer echten
18
+ Statusänderung und einer unveränderten Wiederholung. Unveränderte, wahrheitsgemäße
19
+ Listen bleiben ausführbar und sperren weder Datei- noch Shell-Werkzeuge. Nach
20
+ mehrfach belegter Arbeit ohne Listenfortschritt fordert King einen konkreten,
21
+ evidenzgestützten nächsten Zustand an und vergrößert den Wartungsabstand
22
+ schrittweise bis auf 64 Werkzeugaufrufe. Neue und gewöhnliche sichtbare Listen
23
+ sind auf 32 Punkte begrenzt; ältere größere Listen können ohne Datenverlust nur
24
+ durch ausdrücklich als `done` markierte Punkte schrumpfen. Dieser Wartungsstand
25
+ bleibt im Resume-Checkpoint erhalten.
26
+
15
27
  ## AgentSpine 0.11.4
16
28
 
17
29
  Version 9.1.524 aktualisiert die gebündelte AgentSpine-Laufzeit auf 0.11.4 aus dem offiziellen Commit `8034487cdd24498076bac48d3f24400bca5e6ae4`. Vor der Aufnahme in BLUN King wurden die hermetischen Tests, die Host- und Installationsprüfungen, die Acceptance-Suite, das Audit und der Release-Check erfolgreich abgeschlossen.
@@ -556,6 +568,28 @@ Folgeturns hinweg gespeichert. Solange er offen ist, bleibt `GetMedia`
556
568
  verfügbar; King prüft den tatsächlichen Status, statt fälschlich zu behaupten,
557
569
  die Medienerzeugung sei nicht verfügbar.
558
570
 
571
+ ## Begrenzte programmatische Werkzeugaufrufe
572
+
573
+ Ab BLUN King 9.1.528 kann King bei passenden mehrstufigen Aufgaben mehrere
574
+ aufeinanderfolgende Werkzeugaufrufe in einem isolierten `ProgrammaticTool`
575
+ koordinieren. Zwischenergebnisse bleiben dabei im begrenzten QuickJS-Lauf,
576
+ statt nach jedem Aufruf den Modellkontext zu vergrößern. Das Werkzeug wird wie
577
+ andere nicht zum Kern gehörende Werkzeuge nur bei Bedarf als Schema geladen.
578
+
579
+ Jeder aufrufbare Werkzeugname muss im Auftrag ausdrücklich freigegeben sein.
580
+ Jeder verschachtelte Aufruf durchläuft weiterhin die vorhandene
581
+ Schema-Validierung, Vorbereitungs- und Autorisierungs-Hooks,
582
+ Freigabeentscheidung, Ablaufplanung sowie `tool.call` und `tool.result`.
583
+ Rekursive Interpreteraufrufe werden abgewiesen. Ein verschachteltes
584
+ `stopTurn`-Ergebnis beendet auch den äußeren Zug.
585
+
586
+ Der Gast besitzt keinen direkten Zugriff auf Dateisystem, Netzwerk, Prozesse,
587
+ Paketlader, Uhr oder dynamische Codeauswertung. Speicher, Stack, reine
588
+ Rechenzeit, gesamte Laufzeit, Code, Zustand, Argumente, Ergebnisse und Zahl der
589
+ Werkzeugaufrufe sind begrenzt. Langsame echte Werkzeuge verbrauchen dabei nicht
590
+ das getrennte Rechenzeitbudget des Gastes; Abbruch und Zeitgrenze werden bis in
591
+ laufende verschachtelte Aufrufe weitergegeben.
592
+
559
593
  ## Passende Werkzeuge ohne Such-Zwischenschritt
560
594
 
561
595
  Ab BLUN King 9.1.396 vergleicht King die aktuelle Anfrage mit den bereits
@@ -1,12 +1,39 @@
1
1
  'use strict';
2
2
 
3
3
  const AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION = 1;
4
+ const MAX_TODO_PROGRESS_EVIDENCE = 8;
5
+ const MAX_TODO_PROGRESS_EVIDENCE_CHARS = 200;
4
6
 
5
7
  function copyJsonObject(value, fallback) {
6
8
  if (value === undefined) return fallback;
7
9
  return JSON.parse(JSON.stringify(value));
8
10
  }
9
11
 
12
+ function normalizeTodoProgressState(value) {
13
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
14
+ const nonNegativeInteger = (candidate, maximum) => Number.isSafeInteger(candidate)
15
+ ? Math.max(0, Math.min(candidate, maximum))
16
+ : 0;
17
+ const evidenceRevision = nonNegativeInteger(value.evidenceRevision, Number.MAX_SAFE_INTEGER);
18
+ return {
19
+ workCallsSinceRefresh: nonNegativeInteger(value.workCallsSinceRefresh, 64),
20
+ refreshRequired: value.refreshRequired === true,
21
+ unchangedRefreshStreak: nonNegativeInteger(value.unchangedRefreshStreak, 3),
22
+ recentEvidence: Array.isArray(value.recentEvidence)
23
+ ? value.recentEvidence
24
+ .filter((entry) => typeof entry === 'string')
25
+ .slice(-MAX_TODO_PROGRESS_EVIDENCE)
26
+ .map((entry) => entry.slice(0, MAX_TODO_PROGRESS_EVIDENCE_CHARS))
27
+ : [],
28
+ evidenceRevision,
29
+ lastMaintenanceEvidenceRevision: Math.min(
30
+ nonNegativeInteger(value.lastMaintenanceEvidenceRevision, Number.MAX_SAFE_INTEGER),
31
+ evidenceRevision,
32
+ ),
33
+ allVisibleWorkCompleted: value.allVisibleWorkCompleted === true,
34
+ };
35
+ }
36
+
10
37
  function captureMessageTimes(agent) {
11
38
  const replayTimes = new Map();
12
39
  for (const record of agent.replayBuilder.records) {
@@ -63,6 +90,7 @@ function createAgentResumeSnapshot(agent) {
63
90
  allowedTools: agent.tools.allowedTools,
64
91
  excludedTools: [...agent.tools.excludedTools],
65
92
  },
93
+ todoProgress: normalizeTodoProgressState(agent.goalTodoPolicyState) ?? null,
66
94
  goal: agent.goal.state ?? null,
67
95
  turn: {
68
96
  turnId: agent.turn.turnId,
@@ -175,6 +203,8 @@ function restoreAgentResumeSnapshot(agent, snapshot) {
175
203
  : [];
176
204
  if (agent.config.hasProvider) agent.tools.initializeBuiltinTools();
177
205
 
206
+ agent.goalTodoPolicyState = normalizeTodoProgressState(snapshot.todoProgress);
207
+
178
208
  agent.goal.state = snapshot.goal === null
179
209
  ? undefined
180
210
  : copyJsonObject(snapshot.goal, undefined);
@@ -206,5 +236,6 @@ function restoreAgentResumeSnapshot(agent, snapshot) {
206
236
  module.exports = {
207
237
  AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION,
208
238
  createAgentResumeSnapshot,
239
+ normalizeTodoProgressState,
209
240
  restoreAgentResumeSnapshot,
210
241
  };
@@ -1,5 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ hasHistoricalOffloadPressure,
5
+ historyTextChars,
6
+ } = require('./history-offload-pressure-policy.cjs');
7
+
3
8
  const ASSISTANT_MESSAGE_MAX_CHARS = 4_000;
4
9
  const ASSISTANT_MESSAGE_KEEP_RECENT_MESSAGES = 20;
5
10
  const ASSISTANT_MESSAGE_PREVIEW_LINES = 5;
@@ -34,6 +39,7 @@ function isSyntheticToolArguments(value) {
34
39
  function shouldOffloadHistoricalAssistantMessage(options = {}) {
35
40
  const historyIndex = Number(options.historyIndex);
36
41
  const historyLength = Number(options.historyLength);
42
+ const historyChars = Number(options.historyChars);
37
43
  const textChars = Number(options.textChars);
38
44
  if (
39
45
  !Number.isSafeInteger(historyIndex)
@@ -44,7 +50,9 @@ function shouldOffloadHistoricalAssistantMessage(options = {}) {
44
50
  || historyIndex >= historyLength
45
51
  ) return false;
46
52
  const cutoff = Math.max(0, historyLength - ASSISTANT_MESSAGE_KEEP_RECENT_MESSAGES);
47
- return historyIndex < cutoff && textChars > ASSISTANT_MESSAGE_MAX_CHARS;
53
+ return hasHistoricalOffloadPressure({ historyLength, historyChars })
54
+ && historyIndex < cutoff
55
+ && textChars > ASSISTANT_MESSAGE_MAX_CHARS;
48
56
  }
49
57
 
50
58
  function createAssistantMessagePreview(text) {
@@ -68,6 +76,7 @@ function createAssistantMessagePreview(text) {
68
76
  function shouldCompactHistoricalAssistantToolNarration(options = {}) {
69
77
  const historyIndex = Number(options.historyIndex);
70
78
  const historyLength = Number(options.historyLength);
79
+ const historyChars = Number(options.historyChars);
71
80
  const textChars = Number(options.textChars);
72
81
  const toolCallCount = Number(options.toolCallCount);
73
82
  const completedToolCallCount = Number(options.completedToolCallCount);
@@ -83,13 +92,18 @@ function shouldCompactHistoricalAssistantToolNarration(options = {}) {
83
92
  || toolCallCount <= 0
84
93
  ) return false;
85
94
  const cutoff = Math.max(0, historyLength - ASSISTANT_MESSAGE_KEEP_RECENT_MESSAGES);
86
- return historyIndex < cutoff
95
+ return hasHistoricalOffloadPressure({ historyLength, historyChars })
96
+ && historyIndex < cutoff
87
97
  && textChars > ASSISTANT_TOOL_NARRATION_MIN_CHARS
88
98
  && completedToolCallCount === toolCallCount;
89
99
  }
90
100
 
91
101
  function compactHistoricalPostTelegramReplyNarration(messages) {
92
102
  if (!Array.isArray(messages) || messages.length === 0) return messages;
103
+ if (!hasHistoricalOffloadPressure({
104
+ historyLength: messages.length,
105
+ historyChars: historyTextChars(messages),
106
+ })) return messages;
93
107
 
94
108
  const toolNameByCallId = new Map(messages.flatMap((message) => (
95
109
  message?.role === 'assistant' && Array.isArray(message.toolCalls)
@@ -175,6 +189,10 @@ function compactCompletedSubagentCall(call, completedIds) {
175
189
 
176
190
  function compactHistoricalCompletedSubagentPrompts(messages) {
177
191
  if (!Array.isArray(messages) || messages.length === 0) return messages;
192
+ if (!hasHistoricalOffloadPressure({
193
+ historyLength: messages.length,
194
+ historyChars: historyTextChars(messages),
195
+ })) return messages;
178
196
 
179
197
  const completedIds = completedSubagentResultIds(messages);
180
198
  if (completedIds.size === 0) return messages;
@@ -256,6 +274,7 @@ module.exports = {
256
274
  compactHistoricalCompletedSubagentPrompts,
257
275
  compactHistoricalPostTelegramReplyNarration,
258
276
  createAssistantMessagePreview,
277
+ historyTextChars,
259
278
  isSyntheticToolArguments,
260
279
  removeSyntheticToolArgumentFailures,
261
280
  shouldCompactHistoricalAssistantToolNarration,
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { existsSync } = require('node:fs');
4
+ const { fileURLToPath } = require('node:url');
5
+ const { isAbsolute } = require('node:path');
6
+
7
+ function resolveCodebaseIndexScript(moduleUrl, envPath) {
8
+ if (typeof moduleUrl !== 'string' || moduleUrl.length === 0) return '';
9
+
10
+ if (typeof envPath === 'string' && envPath.length > 0) {
11
+ if (!isAbsolute(envPath) || !existsSync(envPath)) return '';
12
+ return envPath;
13
+ }
14
+
15
+ try {
16
+ const packaged = fileURLToPath(new URL('./codebase-index/codebase_index.py', moduleUrl));
17
+ return existsSync(packaged) ? packaged : '';
18
+ } catch {
19
+ return '';
20
+ }
21
+ }
22
+
23
+ module.exports = { resolveCodebaseIndexScript };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const EMPTY_LENGTH_EFFORT_DOWNGRADE = Object.freeze({
4
+ max: 'high',
5
+ xhigh: 'high',
6
+ high: 'low',
7
+ medium: 'low',
8
+ low: 'off',
9
+ minimal: 'off',
10
+ });
11
+
12
+ function nextThinkingEffortForExhaustedEmpty(input = {}) {
13
+ const budget = Number(input.maxCompletionTokens);
14
+ const completion = Number(input.completionTokens);
15
+ if (
16
+ input.emptyResponseKind !== 'length'
17
+ || !Number.isFinite(budget)
18
+ || budget <= 0
19
+ || !Number.isFinite(completion)
20
+ || completion < budget
21
+ ) return undefined;
22
+
23
+ return EMPTY_LENGTH_EFFORT_DOWNGRADE[String(input.thinkingEffort || '').toLowerCase()];
24
+ }
25
+
26
+ module.exports = {
27
+ EMPTY_LENGTH_EFFORT_DOWNGRADE,
28
+ nextThinkingEffortForExhaustedEmpty,
29
+ };
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+
7
+ const PROVIDER_ID = 'fredrik:zai';
8
+ const PROVIDER_TYPE = 'openai_compatible';
9
+ const BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
10
+ const MODEL_ALIASES = Object.freeze({
11
+ 'fredrik/glm-5.3-flash': Object.freeze({
12
+ model: 'glm-5.3-flash',
13
+ displayName: 'GLM 5.3 Flash',
14
+ }),
15
+ 'fredrik/glm-5.3': Object.freeze({
16
+ model: 'glm-5.3',
17
+ displayName: 'GLM 5.3',
18
+ }),
19
+ });
20
+ const MODEL_EFFORTS = Object.freeze(['low', 'high', 'max']);
21
+ const MAX_SECRET_BYTES = 4096;
22
+
23
+ function nonEmpty(value) {
24
+ if (typeof value !== 'string') return undefined;
25
+ const trimmed = value.trim();
26
+ return trimmed.length === 0 ? undefined : trimmed;
27
+ }
28
+
29
+ function normalizeBaseUrl(value) {
30
+ const raw = nonEmpty(value);
31
+ if (raw === undefined) throw new Error('Fredrik GLM base_url is required.');
32
+ let parsed;
33
+ try {
34
+ parsed = new URL(raw);
35
+ } catch {
36
+ throw new Error('Fredrik GLM base_url is invalid.');
37
+ }
38
+ if (
39
+ parsed.protocol !== 'https:' ||
40
+ parsed.username !== '' ||
41
+ parsed.password !== '' ||
42
+ parsed.search !== '' ||
43
+ parsed.hash !== '' ||
44
+ parsed.hostname.toLowerCase() !== 'api.z.ai'
45
+ ) {
46
+ throw new Error('Fredrik GLM base_url must use the approved api.z.ai Coding endpoint.');
47
+ }
48
+ const normalized = `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}`;
49
+ if (normalized !== BASE_URL) {
50
+ throw new Error('Fredrik GLM base_url must use the approved api.z.ai Coding endpoint.');
51
+ }
52
+ return BASE_URL;
53
+ }
54
+
55
+ function resolveProfileRoot(env = process.env) {
56
+ const configured = nonEmpty(env.BLUN_HOME);
57
+ if (configured !== undefined) return path.resolve(configured);
58
+ const profile = nonEmpty(env.BLUN_PROFILE) ?? 'default';
59
+ return path.resolve(require('node:os').homedir(), '.blun', 'profile', profile);
60
+ }
61
+
62
+ function isContained(root, candidate) {
63
+ const relative = path.relative(root, candidate);
64
+ return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative);
65
+ }
66
+
67
+ function rejectUnsafeRelativePath(value) {
68
+ const relative = nonEmpty(value);
69
+ if (relative === undefined) throw new Error('Fredrik GLM api_key_file is required.');
70
+ if (path.isAbsolute(relative)) throw new Error('Fredrik GLM api_key_file must be profile-relative.');
71
+ if (relative.includes('\0')) throw new Error('Fredrik GLM api_key_file contains a NUL byte.');
72
+ if (relative.split(/[\\/]/u).some((part) => part === '..' || part.includes(':'))) {
73
+ throw new Error('Fredrik GLM api_key_file escapes the profile or uses an alternate data stream.');
74
+ }
75
+ return relative;
76
+ }
77
+
78
+ function assertNoSymlinkPath(root, candidate) {
79
+ const relative = path.relative(root, candidate);
80
+ let cursor = root;
81
+ for (const segment of relative.split(path.sep)) {
82
+ cursor = path.join(cursor, segment);
83
+ const info = fs.lstatSync(cursor);
84
+ if (info.isSymbolicLink()) throw new Error('Fredrik GLM api_key_file must not use links or reparse points.');
85
+ }
86
+ }
87
+
88
+ function assertWindowsAcl(filePath) {
89
+ if (process.platform !== 'win32') return;
90
+ const script = [
91
+ '$target = $env:BLUN_SECRET_ACL_PATH',
92
+ '$unsafe = @("S-1-1-0", "S-1-5-11", "S-1-5-32-545")',
93
+ '$writeMask = [System.Security.AccessControl.FileSystemRights]::Write -bor [System.Security.AccessControl.FileSystemRights]::Modify -bor [System.Security.AccessControl.FileSystemRights]::FullControl -bor [System.Security.AccessControl.FileSystemRights]::CreateFiles -bor [System.Security.AccessControl.FileSystemRights]::AppendData -bor [System.Security.AccessControl.FileSystemRights]::WriteData',
94
+ 'foreach ($rule in (Get-Acl -LiteralPath $target).Access) {',
95
+ ' if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue }',
96
+ ' $sid = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value',
97
+ ' if ($unsafe -contains $sid -and (($rule.FileSystemRights -band $writeMask) -ne 0)) { exit 23 }',
98
+ '}',
99
+ 'exit 0',
100
+ ].join('; ');
101
+ const result = spawnSync('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
102
+ encoding: 'utf8',
103
+ windowsHide: true,
104
+ timeout: 5000,
105
+ env: { ...process.env, BLUN_SECRET_ACL_PATH: filePath },
106
+ });
107
+ if (result.status === 23) throw new Error('Fredrik GLM api_key_file ACL is too permissive.');
108
+ if (result.status !== 0) throw new Error('Fredrik GLM api_key_file ACL could not be verified.');
109
+ }
110
+
111
+ function readApiKeyFile(provider, env = process.env) {
112
+ if (provider?.type !== PROVIDER_TYPE) throw new Error('Fredrik GLM provider type is invalid.');
113
+ if (provider.apiKey !== undefined || provider.env !== undefined || provider.oauth !== undefined || provider.customHeaders !== undefined) {
114
+ throw new Error('Fredrik GLM accepts only api_key_file authentication.');
115
+ }
116
+ normalizeBaseUrl(provider.baseUrl);
117
+ const profileRoot = resolveProfileRoot(env);
118
+ const relative = rejectUnsafeRelativePath(provider.apiKeyFile);
119
+ if (relative.replaceAll('\\', '/') !== 'secrets/zai-api-key.txt') {
120
+ throw new Error('Fredrik GLM api_key_file must use the approved profile-relative path.');
121
+ }
122
+ const candidate = path.resolve(profileRoot, relative);
123
+ if (!isContained(profileRoot, candidate)) throw new Error('Fredrik GLM api_key_file escapes the profile root.');
124
+ let rootReal;
125
+ let candidateReal;
126
+ try {
127
+ rootReal = fs.realpathSync(profileRoot);
128
+ assertNoSymlinkPath(profileRoot, candidate);
129
+ candidateReal = fs.realpathSync(candidate);
130
+ } catch (error) {
131
+ if (error instanceof Error && /links or reparse points/u.test(error.message)) throw error;
132
+ throw new Error('Fredrik GLM api_key_file could not be resolved safely.');
133
+ }
134
+ if (!isContained(rootReal, candidateReal)) throw new Error('Fredrik GLM api_key_file escapes the resolved profile root.');
135
+ let info;
136
+ try {
137
+ info = fs.statSync(candidateReal);
138
+ } catch {
139
+ throw new Error('Fredrik GLM api_key_file could not be inspected safely.');
140
+ }
141
+ if (!info.isFile()) throw new Error('Fredrik GLM api_key_file must be a regular file.');
142
+ if (info.size < 1 || info.size > MAX_SECRET_BYTES) throw new Error('Fredrik GLM api_key_file has an invalid size.');
143
+ assertWindowsAcl(candidateReal);
144
+ let raw;
145
+ try {
146
+ raw = fs.readFileSync(candidateReal);
147
+ } catch {
148
+ throw new Error('Fredrik GLM api_key_file could not be read safely.');
149
+ }
150
+ if (raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf) throw new Error('Fredrik GLM api_key_file must not contain a BOM.');
151
+ if (raw.includes(0)) throw new Error('Fredrik GLM api_key_file must not contain NUL bytes.');
152
+ const text = raw.toString('utf8');
153
+ if (Buffer.from(text, 'utf8').compare(raw) !== 0) throw new Error('Fredrik GLM api_key_file must be valid UTF-8.');
154
+ const lines = text.replace(/\r?\n$/u, '').split(/\r?\n/u);
155
+ if (lines.length !== 1 || lines[0].trim().length === 0 || lines[0] !== lines[0].trim()) {
156
+ throw new Error('Fredrik GLM api_key_file must contain exactly one non-empty line.');
157
+ }
158
+ return lines[0];
159
+ }
160
+
161
+ function isConfiguredAlias(alias, entry) {
162
+ const expected = MODEL_ALIASES[alias];
163
+ const capabilities = Array.isArray(entry?.capabilities) ? entry.capabilities : [];
164
+ const efforts = Array.isArray(entry?.supportEfforts) ? entry.supportEfforts : [];
165
+ return (
166
+ expected !== undefined &&
167
+ entry?.provider === PROVIDER_ID &&
168
+ entry.model === expected.model &&
169
+ entry.displayName === expected.displayName &&
170
+ entry.defaultEffort === 'max' &&
171
+ efforts.length === MODEL_EFFORTS.length &&
172
+ efforts.every((effort, index) => effort === MODEL_EFFORTS[index]) &&
173
+ capabilities.includes('always_thinking') &&
174
+ capabilities.includes('tool_use')
175
+ );
176
+ }
177
+
178
+ function configuredModels(models) {
179
+ const out = {};
180
+ for (const alias of Object.keys(MODEL_ALIASES)) {
181
+ const entry = models?.[alias];
182
+ if (isConfiguredAlias(alias, entry)) out[alias] = entry;
183
+ }
184
+ return out;
185
+ }
186
+
187
+ function configuredProvider(providers) {
188
+ const provider = providers?.[PROVIDER_ID];
189
+ if (provider?.type !== PROVIDER_TYPE) return undefined;
190
+ if (
191
+ provider.apiKey !== undefined ||
192
+ provider.env !== undefined ||
193
+ provider.oauth !== undefined ||
194
+ provider.customHeaders !== undefined ||
195
+ provider.apiKeyFile !== 'secrets/zai-api-key.txt'
196
+ ) return undefined;
197
+ try {
198
+ normalizeBaseUrl(provider.baseUrl);
199
+ } catch {
200
+ return undefined;
201
+ }
202
+ return provider;
203
+ }
204
+
205
+ function isAllowedRuntimeAlias(alias, models) {
206
+ return alias === 'blun/king' || isConfiguredAlias(alias, models?.[alias]);
207
+ }
208
+
209
+ function normalizeEffort(effort) {
210
+ if (!MODEL_EFFORTS.includes(effort)) throw new Error('GLM 5.3 supports only low, high, or max reasoning effort.');
211
+ return effort;
212
+ }
213
+
214
+ function requestControls(effort) {
215
+ return Object.freeze({
216
+ thinking: Object.freeze({ type: 'enabled' }),
217
+ reasoning_effort: normalizeEffort(effort),
218
+ });
219
+ }
220
+
221
+ function safeDiagnostic(alias, models, providers, credentialPresent = false) {
222
+ const entry = models?.[alias];
223
+ if (!isConfiguredAlias(alias, entry)) throw new Error('Unknown Fredrik GLM alias.');
224
+ const provider = configuredProvider(providers);
225
+ if (provider === undefined) throw new Error('Fredrik GLM provider is not configured safely.');
226
+ return Object.freeze({
227
+ alias,
228
+ alias_source: 'profile-config',
229
+ provider: PROVIDER_ID,
230
+ provider_type: PROVIDER_TYPE,
231
+ model: entry.model,
232
+ base_host: 'api.z.ai',
233
+ credential_source: 'profile-file',
234
+ credential_present: credentialPresent === true,
235
+ credential_fingerprint: null,
236
+ wire_format: 'openai-chat-completions',
237
+ health: 'untested',
238
+ });
239
+ }
240
+
241
+ module.exports = Object.freeze({
242
+ PROVIDER_ID,
243
+ PROVIDER_TYPE,
244
+ BASE_URL,
245
+ MODEL_ALIASES,
246
+ MODEL_EFFORTS,
247
+ normalizeBaseUrl,
248
+ readApiKeyFile,
249
+ configuredModels,
250
+ configuredProvider,
251
+ isAllowedRuntimeAlias,
252
+ isConfiguredAlias,
253
+ normalizeEffort,
254
+ requestControls,
255
+ safeDiagnostic,
256
+ });
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const HISTORY_OFFLOAD_MIN_MESSAGES = 64;
4
+ const HISTORY_OFFLOAD_MIN_CHARS = 1_000_000;
5
+
6
+ function messageTextChars(message) {
7
+ if (!Array.isArray(message?.content)) return 0;
8
+ return message.content.reduce((total, part) => (
9
+ total + (part?.type === 'text' && typeof part.text === 'string' ? part.text.length : 0)
10
+ ), 0);
11
+ }
12
+
13
+ function historyTextChars(messages) {
14
+ if (!Array.isArray(messages)) return 0;
15
+ return messages.reduce((total, message) => total + messageTextChars(message), 0);
16
+ }
17
+
18
+ function hasHistoricalOffloadPressure(options = {}) {
19
+ const historyLength = Number(options.historyLength);
20
+ const historyChars = Number(options.historyChars);
21
+ return Number.isSafeInteger(historyLength)
22
+ && Number.isSafeInteger(historyChars)
23
+ && historyLength >= HISTORY_OFFLOAD_MIN_MESSAGES
24
+ && historyChars >= HISTORY_OFFLOAD_MIN_CHARS;
25
+ }
26
+
27
+ module.exports = {
28
+ HISTORY_OFFLOAD_MIN_CHARS,
29
+ HISTORY_OFFLOAD_MIN_MESSAGES,
30
+ hasHistoricalOffloadPressure,
31
+ historyTextChars,
32
+ messageTextChars,
33
+ };