blun-king-cli 9.1.53 → 9.1.58

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.53
12
+ npm install -g blun-king-cli@9.1.57
13
13
 
14
14
  Start
15
15
  -----
@@ -20,10 +20,30 @@ Start
20
20
  Beide Startbefehle verwenden dieselbe Version, dasselbe Konto, dasselbe Modell
21
21
  und dieselben Befehle.
22
22
 
23
+ Zuverlässiger King-Start
24
+ -----------------------
25
+ Der Windows-Hilfsprozess für private Pfade übernimmt TEMP und TMP aus der
26
+ Benutzersitzung. Dadurch arbeitet die ACL-Prüfung auch unter einem normalen
27
+ Benutzerkonto. Bei einem Fehler wird die tatsächliche Ursache des
28
+ Unterprozesses angezeigt.
29
+
30
+ Ein laufender Telegram-Prozess blockiert Aktualisierungen nicht mehr.
31
+ Unveränderte Plugin-Dateien werden wiederverwendet; geänderte Dateien werden in
32
+ einem inhaltsadressierten Verzeichnis daneben installiert. Der neue Prozess
33
+ verwendet den neuen Pfad, während der bisherige Prozess seinen geladenen Stand
34
+ geordnet beenden kann.
35
+
23
36
  Beim Fortsetzen mit --continue oder --session wird der Arbeitsbereich der
24
37
  bestehenden Sitzung direkt verwendet. Die Ordnerauswahl bleibt neuen Starts
25
38
  vorbehalten und kann eine Fortsetzung daher nicht mehr vorzeitig beenden.
26
39
 
40
+ Schnellstart
41
+ ------------
42
+ Beim Starten oder Fortsetzen einer Sitzung werden die Skill-Verzeichnisse
43
+ parallel geprüft und eingelesen. Die Skills werden weiterhin in der
44
+ ursprünglichen, festen Reihenfolge registriert; Priorität und Verhalten bei
45
+ doppelten Namen bleiben unverändert.
46
+
27
47
  Zug-Wächter
28
48
  -----------
29
49
  Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
@@ -63,8 +83,16 @@ Der erste Werkzeugaufruf muss den sichtbaren Plan setzen. Ausgehende Aktionen
63
83
  werden zusätzlich pro Werkzeugaufruf geprüft und bei fehlender Kanal- oder
64
84
  Zugangsfreigabe angehalten.
65
85
 
86
+ /chancenradar <Ziel> startet einen eigenen Modus zur Chancensuche. Der Agent
87
+ durchsucht aktuelle öffentliche Quellen, Foren und Communitys nach echten
88
+ Problemen und bestehenden Lösungsansätzen, trennt Belege von Schlussfolgerungen,
89
+ bewertet Chancen nach Wirkung, Passung, Aufwand und Verlässlichkeit und setzt den
90
+ besten sicheren lokalen nächsten Schritt um. Der sichtbare Aufgabenplan und alle
91
+ Freigabegrenzen von /idea gelten weiterhin. Der Befehl funktioniert auch über
92
+ Telegram.
93
+
66
94
  Im automatisch angebundenen Telegram-Kanal werden ausschließlich /loop, /goal,
67
- /idea und /befehle als Befehle ausgeführt. Ist der Agent beschäftigt, bleiben
95
+ /idea, /chancenradar und /befehle als Befehle ausgeführt. Ist der Agent beschäftigt, bleiben
68
96
  sie in der Warteschlange. Andere Slash-Eingaben werden aus Sicherheitsgründen
69
97
  als normale Chatnachrichten behandelt.
70
98
 
@@ -91,8 +119,11 @@ anschließend mit GetMedia abgerufen.
91
119
  Die Medienleiste erscheint nur bei aktiven Medienaufträgen. Sie zeigt
92
120
  Warteschlangenplatz, Produktionsphase und Laufzeit sowie nur tatsächlich vom
93
121
  Anbieter gemeldete Prozentwerte, Schritte, Frames, FPS, Audiolänge, Restzeit,
94
- Auflösung, Wellenform und Vorschauen. Aktualisierte Vorschaubilder werden
95
- erneuert; Medienpfade und Webadressen sind als Terminalverweise anklickbar.
122
+ Auflösung, Wellenform und Vorschauen. Bei Bild-zu-Video erscheint das lokale
123
+ Ausgangsbild sofort als Vorschau. Tatsächlich gemeldete Phasen bleiben erhalten,
124
+ sodass auch kurze Schritte wie Rendern und Speichern in der Produktionskette
125
+ sichtbar sind. Aktualisierte Vorschaubilder werden erneuert; Medienpfade und
126
+ Webadressen sind als Terminalverweise anklickbar.
96
127
 
97
128
  Aktualisieren
