blun-king-cli 9.1.53 → 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 +35 -4
- package/README.md +35 -4
- package/bin/plugin-bootstrap.js +95 -15
- package/bin/private-paths.js +30 -10
- package/blun.mjs +129 -19
- package/package.json +1 -1
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.
|
|
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.
|
|
95
|
-
|
|
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.
|
|
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.
|
|
116
|
-
|
|
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
|
|
package/bin/plugin-bootstrap.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 = {
|
package/bin/private-paths.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
235062
|
+
const classifiedEntries = await Promise.all(entries.map(async (entry) => {
|
|
235063
235063
|
const entryPath = posix$2.join(dirPath, entry);
|
|
235064
|
-
|
|
235065
|
-
|
|
235066
|
-
|
|
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
|
-
|
|
235070
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
@@ -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));
|