blun-king-cli 9.1.60 → 9.1.62

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/LIESMICH.txt CHANGED
@@ -9,7 +9,7 @@ Installation
9
9
  ------------
10
10
  Die geprüfte Version exakt global installieren:
11
11
 
12
- npm install -g blun-king-cli@9.1.59
12
+ npm install -g blun-king-cli@9.1.61
13
13
 
14
14
  Start
15
15
  -----
@@ -145,5 +145,14 @@ Telegram einrichten
145
145
 
146
146
  ~/.blun/channels/telegram/.env
147
147
 
148
- 3. `king` starten und den angezeigten Pairing-Code mit
149
- /telegram:access pair CODE bestätigen.
148
+ 3. `king` starten und in der Konsole eingeben:
149
+
150
+ /telegram:access connect
151
+
152
+ 4. Den dort erzeugten Einmalcode im gewünschten privaten Chat oder in der
153
+ gewünschten Gruppe senden:
154
+
155
+ /connect CODE
156
+
157
+ Chat-ID und Absender-ID werden intern gespeichert und müssen nie eingegeben
158
+ werden. Der Code verfällt nach zehn Minuten und funktioniert nur einmal.
package/README.md CHANGED
@@ -9,7 +9,7 @@ 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.59
12
+ npm install -g blun-king-cli@9.1.61
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -35,6 +35,11 @@ an. Version, Konto, Modell und Befehle sind ansonsten identisch. Der Unterschied
35
35
  gilt nur für den laufenden Prozess; die gespeicherte
36
36
  Plugin-Konfiguration wird nicht umgeschrieben.
37
37
 
38
+ Telegram-Chats werden ohne manuell eingegebene IDs verbunden. In der Konsole
39
+ erzeugt `/telegram:access connect` einen einmaligen Code. `/connect CODE` im
40
+ gewünschten privaten Chat oder in einer Gruppe speichert Chat und Absender
41
+ intern; der Code verfällt nach zehn Minuten und ist nur einmal verwendbar.
42
+
38
43
  ## Zuverlässiger King-Start
39
44
 
40
45
  Der Windows-Hilfsprozess für private Pfade übernimmt `TEMP` und `TMP` aus der
@@ -106,6 +111,10 @@ besten sicheren lokalen nächsten Schritt um. Der sichtbare Aufgabenplan und all
106
111
  Freigabegrenzen von `/idea` gelten weiterhin. Der Befehl funktioniert auch über
107
112
  Telegram.
108
113
 
114
+ Der sichtbare Verlauf behält standardmäßig die vollständige laufende und
115
+ wiederhergestellte Sitzung. Das Mausrad kann den Verlauf direkt beim ersten Zug
116
+ öffnen; Editor und Fußzeile bleiben dabei fest sichtbar.
117
+
109
118
  Im automatisch angebundenen Telegram-Kanal werden ausschließlich `/loop`,
110
119
  `/goal`, `/idea`, `/chancenradar` und `/befehle` als Befehle ausgeführt. Ist der Agent
111
120
  beschäftigt, bleiben sie in der Warteschlange. Andere Slash-Eingaben werden aus
@@ -5,6 +5,7 @@ const path = require('node:path');
5
5
  const { pathToFileURL } = require('node:url');
6
6
 
7
7
  const { acquireSharedRuntimeLease } = require('./update-lease');
8
+ const { RUNTIME_READY_MESSAGE } = require('./running-update.cjs');
8
9
 
9
10
  const CORE_LOADED_MESSAGE = 'blun-core-bootstrap-loaded';
10
11
 
@@ -31,11 +32,7 @@ async function runCoreBootstrap() {
31
32
 
32
33
  if (process.connected) {
33
34
  try {
34
- process.send({ type: CORE_LOADED_MESSAGE }, () => {
35
- try {
36
- process.disconnect();
37
- } catch {}
38
- });
35
+ process.send({ type: CORE_LOADED_MESSAGE });
39
36
  } catch {}
40
37
  }
41
38
  }