98
129
  -------------
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.53
12
+ npm install -g blun-king-cli@9.1.57
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -35,10 +35,30 @@ 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
+ ## Zuverlässiger King-Start
39
+
40
+ Der Windows-Hilfsprozess für private Pfade übernimmt `TEMP` und `TMP` aus der
41
+ Benutzersitzung. Dadurch kann der C#-Compiler für die ACL-Prüfung auch unter
42
+ einem normalen Benutzerkonto arbeiten. Scheitert der Unterprozess, nennt die
43
+ Fehlermeldung jetzt dessen tatsächliche Ursache.
44
+
45
+ Ein bereits laufender Telegram-Prozess blockiert Aktualisierungen nicht mehr.
46
+ Unveränderte Plugin-Dateien werden wiederverwendet; geänderte Dateien werden in
47
+ einem inhaltsadressierten Verzeichnis daneben installiert. Der neue Prozess
48
+ verwendet den neuen Pfad, während der bisherige Prozess seinen geladenen Stand
49
+ geordnet beenden kann.
50
+
38
51
  Beim Fortsetzen mit `--continue` oder `--session` wird der Arbeitsbereich der
39
52
  bestehenden Sitzung direkt verwendet. Die Ordnerauswahl bleibt neuen Starts
40
53
  vorbehalten und kann eine Fortsetzung daher nicht mehr vorzeitig beenden.
41
54
 
55
+ ## Schnellstart
56
+
57
+ Beim Starten oder Fortsetzen einer Sitzung werden die Skill-Verzeichnisse
58
+ parallel geprüft und eingelesen. Die Skills werden weiterhin in der
59
+ ursprünglichen, festen Reihenfolge registriert; Priorität und Verhalten bei
60
+ doppelten Namen bleiben unverändert.
61
+
42
62
  ## Zug-Wächter
43
63
 
44
64
  Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
@@ -78,8 +98,16 @@ Der erste Werkzeugaufruf muss den sichtbaren Plan setzen. Ausgehende Aktionen
78
98
  werden zusätzlich pro Werkzeugaufruf geprüft und bei fehlender Kanal- oder
79
99
  Zugangsfreigabe angehalten.
80
100
 
101
+ `/chancenradar <Ziel>` startet einen eigenen Modus zur Chancensuche. Der Agent
102
+ durchsucht aktuelle öffentliche Quellen, Foren und Communitys nach echten
103
+ Problemen und bestehenden Lösungsansätzen, trennt Belege von Schlussfolgerungen,
104
+ bewertet Chancen nach Wirkung, Passung, Aufwand und Verlässlichkeit und setzt den
105
+ besten sicheren lokalen nächsten Schritt um. Der sichtbare Aufgabenplan und alle
106
+ Freigabegrenzen von `/idea` gelten weiterhin. Der Befehl funktioniert auch über
107
+ Telegram.
108
+
81
109
  Im automatisch angebundenen Telegram-Kanal werden ausschließlich `/loop`,
82
- `/goal`, `/idea` und `/befehle` als Befehle ausgeführt. Ist der Agent
110
+ `/goal`, `/idea`, `/chancenradar` und `/befehle` als Befehle ausgeführt. Ist der Agent
83
111
  beschäftigt, bleiben sie in der Warteschlange. Andere Slash-Eingaben werden aus
84
112
  Sicherheitsgründen als normale Chatnachrichten behandelt.
85
113
 
@@ -112,8 +140,11 @@ anschließend mit `GetMedia` abgerufen.
112
140
  Die Medienleiste erscheint nur bei aktiven Medienaufträgen. Sie zeigt
113
141
  Warteschlangenplatz, Produktionsphase und Laufzeit sowie nur tatsächlich vom
114
142
  Anbieter gemeldete Prozentwerte, Schritte, Frames, FPS, Audiolänge, Restzeit,
115
- Auflösung, Wellenform und Vorschauen. Aktualisierte Vorschaubilder werden
116
- erneuert; Medienpfade und Webadressen sind als Terminalverweise anklickbar.
143
+ Auflösung, Wellenform und Vorschauen. Bei Bild-zu-Video erscheint das lokale
144
+ Ausgangsbild sofort als Vorschau. Tatsächlich gemeldete Phasen bleiben erhalten,
145
+ sodass auch kurze Schritte wie Rendern und Speichern in der Produktionskette
146
+ sichtbar sind. Aktualisierte Vorschaubilder werden erneuert; Medienpfade und
147
+ Webadressen sind als Terminalverweise anklickbar.
117
148
 
118
149
  ## Aktualisieren
119
150
 
@@ -1,12 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const { randomUUID } = require('node:crypto');
3
+ const { createHash, randomUUID } = require('node:crypto');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
7
7
  const {
8
8
  ensurePrivateDirectory,
9
9
  securePrivateFile,
10
+ securePrivateTree,
10
11
  writePrivateFile,
11
12
  } = require('./private-paths');
12
13
 
@@ -14,6 +15,76 @@ function isObject(value) {
14
15
  return typeof value === 'object' && value !== null && !Array.isArray(value);
15
16
  }
16
17
 
