blun-king-cli 9.1.52 → 9.1.57

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.52
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
@@ -59,8 +79,20 @@ lokale Schritte um und fragt gezielt nach, wenn eine Entscheidung oder ein Zugan
59
79
  fehlt. Blockierte, auf Freigabe wartende und abgebrochene Schritte bleiben mit
60
80
  ihrem tatsächlichen Zustand sichtbar.
61
81
 
82
+ Der erste Werkzeugaufruf muss den sichtbaren Plan setzen. Ausgehende Aktionen
83
+ werden zusätzlich pro Werkzeugaufruf geprüft und bei fehlender Kanal- oder
84
+ Zugangsfreigabe angehalten.
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
+
62
94
  Im automatisch angebundenen Telegram-Kanal werden ausschließlich /loop, /goal,
63
- /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
64
96
  sie in der Warteschlange. Andere Slash-Eingaben werden aus Sicherheitsgründen
65
97
  als normale Chatnachrichten behandelt.
66
98
 
@@ -87,8 +119,11 @@ anschließend mit GetMedia abgerufen.
87
119
  Die Medienleiste erscheint nur bei aktiven Medienaufträgen. Sie zeigt
88
120
  Warteschlangenplatz, Produktionsphase und Laufzeit sowie nur tatsächlich vom
89
121
  Anbieter gemeldete Prozentwerte, Schritte, Frames, FPS, Audiolänge, Restzeit,
90
- Auflösung, Wellenform und Vorschauen. Aktualisierte Vorschaubilder werden
91
- 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.
92
127
 
93
128
  Aktualisieren
94
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.52
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
@@ -74,8 +94,20 @@ lokale Schritte um und fragt gezielt nach, wenn eine Entscheidung oder ein Zugan
74
94
  fehlt. Blockierte, auf Freigabe wartende und abgebrochene Schritte bleiben mit
75
95
  ihrem tatsächlichen Zustand sichtbar.
76
96
 
97
+ Der erste Werkzeugaufruf muss den sichtbaren Plan setzen. Ausgehende Aktionen
98
+ werden zusätzlich pro Werkzeugaufruf geprüft und bei fehlender Kanal- oder
99
+ Zugangsfreigabe angehalten.
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
+
77
109
  Im automatisch angebundenen Telegram-Kanal werden ausschließlich `/loop`,
78
- `/goal`, `/idea` und `/befehle` als Befehle ausgeführt. Ist der Agent
110
+ `/goal`, `/idea`, `/chancenradar` und `/befehle` als Befehle ausgeführt. Ist der Agent
79
111
  beschäftigt, bleiben sie in der Warteschlange. Andere Slash-Eingaben werden aus
80
112
  Sicherheitsgründen als normale Chatnachrichten behandelt.
81
113
 
@@ -108,8 +140,11 @@ anschließend mit `GetMedia` abgerufen.
108
140
  Die Medienleiste erscheint nur bei aktiven Medienaufträgen. Sie zeigt
109
141
  Warteschlangenplatz, Produktionsphase und Laufzeit sowie nur tatsächlich vom
110
142
  Anbieter gemeldete Prozentwerte, Schritte, Frames, FPS, Audiolänge, Restzeit,
111
- Auflösung, Wellenform und Vorschauen. Aktualisierte Vorschaubilder werden
112
- 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.
113
148
 
114
149
  ## Aktualisieren
115
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;
@@ -1108,7 +1109,7 @@ const WINDOWS_ACL_SCRIPT = [
1108
1109
  "[Console]::Out.Write('BLUN_PRIVATE_ACL_OK_V1')",
1109
1110
  'exit 0',
1110
1111
  '} catch {',
1111
- "[Console]::Error.Write('BLUN_PRIVATE_ACL_ERROR_V1')",
1112
+ "[Console]::Error.Write('BLUN_PRIVATE_ACL_ERROR_V1|' + $_.Exception.GetType().FullName + '|' + $_.Exception.Message)",
1112
1113
  'exit 1',
1113
1114
  '}',
1114
1115
  ].join('\n');
@@ -1127,9 +1128,18 @@ function windowsSystemDirectory(options) {
1127
1128
  return path.win32.normalize(directory);
1128
1129
  }
