dsh-tg-bridge 1.5.0 → 1.5.2
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/README.md +14 -0
- package/bridge.mjs +37 -3
- package/package.json +1 -1
- package/say.mjs +24 -5
package/README.md
CHANGED
|
@@ -206,6 +206,20 @@ and a line in `~/.dsh/profiles/headless/cordis.patch.yml`:
|
|
|
206
206
|
The bridge checks the profile itself: if the line is missing, it does not write the task
|
|
207
207
|
(it would just expire) and behaves as before, queueing the message.
|
|
208
208
|
|
|
209
|
+
### Live thoughts
|
|
210
|
+
|
|
211
|
+
The agent can leave a short decision trail inside the progress message instead of sending
|
|
212
|
+
extra chat messages:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
node ~/.dsh/tg-bridge/say.mjs --thought "checking why the watchdog fired twice"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The note is appended to `~/.dsh/tg-bridge/thoughts/<chat>_<thread>.jsonl`; the progress
|
|
219
|
+
message shows the last two as `💭` lines. Inside a turn the chat and thread come from
|
|
220
|
+
`DSH_TG_CHAT` / `DSH_TG_THREAD`, so the command needs no ids. `--thought` never sends a
|
|
221
|
+
Telegram message on its own.
|
|
222
|
+
|
|
209
223
|
### Goals and tasks
|
|
210
224
|
|
|
211
225
|
A topic carries its own goal and checklist, so the aim survives `/new`, a
|
package/bridge.mjs
CHANGED
|
@@ -44,6 +44,7 @@ const LOG = join(DIR, 'bridge.log');
|
|
|
44
44
|
const CRED = join(DSH_HOME, '.credentials.yaml');
|
|
45
45
|
const PENDING_DIR = join(DIR, 'pending');
|
|
46
46
|
const STEER_DIR = join(DIR, 'steer');
|
|
47
|
+
const THOUGHT_DIR = join(DIR, 'thoughts');
|
|
47
48
|
// The helper the agent calls to confirm an addition in the chat while the turn
|
|
48
49
|
// is still running. Lives next to the bridge so an installed copy works too.
|
|
49
50
|
const SAY = join(DIR, 'say.mjs');
|
|
@@ -735,6 +736,9 @@ function runTurn(task, sessionId, key = '', onEvent = null, extraArgs = [], cwd
|
|
|
735
736
|
cwd: cwd && existsSync(cwd) ? cwd : config.cwd,
|
|
736
737
|
env: {
|
|
737
738
|
...process.env,
|
|
739
|
+
DSH_TG_KEY: key,
|
|
740
|
+
DSH_TG_CHAT: String(key).split(':')[0] || '',
|
|
741
|
+
DSH_TG_THREAD: String(key).split(':')[1] || '0',
|
|
738
742
|
ELECTRON_RUN_AS_NODE: '1',
|
|
739
743
|
UNOROUTER_API_KEY: readSecret('UNOROUTER_API_KEY'),
|
|
740
744
|
// Free fallback providers: their keys live next to the main one and are
|
|
@@ -871,6 +875,34 @@ async function startProgress(chatId, threadId, lang = 'ru') {
|
|
|
871
875
|
let dirty = false;
|
|
872
876
|
let timer = null;
|
|
873
877
|
|
|
878
|
+
// Short "💭" notes the agent appends with `say.mjs --thought`. The progress
|
|
879
|
+
// message shows the last two, so the person sees the decision trail without
|
|
880
|
+
// a second stream of chat messages.
|
|
881
|
+
const thoughtFile = join(THOUGHT_DIR, `${chatId}_${threadId || 0}.jsonl`);
|
|
882
|
+
let thoughts = [];
|
|
883
|
+
try {
|
|
884
|
+
mkdirSync(THOUGHT_DIR, { recursive: true });
|
|
885
|
+
writeFileSync(thoughtFile, '');
|
|
886
|
+
} catch {}
|
|
887
|
+
const thoughtTimer = setInterval(() => {
|
|
888
|
+
let next = [];
|
|
889
|
+
try {
|
|
890
|
+
next = readFileSync(thoughtFile, 'utf8')
|
|
891
|
+
.split('\n')
|
|
892
|
+
.filter(Boolean)
|
|
893
|
+
.slice(-2)
|
|
894
|
+
.map((line) => {
|
|
895
|
+
try { return String(JSON.parse(line).text || '').slice(0, 140); } catch { return ''; }
|
|
896
|
+
})
|
|
897
|
+
.filter(Boolean);
|
|
898
|
+
} catch {}
|
|
899
|
+
if (next.join('\n') !== thoughts.join('\n')) {
|
|
900
|
+
thoughts = next;
|
|
901
|
+
schedule();
|
|
902
|
+
}
|
|
903
|
+
}, 3000);
|
|
904
|
+
thoughtTimer.unref?.();
|
|
905
|
+
|
|
874
906
|
const body = (final = false) => {
|
|
875
907
|
const seconds = Math.round((Date.now() - started) / 1000);
|
|
876
908
|
const head = final
|
|
@@ -880,6 +912,7 @@ async function startProgress(chatId, threadId, lang = 'ru') {
|
|
|
880
912
|
// shell commands read as noise on a phone.
|
|
881
913
|
const tail = steps.slice(-3).map((line) => `• ${line}`).join('\n');
|
|
882
914
|
const more = steps.length > 3 ? `\n${t(lang, 'status.more', { n: steps.length - 3 })}` : '';
|
|
915
|
+
const mind = thoughts.length ? `\n${thoughts.map((line) => `💭 ${line}`).join('\n')}` : '';
|
|
883
916
|
// The cache line belongs to the outcome, not to every intermediate edit.
|
|
884
917
|
const cache = final && usage.cacheReadTokens
|
|
885
918
|
? `\n${t(lang, 'status.cacheLine', {
|
|
@@ -887,7 +920,7 @@ async function startProgress(chatId, threadId, lang = 'ru') {
|
|
|
887
920
|
k: Math.round(usage.cacheReadTokens / 1000),
|
|
888
921
|
})}`
|
|
889
922
|
: '';
|
|
890
|
-
return (tail ? `${head}\n${tail}${more}${cache}` : `${head}${cache}`);
|
|
923
|
+
return (tail ? `${head}${mind}\n${tail}${more}${cache}` : `${head}${mind}${cache}`);
|
|
891
924
|
};
|
|
892
925
|
|
|
893
926
|
try {
|
|
@@ -922,7 +955,7 @@ async function startProgress(chatId, threadId, lang = 'ru') {
|
|
|
922
955
|
}
|
|
923
956
|
};
|
|
924
957
|
|
|
925
|
-
|
|
958
|
+
function schedule() {
|
|
926
959
|
dirty = true;
|
|
927
960
|
if (timer) return;
|
|
928
961
|
timer = setTimeout(async () => {
|
|
@@ -932,7 +965,7 @@ async function startProgress(chatId, threadId, lang = 'ru') {
|
|
|
932
965
|
await edit(body());
|
|
933
966
|
}, 3500);
|
|
934
967
|
timer.unref?.();
|
|
935
|
-
}
|
|
968
|
+
}
|
|
936
969
|
|
|
937
970
|
return {
|
|
938
971
|
/** Keep the clock moving during long model thinking: the elapsed time is
|
|
@@ -969,6 +1002,7 @@ async function startProgress(chatId, threadId, lang = 'ru') {
|
|
|
969
1002
|
},
|
|
970
1003
|
async finish(result) {
|
|
971
1004
|
clearTimeout(timer);
|
|
1005
|
+
clearInterval(thoughtTimer);
|
|
972
1006
|
if (result?.error) {
|
|
973
1007
|
// A failed turn leaves the status message as the error report and swaps
|
|
974
1008
|
// «Стоп» for «Повторить»: the person's next move is a retry, not a stop.
|
package/package.json
CHANGED
package/say.mjs
CHANGED
|
@@ -7,16 +7,21 @@
|
|
|
7
7
|
//
|
|
8
8
|
// node say.mjs --steer <job-id> "text"
|
|
9
9
|
// node say.mjs --chat <chat-id> [--thread <id>] "text"
|
|
10
|
+
// node say.mjs --thought "short note" (chat/thread come from DSH_TG_CHAT/DSH_TG_THREAD)
|
|
11
|
+
//
|
|
12
|
+
// `--thought` does not send a message: it appends to the turn's thought file,
|
|
13
|
+
// and the live progress message shows the last two notes as 💭 lines.
|
|
10
14
|
//
|
|
11
15
|
// With `--steer` the chat/thread come from the job file and an ack marker is
|
|
12
16
|
// written, which suppresses the bridge's fallback line.
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
18
|
import { homedir } from 'node:os';
|
|
15
19
|
import { join } from 'node:path';
|
|
16
20
|
|
|
17
21
|
const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
18
22
|
const STEER = join(DSH_HOME, 'tg-bridge', 'steer');
|
|
19
23
|
const ACKS = join(STEER, 'acks');
|
|
24
|
+
const THOUGHTS = join(DSH_HOME, 'tg-bridge', 'thoughts');
|
|
20
25
|
|
|
21
26
|
function findToken() {
|
|
22
27
|
const env = (process.env.DSH_TELEGRAM_BOT_TOKEN || '').trim();
|
|
@@ -39,18 +44,19 @@ function jobOf(id) {
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function usage() {
|
|
42
|
-
console.error('usage: node say.mjs --steer <id> "text" | --chat <id> [--thread <id>] "text"');
|
|
47
|
+
console.error('usage: node say.mjs --steer <id> "text" | --chat <id> [--thread <id>] "text" | --thought "text"');
|
|
43
48
|
process.exit(2);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
function parse(argv) {
|
|
47
|
-
const out = { chatId: 0, threadId: 0, steer: '', text: '' };
|
|
52
|
+
const out = { chatId: 0, threadId: 0, steer: '', thought: false, text: '' };
|
|
48
53
|
const rest = [];
|
|
49
54
|
for (let i = 0; i < argv.length; i += 1) {
|
|
50
55
|
const arg = argv[i];
|
|
51
56
|
if (arg === '--steer') out.steer = argv[++i] || '';
|
|
52
57
|
else if (arg === '--chat') out.chatId = Number(argv[++i] || 0);
|
|
53
58
|
else if (arg === '--thread') out.threadId = Number(argv[++i] || 0);
|
|
59
|
+
else if (arg === '--thought') out.thought = true;
|
|
54
60
|
else rest.push(arg);
|
|
55
61
|
}
|
|
56
62
|
out.text = rest.join(' ').trim();
|
|
@@ -61,8 +67,8 @@ async function main() {
|
|
|
61
67
|
const args = parse(process.argv.slice(2));
|
|
62
68
|
if (!args.text) usage();
|
|
63
69
|
|
|
64
|
-
let chatId = args.chatId;
|
|
65
|
-
let threadId = args.threadId;
|
|
70
|
+
let chatId = args.chatId || Number(process.env.DSH_TG_CHAT || 0);
|
|
71
|
+
let threadId = args.threadId || Number(process.env.DSH_TG_THREAD || 0);
|
|
66
72
|
let jobPath = '';
|
|
67
73
|
if (args.steer) {
|
|
68
74
|
const found = jobOf(args.steer);
|
|
@@ -76,6 +82,19 @@ async function main() {
|
|
|
76
82
|
}
|
|
77
83
|
if (!chatId) usage();
|
|
78
84
|
|
|
85
|
+
if (args.thought) {
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(THOUGHTS, { recursive: true });
|
|
88
|
+
const file = join(THOUGHTS, `${chatId}_${threadId || 0}.jsonl`);
|
|
89
|
+
appendFileSync(file, `${JSON.stringify({ at: Date.now(), text: args.text })}\n`, 'utf8');
|
|
90
|
+
console.log(file);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(String(error?.message ?? error));
|
|
93
|
+
process.exit(6);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
const token = findToken();
|
|
80
99
|
if (!token) {
|
|
81
100
|
console.error('bot token not found (DSH_TELEGRAM_BOT_TOKEN or ~/.dsh/.credentials.yaml)');
|