@kenkaiiii/gg-boss 4.3.155 → 4.3.157
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/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --max-old-space-size=8192 --expose-gc
|
|
2
2
|
import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
ActivityIndicator,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CompactionSpinner,
|
|
10
10
|
GGBoss,
|
|
11
11
|
InputArea,
|
|
12
|
+
MODELS,
|
|
12
13
|
MessageResponse,
|
|
13
14
|
ModelSelector,
|
|
14
15
|
SelectList,
|
|
@@ -21,7 +22,9 @@ import {
|
|
|
21
22
|
ToolUseLoader,
|
|
22
23
|
UserMessage,
|
|
23
24
|
bossStore,
|
|
25
|
+
closeLogger,
|
|
24
26
|
getAppPaths,
|
|
27
|
+
getBossState,
|
|
25
28
|
getContextWindow,
|
|
26
29
|
getSplashAudioDurationMs,
|
|
27
30
|
initLogger,
|
|
@@ -32,6 +35,8 @@ import {
|
|
|
32
35
|
render_default,
|
|
33
36
|
require_jsx_runtime,
|
|
34
37
|
require_react,
|
|
38
|
+
saveSettings,
|
|
39
|
+
subscribeToBossStore,
|
|
35
40
|
tasksStore,
|
|
36
41
|
useAnimationActive,
|
|
37
42
|
useAnimationTick,
|
|
@@ -43,7 +48,7 @@ import {
|
|
|
43
48
|
use_app_default,
|
|
44
49
|
use_input_default,
|
|
45
50
|
use_stdout_default
|
|
46
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-FXGI4NWS.js";
|
|
47
52
|
import "./chunk-QT366Y52.js";
|
|
48
53
|
import {
|
|
49
54
|
source_default
|
|
@@ -55,7 +60,7 @@ import {
|
|
|
55
60
|
|
|
56
61
|
// src/cli.ts
|
|
57
62
|
init_esm_shims();
|
|
58
|
-
import
|
|
63
|
+
import path5 from "path";
|
|
59
64
|
|
|
60
65
|
// src/links.ts
|
|
61
66
|
init_esm_shims();
|
|
@@ -336,7 +341,7 @@ init_esm_shims();
|
|
|
336
341
|
// package.json
|
|
337
342
|
var package_default = {
|
|
338
343
|
name: "@kenkaiiii/gg-boss",
|
|
339
|
-
version: "4.3.
|
|
344
|
+
version: "4.3.157",
|
|
340
345
|
type: "module",
|
|
341
346
|
description: "Orchestrator agent that drives multiple ggcoder sessions across projects from a single chat",
|
|
342
347
|
license: "MIT",
|
|
@@ -376,6 +381,10 @@ var package_default = {
|
|
|
376
381
|
vitest: "^4.1.4",
|
|
377
382
|
zod: "^4.4.3"
|
|
378
383
|
},
|
|
384
|
+
optionalDependencies: {
|
|
385
|
+
"@huggingface/transformers": "^3.6.0",
|
|
386
|
+
"ogg-opus-decoder": "^1.6.13"
|
|
387
|
+
},
|
|
379
388
|
publishConfig: {
|
|
380
389
|
access: "public"
|
|
381
390
|
}
|
|
@@ -632,7 +641,7 @@ async function runLinkCommand() {
|
|
|
632
641
|
process.stdout.write(source_default.hex(COLORS.textDim)("\nCancelled. No changes saved.\n"));
|
|
633
642
|
return;
|
|
634
643
|
}
|
|
635
|
-
const linked = result.selected.map((
|
|
644
|
+
const linked = result.selected.map((path6) => projects.find((p) => p.path === path6)).filter((p) => Boolean(p)).map((p) => ({ name: p.name, cwd: p.path }));
|
|
636
645
|
await saveLinks({ projects: linked });
|
|
637
646
|
process.stdout.write("\n");
|
|
638
647
|
if (linked.length === 0) {
|
|
@@ -657,6 +666,868 @@ async function runLinkCommand() {
|
|
|
657
666
|
}
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
// src/serve-mode.ts
|
|
670
|
+
init_esm_shims();
|
|
671
|
+
import path3 from "path";
|
|
672
|
+
import fs3 from "fs/promises";
|
|
673
|
+
|
|
674
|
+
// src/voice-transcriber.ts
|
|
675
|
+
init_esm_shims();
|
|
676
|
+
var TARGET_SAMPLE_RATE = 16e3;
|
|
677
|
+
var MODEL_ID = "Xenova/whisper-tiny.en";
|
|
678
|
+
var transcriber = null;
|
|
679
|
+
var loadPromise = null;
|
|
680
|
+
var onProgress = null;
|
|
681
|
+
function setProgressCallback(cb) {
|
|
682
|
+
onProgress = cb;
|
|
683
|
+
}
|
|
684
|
+
function resample(audio, fromRate, toRate) {
|
|
685
|
+
if (fromRate === toRate) return audio;
|
|
686
|
+
const ratio = fromRate / toRate;
|
|
687
|
+
const newLength = Math.round(audio.length / ratio);
|
|
688
|
+
const result = new Float32Array(newLength);
|
|
689
|
+
for (let i = 0; i < newLength; i++) {
|
|
690
|
+
const srcIndex = i * ratio;
|
|
691
|
+
const low = Math.floor(srcIndex);
|
|
692
|
+
const high = Math.min(low + 1, audio.length - 1);
|
|
693
|
+
const frac = srcIndex - low;
|
|
694
|
+
result[i] = audio[low] * (1 - frac) + audio[high] * frac;
|
|
695
|
+
}
|
|
696
|
+
return result;
|
|
697
|
+
}
|
|
698
|
+
function downmixToMono(channelData) {
|
|
699
|
+
if (channelData.length === 0) return new Float32Array();
|
|
700
|
+
if (channelData.length === 1) return channelData[0];
|
|
701
|
+
const samples = channelData[0].length;
|
|
702
|
+
const out = new Float32Array(samples);
|
|
703
|
+
const scale = 1 / channelData.length;
|
|
704
|
+
for (let i = 0; i < samples; i++) {
|
|
705
|
+
let mixed = 0;
|
|
706
|
+
for (const channel of channelData) mixed += channel[i] ?? 0;
|
|
707
|
+
out[i] = mixed * scale;
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
711
|
+
async function decodeOggOpus(buffer) {
|
|
712
|
+
const { OggOpusDecoder } = await import("ogg-opus-decoder");
|
|
713
|
+
const decoder = new OggOpusDecoder();
|
|
714
|
+
await decoder.ready;
|
|
715
|
+
try {
|
|
716
|
+
const decoded = await decoder.decodeFile(buffer);
|
|
717
|
+
if (!decoded.channelData?.length || !decoded.channelData[0]?.length) {
|
|
718
|
+
throw new Error("Decoded audio is empty");
|
|
719
|
+
}
|
|
720
|
+
const mono = downmixToMono(decoded.channelData);
|
|
721
|
+
return resample(mono, decoded.sampleRate, TARGET_SAMPLE_RATE);
|
|
722
|
+
} finally {
|
|
723
|
+
decoder.free();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function getTranscriber() {
|
|
727
|
+
if (transcriber) return transcriber;
|
|
728
|
+
if (!loadPromise) {
|
|
729
|
+
loadPromise = (async () => {
|
|
730
|
+
const { pipeline } = await import("@huggingface/transformers");
|
|
731
|
+
const instance = await pipeline("automatic-speech-recognition", MODEL_ID, {
|
|
732
|
+
dtype: "fp32",
|
|
733
|
+
progress_callback: onProgress ?? void 0
|
|
734
|
+
});
|
|
735
|
+
transcriber = instance;
|
|
736
|
+
return instance;
|
|
737
|
+
})();
|
|
738
|
+
}
|
|
739
|
+
return loadPromise;
|
|
740
|
+
}
|
|
741
|
+
function isModelLoaded() {
|
|
742
|
+
return transcriber !== null;
|
|
743
|
+
}
|
|
744
|
+
async function transcribeVoice(fileUrl) {
|
|
745
|
+
const response = await fetch(fileUrl);
|
|
746
|
+
if (!response.ok) throw new Error(`Failed to download voice file: ${response.status}`);
|
|
747
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
748
|
+
const pcm = await decodeOggOpus(buffer);
|
|
749
|
+
const asr = await getTranscriber();
|
|
750
|
+
const result = await asr(pcm);
|
|
751
|
+
const text = Array.isArray(result) ? result[0]?.text : result.text;
|
|
752
|
+
return (text ?? "").trim();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/telegram.ts
|
|
756
|
+
init_esm_shims();
|
|
757
|
+
var TELEGRAM_API = "https://api.telegram.org";
|
|
758
|
+
var MAX_MESSAGE_LENGTH = 4096;
|
|
759
|
+
var TelegramBot = class {
|
|
760
|
+
token;
|
|
761
|
+
allowedUserId;
|
|
762
|
+
offset = 0;
|
|
763
|
+
running = false;
|
|
764
|
+
onMessage = null;
|
|
765
|
+
onVoiceMessage = null;
|
|
766
|
+
onCallback = null;
|
|
767
|
+
onBotAdded = null;
|
|
768
|
+
onBotRemoved = null;
|
|
769
|
+
constructor(config) {
|
|
770
|
+
this.token = config.botToken;
|
|
771
|
+
this.allowedUserId = config.allowedUserId;
|
|
772
|
+
}
|
|
773
|
+
/** Register handler for incoming text messages. */
|
|
774
|
+
onText(handler) {
|
|
775
|
+
this.onMessage = handler;
|
|
776
|
+
}
|
|
777
|
+
/** Register handler for incoming voice notes. */
|
|
778
|
+
onVoice(handler) {
|
|
779
|
+
this.onVoiceMessage = handler;
|
|
780
|
+
}
|
|
781
|
+
/** Register handler for inline keyboard button presses. */
|
|
782
|
+
onCallbackQuery(handler) {
|
|
783
|
+
this.onCallback = handler;
|
|
784
|
+
}
|
|
785
|
+
/** Register handler for when the bot is added to a group. */
|
|
786
|
+
onAddedToGroup(handler) {
|
|
787
|
+
this.onBotAdded = handler;
|
|
788
|
+
}
|
|
789
|
+
/** Register handler for when the bot is removed from a group. */
|
|
790
|
+
onRemovedFromGroup(handler) {
|
|
791
|
+
this.onBotRemoved = handler;
|
|
792
|
+
}
|
|
793
|
+
/** Start long polling. Blocks until stop() is called. */
|
|
794
|
+
async start() {
|
|
795
|
+
this.running = true;
|
|
796
|
+
const me = await this.apiCall("getMe");
|
|
797
|
+
if (!me.ok) {
|
|
798
|
+
throw new Error(`Invalid bot token: ${JSON.stringify(me)}`);
|
|
799
|
+
}
|
|
800
|
+
while (this.running) {
|
|
801
|
+
try {
|
|
802
|
+
const updates = await this.getUpdates();
|
|
803
|
+
for (const update of updates) {
|
|
804
|
+
await this.handleUpdate(update);
|
|
805
|
+
}
|
|
806
|
+
} catch (err) {
|
|
807
|
+
if (!this.running) break;
|
|
808
|
+
console.error(`[telegram] Poll error: ${err instanceof Error ? err.message : err}`);
|
|
809
|
+
await sleep(3e3);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/** Stop long polling. */
|
|
814
|
+
stop() {
|
|
815
|
+
this.running = false;
|
|
816
|
+
}
|
|
817
|
+
/** Send a text message to a specific chat. Converts markdown and splits long messages. */
|
|
818
|
+
async send(chatId, text, buttons) {
|
|
819
|
+
const converted = toTelegramMarkdown(text);
|
|
820
|
+
const chunks = splitMessage(converted);
|
|
821
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
822
|
+
const isLast = i === chunks.length - 1;
|
|
823
|
+
const replyMarkup = isLast && buttons ? {
|
|
824
|
+
inline_keyboard: buttons.map(
|
|
825
|
+
(row) => row.map((b) => ({ text: b.text, callback_data: b.callback_data }))
|
|
826
|
+
)
|
|
827
|
+
} : void 0;
|
|
828
|
+
await this.apiCall("sendMessage", {
|
|
829
|
+
chat_id: chatId,
|
|
830
|
+
text: chunks[i],
|
|
831
|
+
parse_mode: "Markdown",
|
|
832
|
+
...replyMarkup ? { reply_markup: replyMarkup } : {}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/** Send a plain text message (no markdown parsing) to a specific chat. */
|
|
837
|
+
async sendPlain(chatId, text) {
|
|
838
|
+
const chunks = splitMessage(text);
|
|
839
|
+
for (const chunk of chunks) {
|
|
840
|
+
await this.apiCall("sendMessage", {
|
|
841
|
+
chat_id: chatId,
|
|
842
|
+
text: chunk
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** Send a typing indicator to a specific chat. */
|
|
847
|
+
async sendTyping(chatId) {
|
|
848
|
+
await this.apiCall("sendChatAction", {
|
|
849
|
+
chat_id: chatId,
|
|
850
|
+
action: "typing"
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/** Get a direct download URL for a Telegram file. */
|
|
854
|
+
async getFileUrl(fileId) {
|
|
855
|
+
const result = await this.apiCall("getFile", { file_id: fileId });
|
|
856
|
+
if (!result.ok) throw new Error(`Failed to get file: ${JSON.stringify(result)}`);
|
|
857
|
+
const filePath = result.result.file_path;
|
|
858
|
+
return `${TELEGRAM_API}/file/bot${this.token}/${filePath}`;
|
|
859
|
+
}
|
|
860
|
+
// ── Private ───────────────────────────────────────────
|
|
861
|
+
async getUpdates() {
|
|
862
|
+
const result = await this.apiCall("getUpdates", {
|
|
863
|
+
offset: this.offset,
|
|
864
|
+
timeout: 30,
|
|
865
|
+
allowed_updates: ["message", "callback_query", "my_chat_member"]
|
|
866
|
+
});
|
|
867
|
+
if (!result.ok || !Array.isArray(result.result)) return [];
|
|
868
|
+
const updates = result.result;
|
|
869
|
+
if (updates.length > 0) {
|
|
870
|
+
this.offset = updates[updates.length - 1].update_id + 1;
|
|
871
|
+
}
|
|
872
|
+
return updates;
|
|
873
|
+
}
|
|
874
|
+
async handleUpdate(update) {
|
|
875
|
+
if (update.message) {
|
|
876
|
+
const msg = update.message;
|
|
877
|
+
if (msg.from.id !== this.allowedUserId) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (msg.text && this.onMessage) {
|
|
881
|
+
this.onMessage({
|
|
882
|
+
text: msg.text,
|
|
883
|
+
chatId: msg.chat.id,
|
|
884
|
+
chatType: msg.chat.type,
|
|
885
|
+
chatTitle: msg.chat.title
|
|
886
|
+
});
|
|
887
|
+
} else if (msg.voice && this.onVoiceMessage) {
|
|
888
|
+
this.onVoiceMessage({
|
|
889
|
+
fileId: msg.voice.file_id,
|
|
890
|
+
duration: msg.voice.duration,
|
|
891
|
+
chatId: msg.chat.id,
|
|
892
|
+
chatType: msg.chat.type,
|
|
893
|
+
chatTitle: msg.chat.title
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (update.my_chat_member) {
|
|
898
|
+
const member = update.my_chat_member;
|
|
899
|
+
const status = member.new_chat_member.status;
|
|
900
|
+
if ((status === "member" || status === "administrator") && this.onBotAdded) {
|
|
901
|
+
this.onBotAdded(member.chat.id, member.chat.title);
|
|
902
|
+
} else if ((status === "left" || status === "kicked") && this.onBotRemoved) {
|
|
903
|
+
this.onBotRemoved(member.chat.id);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (update.callback_query) {
|
|
907
|
+
const cb = update.callback_query;
|
|
908
|
+
if (cb.from.id !== this.allowedUserId) return;
|
|
909
|
+
await this.apiCall("answerCallbackQuery", { callback_query_id: cb.id });
|
|
910
|
+
if (cb.data && this.onCallback) {
|
|
911
|
+
this.onCallback(cb.data, cb.message.chat.id);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async apiCall(method, body) {
|
|
916
|
+
const url = `${TELEGRAM_API}/bot${this.token}/${method}`;
|
|
917
|
+
const response = await fetch(url, {
|
|
918
|
+
method: "POST",
|
|
919
|
+
headers: { "Content-Type": "application/json" },
|
|
920
|
+
body: body ? JSON.stringify(body) : void 0
|
|
921
|
+
});
|
|
922
|
+
if (!response.ok) {
|
|
923
|
+
return { ok: false };
|
|
924
|
+
}
|
|
925
|
+
return response.json();
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
function toTelegramMarkdown(text) {
|
|
929
|
+
const lines = text.split("\n");
|
|
930
|
+
const result = [];
|
|
931
|
+
let inCodeBlock = false;
|
|
932
|
+
for (const line of lines) {
|
|
933
|
+
if (line.trimStart().startsWith("```")) {
|
|
934
|
+
inCodeBlock = !inCodeBlock;
|
|
935
|
+
result.push(line);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (inCodeBlock) {
|
|
939
|
+
result.push(line);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
let transformed = line;
|
|
943
|
+
const headingMatch = transformed.match(/^(#{1,6})\s+(.+)$/);
|
|
944
|
+
if (headingMatch) {
|
|
945
|
+
transformed = `*${headingMatch[2]}*`;
|
|
946
|
+
result.push(transformed);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (/^(-{3,}|_{3,}|\*{3,})$/.test(transformed.trim())) {
|
|
950
|
+
result.push("");
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
transformed = transformed.replace(/\*\*(.+?)\*\*/g, "*$1*");
|
|
954
|
+
result.push(transformed);
|
|
955
|
+
}
|
|
956
|
+
return result.join("\n");
|
|
957
|
+
}
|
|
958
|
+
function splitMessage(text) {
|
|
959
|
+
if (text.length <= MAX_MESSAGE_LENGTH) return [text];
|
|
960
|
+
const chunks = [];
|
|
961
|
+
let remaining = text;
|
|
962
|
+
while (remaining.length > 0) {
|
|
963
|
+
if (remaining.length <= MAX_MESSAGE_LENGTH) {
|
|
964
|
+
chunks.push(remaining);
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
let splitAt = remaining.lastIndexOf("\n", MAX_MESSAGE_LENGTH);
|
|
968
|
+
if (splitAt === -1 || splitAt < MAX_MESSAGE_LENGTH * 0.5) {
|
|
969
|
+
splitAt = remaining.lastIndexOf(" ", MAX_MESSAGE_LENGTH);
|
|
970
|
+
}
|
|
971
|
+
if (splitAt === -1 || splitAt < MAX_MESSAGE_LENGTH * 0.5) {
|
|
972
|
+
splitAt = MAX_MESSAGE_LENGTH;
|
|
973
|
+
}
|
|
974
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
975
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
976
|
+
}
|
|
977
|
+
return chunks;
|
|
978
|
+
}
|
|
979
|
+
function sleep(ms) {
|
|
980
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/serve-mode.ts
|
|
984
|
+
function getTelegramConfigPath() {
|
|
985
|
+
return path3.join(getAppPaths().agentDir, "boss", "telegram.json");
|
|
986
|
+
}
|
|
987
|
+
async function loadBossTelegramConfig() {
|
|
988
|
+
try {
|
|
989
|
+
const raw = await fs3.readFile(getTelegramConfigPath(), "utf-8");
|
|
990
|
+
const data = JSON.parse(raw);
|
|
991
|
+
if (data.botToken && data.userId) return data;
|
|
992
|
+
return null;
|
|
993
|
+
} catch {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function saveBossTelegramConfig(config) {
|
|
998
|
+
const file = getTelegramConfigPath();
|
|
999
|
+
await fs3.mkdir(path3.dirname(file), { recursive: true });
|
|
1000
|
+
await fs3.writeFile(file, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1001
|
+
}
|
|
1002
|
+
function formatItemForTelegram(item) {
|
|
1003
|
+
switch (item.kind) {
|
|
1004
|
+
case "user":
|
|
1005
|
+
case "tool":
|
|
1006
|
+
case "worker_event":
|
|
1007
|
+
return null;
|
|
1008
|
+
case "assistant": {
|
|
1009
|
+
const cleaned = stripScopePrefix(item.text).trim();
|
|
1010
|
+
return cleaned ? truncate(cleaned, 1500) : null;
|
|
1011
|
+
}
|
|
1012
|
+
case "worker_error":
|
|
1013
|
+
return `\u2717 *${item.project}* \u2014 ${truncate(item.message, 300)}`;
|
|
1014
|
+
case "info":
|
|
1015
|
+
if (item.level !== "warning" && item.level !== "error") return null;
|
|
1016
|
+
return `${item.level === "error" ? "\u2717 " : "\u26A0 "}_${truncate(item.text, 300)}_`;
|
|
1017
|
+
case "task_dispatch": {
|
|
1018
|
+
if (item.tasks.length === 0) return null;
|
|
1019
|
+
const projects = [...new Set(item.tasks.map((t) => t.project))];
|
|
1020
|
+
if (item.tasks.length === 1) {
|
|
1021
|
+
const t = item.tasks[0];
|
|
1022
|
+
return `\u2192 *${t.project}*: ${truncate(t.title, 140)}`;
|
|
1023
|
+
}
|
|
1024
|
+
return `\u2192 Dispatched ${item.tasks.length} tasks across ${projects.length} project${projects.length === 1 ? "" : "s"}`;
|
|
1025
|
+
}
|
|
1026
|
+
case "update_notice":
|
|
1027
|
+
return `\u2728 ${item.text}`;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function stripScopePrefix(text) {
|
|
1031
|
+
return text.replace(/^\s*\[scope:[^\]]+\]\s*/, "");
|
|
1032
|
+
}
|
|
1033
|
+
function truncate(text, max) {
|
|
1034
|
+
if (text.length <= max) return text;
|
|
1035
|
+
return text.slice(0, max - 1).trimEnd() + "\u2026";
|
|
1036
|
+
}
|
|
1037
|
+
function scopePrefix(scope) {
|
|
1038
|
+
if (scope === "all") return "[scope:all] ";
|
|
1039
|
+
return `[scope:${scope}] `;
|
|
1040
|
+
}
|
|
1041
|
+
async function runBossServeMode(options) {
|
|
1042
|
+
initLogger({
|
|
1043
|
+
version: VERSION,
|
|
1044
|
+
bossProvider: options.bossProvider,
|
|
1045
|
+
bossModel: options.bossModel,
|
|
1046
|
+
bossThinking: options.bossThinkingLevel,
|
|
1047
|
+
workerProvider: options.workerProvider,
|
|
1048
|
+
workerModel: options.workerModel,
|
|
1049
|
+
projectCount: 0
|
|
1050
|
+
});
|
|
1051
|
+
const links = await loadLinks();
|
|
1052
|
+
if (links.projects.length === 0) {
|
|
1053
|
+
console.error(
|
|
1054
|
+
source_default.hex(COLORS.error)("No linked projects.\n") + source_default.hex(COLORS.textDim)("Run ") + source_default.hex(COLORS.accent)("ggboss link") + source_default.hex(COLORS.textDim)(" first to choose which projects the boss should manage.")
|
|
1055
|
+
);
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
const projects = links.projects.map((p) => ({ name: p.name, cwd: p.cwd }));
|
|
1059
|
+
await tasksStore.load();
|
|
1060
|
+
const bot = new TelegramBot({
|
|
1061
|
+
botToken: options.telegram.botToken,
|
|
1062
|
+
allowedUserId: options.telegram.userId
|
|
1063
|
+
});
|
|
1064
|
+
const boss = new GGBoss({
|
|
1065
|
+
bossProvider: options.bossProvider,
|
|
1066
|
+
bossModel: options.bossModel,
|
|
1067
|
+
bossThinkingLevel: options.bossThinkingLevel,
|
|
1068
|
+
workerProvider: options.workerProvider,
|
|
1069
|
+
workerModel: options.workerModel,
|
|
1070
|
+
workerThinkingLevel: options.workerThinkingLevel,
|
|
1071
|
+
projects
|
|
1072
|
+
});
|
|
1073
|
+
await boss.initialize();
|
|
1074
|
+
log("INFO", "serve", "boss initialized", { projects: projects.map((p) => p.name).join(",") });
|
|
1075
|
+
const allowedChatId = options.telegram.userId;
|
|
1076
|
+
const pendingScopeSelections = /* @__PURE__ */ new Map();
|
|
1077
|
+
const pendingModelSelections = /* @__PURE__ */ new Map();
|
|
1078
|
+
let lastHistoryLen = getBossState().history.length;
|
|
1079
|
+
let typingInterval = null;
|
|
1080
|
+
let isStreaming = false;
|
|
1081
|
+
function sendQueued(text) {
|
|
1082
|
+
bot.send(allowedChatId, text).catch((err) => {
|
|
1083
|
+
log("WARN", "telegram", `send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1084
|
+
bot.sendPlain(allowedChatId, text.replace(/[*_`]/g, "")).catch(() => {
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
function startTyping() {
|
|
1089
|
+
if (typingInterval) return;
|
|
1090
|
+
bot.sendTyping(allowedChatId).catch(() => {
|
|
1091
|
+
});
|
|
1092
|
+
typingInterval = setInterval(() => {
|
|
1093
|
+
bot.sendTyping(allowedChatId).catch(() => {
|
|
1094
|
+
});
|
|
1095
|
+
}, 4e3);
|
|
1096
|
+
}
|
|
1097
|
+
function stopTyping() {
|
|
1098
|
+
if (typingInterval) {
|
|
1099
|
+
clearInterval(typingInterval);
|
|
1100
|
+
typingInterval = null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function flushNewItems(state) {
|
|
1104
|
+
const len = state.history.length;
|
|
1105
|
+
if (len <= lastHistoryLen) return;
|
|
1106
|
+
const fresh = state.history.slice(lastHistoryLen);
|
|
1107
|
+
lastHistoryLen = len;
|
|
1108
|
+
for (const item of fresh) {
|
|
1109
|
+
const formatted = formatItemForTelegram(item);
|
|
1110
|
+
if (formatted) sendQueued(formatted);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async function applyModelChoice(target, selected) {
|
|
1114
|
+
try {
|
|
1115
|
+
if (target === "boss") {
|
|
1116
|
+
await boss.switchBossModel(selected.provider, selected.id);
|
|
1117
|
+
await saveSettings({ bossProvider: selected.provider, bossModel: selected.id });
|
|
1118
|
+
await bot.send(allowedChatId, `Boss \u2192 *${selected.name}*`);
|
|
1119
|
+
} else {
|
|
1120
|
+
await boss.switchWorkerModel(selected.provider, selected.id);
|
|
1121
|
+
await saveSettings({ workerProvider: selected.provider, workerModel: selected.id });
|
|
1122
|
+
await bot.send(allowedChatId, `Workers \u2192 *${selected.name}*`);
|
|
1123
|
+
}
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1126
|
+
log("WARN", "model_switch", message, { target, model: selected.id });
|
|
1127
|
+
await bot.send(allowedChatId, `Failed to switch ${target}: ${message}`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const unsubscribe = subscribeToBossStore(() => {
|
|
1131
|
+
if (getBossState().pendingFlush.length > 0) {
|
|
1132
|
+
bossStore.commitPendingFlush();
|
|
1133
|
+
}
|
|
1134
|
+
const state = getBossState();
|
|
1135
|
+
flushNewItems(state);
|
|
1136
|
+
const streamingNow = state.streaming !== null;
|
|
1137
|
+
if (streamingNow && !isStreaming) {
|
|
1138
|
+
isStreaming = true;
|
|
1139
|
+
startTyping();
|
|
1140
|
+
} else if (!streamingNow && isStreaming) {
|
|
1141
|
+
isStreaming = false;
|
|
1142
|
+
stopTyping();
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
bot.onText(async (msg) => {
|
|
1146
|
+
const { text, chatId } = msg;
|
|
1147
|
+
if (chatId !== allowedChatId) return;
|
|
1148
|
+
const pendingScopes = pendingScopeSelections.get(chatId);
|
|
1149
|
+
if (pendingScopes && /^\s*\d+\s*$/.test(text)) {
|
|
1150
|
+
pendingScopeSelections.delete(chatId);
|
|
1151
|
+
const num = parseInt(text.trim(), 10);
|
|
1152
|
+
if (num < 1 || num > pendingScopes.length) {
|
|
1153
|
+
await bot.send(chatId, "Invalid selection. Send /scope to try again.");
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const chosen = pendingScopes[num - 1];
|
|
1157
|
+
bossStore.setScope(chosen);
|
|
1158
|
+
await bot.send(chatId, `Scope: *${chosen}*`);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const pendingModel = pendingModelSelections.get(chatId);
|
|
1162
|
+
if (pendingModel && /^\s*\d+\s*$/.test(text)) {
|
|
1163
|
+
pendingModelSelections.delete(chatId);
|
|
1164
|
+
const num = parseInt(text.trim(), 10);
|
|
1165
|
+
if (num < 1 || num > pendingModel.models.length) {
|
|
1166
|
+
await bot.send(chatId, "Invalid selection. Send /m to try again.");
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const selected = pendingModel.models[num - 1];
|
|
1170
|
+
await applyModelChoice(pendingModel.target, selected);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (!text.startsWith("/")) {
|
|
1174
|
+
const scoped = scopePrefix(getBossState().scope) + text;
|
|
1175
|
+
boss.enqueueUserMessage(scoped);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const parts = text.trim().split(/\s+/);
|
|
1179
|
+
const cmd = parts[0].slice(1).toLowerCase().replace(/@\w+$/, "");
|
|
1180
|
+
if (cmd === "help" || cmd === "start") {
|
|
1181
|
+
await bot.send(chatId, buildTelegramHelpText());
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (cmd === "m" || cmd === "model" || cmd === "model-boss" || cmd === "model-workers") {
|
|
1185
|
+
const target = cmd === "model-workers" ? "workers" : "boss";
|
|
1186
|
+
const arg = parts.slice(1).join(" ").trim().toLowerCase();
|
|
1187
|
+
const state = getBossState();
|
|
1188
|
+
const currentId = target === "boss" ? state.bossModel : state.workerModel;
|
|
1189
|
+
if (arg) {
|
|
1190
|
+
const num = parseInt(arg, 10);
|
|
1191
|
+
let match;
|
|
1192
|
+
if (!isNaN(num) && num >= 1 && num <= MODELS.length) {
|
|
1193
|
+
match = MODELS[num - 1];
|
|
1194
|
+
} else {
|
|
1195
|
+
match = MODELS.find(
|
|
1196
|
+
(m) => m.name.toLowerCase().includes(arg) || m.id.toLowerCase().includes(arg)
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
if (!match) {
|
|
1200
|
+
await bot.send(chatId, `No model matching "${arg}". Send /${cmd} to see the list.`);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
await applyModelChoice(target, match);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
let listText = `*${target === "boss" ? "Boss" : "Worker"} model*
|
|
1207
|
+
`;
|
|
1208
|
+
let lastProvider = "";
|
|
1209
|
+
MODELS.forEach((m, i) => {
|
|
1210
|
+
if (m.provider !== lastProvider) {
|
|
1211
|
+
lastProvider = m.provider;
|
|
1212
|
+
listText += `
|
|
1213
|
+
_${providerLabel(m.provider)}_
|
|
1214
|
+
`;
|
|
1215
|
+
}
|
|
1216
|
+
const active = m.id === currentId ? " \u2190" : "";
|
|
1217
|
+
listText += ` *${i + 1}.* ${m.name}${active}
|
|
1218
|
+
`;
|
|
1219
|
+
});
|
|
1220
|
+
listText += `
|
|
1221
|
+
Send the number, or \`/${cmd} <name>\`.`;
|
|
1222
|
+
pendingModelSelections.set(chatId, { target, models: [...MODELS] });
|
|
1223
|
+
await bot.send(chatId, listText);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (cmd === "scope" || cmd === "s") {
|
|
1227
|
+
const state = getBossState();
|
|
1228
|
+
const arg = parts.slice(1).join(" ").trim().toLowerCase();
|
|
1229
|
+
const names = ["all", ...state.workers.map((w) => w.name)];
|
|
1230
|
+
if (!arg) {
|
|
1231
|
+
const lines = names.map((n, i) => {
|
|
1232
|
+
const active = n === state.scope ? " \u2190" : "";
|
|
1233
|
+
const label = n === "all" ? "*All*" : `*${n}*`;
|
|
1234
|
+
return `*${i + 1}.* ${label}${active}`;
|
|
1235
|
+
});
|
|
1236
|
+
pendingScopeSelections.set(chatId, names);
|
|
1237
|
+
await bot.send(
|
|
1238
|
+
chatId,
|
|
1239
|
+
`*Scope* \u2014 current: *${state.scope}*
|
|
1240
|
+
|
|
1241
|
+
${lines.join("\n")}
|
|
1242
|
+
|
|
1243
|
+
Send the number, or \`/scope <name>\`.`
|
|
1244
|
+
);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const num = parseInt(arg, 10);
|
|
1248
|
+
let chosen;
|
|
1249
|
+
if (!isNaN(num) && num >= 1 && num <= names.length) {
|
|
1250
|
+
chosen = names[num - 1];
|
|
1251
|
+
} else {
|
|
1252
|
+
const exact = names.find((n) => n.toLowerCase() === arg);
|
|
1253
|
+
chosen = exact ?? names.find((n) => n.toLowerCase().includes(arg)) ?? null;
|
|
1254
|
+
}
|
|
1255
|
+
if (!chosen) {
|
|
1256
|
+
await bot.send(chatId, `No scope matching "${arg}". Send /scope to see the list.`);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
bossStore.setScope(chosen);
|
|
1260
|
+
await bot.send(chatId, `Scope: *${chosen}*`);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (cmd === "status") {
|
|
1264
|
+
const state = getBossState();
|
|
1265
|
+
const lines = [`*${BRAND}* \u2014 ${state.bossModel}`, `Scope *${state.scope}*`, ""];
|
|
1266
|
+
lines.push("*Workers*");
|
|
1267
|
+
for (const w of state.workers) {
|
|
1268
|
+
const dot = w.status === "working" ? "\u25CF" : w.status === "error" ? "\u2717" : "\u25CB";
|
|
1269
|
+
lines.push(` ${dot} *${w.name}* \u2014 _${w.status}_`);
|
|
1270
|
+
}
|
|
1271
|
+
const tasks = tasksStore.list();
|
|
1272
|
+
const open = tasks.filter((t) => t.status === "pending" || t.status === "in_progress").length;
|
|
1273
|
+
lines.push("");
|
|
1274
|
+
lines.push(`Tasks ${open} open \xB7 ${tasks.length} total`);
|
|
1275
|
+
await bot.send(chatId, lines.join("\n"));
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (cmd === "cancel") {
|
|
1279
|
+
boss.abort();
|
|
1280
|
+
await bot.send(chatId, "_Aborted current boss turn._");
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (cmd === "new" || cmd === "n") {
|
|
1284
|
+
await boss.newSession();
|
|
1285
|
+
await bot.send(chatId, "\u2500\u2500 *New session* \u2500\u2500");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (cmd === "tasks") {
|
|
1289
|
+
const tasks = tasksStore.list();
|
|
1290
|
+
if (tasks.length === 0) {
|
|
1291
|
+
await bot.send(chatId, "_No tasks._");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const lines = tasks.slice(0, 30).map((t, i) => {
|
|
1295
|
+
const status = t.status.replace("_", " ");
|
|
1296
|
+
return `*${i + 1}.* [${status}] *${t.project}* \u2014 ${t.description.split("\n")[0]}`;
|
|
1297
|
+
});
|
|
1298
|
+
await bot.send(chatId, `*Tasks*
|
|
1299
|
+
|
|
1300
|
+
${lines.join("\n")}`);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
boss.enqueueUserMessage(text);
|
|
1304
|
+
});
|
|
1305
|
+
bot.onVoice(async (msg) => {
|
|
1306
|
+
const { chatId } = msg;
|
|
1307
|
+
if (chatId !== allowedChatId) return;
|
|
1308
|
+
try {
|
|
1309
|
+
if (!isModelLoaded()) {
|
|
1310
|
+
await bot.send(
|
|
1311
|
+
chatId,
|
|
1312
|
+
"Setting up voice transcription \u2014 downloading Whisper model. This only happens once."
|
|
1313
|
+
);
|
|
1314
|
+
setProgressCallback((info) => {
|
|
1315
|
+
if (info.status === "progress" && info.progress !== void 0) {
|
|
1316
|
+
const pct = Math.round(info.progress);
|
|
1317
|
+
if (pct % 25 === 0 && pct > 0) {
|
|
1318
|
+
bot.sendTyping(chatId).catch(() => {
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
await bot.sendTyping(chatId);
|
|
1325
|
+
const fileUrl = await bot.getFileUrl(msg.fileId);
|
|
1326
|
+
const transcribed = await transcribeVoice(fileUrl);
|
|
1327
|
+
if (!transcribed) {
|
|
1328
|
+
await bot.send(chatId, "_Could not transcribe voice note._");
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
await bot.send(chatId, `_Voice: "${transcribed}"_`);
|
|
1332
|
+
const scoped = scopePrefix(getBossState().scope) + transcribed;
|
|
1333
|
+
boss.enqueueUserMessage(scoped);
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1336
|
+
log("ERROR", "voice", message);
|
|
1337
|
+
const hint = /Cannot find module|Cannot resolve|MODULE_NOT_FOUND/.test(message) ? "\n\nVoice transcription needs the optional `@huggingface/transformers` and `ogg-opus-decoder` packages. Reinstall with `npm i -g @kenkaiiii/gg-boss` and ensure optional deps installed." : "";
|
|
1338
|
+
await bot.send(chatId, `_Voice transcription failed: ${message}_${hint}`);
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
1342
|
+
printBanner({
|
|
1343
|
+
bossModel: options.bossModel,
|
|
1344
|
+
workerModel: options.workerModel,
|
|
1345
|
+
userId: options.telegram.userId,
|
|
1346
|
+
projectCount: projects.length
|
|
1347
|
+
});
|
|
1348
|
+
let shuttingDown = false;
|
|
1349
|
+
const shutdown = async () => {
|
|
1350
|
+
if (shuttingDown) return;
|
|
1351
|
+
shuttingDown = true;
|
|
1352
|
+
console.log(source_default.hex(COLORS.textDim)("\nShutting down..."));
|
|
1353
|
+
bot.stop();
|
|
1354
|
+
stopTyping();
|
|
1355
|
+
unsubscribe();
|
|
1356
|
+
await boss.dispose().catch(() => {
|
|
1357
|
+
});
|
|
1358
|
+
closeLogger();
|
|
1359
|
+
process.exit(0);
|
|
1360
|
+
};
|
|
1361
|
+
process.on("SIGINT", () => void shutdown());
|
|
1362
|
+
process.on("SIGTERM", () => void shutdown());
|
|
1363
|
+
const runPromise = boss.run().catch((err) => {
|
|
1364
|
+
log("ERROR", "boss", err instanceof Error ? err.message : String(err));
|
|
1365
|
+
});
|
|
1366
|
+
await bot.start();
|
|
1367
|
+
await runPromise;
|
|
1368
|
+
}
|
|
1369
|
+
function buildTelegramHelpText() {
|
|
1370
|
+
return [
|
|
1371
|
+
"*GG Boss* \u2014 orchestrator over Telegram",
|
|
1372
|
+
"",
|
|
1373
|
+
"*Commands*",
|
|
1374
|
+
"/scope (/s) \u2014 switch project focus (All / per-worker)",
|
|
1375
|
+
"/m, /model-boss \u2014 switch the orchestrator's model",
|
|
1376
|
+
"/model-workers \u2014 switch every worker's model",
|
|
1377
|
+
"/status \u2014 workers + open tasks",
|
|
1378
|
+
"/tasks \u2014 list tasks",
|
|
1379
|
+
"/new \u2014 fresh boss session",
|
|
1380
|
+
"/cancel \u2014 abort the current boss turn",
|
|
1381
|
+
"/help \u2014 this message",
|
|
1382
|
+
"",
|
|
1383
|
+
"Voice notes are transcribed locally with Whisper and sent as prompts.",
|
|
1384
|
+
"Send any message to talk to the boss."
|
|
1385
|
+
].join("\n");
|
|
1386
|
+
}
|
|
1387
|
+
function providerLabel(provider) {
|
|
1388
|
+
switch (provider) {
|
|
1389
|
+
case "anthropic":
|
|
1390
|
+
return "Anthropic";
|
|
1391
|
+
case "openai":
|
|
1392
|
+
return "OpenAI";
|
|
1393
|
+
case "glm":
|
|
1394
|
+
return "Z.AI";
|
|
1395
|
+
case "moonshot":
|
|
1396
|
+
return "Moonshot";
|
|
1397
|
+
case "minimax":
|
|
1398
|
+
return "MiniMax";
|
|
1399
|
+
case "deepseek":
|
|
1400
|
+
return "DeepSeek";
|
|
1401
|
+
case "openrouter":
|
|
1402
|
+
return "OpenRouter";
|
|
1403
|
+
case "xiaomi":
|
|
1404
|
+
return "Xiaomi";
|
|
1405
|
+
default:
|
|
1406
|
+
return provider;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function gradientText(text) {
|
|
1410
|
+
let i = 0;
|
|
1411
|
+
return text.split("").map((ch) => ch === " " ? ch : source_default.hex(GRADIENT[i++ % GRADIENT.length])(ch)).join("");
|
|
1412
|
+
}
|
|
1413
|
+
function printBanner(opts) {
|
|
1414
|
+
console.log();
|
|
1415
|
+
console.log(
|
|
1416
|
+
` ${gradientText(LOGO_LINES[0])}${LOGO_GAP}` + source_default.hex(COLORS.primary).bold(BRAND) + source_default.hex(COLORS.textDim)(` v${VERSION}`) + source_default.hex(COLORS.textDim)(" \xB7 By ") + source_default.white.bold(AUTHOR)
|
|
1417
|
+
);
|
|
1418
|
+
console.log(
|
|
1419
|
+
` ${gradientText(LOGO_LINES[1])}${LOGO_GAP}` + source_default.hex(COLORS.accent)(`Boss: ${opts.bossModel}`)
|
|
1420
|
+
);
|
|
1421
|
+
console.log(
|
|
1422
|
+
` ${gradientText(LOGO_LINES[2])}${LOGO_GAP}` + source_default.hex(COLORS.textDim)(`Workers: ${opts.workerModel}`)
|
|
1423
|
+
);
|
|
1424
|
+
console.log();
|
|
1425
|
+
console.log(
|
|
1426
|
+
source_default.hex(COLORS.textDim)(" Mode ") + source_default.hex(COLORS.accent)("Telegram") + source_default.hex(COLORS.textDim)(" \xB7 User ") + source_default.white(String(opts.userId)) + source_default.hex(COLORS.textDim)(
|
|
1427
|
+
` \xB7 ${opts.projectCount} project${opts.projectCount === 1 ? "" : "s"}`
|
|
1428
|
+
)
|
|
1429
|
+
);
|
|
1430
|
+
console.log();
|
|
1431
|
+
console.log(
|
|
1432
|
+
source_default.hex(COLORS.success)(" Ready. ") + source_default.hex(COLORS.textDim)("Open Telegram and message your bot.")
|
|
1433
|
+
);
|
|
1434
|
+
console.log();
|
|
1435
|
+
console.log(
|
|
1436
|
+
source_default.hex(COLORS.textDim)(" /help ") + source_default.hex(COLORS.textDim)("commands") + source_default.hex(COLORS.textDim)(" /status ") + source_default.hex(COLORS.textDim)("workers + tasks") + source_default.hex(COLORS.textDim)(" /cancel ") + source_default.hex(COLORS.textDim)("abort turn")
|
|
1437
|
+
);
|
|
1438
|
+
console.log();
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/telegram-setup.ts
|
|
1442
|
+
init_esm_shims();
|
|
1443
|
+
import readline2 from "readline/promises";
|
|
1444
|
+
async function runBossTelegramSetup() {
|
|
1445
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
1446
|
+
printSetupBanner();
|
|
1447
|
+
const existing = await loadBossTelegramConfig();
|
|
1448
|
+
if (existing) {
|
|
1449
|
+
console.log(
|
|
1450
|
+
source_default.hex(COLORS.textDim)(" Current config:\n") + source_default.hex(COLORS.textDim)(
|
|
1451
|
+
` Bot token: ${existing.botToken.slice(0, 10)}...${existing.botToken.slice(-4)}
|
|
1452
|
+
`
|
|
1453
|
+
) + source_default.hex(COLORS.textDim)(` User ID: ${existing.userId}
|
|
1454
|
+
`)
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1458
|
+
try {
|
|
1459
|
+
console.log(
|
|
1460
|
+
source_default.hex(COLORS.accent)(" Step 1: Bot Token\n") + source_default.hex(COLORS.textDim)(" 1. Open BotFather: ") + source_default.hex(COLORS.primary).underline("https://t.me/BotFather") + "\n" + source_default.hex(COLORS.textDim)(" 2. Send /newbot and follow the prompts\n") + source_default.hex(COLORS.textDim)(" 3. Copy the bot token\n")
|
|
1461
|
+
);
|
|
1462
|
+
const tokenPrompt = existing ? source_default.hex(COLORS.primary)(" Paste bot token (enter to keep current): ") : source_default.hex(COLORS.primary)(" Paste bot token: ");
|
|
1463
|
+
const tokenInput = await rl.question(tokenPrompt);
|
|
1464
|
+
const botToken = tokenInput.trim() || existing?.botToken;
|
|
1465
|
+
if (!botToken) {
|
|
1466
|
+
console.log(source_default.hex(COLORS.error)("\n No bot token provided. Setup cancelled."));
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
|
|
1470
|
+
console.log(
|
|
1471
|
+
source_default.hex(COLORS.error)("\n Invalid token format. Expected: 123456789:ABCdef...")
|
|
1472
|
+
);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
console.log(
|
|
1476
|
+
source_default.hex(COLORS.accent)("\n Step 2: User ID\n") + source_default.hex(COLORS.textDim)(" 1. Open userinfobot: ") + source_default.hex(COLORS.primary).underline("https://t.me/userinfobot") + "\n" + source_default.hex(COLORS.textDim)(" 2. Send any message \u2014 it replies with your numeric ID\n") + source_default.hex(COLORS.textDim)(" Only this user ID can control the boss.\n")
|
|
1477
|
+
);
|
|
1478
|
+
const userPrompt = existing ? source_default.hex(COLORS.primary)(` Your Telegram user ID (enter to keep ${existing.userId}): `) : source_default.hex(COLORS.primary)(" Your Telegram user ID: ");
|
|
1479
|
+
const userInput = await rl.question(userPrompt);
|
|
1480
|
+
const userId = userInput.trim() ? parseInt(userInput.trim(), 10) : existing?.userId;
|
|
1481
|
+
if (!userId || isNaN(userId)) {
|
|
1482
|
+
console.log(source_default.hex(COLORS.error)("\n Invalid user ID. Must be a number."));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
console.log(source_default.hex(COLORS.textDim)("\n Verifying bot token..."));
|
|
1486
|
+
const verifyRes = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
|
|
1487
|
+
method: "POST"
|
|
1488
|
+
});
|
|
1489
|
+
const verifyData = await verifyRes.json();
|
|
1490
|
+
if (!verifyData.ok || !verifyData.result) {
|
|
1491
|
+
console.log(
|
|
1492
|
+
source_default.hex(COLORS.error)(
|
|
1493
|
+
"\n Invalid bot token \u2014 Telegram rejected it. Check and try again."
|
|
1494
|
+
)
|
|
1495
|
+
);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const config = { botToken, userId };
|
|
1499
|
+
await saveBossTelegramConfig(config);
|
|
1500
|
+
console.log(
|
|
1501
|
+
source_default.hex(COLORS.success)(
|
|
1502
|
+
`
|
|
1503
|
+
\u2713 Connected to @${verifyData.result.username} (${verifyData.result.first_name})
|
|
1504
|
+
`
|
|
1505
|
+
) + source_default.hex(COLORS.success)(` \u2713 Authorized user ID: ${userId}
|
|
1506
|
+
|
|
1507
|
+
`) + source_default.hex(COLORS.primary)(" To start:\n") + source_default.hex(COLORS.textDim)(" ggboss serve\n")
|
|
1508
|
+
);
|
|
1509
|
+
} finally {
|
|
1510
|
+
rl.close();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function gradientText2(text) {
|
|
1514
|
+
let i = 0;
|
|
1515
|
+
return text.split("").map((ch) => ch === " " ? ch : source_default.hex(GRADIENT[i++ % GRADIENT.length])(ch)).join("");
|
|
1516
|
+
}
|
|
1517
|
+
function printSetupBanner() {
|
|
1518
|
+
console.log();
|
|
1519
|
+
console.log(
|
|
1520
|
+
` ${gradientText2(LOGO_LINES[0])}${LOGO_GAP}` + source_default.hex(COLORS.primary).bold(BRAND) + source_default.hex(COLORS.textDim)(` v${VERSION}`) + source_default.hex(COLORS.textDim)(" \xB7 By ") + source_default.white.bold(AUTHOR)
|
|
1521
|
+
);
|
|
1522
|
+
console.log(
|
|
1523
|
+
` ${gradientText2(LOGO_LINES[1])}${LOGO_GAP}` + source_default.hex(COLORS.accent)("Telegram Setup")
|
|
1524
|
+
);
|
|
1525
|
+
console.log(
|
|
1526
|
+
` ${gradientText2(LOGO_LINES[2])}${LOGO_GAP}` + source_default.hex(COLORS.textDim)("Remote Control")
|
|
1527
|
+
);
|
|
1528
|
+
console.log();
|
|
1529
|
+
}
|
|
1530
|
+
|
|
660
1531
|
// src/orchestrator-app.tsx
|
|
661
1532
|
init_esm_shims();
|
|
662
1533
|
var import_react11 = __toESM(require_react(), 1);
|
|
@@ -903,7 +1774,7 @@ function projectColor(name) {
|
|
|
903
1774
|
}
|
|
904
1775
|
|
|
905
1776
|
// src/tool-formatters.ts
|
|
906
|
-
function
|
|
1777
|
+
function truncate2(s, max) {
|
|
907
1778
|
if (max <= 1) return "\u2026";
|
|
908
1779
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
909
1780
|
}
|
|
@@ -933,13 +1804,13 @@ var bossToolFormatters = {
|
|
|
933
1804
|
return "";
|
|
934
1805
|
case "get_worker_status":
|
|
935
1806
|
case "get_worker_summary":
|
|
936
|
-
return
|
|
1807
|
+
return truncate2(String(args.project ?? ""), 40);
|
|
937
1808
|
case "prompt_worker": {
|
|
938
1809
|
const project = String(args.project ?? "");
|
|
939
1810
|
const message = String(args.message ?? "").replace(/\s+/g, " ");
|
|
940
1811
|
const fresh = args.fresh === true;
|
|
941
1812
|
const maxMsg = promptWorkerDetailLen(project) - (fresh ? 8 : 0);
|
|
942
|
-
const truncMsg =
|
|
1813
|
+
const truncMsg = truncate2(message, Math.max(15, maxMsg));
|
|
943
1814
|
const head = fresh ? "fresh \xB7 " : "";
|
|
944
1815
|
return project ? `${head}${project} \xB7 ${truncMsg}` : `${head}${truncMsg}`;
|
|
945
1816
|
}
|
|
@@ -1456,19 +2327,19 @@ function RadioPicker({
|
|
|
1456
2327
|
// src/auto-update.ts
|
|
1457
2328
|
init_esm_shims();
|
|
1458
2329
|
import { spawn as spawn2 } from "child_process";
|
|
1459
|
-
import
|
|
1460
|
-
import
|
|
2330
|
+
import fs4 from "fs";
|
|
2331
|
+
import path4 from "path";
|
|
1461
2332
|
import os2 from "os";
|
|
1462
2333
|
var PACKAGE_NAME = "@kenkaiiii/gg-boss";
|
|
1463
2334
|
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
1464
2335
|
var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
|
|
1465
2336
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
1466
2337
|
function getStateFilePath() {
|
|
1467
|
-
return
|
|
2338
|
+
return path4.join(os2.homedir(), ".gg", "boss", "update-state.json");
|
|
1468
2339
|
}
|
|
1469
2340
|
function readState() {
|
|
1470
2341
|
try {
|
|
1471
|
-
const raw =
|
|
2342
|
+
const raw = fs4.readFileSync(getStateFilePath(), "utf-8");
|
|
1472
2343
|
return JSON.parse(raw);
|
|
1473
2344
|
} catch {
|
|
1474
2345
|
return null;
|
|
@@ -1476,9 +2347,9 @@ function readState() {
|
|
|
1476
2347
|
}
|
|
1477
2348
|
function writeState(state) {
|
|
1478
2349
|
try {
|
|
1479
|
-
const dir =
|
|
1480
|
-
|
|
1481
|
-
|
|
2350
|
+
const dir = path4.dirname(getStateFilePath());
|
|
2351
|
+
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2352
|
+
fs4.writeFileSync(getStateFilePath(), JSON.stringify(state));
|
|
1482
2353
|
} catch {
|
|
1483
2354
|
}
|
|
1484
2355
|
}
|
|
@@ -1770,7 +2641,7 @@ function BossAppInner({ boss, resetUI }) {
|
|
|
1770
2641
|
}
|
|
1771
2642
|
bossStore.appendUser(trimmed);
|
|
1772
2643
|
setLastUserMessage(trimmed);
|
|
1773
|
-
const scoped =
|
|
2644
|
+
const scoped = scopePrefix2(state.scope) + trimmed;
|
|
1774
2645
|
boss.enqueueUserMessage(scoped);
|
|
1775
2646
|
};
|
|
1776
2647
|
const handleAbort = () => {
|
|
@@ -1907,7 +2778,7 @@ function ScopePill({ scope }) {
|
|
|
1907
2778
|
] })
|
|
1908
2779
|
] });
|
|
1909
2780
|
}
|
|
1910
|
-
function
|
|
2781
|
+
function scopePrefix2(scope) {
|
|
1911
2782
|
if (scope === "all") return "[scope:all] ";
|
|
1912
2783
|
return `[scope:${scope}] `;
|
|
1913
2784
|
}
|
|
@@ -2448,11 +3319,11 @@ function parseProjectSpec(raw) {
|
|
|
2448
3319
|
const eq = raw.indexOf("=");
|
|
2449
3320
|
if (eq > 0) {
|
|
2450
3321
|
const name = raw.slice(0, eq);
|
|
2451
|
-
const cwd2 =
|
|
3322
|
+
const cwd2 = path5.resolve(raw.slice(eq + 1));
|
|
2452
3323
|
return { name, cwd: cwd2 };
|
|
2453
3324
|
}
|
|
2454
|
-
const cwd =
|
|
2455
|
-
return { name:
|
|
3325
|
+
const cwd = path5.resolve(raw);
|
|
3326
|
+
return { name: path5.basename(cwd), cwd };
|
|
2456
3327
|
}
|
|
2457
3328
|
function parseArgs(argv) {
|
|
2458
3329
|
const args = {
|
|
@@ -2490,10 +3361,54 @@ function printHelpAndExit() {
|
|
|
2490
3361
|
"\n" + c(COLORS.primary, "GG Boss") + c(COLORS.textDim, " \u2014 orchestrator that drives multiple ggcoder workers from one chat.\n\n") + c(COLORS.text, "Usage\n") + " " + c(COLORS.accent, "ggboss") + c(
|
|
2491
3362
|
COLORS.textDim,
|
|
2492
3363
|
" start orchestrator using linked projects\n"
|
|
2493
|
-
) + " " + c(COLORS.accent, "ggboss link") + c(COLORS.textDim, " pick which projects to link (interactive)\n") + " " + c(COLORS.accent, "ggboss continue") + c(COLORS.textDim, " resume the most recent boss session\n") + " " + c(COLORS.accent, "ggboss --resume <id>") + c(COLORS.textDim, " resume a specific boss session\n") + " " + c(COLORS.accent, "ggboss --project <spec> [...]") + c(COLORS.textDim, " override links with explicit project(s)\n\n") + c(COLORS.text, "Options\n") + " " + c(COLORS.primary, "--project, -p <spec>") + c(COLORS.textDim, ' project to manage. spec is "cwd" or "name=cwd". repeatable.\n') + " " + c(COLORS.primary, "--boss-model <id>") + c(COLORS.textDim, " model for the orchestrator (default: claude-opus-4-7)\n") + " " + c(COLORS.primary, "--worker-model <id>") + c(COLORS.textDim, " model for workers (default: claude-sonnet-4-6)\n") + " " + c(COLORS.primary, "--help, -h") + c(COLORS.textDim, " show this help\n\n") + c(COLORS.textDim, "Talk to the boss at the prompt. Press ") + c(COLORS.accent, "Ctrl+C") + c(COLORS.textDim, " twice to exit.\n\n")
|
|
3364
|
+
) + " " + c(COLORS.accent, "ggboss link") + c(COLORS.textDim, " pick which projects to link (interactive)\n") + " " + c(COLORS.accent, "ggboss telegram") + c(COLORS.textDim, " configure Telegram bot integration\n") + " " + c(COLORS.accent, "ggboss serve") + c(COLORS.textDim, " run the boss over Telegram (no TUI)\n") + " " + c(COLORS.accent, "ggboss continue") + c(COLORS.textDim, " resume the most recent boss session\n") + " " + c(COLORS.accent, "ggboss --resume <id>") + c(COLORS.textDim, " resume a specific boss session\n") + " " + c(COLORS.accent, "ggboss --project <spec> [...]") + c(COLORS.textDim, " override links with explicit project(s)\n\n") + c(COLORS.text, "Options\n") + " " + c(COLORS.primary, "--project, -p <spec>") + c(COLORS.textDim, ' project to manage. spec is "cwd" or "name=cwd". repeatable.\n') + " " + c(COLORS.primary, "--boss-model <id>") + c(COLORS.textDim, " model for the orchestrator (default: claude-opus-4-7)\n") + " " + c(COLORS.primary, "--worker-model <id>") + c(COLORS.textDim, " model for workers (default: claude-sonnet-4-6)\n") + " " + c(COLORS.primary, "--help, -h") + c(COLORS.textDim, " show this help\n\n") + c(COLORS.textDim, "Talk to the boss at the prompt. Press ") + c(COLORS.accent, "Ctrl+C") + c(COLORS.textDim, " twice to exit.\n\n")
|
|
2494
3365
|
);
|
|
2495
3366
|
process.exit(0);
|
|
2496
3367
|
}
|
|
3368
|
+
async function runServeSubcommand(argv) {
|
|
3369
|
+
let cliBotToken;
|
|
3370
|
+
let cliUserId;
|
|
3371
|
+
let cliBossModel;
|
|
3372
|
+
let cliWorkerModel;
|
|
3373
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3374
|
+
const a = argv[i];
|
|
3375
|
+
if (a === "--bot-token") cliBotToken = argv[++i];
|
|
3376
|
+
else if (a === "--user-id") cliUserId = argv[++i];
|
|
3377
|
+
else if (a === "--boss-model") cliBossModel = argv[++i];
|
|
3378
|
+
else if (a === "--worker-model") cliWorkerModel = argv[++i];
|
|
3379
|
+
else if (a === "--help" || a === "-h") {
|
|
3380
|
+
process.stdout.write(
|
|
3381
|
+
"\nggboss serve \u2014 drive the boss from Telegram\n\nOptions\n --bot-token <token> Telegram bot token (or env GG_BOSS_TELEGRAM_BOT_TOKEN)\n --user-id <id> Allowed Telegram user ID (or env GG_BOSS_TELEGRAM_USER_ID)\n --boss-model <id> Override boss model\n --worker-model <id> Override worker model\n\nRun `ggboss telegram` first to save credentials interactively.\n\n"
|
|
3382
|
+
);
|
|
3383
|
+
process.exit(0);
|
|
3384
|
+
} else {
|
|
3385
|
+
throw new Error(`Unknown argument: ${a}`);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
const saved = await loadBossTelegramConfig();
|
|
3389
|
+
const botToken = cliBotToken ?? process.env.GG_BOSS_TELEGRAM_BOT_TOKEN ?? saved?.botToken;
|
|
3390
|
+
const userIdStr = cliUserId ?? process.env.GG_BOSS_TELEGRAM_USER_ID;
|
|
3391
|
+
const userId = userIdStr ? parseInt(userIdStr, 10) : saved?.userId;
|
|
3392
|
+
if (!botToken || !userId || isNaN(userId)) {
|
|
3393
|
+
process.stderr.write(
|
|
3394
|
+
source_default.hex(COLORS.error)("Telegram not configured.\n\n") + "Run " + source_default.hex(COLORS.primary).bold("ggboss telegram") + " to set up your bot token and user ID.\n\n" + source_default.hex(COLORS.textDim)("Or provide manually:\n") + source_default.hex(COLORS.textDim)(" ggboss serve --bot-token TOKEN --user-id ID\n")
|
|
3395
|
+
);
|
|
3396
|
+
process.exit(1);
|
|
3397
|
+
}
|
|
3398
|
+
const settings = await loadSettings();
|
|
3399
|
+
const bossProvider = settings.bossProvider ?? "anthropic";
|
|
3400
|
+
const bossModel = cliBossModel ?? settings.bossModel ?? "claude-opus-4-7";
|
|
3401
|
+
const workerProvider = settings.workerProvider ?? "anthropic";
|
|
3402
|
+
const workerModel = cliWorkerModel ?? settings.workerModel ?? "claude-sonnet-4-6";
|
|
3403
|
+
await runBossServeMode({
|
|
3404
|
+
bossProvider,
|
|
3405
|
+
bossModel,
|
|
3406
|
+
bossThinkingLevel: settings.bossThinkingLevel,
|
|
3407
|
+
workerProvider,
|
|
3408
|
+
workerModel,
|
|
3409
|
+
telegram: { botToken, userId }
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
2497
3412
|
async function runOrchestrator(args) {
|
|
2498
3413
|
if (args.projects.length === 0) {
|
|
2499
3414
|
const links = await loadLinks();
|
|
@@ -2556,6 +3471,14 @@ async function main() {
|
|
|
2556
3471
|
await runLinkCommand();
|
|
2557
3472
|
process.exit(0);
|
|
2558
3473
|
}
|
|
3474
|
+
if (argv[0] === "telegram") {
|
|
3475
|
+
await runBossTelegramSetup();
|
|
3476
|
+
process.exit(0);
|
|
3477
|
+
}
|
|
3478
|
+
if (argv[0] === "serve") {
|
|
3479
|
+
await runServeSubcommand(argv.slice(1));
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
2559
3482
|
const isContinue = argv[0] === "continue";
|
|
2560
3483
|
const args = parseArgs(isContinue ? argv.slice(1) : argv);
|
|
2561
3484
|
if (isContinue) args.continueRecent = true;
|