@@ -49,4 +46,4 @@ if (require.main === module) {
49
46
  });
50
47
  }
51
48
 
52
- module.exports = { CORE_LOADED_MESSAGE, runCoreBootstrap };
49
+ module.exports = { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE, runCoreBootstrap };
@@ -21,11 +21,21 @@ const {
21
21
  seedStandardDesignSkill,
22
22
  seedStandardTools,
23
23
  } = require('./standard-tools-bootstrap');
24
- const { CORE_LOADED_MESSAGE } = require('./core-bootstrap');
24
+ const { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE } = require('./core-bootstrap');
25
25
  const { prepareManagedNodeRuntime } = require('./node-runtime');
26
26
  const { repairConfiguredNativeModules } = require('./native-module-repair');
27
27
  const { acquireSharedRuntimeLease, tryAcquireUpdateLease } = require('./update-lease');
28
- const { runExplicitUpdate, runUpdateNotice } = require('./update-notice');
28
+ const { compareSemver, runExplicitUpdate, runUpdateNotice } = require('./update-notice');
29
+ const {
30
+ RUNNING_UPDATE_HANDOFF_EXIT_CODE,
31
+ RUNNING_UPDATE_HANDOFF_MESSAGE,
32
+ RUNNING_UPDATE_PREPARED_MESSAGE,
33
+ activateRuntime,
34
+ handoffRuntime,
35
+ prepareRunningUpdate,
36
+ readActiveRuntime,
37
+ resumeArgsForHandoff,
38
+ } = require('./running-update.cjs');
29
39
  const {
30
40
  ensurePrivateDirectory,
31
41
  securePrivateFile,
@@ -42,6 +52,7 @@ const RAW_ARGS = process.argv.slice(2);
42
52
  const PROFILE = parseProfileLaunchArgs(RAW_ARGS, launcherModeFromArgv(process.argv));
43
53
  const ARGS = PROFILE.args;
44
54
  const CORE_LOAD_TIMEOUT_MS = 30_000;
55
+ const RUNTIME_READY_TIMEOUT_MS = 60_000;
45
56
 
46
57
  function normalizeWindowsPath(value) {
47
58
  return path.win32.normalize(value).replace(/\\+$/u, '').toLowerCase();
@@ -130,10 +141,11 @@ function shouldDetachProtectedCore(options = {}) {
130
141
  return platform !== 'win32' || stdinIsTTY !== true;
131
142
  }
132
143
 
133
- function spawnProtectedCore(args, env, cwd) {
144
+ function spawnProtectedCore(args, env, cwd, options = {}) {
145
+ const packageRoot = path.resolve(options.packageRoot || PKG);
134
146
  const child = spawn(
135
147
  process.execPath,
136
- [path.join(PKG, 'bin', 'core-bootstrap.js'), ...args],
148
+ [path.join(packageRoot, 'bin', 'core-bootstrap.js'), ...args],
137
149
  {
138
150
  cwd,
139
151
  detached: shouldDetachProtectedCore(),
@@ -143,8 +155,10 @@ function spawnProtectedCore(args, env, cwd) {
143
155
  },
144
156
  );
145
157
  let loadedSettled = false;
158
+ let readySettled = false;
146
159
  let completedSettled = false;
147
160
  let resolveLoaded;
161
+ let resolveReady;
148
162
  let resolveCompleted;
149
163
  const loadTimer = setTimeout(() => {
150
164
  try {
@@ -152,6 +166,7 @@ function spawnProtectedCore(args, env, cwd) {
152
166
  } catch {}
153
167
  }, CORE_LOAD_TIMEOUT_MS);
154
168
  const loaded = new Promise((resolve) => { resolveLoaded = resolve; });
169
+ const ready = new Promise((resolve) => { resolveReady = resolve; });
155
170
  const completed = new Promise((resolve) => { resolveCompleted = resolve; });
156
171
  const settleLoaded = (value) => {
157
172
  if (loadedSettled) return;
@@ -159,11 +174,17 @@ function spawnProtectedCore(args, env, cwd) {
159
174
  clearTimeout(loadTimer);
160
175
  resolveLoaded(value);
161
176
  };
177
+ const settleReady = (value) => {
178
+ if (readySettled) return;
179
+ readySettled = true;
180
+ resolveReady(value);
181
+ };
162
182
  const settleCompleted = (result) => {
163
183
  if (completedSettled) return;
164
184
  completedSettled = true;
165
185
  for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
166
186
  settleLoaded(false);
187
+ settleReady(false);
167
188
  resolveCompleted(result);
168
189
  };
169
190
  const signalHandlers = new Map();
@@ -177,23 +198,93 @@ function spawnProtectedCore(args, env, cwd) {
177
198
  process.once(signal, handler);
178
199
  }
179
200
  child.on('message', (message) => {
180
- if (message?.type !== CORE_LOADED_MESSAGE) return;
181
- settleLoaded(true);
182
- try {
183
- child.disconnect();
184
- } catch {}
201
+ if (message?.type === CORE_LOADED_MESSAGE) settleLoaded(true);
202
+ if (message?.type === RUNTIME_READY_MESSAGE) settleReady(true);
203
+ options.onMessage?.(message, child);
185
204
  });
186
205
  child.once('error', (error) => settleCompleted({ error }));
187
206
  child.once('exit', (code, signal) => settleCompleted({ code, signal }));
188
- return { child, completed, loaded };
207
+ return { child, completed, loaded, packageRoot, ready };
208
+ }
209
+
210
+ async function waitForRuntimeReady(core, timeoutMs = RUNTIME_READY_TIMEOUT_MS) {
211
+ let timer;
212
+ const timedOut = new Promise((resolve) => {
213
+ timer = setTimeout(() => resolve(false), timeoutMs);
214
+ timer.unref?.();
215
+ });
216
+ try {
217
+ return await Promise.race([core.ready, timedOut]);
218
+ } finally {
219
+ clearTimeout(timer);
220
+ }
189
221
  }
190
222
 
191
- async function superviseProtectedCore(args, env, cwd, releaseNotice) {
192
- const core = spawnProtectedCore(args, env, cwd);
223
+ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {}) {
224
+ const packageRoot = path.resolve(options.packageRoot || PKG);
225
+ let preparedTarget;
226
+ let handoffSessionId;
227
+ let updateStarted = false;
228
+ const startPreparation = (child) => {
229
+ if (updateStarted || options.runningUpdate === false) return;
230
+ updateStarted = true;
231
+ const sharedHome = env.BLUN_SHARED_HOME;
232
+ if (typeof sharedHome !== 'string' || sharedHome.length === 0) return;
233
+ Promise.resolve((options.prepareRunningUpdate || prepareRunningUpdate)({
234
+ currentVersion: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version,
235
+ sharedHome,
236
+ env,
237
+ })).then((target) => {
238
+ if (target === null || child.connected !== true) return;
239
+ preparedTarget = target;
240
+ child.send({ type: RUNNING_UPDATE_PREPARED_MESSAGE, version: target.version });
241
+ }).catch((error) => {
242
+ options.onRunningUpdateError?.(error);
243
+ });
244
+ };
245
+ const core = spawnProtectedCore(args, env, cwd, {
246
+ packageRoot,
247
+ onMessage(message, child) {
248
+ if (message?.type === RUNTIME_READY_MESSAGE) startPreparation(child);
249
+ if (message?.type === RUNNING_UPDATE_HANDOFF_MESSAGE
250
+ && typeof message.sessionId === 'string'
251
+ && message.sessionId.length > 0) {
252
+ handoffSessionId = message.sessionId;
253
+ }
254
+ },
255
+ });
193
256
  const loaded = await core.loaded;
194
257
  await releaseNotice();
195
258
  const result = await core.completed;
196
259
  if (result.error) throw result.error;
260
+ if (result.code === RUNNING_UPDATE_HANDOFF_EXIT_CODE
261
+ && preparedTarget !== undefined
262
+ && handoffSessionId !== undefined) {
263
+ const handoff = await handoffRuntime({
264
+ target: preparedTarget,
265
+ previous: { packageRoot, version: readPackageVersion() },
266
+ sessionId: handoffSessionId,
267
+ args,
268
+ probeTarget: options.probeRunningUpdate || (() => true),
269
+ stopOld: async () => {},
270
+ startCore: async ({ packageRoot: nextRoot, args: nextArgs }) => {
271
+ const nextCore = spawnProtectedCore(nextArgs, env, cwd, { packageRoot: nextRoot });
272
+ const nextLoaded = await nextCore.loaded;
273
+ const ready = nextLoaded && await waitForRuntimeReady(nextCore, options.readyTimeoutMs);
274
+ if (!ready) {
275
+ try { nextCore.child.kill(); } catch {}
276
+ await nextCore.completed;
277
+ }
278
+ return { ...nextCore, ready };
279
+ },
280
+ activateTarget: (target) => (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target),
281
+ });
282
+ const resumed = handoff.core;
283
+ if (resumed === undefined || resumed.ready !== true) return 1;
284
+ const resumedResult = await resumed.completed;
285
+ if (resumedResult.error) throw resumedResult.error;
286
+ return exitCodeForChild(resumedResult, true);
287
+ }
197
288
  return exitCodeForChild(result, loaded);
198
289
  }
199
290
 
@@ -246,8 +337,40 @@ function spawnManagedLauncher(binary, cwd) {
246
337
  });
247
338
  }
248
339
 
340
+ function spawnActiveLauncher(packageRoot, cwd) {
341
+ const entryName = path.basename(process.argv[1] || 'king.js').toLowerCase() === 'blun.js'
342
+ ? 'blun.js'
343
+ : 'king.js';
344
+ return new Promise((resolve, reject) => {
345
+ const child = spawn(process.execPath, [path.join(packageRoot, 'bin', entryName), ...RAW_ARGS], {
346
+ cwd,
347
+ env: { ...process.env, BLUN_ACTIVE_RUNTIME_ROOT: packageRoot },
348
+ stdio: 'inherit',
349
+ windowsHide: true,
350
+ });
351
+ child.once('error', reject);
352
+ child.once('exit', (code, signal) => {
353
+ if (Number.isInteger(code)) resolve(code);
354
+ else if (signal === 'SIGINT') resolve(130);
355
+ else if (signal === 'SIGTERM') resolve(143);
356
+ else resolve(1);
357
+ });
358
+ });
359
+ }
360
+
249
361
  async function runLauncher(options = {}) {
250
362
  const callerCwd = process.cwd();
363
+ if (options.skipActiveRuntime !== true) {
364
+ const activeHome = resolveLauncherPrivatePaths().sharedHome;
365
+ const activeRuntime = readActiveRuntime(activeHome);
366
+ const currentVersion = readPackageVersion();
367
+ if (activeRuntime !== null
368
+ && path.resolve(activeRuntime.packageRoot) !== PKG
369
+ && compareSemver(currentVersion, activeRuntime.version) === -1) {
370
+ process.exitCode = await spawnActiveLauncher(activeRuntime.packageRoot, callerCwd);
371
+ return;
372
+ }
373
+ }
251
374
  const mode = options.mode || launcherModeFromArgv(process.argv);
252
375
  const explicitUpdateRequest = isNpmPackageUpdateRequest(ARGS);
253
376
  const explicitToolsRequest = String(ARGS[0] || '').toLowerCase() === 'tools';
@@ -26,6 +26,8 @@ const LEGACY_PROFILE_ENTRIES = Object.freeze([
26
26
  'tui.toml',
27
27
  ]);
28
28
  const MIGRATION_MARKER = '.legacy-profile-migrated-v1';
29
+ const SESSION_PATH_MIGRATION_MARKER = '.legacy-profile-session-paths-v2';
30
+ const SESSION_PATH_BACKUP_DIR = '.legacy-profile-session-paths-v2-backup';
29
31
 
30
32
  function normalizeProfileName(value) {
31
33
  const normalized = String(value || '').trim().toLowerCase();
@@ -102,27 +104,161 @@ function copyLegacyEntry(source, target, fsImpl) {
102
104
  if (stat.isFile()) fsImpl.copyFileSync(source, target);
103
105
  }
104
106
 
105
- function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
106
- const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
107
- const marker = path.join(paths.home, MIGRATION_MARKER);
108
- if (fsImpl.existsSync(marker)) return paths;
107
+ function relativePathInside(parent, child) {
108
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
109
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
110
+ return null;
111
+ }
112
+ return relative;
113
+ }
109
114
 
110
- fsImpl.mkdirSync(paths.home, { recursive: true });
111
- for (const entry of LEGACY_PROFILE_ENTRIES) {
112
- copyLegacyEntry(path.join(sharedHome, entry), path.join(paths.home, entry), fsImpl);
115
+ function replaceTextFileIfUnchanged(target, original, replacement, fsImpl) {
116
+ if (fsImpl.readFileSync(target, 'utf8') !== original) {
117
+ throw new Error(`Sitzungsdatei wurde waehrend der Migration geaendert: ${target}`);
113
118
  }
119
+ const temporary = `${target}.migration-${process.pid}-${Date.now()}.tmp`;
120
+ fsImpl.writeFileSync(temporary, replacement, 'utf8');
114
121
  try {
115
- fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
122
+ fsImpl.renameSync(temporary, target);
116
123
  } catch (error) {
117
- if (error?.code !== 'EEXIST') throw error;
124
+ try {
125
+ fsImpl.rmSync(temporary, { force: true });
126
+ } catch {}
127
+ throw error;
118
128
  }
129
+ }
130
+
131
+ function backupFile(source, target, fsImpl) {
132
+ if (fsImpl.existsSync(target)) return;
133
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
134
+ fsImpl.copyFileSync(source, target);
135
+ }
136
+
137
+ function rewriteSessionState(statePath, legacySessionDir, profileSessionDir, backupRoot, relativeSessionDir, fsImpl) {
138
+ if (!fsImpl.existsSync(statePath)) return false;
139
+ const original = fsImpl.readFileSync(statePath, 'utf8');
140
+ let state;
141
+ try {
142
+ state = JSON.parse(original);
143
+ } catch {
144
+ return false;
145
+ }
146
+ if (!state || typeof state !== 'object' || Array.isArray(state) || !state.agents || typeof state.agents !== 'object') {
147
+ return false;
148
+ }
149
+
150
+ let changed = false;
151
+ const agents = {};
152
+ for (const [agentId, value] of Object.entries(state.agents)) {
153
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.homedir !== 'string') {
154
+ agents[agentId] = value;
155
+ continue;
156
+ }
157
+ const relativeAgentPath = relativePathInside(legacySessionDir, value.homedir);
158
+ if (relativeAgentPath === null) {
159
+ agents[agentId] = value;
160
+ continue;
161
+ }
162
+ agents[agentId] = {
163
+ ...value,
164
+ homedir: path.join(profileSessionDir, relativeAgentPath),
165
+ };
166
+ changed = true;
167
+ }
168
+ if (!changed) return false;
169
+
170
+ backupFile(statePath, path.join(backupRoot, 'sessions', relativeSessionDir, 'state.json'), fsImpl);
171
+ const replacement = `${JSON.stringify({ ...state, agents }, null, 2)}\n`;
172
+ replaceTextFileIfUnchanged(statePath, original, replacement, fsImpl);
173
+ return true;
174
+ }
175
+
176
+ function repairDefaultProfileSessionPaths(sharedHome, fsImpl = fs, options = {}) {
177
+ const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
178
+ const marker = path.join(paths.home, SESSION_PATH_MIGRATION_MARKER);
179
+ const filteredWorkDir = options.workDir === undefined ? null : path.resolve(options.workDir);
180
+ if (filteredWorkDir === null && fsImpl.existsSync(marker)) return paths;
181
+ if (!fsImpl.existsSync(paths.sessionIndex)) {
182
+ if (filteredWorkDir === null) fsImpl.writeFileSync(marker, 'legacy session paths repaired once\n', 'utf8');
183
+ return paths;
184
+ }
185
+
186
+ const legacySessions = path.join(sharedHome, 'sessions');
187
+ const backupRoot = path.join(paths.home, SESSION_PATH_BACKUP_DIR);
188
+ const original = fsImpl.readFileSync(paths.sessionIndex, 'utf8');
189
+ const output = [];
190
+ let changed = false;
191
+ for (const line of original.split(/\r?\n/u)) {
192
+ if (line.trim() === '') {
193
+ output.push(line);
194
+ continue;
195
+ }
196
+ let entry;
197
+ try {
198
+ entry = JSON.parse(line);
199
+ } catch {
200
+ output.push(line);
201
+ continue;
202
+ }
203
+ if (!entry || typeof entry !== 'object' || typeof entry.sessionId !== 'string'
204
+ || typeof entry.sessionDir !== 'string' || typeof entry.workDir !== 'string'
205
+ || (filteredWorkDir !== null && path.resolve(entry.workDir) !== filteredWorkDir)) {
206
+ output.push(line);
207
+ continue;
208
+ }
209
+ const relativeSessionDir = relativePathInside(legacySessions, entry.sessionDir);
210
+ if (relativeSessionDir === null || path.basename(relativeSessionDir) !== entry.sessionId) {
211
+ output.push(line);
212
+ continue;
213
+ }
214
+ const profileSessionDir = path.join(paths.sessions, relativeSessionDir);
215
+ if (!fsImpl.existsSync(profileSessionDir)) {
216
+ output.push(line);
217
+ continue;
218
+ }
219
+
220
+ rewriteSessionState(
221
+ path.join(profileSessionDir, 'state.json'),
222
+ path.resolve(entry.sessionDir),
223
+ profileSessionDir,
224
+ backupRoot,
225
+ relativeSessionDir,
226
+ fsImpl,
227
+ );
228
+ output.push(JSON.stringify({ ...entry, sessionDir: profileSessionDir }));
229
+ changed = true;
230
+ }
231
+
232
+ if (changed) {
233
+ backupFile(paths.sessionIndex, path.join(backupRoot, 'session_index.jsonl'), fsImpl);
234
+ replaceTextFileIfUnchanged(paths.sessionIndex, original, output.join('\n'), fsImpl);
235
+ }
236
+ if (filteredWorkDir === null) fsImpl.writeFileSync(marker, 'legacy session paths repaired once\n', 'utf8');
119
237
  return paths;
120
238
  }
121
239
 
240
+ function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
241
+ const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
242
+ const marker = path.join(paths.home, MIGRATION_MARKER);
243
+ fsImpl.mkdirSync(paths.home, { recursive: true });
244
+ if (!fsImpl.existsSync(marker)) {
245
+ for (const entry of LEGACY_PROFILE_ENTRIES) {
246
+ copyLegacyEntry(path.join(sharedHome, entry), path.join(paths.home, entry), fsImpl);
247
+ }
248
+ try {
249
+ fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
250
+ } catch (error) {
251
+ if (error?.code !== 'EEXIST') throw error;
252
+ }
253
+ }
254
+ return repairDefaultProfileSessionPaths(sharedHome, fsImpl);
255
+ }
256
+
122
257
  module.exports = {
123
258
  DEFAULT_PROFILE,
124
259
  normalizeProfileName,
125
260
  parseProfileLaunchArgs,
261
+ repairDefaultProfileSessionPaths,
126
262
  resolveProfilePaths,
127
263
  seedDefaultProfileFromLegacy,
128
264
  };