1129
1130
 
1130
- function windowsHostEnvironment(systemDirectory) {
1131
+ function windowsHostEnvironment(systemDirectory, env = process.env) {
1131
1132
  const systemRoot = path.win32.dirname(systemDirectory);
1132
- return { SystemRoot: systemRoot, WINDIR: systemRoot };
1133
+ const temporaryDirectory = env.TEMP || env.TMP;
1134
+ if (!temporaryDirectory) {
1135
+ throw new Error('Windows private ACL temporary directory is unavailable.');
1136
+ }
1137
+ return {
1138
+ SystemRoot: systemRoot,
1139
+ WINDIR: systemRoot,
1140
+ TEMP: temporaryDirectory,
1141
+ TMP: env.TMP || temporaryDirectory,
1142
+ };
1133
1143
  }
1134
1144
 
1135
1145
  function runWindowsCommand(executable, args, options, env, input) {
@@ -1190,14 +1200,14 @@ function resolveWindowsPrivateTarget(targetPath) {
1190
1200
  throw new Error('Invalid Windows private path.');
1191
1201
  }
1192
1202
  return absoluteTarget;
1193
- } catch {
1194
- throw new Error('Windows private ACL hardening or verification failed.');
1203
+ } catch (error) {
1204
+ throw new Error('Windows private path validation failed.', { cause: error });
1195
1205
  }
1196
1206
  }
1197
1207
 