18
+ function snapshotPluginTree(root) {
19
+ const entries = [];
20
+ const visit = (directory, relativeDirectory = '') => {
21
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })
22
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
23
+ const relativePath = path.join(relativeDirectory, entry.name);
24
+ const absolutePath = path.join(directory, entry.name);
25
+ const stat = fs.lstatSync(absolutePath);
26
+ if (stat.isSymbolicLink()) return false;
27
+ if (stat.isDirectory()) {
28
+ entries.push({ kind: 'directory', path: relativePath });
29
+ if (!visit(absolutePath, relativePath)) return false;
30
+ } else if (stat.isFile()) {
31
+ entries.push({
32
+ kind: 'file',
33
+ path: relativePath,
34
+ content: fs.readFileSync(absolutePath),
35
+ });
36
+ } else {
37
+ return false;
38
+ }
39
+ }
40
+ return true;
41
+ };
42
+ return visit(root) ? entries : null;
43
+ }
44
+
45
+ function pluginTreesAreIdentical(sourceRoot, pluginRoot) {
46
+ try {
47
+ const source = snapshotPluginTree(sourceRoot);
48
+ const installed = snapshotPluginTree(pluginRoot);
49
+ if (source === null || installed === null || source.length !== installed.length) return false;
50
+ return source.every((entry, index) => {
51
+ const other = installed[index];
52
+ return entry.kind === other.kind
53
+ && entry.path === other.path
54
+ && (entry.kind !== 'file' || entry.content.equals(other.content));
55
+ });
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ function pluginTreeDigest(root) {
62
+ const entries = snapshotPluginTree(root);
63
+ if (entries === null) throw new Error(`Invalid managed plugin tree: ${root}`);
64
+ const hash = createHash('sha256');
65
+ for (const entry of entries) {
66
+ const metadata = Buffer.from(`${entry.kind}\0${entry.path}\0`, 'utf8');
67
+ hash.update(metadata);
68
+ if (entry.kind === 'file') {
69
+ const length = Buffer.allocUnsafe(8);
70
+ length.writeBigUInt64BE(BigInt(entry.content.length));
71
+ hash.update(length);
72
+ hash.update(entry.content);
73
+ }
74
+ }
75
+ return hash.digest('hex');
76
+ }
77
+
78
+ function hasCurrentTelegramRecord(installed, pluginRoot) {
79
+ const records = installed.plugins.filter((plugin) => plugin.id === 'telegram');
80
+ if (records.length !== 1) return false;
81
+ const [telegram] = records;
82
+ return telegram.root === pluginRoot
83
+ && telegram.source === 'local-path'
84
+ && telegram.enabled === true
85
+ && telegram.capabilities?.mcpServers?.telegram?.enabled === true;
86
+ }
87
+
17
88
  function readInstalledPlugins(filePath) {
18
89
  if (!fs.existsSync(filePath)) return { version: 1, plugins: [] };
19
90
 
@@ -82,43 +153,52 @@ function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().t
82
153
  const sourceRoot = path.join(packageRoot, 'telegram-plugin');
83
154
  const pluginsDir = path.join(blunDir, 'plugins');
84
155
  const managedDir = path.join(pluginsDir, 'managed');
85
- const pluginRoot = path.join(managedDir, 'telegram');
156
+ const legacyPluginRoot = path.join(managedDir, 'telegram');
86
157
  const installedFile = path.join(pluginsDir, 'installed.json');
87
158
 
88
159
  ensurePrivateDirectory(pluginsDir);
89
160
  if (fs.existsSync(installedFile)) securePrivateFile(installedFile);
90
161
  const installed = readInstalledPlugins(installedFile);
91
- const updatedInstalled = updatedInstalledPlugins(installed, pluginRoot, now);
92
162
  ensurePrivateDirectory(managedDir);
163
+ if (fs.existsSync(legacyPluginRoot)
164
+ && pluginTreesAreIdentical(sourceRoot, legacyPluginRoot)) {
165
+ if (!hasCurrentTelegramRecord(installed, legacyPluginRoot)) {
166
+ const updatedInstalled = updatedInstalledPlugins(installed, legacyPluginRoot, now);
167
+ writeInstalledPlugins(installedFile, updatedInstalled);
168
+ }
169
+ return { installedFile, pluginRoot: legacyPluginRoot, reused: true };
170
+ }
171
+ const sourceDigest = pluginTreeDigest(sourceRoot);
172
+ const pluginRoot = path.join(managedDir, `telegram-${sourceDigest}`);
173
+ const updatedInstalled = updatedInstalledPlugins(installed, pluginRoot, now);
174
+ if (fs.existsSync(pluginRoot)) {
175
+ if (!pluginTreesAreIdentical(sourceRoot, pluginRoot)) {
176
+ throw new Error(`Managed Telegram plugin digest mismatch: ${pluginRoot}`);
177
+ }
178
+ if (!hasCurrentTelegramRecord(installed, pluginRoot)) {
179
+ writeInstalledPlugins(installedFile, updatedInstalled);
180
+ }
181
+ return { installedFile, pluginRoot, reused: true };
182
+ }
93
183
  const nonce = `${process.pid}-${randomUUID()}`;
94
184
  const stagingRoot = path.join(managedDir, `.telegram-${nonce}.tmp`);
95
- const backupRoot = path.join(managedDir, `.telegram-${nonce}.bak`);
96
- let hadPreviousPlugin = false;
97
185
  let installedNewPlugin = false;
98
186
  try {
99
- if (fs.existsSync(pluginRoot)) {
100
- ensurePrivateDirectory(pluginRoot);
101
- hadPreviousPlugin = true;
102
- }
103
187
  fs.cpSync(sourceRoot, stagingRoot, { recursive: true, force: true });
104
- if (hadPreviousPlugin) fs.renameSync(pluginRoot, backupRoot);
188
+ securePrivateTree(stagingRoot);
105
189
  try {
106
190
  fs.renameSync(stagingRoot, pluginRoot);
107
191
  installedNewPlugin = true;
108
192
  writeInstalledPlugins(installedFile, updatedInstalled);
109
193
  } catch (error) {
110
194
  if (installedNewPlugin) fs.rmSync(pluginRoot, { recursive: true, force: true });
111
- if (hadPreviousPlugin && fs.existsSync(backupRoot)) {
112
- fs.renameSync(backupRoot, pluginRoot);
113
- }
114
195
  throw error;
115
196
  }
116
- if (hadPreviousPlugin) fs.rmSync(backupRoot, { recursive: true, force: true });
117
197
  } finally {
118
198
  fs.rmSync(stagingRoot, { recursive: true, force: true });
119
199
  }
120
200
 
121
- return { installedFile, pluginRoot };
201
+ return { installedFile, pluginRoot, reused: false };
122
202
  }
123
203
 
124
204
  module.exports = {
@@ -11,6 +11,7 @@ const PRIVATE_TREE_MAX_DEPTH = 64;
11
11
  const PRIVATE_TREE_MAX_ENTRIES = 4096;
12
12
  const PRIVATE_TREE_MAX_PATH_LENGTH = 4096;
13
13
  const WINDOWS_ACL_OK_SENTINEL = 'BLUN_PRIVATE_ACL_OK_V1';
14
+ const WINDOWS_ACL_ERROR_SENTINEL = 'BLUN_PRIVATE_ACL_ERROR_V1';
14
15
  const WINDOWS_SID_PATTERN = /S-\d-\d+(?:-\d+)+/gi;
15
16
  const WINDOWS_LOCAL_DOS_PATH_PATTERN = /^[A-Za-z]:\\/u;
16
17
  const WINDOWS_RESERVED_PATH_SEGMENT = /^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9]|LPT[1-9])(?:\..*)?$/iu;
@@ -33,6 +34,9 @@ public static class BlunPrivateAclNative
33
34
  private const uint READ_CONTROL = 0x00020000;
34
35
  private const uint WRITE_DAC = 0x00040000;
35
36
  private const uint SYNCHRONIZE = 0x00100000;
37
+ private const uint FILE_SHARE_READ = 0x00000001;
38
+ private const uint FILE_SHARE_WRITE = 0x00000002;
39
+ private const uint FILE_SHARE_DELETE = 0x00000004;
36
40
  private const uint CREATE_NEW = 1;
37
41
  private const uint OPEN_EXISTING = 3;
38
42
  private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
@@ -607,7 +611,7 @@ public static class BlunPrivateAclNative
607
611
  out ioStatus,
608
612
  IntPtr.Zero,
609
613
  0,
610
- 0,
614
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
611
615
  FILE_OPEN_IF,
612
616
  FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT,
613
617
  IntPtr.Zero,
@@ -1108,7 +1112,7 @@ const WINDOWS_ACL_SCRIPT = [
1108
1112
  "[Console]::Out.Write('BLUN_PRIVATE_ACL_OK_V1')",
1109
1113
  'exit 0',
1110
1114
  '} catch {',
1111
- "[Console]::Error.Write('BLUN_PRIVATE_ACL_ERROR_V1')",
1115
+ "[Console]::Error.Write('BLUN_PRIVATE_ACL_ERROR_V1|' + $_.Exception.GetType().FullName + '|' + $_.Exception.Message)",
1112
1116
  'exit 1',
1113
1117
  '}',
1114
1118
  ].join('\n');
@@ -1127,9 +1131,18 @@ function windowsSystemDirectory(options) {
1127
1131
  return path.win32.normalize(directory);
1128
1132
  }
1129
1133
 
1130
- function windowsHostEnvironment(systemDirectory) {
1134
+ function windowsHostEnvironment(systemDirectory, env = process.env) {
1131
1135
  const systemRoot = path.win32.dirname(systemDirectory);
1132
- return { SystemRoot: systemRoot, WINDIR: systemRoot };
1136
+ const temporaryDirectory = env.TEMP || env.TMP;
1137
+ if (!temporaryDirectory) {
1138
+ throw new Error('Windows private ACL temporary directory is unavailable.');
1139
+ }
1140
+ return {
1141
+ SystemRoot: systemRoot,
1142
+ WINDIR: systemRoot,
1143
+ TEMP: temporaryDirectory,
1144
+ TMP: env.TMP || temporaryDirectory,
1145
+ };
1133
1146
  }
1134
1147
 
1135
1148
  function runWindowsCommand(executable, args, options, env, input) {
@@ -1190,14 +1203,14 @@ function resolveWindowsPrivateTarget(targetPath) {
1190
1203
  throw new Error('Invalid Windows private path.');
1191
1204
  }
1192
1205
  return absoluteTarget;
1193
- } catch {
1194
- throw new Error('Windows private ACL hardening or verification failed.');
1206
+ } catch (error) {
1207
+ throw new Error('Windows private path validation failed.', { cause: error });
1195
1208
  }
1196
1209
  }
1197
1210
 
1198
1211
  function windowsAclController(options) {
1199
1212
  const systemDirectory = windowsSystemDirectory(options);
1200
- const hostEnvironment = windowsHostEnvironment(systemDirectory);
1213
+ const hostEnvironment = windowsHostEnvironment(systemDirectory, options.env || process.env);
1201
1214
  const whoami = path.win32.join(systemDirectory, 'whoami.exe');
1202
1215
  const powershell = path.win32.join(
1203
1216
  systemDirectory,
@@ -1235,7 +1248,7 @@ function windowsAclController(options) {
1235
1248
  if (!path.isAbsolute(absoluteTarget)
1236
1249
  || root.toLowerCase() === absoluteTarget.toLowerCase()
1237
1250
  || !['directory', 'file', 'file-write', 'tree'].includes(kind)) {
1238
- throw new Error('Windows private ACL hardening or verification failed.');
1251
+ throw new Error('Windows private ACL target validation failed.');
1239
1252
  }
1240
1253
  const env = {
1241
1254
  ...hostEnvironment,
@@ -1253,11 +1266,21 @@ function windowsAclController(options) {
1253
1266
  env,
1254
1267
  input,
1255
1268
  );
1256
- } catch {
1257
- throw new Error('Windows private ACL hardening or verification failed.');
1269
+ } catch (error) {
1270
+ const stderr = typeof error?.stderr === 'string'
1271
+ ? error.stderr : Buffer.isBuffer(error?.stderr) ? error.stderr.toString('utf8') : '';
1272
+ const marker = `${WINDOWS_ACL_ERROR_SENTINEL}|`;
1273
+ const rawDetail = stderr.startsWith(marker) ? stderr.slice(marker.length) : '';
1274
+ const detail = rawDetail.replace(/[\u0000-\u001f\u007f]+/gu, ' ').trim().slice(0, 500);
1275
+ throw new Error(
1276
+ detail.length > 0
1277
+ ? `Windows private ACL operation failed for ${kind} target "${absoluteTarget}": ${detail}`
1278
+ : `Windows private ACL operation failed for ${kind} target "${absoluteTarget}".`,
1279
+ { cause: error },
1280
+ );
1258
1281
  }
1259
1282
  if (output !== WINDOWS_ACL_OK_SENTINEL) {
1260
- throw new Error('Windows private ACL hardening or verification failed.');
1283
+ throw new Error('Windows private ACL verification returned an unexpected response.');
1261
1284
  }
1262
1285
  } finally {
1263
1286
  input?.fill(0);
package/blun.mjs CHANGED
@@ -235059,15 +235059,25 @@ async function discoverSkills(options) {
235059
235059
  }
235060
235060
  const directorySkills = /* @__PURE__ */ new Set();
235061
235061
  const subdirs = [];
235062
- for (const entry of entries) {
235062
+ const classifiedEntries = await Promise.all(entries.map(async (entry) => {
235063
235063
  const entryPath = posix$2.join(dirPath, entry);
235064
- if (await isFile(posix$2.join(entryPath, "SKILL.md"))) directorySkills.add(entry);
235065
- if (entry === "node_modules" || entry.startsWith(".")) continue;
235066
- if (await isDir(entryPath)) subdirs.push(entry);
235064
+ const [hasSkillFile, directory] = await Promise.all([
235065
+ isFile(posix$2.join(entryPath, "SKILL.md")),
235066
+ entry === "node_modules" || entry.startsWith(".") ? false : isDir(entryPath)
235067
+ ]);
235068
+ return {
235069
+ entry,
235070
+ hasSkillFile,
235071
+ directory
235072
+ };
235073
+ }));
235074
+ for (const classified of classifiedEntries) {
235075
+ if (classified.hasSkillFile) directorySkills.add(classified.entry);
235076
+ if (classified.directory) subdirs.push(classified.entry);
235067
235077
  }
235068
235078
  const allowedSubSkillBundles = /* @__PURE__ */ new Map();
235069
- for (const entry of directorySkills) {
235070
- const skill = await parseAndRegister({
235079
+ const directorySkillEntries = [...directorySkills];
235080
+ const directorySkillResults = await parseAndRegisterAll(directorySkillEntries.map((entry) => ({
235071
235081
  parse,
235072
235082
  byName,
235073
235083
  skillMdPath: posix$2.join(dirPath, entry, "SKILL.md"),
@@ -235077,7 +235087,10 @@ async function discoverSkills(options) {
235077
235087
  warn,
235078
235088
  skip,
235079
235089
  subSkillParentName
235080
- });
235090
+ })));
235091
+ for (let index = 0; index < directorySkillEntries.length; index++) {
235092
+ const entry = directorySkillEntries[index];
235093
+ const skill = directorySkillResults[index];
235081
235094
  if (skill !== void 0 && hasSubSkillEnabled(skill)) allowedSubSkillBundles.set(entry, skill.name);
235082
235095
  }
235083
235096
  if (isTopLevel) {
@@ -235094,6 +235107,7 @@ async function discoverSkills(options) {
235094
235107
  skip
235095
235108
  });
235096
235109
  }
235110
+ const flatInputs = [];
235097
235111
  for (const entry of entries) {
235098
235112
  if (!entry.endsWith(".md")) continue;
235099
235113
  if (entry === "SKILL.md") continue;
@@ -235104,7 +235118,7 @@ async function discoverSkills(options) {
235104
235118
  }
235105
235119
  const skillMdPath = posix$2.join(dirPath, entry);
235106
235120
  if (!await isFile(skillMdPath)) continue;
235107
- await parseAndRegister({
235121
+ flatInputs.push({
235108
235122
  parse,
235109
235123
  byName,
235110
235124
  skillMdPath,
@@ -235115,6 +235129,7 @@ async function discoverSkills(options) {
235115
235129
  skip
235116
235130
  });
235117
235131
  }
235132
+ await parseAndRegisterAll(flatInputs);
235118
235133
  }
235119
235134
  for (const entry of subdirs) {
235120
235135
  if (directorySkills.has(entry) && !allowedSubSkillBundles.has(entry)) continue;
@@ -235181,7 +235196,7 @@ async function pushProvidedRoot(out, root, isDir, realpath) {
235181
235196
  };
235182
235197
  return true;
235183
235198
  }
235184
- async function parseAndRegister(input) {
235199
+ async function parseSkillCandidate(input) {
235185
235200
  try {
235186
235201
  const parsed = await input.parse({
235187
235202
  skillMdPath: input.skillMdPath,
@@ -235201,9 +235216,6 @@ async function parseAndRegister(input) {
235201
235216
  ...skill,
235202
235217
  plugin: input.root.plugin
235203
235218
  };
235204
- input.onDiscoveredSkill?.(discovered);
235205
- const key = normalizeSkillName(discovered.name);
235206
- if (!input.byName.has(key)) input.byName.set(key, discovered);
235207
235219
  return discovered;
235208
235220
  } catch (error) {
235209
235221
  if (error instanceof UnsupportedSkillTypeError) input.skip({
@@ -235216,6 +235228,20 @@ async function parseAndRegister(input) {
235216
235228
  return;
235217
235229
  }
235218
235230
  }
235231
+ function registerSkillCandidate(input, discovered) {
235232
+ if (discovered === void 0) return;
235233
+ input.onDiscoveredSkill?.(discovered);
235234
+ const key = normalizeSkillName(discovered.name);
235235
+ if (!input.byName.has(key)) input.byName.set(key, discovered);
235236
+ return discovered;
235237
+ }
235238
+ async function parseAndRegister(input) {
235239
+ return registerSkillCandidate(input, await parseSkillCandidate(input));
235240
+ }
235241
+ async function parseAndRegisterAll(inputs) {
235242
+ const discovered = await Promise.all(inputs.map((input) => parseSkillCandidate(input)));
235243
+ return discovered.map((skill, index) => registerSkillCandidate(inputs[index], skill));
235244
+ }
235219
235245
  function qualifySubSkillName(parentName, skillName) {
235220
235246
  if (skillName === parentName || skillName.startsWith(`${parentName}.`)) return skillName;
235221
235247
  return `${parentName}.${skillName}`;
@@ -262010,6 +262036,14 @@ function isMediaToolName(name) {
262010
262036
  "GetMedia"
262011
262037
  ].includes(name);
262012
262038
  }
262039
+ const MEDIA_INLINE_PREVIEW_MAX_BYTES = 1536 * 1024;
262040
+ function mediaInlinePreviewPayload(data, mimeType) {
262041
+ if (data.byteLength === 0 || data.byteLength > MEDIA_INLINE_PREVIEW_MAX_BYTES) return {};
262042
+ return {
262043
+ previewDataBase64: Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64"),
262044
+ previewMimeType: mimeType
262045
+ };
262046
+ }
262013
262047
  function emitMediaProgress(ctx, toolName, phase, data = {}) {
262014
262048
  ctx.onUpdate({
262015
262049
  kind: "custom",
@@ -262163,6 +262197,7 @@ var init_blun_media$1 = __esmMin((() => {
262163
262197
  isError: true,
262164
262198
  output: `"${args.path}" is empty.`
262165
262199
  };
262200
+ emitMediaProgress(ctx, this.name, "uploading", mediaInlinePreviewPayload(data, fileType.mimeType));
262166
262201
  const options = {
262167
262202
  signal: ctx.signal,
262168
262203
  toolCallId: ctx.toolCallId
@@ -310934,6 +310969,14 @@ function mediaPayloadNumberArray(payload, keys) {
310934
310969
  if (samples.length > 0) return samples.slice(0, 512);
310935
310970
  }
310936
310971
  }
310972
+ function mediaPayloadTextArray(payload, keys) {
310973
+ for (const key of keys) {
310974
+ const value = payload[key];
310975
+ if (!Array.isArray(value)) continue;
310976
+ const entries = value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
310977
+ if (entries.length > 0) return entries.slice(-32);
310978
+ }
310979
+ }
310937
310980
  function mediaProgressFromPayload(payload) {
310938
310981
  const nestedProgress = payload["progress"];
310939
310982
  const source = typeof nestedProgress === "object" && nestedProgress !== null && !Array.isArray(nestedProgress) ? {
@@ -310964,7 +311007,8 @@ function mediaProgressFromPayload(payload) {
310964
311007
  previewMimeType: mediaPayloadText(source, ["preview_mime_type", "previewMimeType", "thumbnail_mime_type", "thumbnailMimeType"]),
310965
311008
  sampleUrl: mediaPayloadText(source, ["sample_url", "sampleUrl", "audio_preview_url", "audioPreviewUrl"]),
310966
311009
  localPath: mediaPayloadText(source, ["local_path", "localPath"]),
310967
- waveform: mediaPayloadNumberArray(source, ["waveform", "waveform_samples", "waveformSamples"])
311010
+ waveform: mediaPayloadNumberArray(source, ["waveform", "waveform_samples", "waveformSamples"]),
311011
+ phaseHistory: mediaPayloadTextArray(source, ["phase_history", "phaseHistory"])
310968
311012
  };
310969
311013
  return Object.fromEntries(Object.entries(progress).filter(([, value]) => value !== void 0));
310970
311014
  }
@@ -401409,6 +401453,21 @@ const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
401409
401453
  return "idle-only";
401410
401454
  }
401411
401455
  },
401456
+ {
401457
+ name: "chancenradar",
401458
+ aliases: [],
401459
+ descriptionKey: "command.idea.description",
401460
+ priority: 80,
401461
+ argumentHint: "<objective>",
401462
+ availability: (args) => {
401463
+ const trimmed = args.trim();
401464
+ if (trimmed === "") return "always";
401465
+ const tokens = trimmed.split(/\s+/);
401466
+ const action = tokens[0]?.toLowerCase();
401467
+ if (tokens.length === 1 && (action === "status" || action === "pause" || action === "paused" || action === "resume" || action === "cancel" || action === "clear" || action === "stop")) return "always";
401468
+ return "idle-only";
401469
+ }
401470
+ },
401412
401471
  {
401413
401472
  name: "loop",
401414
401473
  aliases: [],
@@ -416698,7 +416757,7 @@ function bufferContext(state, chatId, tag) {
416698
416757
  function channelOrigin(envelope) {
416699
416758
  return `Telegram · ${envelope.meta.user ?? envelope.meta.chat_id}`;
416700
416759
  }
416701
- const TELEGRAM_REMOTE_COMMANDS = Object.freeze(["loop", "goal", "idea", "befehle"]);
416760
+ const TELEGRAM_REMOTE_COMMANDS = Object.freeze(["loop", "goal", "idea", "chancenradar", "befehle"]);
416702
416761
  function telegramRemoteCommand(text) {
416703
416762
  const trimmed = text.trim();
416704
416763
  const parsed = parseSlashInput(trimmed);
@@ -416912,19 +416971,25 @@ function sessionMcpServers() {
416912
416971
  return telegramChannelMcpServers();
416913
416972
  }
416914
416973
  function managedTelegramPluginEnabled() {
416974
+ return managedTelegramPluginRoot() !== void 0;
416975
+ }
416976
+ function managedTelegramPluginRoot() {
416915
416977
  const blunHome = join(homedir(), ".blun");
416916
- const managedRoot = join(blunHome, "plugins", "managed", "telegram");
416917
- if (!existsSync(join(managedRoot, "dist", "mcp-server.mjs"))) return false;
416918
416978
  try {
416919
- return JSON.parse(readFileSync(join(blunHome, "plugins", "installed.json"), "utf8")).plugins?.some((plugin) => plugin.id === "telegram" && plugin.enabled === true && (typeof plugin.root !== "string" || plugin.root === managedRoot)) ?? false;
416979
+ const telegram = JSON.parse(readFileSync(join(blunHome, "plugins", "installed.json"), "utf8")).plugins?.find((plugin) => plugin.id === "telegram" && plugin.enabled === true);
416980
+ if (typeof telegram?.root !== "string" || telegram.root.length === 0) return void 0;
416981
+ if (!existsSync(join(telegram.root, "dist", "mcp-server.mjs"))) return void 0;
416982
+ return telegram.root;
416920
416983
  } catch {
416921
- return false;
416984
+ return void 0;
416922
416985
  }
416923
416986
  }
416924
416987
  function resolveTelegramMcpEntry() {
416925
416988
  const main = process.argv[1];
416989
+ const managedRoot = managedTelegramPluginRoot();
416926
416990
  const candidates = [
416927
416991
  process.env["BLUN_TELEGRAM_MCP_SERVER"],
416992
+ managedRoot === void 0 ? void 0 : join(managedRoot, "dist", "mcp-server.mjs"),
416928
416993
  join(homedir(), ".blun", "plugins", "managed", "telegram", "dist", "mcp-server.mjs"),
416929
416994
  main === void 0 ? void 0 : join(dirname(main), "..", "..", "..", "plugins", "telegram", "dist", "mcp-server.mjs")
416930
416995
  ];
@@ -417330,8 +417395,10 @@ var TelegramChannelController = class {
417330
417395
  }
417331
417396
  resolveBridgeEntry() {
417332
417397
  const main = process.argv[1];
417398
+ const managedRoot = managedTelegramPluginRoot();
417333
417399
  const candidates = [
417334
417400
  process.env["BLUN_TELEGRAM_BRIDGE"],
417401
+ managedRoot === void 0 ? void 0 : join(managedRoot, "dist", "bridge.mjs"),
417335
417402
  join(homedir(), ".blun", "plugins", "managed", "telegram", "dist", "bridge.mjs"),
417336
417403
  main === void 0 ? void 0 : join(dirname(main), "..", "..", "..", "plugins", "telegram", "dist", "bridge.mjs")
417337
417404
  ];
@@ -420255,6 +420322,42 @@ async function handleIdeaCommand(host, args) {
420255
420322
  }
420256
420323
  });
420257
420324
  }
420325
+ const CHANCE_RADAR_CONTRACT_MARKER = "Operate as an evidence-driven opportunity radar:";
420326
+ function buildChanceRadarContract() {
420327
+ return `${buildIdeaContract()}
420328
+
420329
+ ${CHANCE_RADAR_CONTRACT_MARKER} search current public sources, relevant forums, communities, reviews, and competing approaches for concrete unmet needs. Prefer multiple independent sources when available. Preserve the source URL and retrieval date for material evidence. Never invent demand, users, quotes, reach, or market size.
420330
+
420331
+ Separate observations, inferences, and unknowns. For every serious opportunity record the problem, affected people, evidence, existing alternatives, fit with the user's objective, likely impact, implementation effort, and confidence. Justify every ranking; a numeric score without evidence is not a finding. Drop weak or duplicated opportunities instead of padding the list.
420332
+
420333
+ Choose the strongest supported opportunity, update TodoList with the decision, and execute the best safe local next step. Ask a focused question only when the answer changes the ranking or next action. Keep all Idea-mode approval and access boundaries in force.`;
420334
+ }
420335
+ function buildChanceRadarPrompt(objective) {
420336
+ return `<chance-radar-mode>
420337
+ The user's objective is:
420338
+ ${objective}
420339
+
420340
+ Your first tool call MUST be TodoList. Publish a task-specific discovery, research, ranking, and execution plan before any research or action.
420341
+
420342
+ ${buildChanceRadarContract()}
420343
+ </chance-radar-mode>`;
420344
+ }
420345
+ async function handleChanceRadarCommand(host, args) {
420346
+ const parsed = parseGoalCommand(args);
420347
+ if (parsed.kind !== "create") {
420348
+ await handleGoalCommand(host, args);
420349
+ return;
420350
+ }
420351
+ await createGoal(host, {
420352
+ ...parsed,
420353
+ completionCriterion: buildChanceRadarContract()
420354
+ }, args, {
420355
+ commandName: "chancenradar",
420356
+ sendInput: (objective) => {
420357
+ host.sendMessage(host.requireSession(), objective, { parts: buildChanceRadarPrompt(objective) });
420358
+ }
420359
+ });
420360
+ }
420258
420361
  function parseNextGoalCommand(tokens) {
420259
420362
  if (tokens.length === 2 && tokens[1]?.toLowerCase() === "manage") return { kind: "next-manage" };
420260
420363
  let index = 1;
@@ -493272,6 +493375,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
493272
493375
  case "idea":
493273
493376
  await handleIdeaCommand(host, args);
493274
493377
  return;
493378
+ case "chancenradar":
493379
+ await handleChanceRadarCommand(host, args);
493380
+ return;
493275
493381
  case "loop":
493276
493382
  await handleLoopCommand(host, args);
493277
493383
  return;
@@ -498562,6 +498668,10 @@ var MediaActivityStore = class {
498562
498668
  const mediaKind = event.mediaKind === "media" && previous?.mediaKind !== void 0 ? previous.mediaKind : event.mediaKind ?? previous?.mediaKind ?? "media";
498563
498669
  const phase = mediaActivityStatusKey(event.phase ?? previous?.phase ?? "processing");
498564
498670
  const phaseHistory = [...previous?.phaseHistory ?? []];
498671
+ if (Array.isArray(event.phaseHistory)) for (const reportedPhase of event.phaseHistory) {
498672
+ const normalized = mediaActivityStatusKey(reportedPhase);
498673
+ if (phaseHistory.at(-1) !== normalized) phaseHistory.push(normalized);
498674
+ }
498565
498675
  if (phaseHistory.at(-1) !== phase) phaseHistory.push(phase);
498566
498676
  this.jobs.set(key, {
498567
498677
  ...previous,
@@ -513379,7 +513489,6 @@ var BlunTUI = class {
513379
513489
  this.showStatus(warning, "warning");
513380
513490
  }
513381
513491
  async init() {
513382
- if (this.startupWorkspaceSelectionPending) return false;
513383
513492
  await this.measureStartupPhase("experimental_ms", async () => {
513384
513493
  setExperimentalFeatures(await this.harness.getExperimentalFeatures());
513385
513494
  });
@@ -513392,6 +513501,7 @@ var BlunTUI = class {
513392
513501
  } else this.authFlow.enterManagedAccountUnavailableStartupState(accountContext.kind);
513393
513502
  return false;
513394
513503
  }
513504
+ if (this.startupWorkspaceSelectionPending) return false;
513395
513505
  await this.measureStartupPhase("provider_models_ms", () => this.refreshProviderModelsBeforeSession());
513396
513506
  const { startup } = this.options;
513397
513507
  const { workDir } = this.state.appState;
@@ -514089,7 +514199,7 @@ var BlunTUI = class {
514089
514199
  };
514090
514200
  this.telegramRemoteCommandContext = context;
514091
514201
  const execution = item.telegramCommandName === "befehle" ? Promise.resolve().then(() => {
514092
- context.responses.push("/loop\n/goal\n/idea\n/befehle");
514202
+ context.responses.push("/loop\n/goal\n/idea\n/chancenradar\n/befehle");
514093
514203
  }) : dispatchInput(this, item.text);
514094
514204
  execution.catch((error) => {
514095
514205
  this.showError(formatErrorMessage$2(error));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.53",
3
+ "version": "9.1.58",
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": {