1198
1208
  function windowsAclController(options) {
1199
1209
  const systemDirectory = windowsSystemDirectory(options);
1200
- const hostEnvironment = windowsHostEnvironment(systemDirectory);
1210
+ const hostEnvironment = windowsHostEnvironment(systemDirectory, options.env || process.env);
1201
1211
  const whoami = path.win32.join(systemDirectory, 'whoami.exe');
1202
1212
  const powershell = path.win32.join(
1203
1213
  systemDirectory,
@@ -1235,7 +1245,7 @@ function windowsAclController(options) {
1235
1245
  if (!path.isAbsolute(absoluteTarget)
1236
1246
  || root.toLowerCase() === absoluteTarget.toLowerCase()
1237
1247
  || !['directory', 'file', 'file-write', 'tree'].includes(kind)) {
1238
- throw new Error('Windows private ACL hardening or verification failed.');
1248
+ throw new Error('Windows private ACL target validation failed.');
1239
1249
  }
1240
1250
  const env = {
1241
1251
  ...hostEnvironment,
@@ -1253,11 +1263,21 @@ function windowsAclController(options) {
1253
1263
  env,
1254
1264
  input,
1255
1265
  );
1256
- } catch {
1257
- throw new Error('Windows private ACL hardening or verification failed.');
1266
+ } catch (error) {
1267
+ const stderr = typeof error?.stderr === 'string'
1268
+ ? error.stderr : Buffer.isBuffer(error?.stderr) ? error.stderr.toString('utf8') : '';
1269
+ const marker = `${WINDOWS_ACL_ERROR_SENTINEL}|`;
1270
+ const rawDetail = stderr.startsWith(marker) ? stderr.slice(marker.length) : '';
1271
+ const detail = rawDetail.replace(/[\u0000-\u001f\u007f]+/gu, ' ').trim().slice(0, 500);
1272
+ throw new Error(
1273
+ detail.length > 0
1274
+ ? `Windows private ACL operation failed: ${detail}`
1275
+ : 'Windows private ACL operation failed.',
1276
+ { cause: error },
1277
+ );
1258
1278
  }
1259
1279
  if (output !== WINDOWS_ACL_OK_SENTINEL) {
1260
- throw new Error('Windows private ACL hardening or verification failed.');
1280
+ throw new Error('Windows private ACL verification returned an unexpected response.');
1261
1281
  }
1262
1282
  } finally {
1263
1283
  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}`;
@@ -260655,7 +260681,7 @@ var init_turn = __esmMin((() => {
260655
260681
  this.agent.log.warn("goal token accounting failed", { error });
260656
260682
  }
260657
260683
  },
260658
- hooks: {
260684
+ hooks: {
260659
260685
  beforeStep: async ({ signal: stepSignal, llm }) => {
260660
260686
  this.agent.microCompaction.detect();
260661
260687
  await this.agent.injection.inject();
@@ -260705,6 +260731,8 @@ var init_turn = __esmMin((() => {
260705
260731
  return { continue: false };
260706
260732
  },
260707
260733
  prepareToolExecution: async (ctx) => {
260734
+ const ideaPolicy = enforceIdeaToolPolicy(this.agent, ctx);
260735
+ if (ideaPolicy !== void 0) return ideaPolicy;
260708
260736
  const cached = deduper.checkSameStep(ctx.toolCall.id, ctx.toolCall.name, ctx.args);
260709
260737
  if (cached !== null) return { syntheticResult: cached };
260710
260738
  },
@@ -262008,6 +262036,14 @@ function isMediaToolName(name) {
262008
262036
  "GetMedia"
262009
262037
  ].includes(name);
262010
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
+ }
262011
262047
  function emitMediaProgress(ctx, toolName, phase, data = {}) {
262012
262048
  ctx.onUpdate({
262013
262049
  kind: "custom",
@@ -262161,6 +262197,7 @@ var init_blun_media$1 = __esmMin((() => {
262161
262197
  isError: true,
262162
262198
  output: `"${args.path}" is empty.`
262163
262199
  };
262200
+ emitMediaProgress(ctx, this.name, "uploading", mediaInlinePreviewPayload(data, fileType.mimeType));
262164
262201
  const options = {
262165
262202
  signal: ctx.signal,
262166
262203
  toolCallId: ctx.toolCallId
@@ -310932,6 +310969,14 @@ function mediaPayloadNumberArray(payload, keys) {
310932
310969
  if (samples.length > 0) return samples.slice(0, 512);
310933
310970
  }
310934
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
+ }
310935
310980
  function mediaProgressFromPayload(payload) {
310936
310981
  const nestedProgress = payload["progress"];
310937
310982
  const source = typeof nestedProgress === "object" && nestedProgress !== null && !Array.isArray(nestedProgress) ? {
@@ -310962,7 +311007,8 @@ function mediaProgressFromPayload(payload) {
310962
311007
  previewMimeType: mediaPayloadText(source, ["preview_mime_type", "previewMimeType", "thumbnail_mime_type", "thumbnailMimeType"]),
310963
311008
  sampleUrl: mediaPayloadText(source, ["sample_url", "sampleUrl", "audio_preview_url", "audioPreviewUrl"]),
310964
311009
  localPath: mediaPayloadText(source, ["local_path", "localPath"]),
310965
- waveform: mediaPayloadNumberArray(source, ["waveform", "waveform_samples", "waveformSamples"])
311010
+ waveform: mediaPayloadNumberArray(source, ["waveform", "waveform_samples", "waveformSamples"]),
311011
+ phaseHistory: mediaPayloadTextArray(source, ["phase_history", "phaseHistory"])
310966
311012
  };
310967
311013
  return Object.fromEntries(Object.entries(progress).filter(([, value]) => value !== void 0));
310968
311014
  }
@@ -400692,6 +400738,7 @@ registerUiCatalogFragment({
400692
400738
  "command.reload-tui.description": "Reload interface settings only",
400693
400739
  "command.compact.description": "Compact the conversation context",
400694
400740
  "command.goal.description": "Start or manage an autonomous goal",
400741
+ "command.idea.description": "Start or manage an autonomous idea",
400695
400742
  "command.loop.description": "Start or manage a recurring prompt",
400696
400743
  "command.init.description": "Analyze the codebase and generate AGENTS.md",
400697
400744
  "command.fork.description": "Fork the current session",
@@ -400749,6 +400796,7 @@ registerUiCatalogFragment({
400749
400796
  "command.reload-tui.description": "Nur Oberflächeneinstellungen neu laden",
400750
400797
  "command.compact.description": "Konversationskontext komprimieren",
400751
400798
  "command.goal.description": "Autonomes Ziel starten oder verwalten",
400799
+ "command.idea.description": "Autonome Idee starten oder verwalten",
400752
400800
  "command.loop.description": "Wiederkehrenden Prompt starten oder verwalten",
400753
400801
  "command.init.description": "Codebasis analysieren und AGENTS.md erstellen",
400754
400802
  "command.fork.description": "Aktuelle Sitzung abzweigen",
@@ -400806,6 +400854,7 @@ registerUiCatalogFragment({
400806
400854
  "command.reload-tui.description": "Recargar solo los ajustes de la interfaz",
400807
400855
  "command.compact.description": "Compactar el contexto de la conversación",
400808
400856
  "command.goal.description": "Iniciar o gestionar un objetivo autónomo",
400857
+ "command.idea.description": "Iniciar o gestionar una idea autónoma",
400809
400858
  "command.loop.description": "Iniciar o gestionar una instrucción recurrente",
400810
400859
  "command.init.description": "Analizar el código base y generar AGENTS.md",
400811
400860
  "command.fork.description": "Bifurcar la sesión actual",
@@ -400863,6 +400912,7 @@ registerUiCatalogFragment({
400863
400912
  "command.reload-tui.description": "Recharger uniquement les paramètres de l’interface",
400864
400913
  "command.compact.description": "Compacter le contexte de la conversation",
400865
400914
  "command.goal.description": "Démarrer ou gérer un objectif autonome",
400915
+ "command.idea.description": "Démarrer ou gérer une idée autonome",
400866
400916
  "command.loop.description": "Démarrer ou gérer une demande récurrente",
400867
400917
  "command.init.description": "Analyser la base de code et générer AGENTS.md",
400868
400918
  "command.fork.description": "Créer une branche de la session actuelle",
@@ -400920,6 +400970,7 @@ registerUiCatalogFragment({
400920
400970
  "command.reload-tui.description": "Läs bara in gränssnittsinställningarna på nytt",
400921
400971
  "command.compact.description": "Komprimera konversationskontexten",
400922
400972
  "command.goal.description": "Starta eller hantera ett autonomt mål",
400973
+ "command.idea.description": "Starta eller hantera en självständig idé",
400923
400974
  "command.loop.description": "Starta eller hantera en återkommande prompt",
400924
400975
  "command.init.description": "Analysera kodbasen och skapa AGENTS.md",
400925
400976
  "command.fork.description": "Förgrena den aktuella sessionen",
@@ -400977,6 +401028,7 @@ registerUiCatalogFragment({
400977
401028
  "command.reload-tui.description": "Znovu načíst pouze nastavení rozhraní",
400978
401029
  "command.compact.description": "Komprimovat kontext konverzace",
400979
401030
  "command.goal.description": "Spustit nebo spravovat autonomní cíl",
401031
+ "command.idea.description": "Spustit nebo spravovat autonomní nápad",
400980
401032
  "command.loop.description": "Spustit nebo spravovat opakované zadání",
400981
401033
  "command.init.description": "Analyzovat kódovou základnu a vygenerovat AGENTS.md",
400982
401034
  "command.fork.description": "Vytvořit větev aktuální relace",
@@ -401389,7 +401441,22 @@ const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
401389
401441
  {
401390
401442
  name: "idea",
401391
401443
  aliases: [],
401392
- descriptionKey: "command.goal.description",
401444
+ descriptionKey: "command.idea.description",
401445
+ priority: 80,
401446
+ argumentHint: "<objective>",
401447
+ availability: (args) => {
401448
+ const trimmed = args.trim();
401449
+ if (trimmed === "") return "always";
401450
+ const tokens = trimmed.split(/\s+/);
401451
+ const action = tokens[0]?.toLowerCase();
401452
+ if (tokens.length === 1 && (action === "status" || action === "pause" || action === "paused" || action === "resume" || action === "cancel" || action === "clear" || action === "stop")) return "always";
401453
+ return "idle-only";
401454
+ }
401455
+ },
401456
+ {
401457
+ name: "chancenradar",
401458
+ aliases: [],
401459
+ descriptionKey: "command.idea.description",
401393
401460
  priority: 80,
401394
401461
  argumentHint: "<objective>",
401395
401462
  availability: (args) => {
@@ -416690,7 +416757,7 @@ function bufferContext(state, chatId, tag) {
416690
416757
  function channelOrigin(envelope) {
416691
416758
  return `Telegram · ${envelope.meta.user ?? envelope.meta.chat_id}`;
416692
416759
  }
416693
- const TELEGRAM_REMOTE_COMMANDS = Object.freeze(["loop", "goal", "idea", "befehle"]);
416760
+ const TELEGRAM_REMOTE_COMMANDS = Object.freeze(["loop", "goal", "idea", "chancenradar", "befehle"]);
416694
416761
  function telegramRemoteCommand(text) {
416695
416762
  const trimmed = text.trim();
416696
416763
  const parsed = parseSlashInput(trimmed);
@@ -416904,19 +416971,25 @@ function sessionMcpServers() {
416904
416971
  return telegramChannelMcpServers();
416905
416972
  }
416906
416973
  function managedTelegramPluginEnabled() {
416974
+ return managedTelegramPluginRoot() !== void 0;
416975
+ }
416976
+ function managedTelegramPluginRoot() {
416907
416977
  const blunHome = join(homedir(), ".blun");
416908
- const managedRoot = join(blunHome, "plugins", "managed", "telegram");
416909
- if (!existsSync(join(managedRoot, "dist", "mcp-server.mjs"))) return false;
416910
416978
  try {
416911
- 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;
416912
416983
  } catch {
416913
- return false;
416984
+ return void 0;
416914
416985
  }
416915
416986
  }
416916
416987
  function resolveTelegramMcpEntry() {
416917
416988
  const main = process.argv[1];
416989
+ const managedRoot = managedTelegramPluginRoot();
416918
416990
  const candidates = [
416919
416991
  process.env["BLUN_TELEGRAM_MCP_SERVER"],
416992
+ managedRoot === void 0 ? void 0 : join(managedRoot, "dist", "mcp-server.mjs"),
416920
416993
  join(homedir(), ".blun", "plugins", "managed", "telegram", "dist", "mcp-server.mjs"),
416921
416994
  main === void 0 ? void 0 : join(dirname(main), "..", "..", "..", "plugins", "telegram", "dist", "mcp-server.mjs")
416922
416995
  ];
@@ -417322,8 +417395,10 @@ var TelegramChannelController = class {
417322
417395
  }
417323
417396
  resolveBridgeEntry() {
417324
417397
  const main = process.argv[1];
417398
+ const managedRoot = managedTelegramPluginRoot();
417325
417399
  const candidates = [
417326
417400
  process.env["BLUN_TELEGRAM_BRIDGE"],
417401
+ managedRoot === void 0 ? void 0 : join(managedRoot, "dist", "bridge.mjs"),
417327
417402
  join(homedir(), ".blun", "plugins", "managed", "telegram", "dist", "bridge.mjs"),
417328
417403
  main === void 0 ? void 0 : join(dirname(main), "..", "..", "..", "plugins", "telegram", "dist", "bridge.mjs")
417329
417404
  ];
@@ -420130,6 +420205,7 @@ async function handleGoalCommand(host, args) {
420130
420205
  return;
420131
420206
  }
420132
420207
  }
420208
+ const IDEA_CONTRACT_MARKER = "Work as a self-directing employee:";
420133
420209
  const IDEA_ALLOWED_CHANNELS = Object.freeze([]);
420134
420210
  const IDEA_TERMINAL_TODO_STATUSES = Object.freeze([
420135
420211
  "done",
@@ -420137,6 +420213,77 @@ const IDEA_TERMINAL_TODO_STATUSES = Object.freeze([
420137
420213
  "waiting_approval",
420138
420214
  "aborted"
420139
420215
  ]);
420216
+ const IDEA_OUTBOUND_TOOL_LEAF = /(?:^|_)(?:send|reply|post|publish|upload|purchase|buy|register|signup|merge|push|deploy|create_(?:account|user|issue|pull|pr|comment|post|order|campaign|repo(?:sitory)?)|update_(?:issue|pull|pr|comment|post)|delete_(?:issue|pull|pr|comment|post|account|user)|release)(?:_|$)/iu;
420217
+ const IDEA_OUTBOUND_SHELL = /(?:\bgit\s+push\b|\bnpm\s+publish\b|\bgh\s+(?:pr|issue|release)\s+(?:create|edit|comment|merge|delete)\b|\b(?:ssh|scp|rsync)\b|\bSend-MailMessage\b|\b(?:curl|wget)\b[^\r\n]*(?:-X|--request)\s*(?:POST|PUT|PATCH|DELETE)\b|\bInvoke-(?:WebRequest|RestMethod)\b[^\r\n]*-Method\s+(?:POST|PUT|PATCH|DELETE)\b)/iu;
420218
+ function isIdeaGoal(agent) {
420219
+ const goal = agent.goal.getActiveGoal();
420220
+ return goal !== null && goal.completionCriterion?.includes(IDEA_CONTRACT_MARKER) === true;
420221
+ }
420222
+ function ideaTodos(agent) {
420223
+ const value = agent.tools.store?.["todo"];
420224
+ return Array.isArray(value) ? value : [];
420225
+ }
420226
+ function validInitialIdeaPlan(value) {
420227
+ if (!Array.isArray(value) || value.length === 0) return false;
420228
+ let active = 0;
420229
+ for (const todo of value) {
420230
+ if (typeof todo !== "object" || todo === null || typeof todo.title !== "string" || todo.title.trim().length === 0) return false;
420231
+ if (todo.status === "in_progress") active += 1;
420232
+ else if (todo.status !== "pending") return false;
420233
+ }
420234
+ return active === 1;
420235
+ }
420236
+ function ideaOutboundChannel(toolName, args) {
420237
+ if (toolName === "Bash") {
420238
+ const command = typeof args?.command === "string" ? args.command : "";
420239
+ return IDEA_OUTBOUND_SHELL.test(command) ? "shell-external" : void 0;
420240
+ }
420241
+ if (!toolName.startsWith("mcp__")) return void 0;
420242
+ const lower = toolName.toLowerCase();
420243
+ if (lower.includes("blun_language_guard")) return void 0;
420244
+ const leaf = lower.split("__").at(-1) ?? lower;
420245
+ if (!IDEA_OUTBOUND_TOOL_LEAF.test(leaf)) return void 0;
420246
+ for (const channel of ["telegram", "github", "reddit", "linkedin", "instagram", "facebook", "youtube", "gmail", "email", "smtp", "canva"]) if (lower.includes(channel)) return channel;
420247
+ return lower.split("__")[1] ?? "external";
420248
+ }
420249
+ function enforceIdeaToolPolicy(agent, context) {
420250
+ if (!isIdeaGoal(agent)) return;
420251
+ const todos = ideaTodos(agent);
420252
+ const batchKey = `${String(context.turnId)}:${String(context.stepNumber)}`;
420253
+ if (todos.length === 0) {
420254
+ const firstCall = context.toolCalls[0];
420255
+ if (context.toolCall.id !== firstCall?.id || context.toolCall.name !== "TodoList") return {
420256
+ block: true,
420257
+ reason: "Idea mode requires TodoList to be the first tool call. Publish the visible plan before research or action."
420258
+ };
420259
+ if (!validInitialIdeaPlan(context.args?.todos)) return {
420260
+ block: true,
420261
+ reason: "The first Idea TodoList must contain at least one item, exactly one in_progress item, and only pending items otherwise."
420262
+ };
420263
+ agent.ideaPlanBatchKey = batchKey;
420264
+ return;
420265
+ }
420266
+ if (agent.ideaPlanBatchKey === batchKey && context.toolCall.name !== "TodoList") return {
420267
+ block: true,
420268
+ reason: "Idea mode publishes its plan in a separate first step. Continue research or action in the next step."
420269
+ };
420270
+ if (context.toolCall.name === "UpdateGoal" && (context.args?.status === "complete" || context.args?.status === "blocked")) {
420271
+ if (todos.some((todo) => !IDEA_TERMINAL_TODO_STATUSES.includes(todo.status))) return {
420272
+ block: true,
420273
+ reason: "Idea mode cannot finish while a plan item has no truthful terminal status."
420274
+ };
420275
+ if (context.args.status === "complete" && todos.some((todo) => todo.status !== "done")) return {
420276
+ block: true,
420277
+ reason: "Idea mode can complete only when every plan item is done. Use blocked when any item is blocked, waiting_approval, or aborted."
420278
+ };
420279
+ }
420280
+ const channel = ideaOutboundChannel(context.toolCall.name, context.args);
420281
+ if (channel === void 0 || IDEA_ALLOWED_CHANNELS.includes(channel)) return;
420282
+ return {
420283
+ block: true,
420284
+ reason: `Idea mode blocked an external action on channel "${channel}". Prepare the result, mark the step waiting_approval with the required access or approval, and ask the user.`
420285
+ };
420286
+ }
420140
420287
  function buildIdeaPrompt(objective) {
420141
420288
  const allowedChannels = IDEA_ALLOWED_CHANNELS.length === 0 ? "none" : IDEA_ALLOWED_CHANNELS.join(", ");
420142
420289
  return `<idea-mode>
@@ -420149,7 +420296,7 @@ ${buildIdeaContract(allowedChannels)}
420149
420296
  </idea-mode>`;
420150
420297
  }
420151
420298
  function buildIdeaContract(allowedChannels = IDEA_ALLOWED_CHANNELS.length === 0 ? "none" : IDEA_ALLOWED_CHANNELS.join(", ")) {
420152
- return `Work as a self-directing employee: discover a useful route, research current examples and real user problems when relevant, decide, implement reversible local work, verify results, and keep going without waiting for step-by-step instructions.
420299
+ return `${IDEA_CONTRACT_MARKER} discover a useful route, research current examples and real user problems when relevant, decide, implement reversible local work, verify results, and keep going without waiting for step-by-step instructions.
420153
420300
 
420154
420301
  Maintain the task-specific plan in TodoList. Keep exactly one item in_progress. Every plan item must end in exactly one truthful terminal status from: ${IDEA_TERMINAL_TODO_STATUSES.join(", ")}. A blocked title must include the reason and what would unblock it. A waiting_approval title must include the prepared result and the exact approval needed. An aborted title must include the last reached state.
420155
420302
 
@@ -420175,6 +420322,42 @@ async function handleIdeaCommand(host, args) {
420175
420322
  }
420176
420323
  });
420177
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
+ }
420178
420361
  function parseNextGoalCommand(tokens) {
420179
420362
  if (tokens.length === 2 && tokens[1]?.toLowerCase() === "manage") return { kind: "next-manage" };
420180
420363
  let index = 1;
@@ -493192,6 +493375,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
493192
493375
  case "idea":
493193
493376
  await handleIdeaCommand(host, args);
493194
493377
  return;
493378
+ case "chancenradar":
493379
+ await handleChanceRadarCommand(host, args);
493380
+ return;
493195
493381
  case "loop":
493196
493382
  await handleLoopCommand(host, args);
493197
493383
  return;
@@ -498482,6 +498668,10 @@ var MediaActivityStore = class {
498482
498668
  const mediaKind = event.mediaKind === "media" && previous?.mediaKind !== void 0 ? previous.mediaKind : event.mediaKind ?? previous?.mediaKind ?? "media";
498483
498669
  const phase = mediaActivityStatusKey(event.phase ?? previous?.phase ?? "processing");
498484
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
+ }
498485
498675
  if (phaseHistory.at(-1) !== phase) phaseHistory.push(phase);
498486
498676
  this.jobs.set(key, {
498487
498677
  ...previous,
@@ -514009,7 +514199,7 @@ var BlunTUI = class {
514009
514199
  };
514010
514200
  this.telegramRemoteCommandContext = context;
514011
514201
  const execution = item.telegramCommandName === "befehle" ? Promise.resolve().then(() => {
514012
- context.responses.push("/loop\n/goal\n/idea\n/befehle");
514202
+ context.responses.push("/loop\n/goal\n/idea\n/chancenradar\n/befehle");
514013
514203
  }) : dispatchInput(this, item.text);
514014
514204
  execution.catch((error) => {
514015
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.52",
3
+ "version": "9.1.57",
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": {