@aion0/forge 0.2.4 → 0.2.6
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/app/api/code/route.ts +7 -5
- package/app/api/git/route.ts +2 -2
- package/app/api/pipelines/route.ts +16 -0
- package/app/api/preview/route.ts +101 -87
- package/app/global-error.tsx +15 -0
- package/bin/forge-server.mjs +23 -0
- package/cli/mw.ts +13 -0
- package/components/CodeViewer.tsx +6 -6
- package/components/Dashboard.tsx +1 -1
- package/components/PipelineEditor.tsx +1 -1
- package/components/PipelineView.tsx +27 -7
- package/components/PreviewPanel.tsx +104 -91
- package/components/SettingsModal.tsx +57 -0
- package/components/WebTerminal.tsx +12 -2
- package/dev-test.sh +1 -1
- package/install.sh +29 -0
- package/instrumentation.ts +2 -3
- package/lib/init.ts +4 -3
- package/lib/notify.ts +8 -0
- package/lib/password.ts +1 -1
- package/lib/pipeline.ts +66 -3
- package/lib/settings.ts +6 -0
- package/lib/task-manager.ts +20 -1
- package/lib/telegram-bot.ts +161 -116
- package/package.json +1 -1
- package/tsconfig.json +2 -1
package/lib/task-manager.ts
CHANGED
|
@@ -39,6 +39,9 @@ function db() {
|
|
|
39
39
|
return getDb(getDbPath());
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Per-task model overrides (used by pipeline to set pipelineModel)
|
|
43
|
+
export const taskModelOverrides = new Map<string, string>();
|
|
44
|
+
|
|
42
45
|
// ─── CRUD ────────────────────────────────────────────────────
|
|
43
46
|
|
|
44
47
|
export function createTask(opts: {
|
|
@@ -239,6 +242,12 @@ function executeTask(task: Task): Promise<void> {
|
|
|
239
242
|
|
|
240
243
|
const args = ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'];
|
|
241
244
|
|
|
245
|
+
// Use model override if set, otherwise fall back to taskModel setting
|
|
246
|
+
const model = taskModelOverrides.get(task.id) || settings.taskModel;
|
|
247
|
+
if (model && model !== 'default') {
|
|
248
|
+
args.push('--model', model);
|
|
249
|
+
}
|
|
250
|
+
|
|
242
251
|
// Resume specific session to continue the conversation
|
|
243
252
|
if (task.conversationId) {
|
|
244
253
|
args.push('--resume', task.conversationId);
|
|
@@ -254,7 +263,7 @@ function executeTask(task: Task): Promise<void> {
|
|
|
254
263
|
|
|
255
264
|
// Resolve the actual claude CLI script path (claude is a symlink to a .js file)
|
|
256
265
|
const resolvedClaude = resolveClaudePath(claudePath);
|
|
257
|
-
console.log(`[task] ${task.projectName}: "${task.prompt.slice(0, 60)}..."`);
|
|
266
|
+
console.log(`[task] ${task.projectName} [${model || 'default'}]: "${task.prompt.slice(0, 60)}..."`);
|
|
258
267
|
|
|
259
268
|
const child = spawn(resolvedClaude.cmd, [...resolvedClaude.prefix, ...args], {
|
|
260
269
|
cwd: task.projectPath,
|
|
@@ -266,6 +275,7 @@ function executeTask(task: Task): Promise<void> {
|
|
|
266
275
|
let resultText = '';
|
|
267
276
|
let totalCost = 0;
|
|
268
277
|
let sessionId = '';
|
|
278
|
+
let modelUsed = '';
|
|
269
279
|
|
|
270
280
|
child.on('error', (err) => {
|
|
271
281
|
console.error(`[task-runner] Spawn error:`, err.message);
|
|
@@ -296,6 +306,9 @@ function executeTask(task: Task): Promise<void> {
|
|
|
296
306
|
}
|
|
297
307
|
|
|
298
308
|
if (parsed.session_id) sessionId = parsed.session_id;
|
|
309
|
+
if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.model) {
|
|
310
|
+
modelUsed = parsed.model;
|
|
311
|
+
}
|
|
299
312
|
if (parsed.type === 'result') {
|
|
300
313
|
resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
|
|
301
314
|
totalCost = parsed.total_cost_usd || 0;
|
|
@@ -381,6 +394,12 @@ function executeTask(task: Task): Promise<void> {
|
|
|
381
394
|
* Sends a visible bell character so the user knows to resume.
|
|
382
395
|
*/
|
|
383
396
|
function notifyTerminalSession(task: Task, status: 'done' | 'failed', sessionId?: string) {
|
|
397
|
+
// Skip pipeline tasks — they have their own notification system
|
|
398
|
+
try {
|
|
399
|
+
const { pipelineTaskIds } = require('./pipeline');
|
|
400
|
+
if (pipelineTaskIds.has(task.id)) return;
|
|
401
|
+
} catch {}
|
|
402
|
+
|
|
384
403
|
try {
|
|
385
404
|
const out = execSync(
|
|
386
405
|
`tmux list-sessions -F "#{session_name}" 2>/dev/null`,
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram Bot — remote interface for
|
|
2
|
+
* Telegram Bot — remote interface for Forge.
|
|
3
3
|
*
|
|
4
4
|
* Optimized for mobile:
|
|
5
5
|
* - /tasks shows numbered list, reply with number to see details
|
|
@@ -19,8 +19,8 @@ import type { Task, TaskLogEntry } from '@/src/types';
|
|
|
19
19
|
// Prevent duplicate polling and state loss across hot-reloads
|
|
20
20
|
const globalKey = Symbol.for('mw-telegram-state');
|
|
21
21
|
const g = globalThis as any;
|
|
22
|
-
if (!g[globalKey]) g[globalKey] = { polling: false, pollTimer: null, lastUpdateId: 0 };
|
|
23
|
-
const botState: { polling: boolean; pollTimer: ReturnType<typeof setTimeout> | null; lastUpdateId: number } = g[globalKey];
|
|
22
|
+
if (!g[globalKey]) g[globalKey] = { polling: false, pollTimer: null, lastUpdateId: 0, taskListenerAttached: false, pollActive: false, processedMsgIds: new Set<number>(), pollGeneration: 0 };
|
|
23
|
+
const botState: { polling: boolean; pollTimer: ReturnType<typeof setTimeout> | null; lastUpdateId: number; taskListenerAttached: boolean; pollActive: boolean; processedMsgIds: Set<number>; pollGeneration: number } = g[globalKey];
|
|
24
24
|
|
|
25
25
|
// Track which Telegram message maps to which task (for reply-based interaction)
|
|
26
26
|
const taskMessageMap = new Map<number, string>(); // messageId → taskId
|
|
@@ -46,64 +46,110 @@ const logBuffers = new Map<string, { entries: string[]; timer: ReturnType<typeof
|
|
|
46
46
|
// ─── Start/Stop ──────────────────────────────────────────────
|
|
47
47
|
|
|
48
48
|
export function startTelegramBot() {
|
|
49
|
-
if (botState.polling) return;
|
|
50
49
|
const settings = loadSettings();
|
|
51
50
|
if (!settings.telegramBotToken || !settings.telegramChatId) return;
|
|
52
51
|
|
|
52
|
+
// Kill any existing poll loop (handles hot-reload creating duplicates)
|
|
53
|
+
if (botState.polling) {
|
|
54
|
+
botState.pollGeneration++;
|
|
55
|
+
if (botState.pollTimer) { clearTimeout(botState.pollTimer); botState.pollTimer = null; }
|
|
56
|
+
botState.pollActive = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
botState.polling = true;
|
|
54
60
|
console.log('[telegram] Bot started');
|
|
55
61
|
|
|
56
62
|
// Set bot command menu
|
|
57
63
|
setBotCommands(settings.telegramBotToken);
|
|
58
64
|
|
|
59
|
-
// Listen for task events → stream to Telegram
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
// Listen for task events → stream to Telegram (only once)
|
|
66
|
+
if (!botState.taskListenerAttached) {
|
|
67
|
+
botState.taskListenerAttached = true;
|
|
68
|
+
onTaskEvent((taskId, event, data) => {
|
|
69
|
+
const settings = loadSettings();
|
|
70
|
+
if (!settings.telegramBotToken || !settings.telegramChatId) return;
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
72
|
+
// Skip pipeline tasks — they have their own notification
|
|
73
|
+
try {
|
|
74
|
+
const { pipelineTaskIds } = require('./pipeline');
|
|
75
|
+
if (pipelineTaskIds.has(taskId)) return;
|
|
76
|
+
} catch {}
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
const chatId = Number(settings.telegramChatId.split(',')[0].trim());
|
|
79
|
+
|
|
80
|
+
if (event === 'log') {
|
|
81
|
+
bufferLogEntry(taskId, chatId, data as TaskLogEntry);
|
|
82
|
+
} else if (event === 'status') {
|
|
83
|
+
handleStatusChange(taskId, chatId, data as string);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Skip stale updates on startup — set offset to -1 to get only new messages
|
|
89
|
+
if (botState.lastUpdateId === 0) {
|
|
90
|
+
fetch(`https://api.telegram.org/bot${settings.telegramBotToken}/getUpdates?offset=-1`)
|
|
91
|
+
.then(r => r.json())
|
|
92
|
+
.then(data => {
|
|
93
|
+
if (data.ok && data.result?.length > 0) {
|
|
94
|
+
botState.lastUpdateId = data.result[data.result.length - 1].update_id;
|
|
95
|
+
}
|
|
96
|
+
poll();
|
|
97
|
+
})
|
|
98
|
+
.catch(() => poll());
|
|
99
|
+
} else {
|
|
100
|
+
poll();
|
|
101
|
+
}
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
export function stopTelegramBot() {
|
|
76
105
|
botState.polling = false;
|
|
106
|
+
botState.pollActive = false;
|
|
77
107
|
if (botState.pollTimer) { clearTimeout(botState.pollTimer); botState.pollTimer = null; }
|
|
78
108
|
}
|
|
79
109
|
|
|
80
110
|
// ─── Polling ─────────────────────────────────────────────────
|
|
81
111
|
|
|
112
|
+
function schedulePoll(delay: number = 1000) {
|
|
113
|
+
if (botState.pollTimer) clearTimeout(botState.pollTimer);
|
|
114
|
+
botState.pollTimer = setTimeout(poll, delay);
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
async function poll() {
|
|
83
|
-
|
|
118
|
+
const myGeneration = botState.pollGeneration;
|
|
119
|
+
|
|
120
|
+
// Prevent concurrent polls
|
|
121
|
+
if (!botState.polling || botState.pollActive) return;
|
|
122
|
+
botState.pollActive = true;
|
|
84
123
|
|
|
85
124
|
try {
|
|
86
125
|
const settings = loadSettings();
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timeout = setTimeout(() => controller.abort(), 35000);
|
|
128
|
+
|
|
87
129
|
const url = `https://api.telegram.org/bot${settings.telegramBotToken}/getUpdates?offset=${botState.lastUpdateId + 1}&timeout=30`;
|
|
88
|
-
const res = await fetch(url);
|
|
130
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
|
|
89
133
|
const data = await res.json();
|
|
90
134
|
|
|
91
|
-
if (data.ok && data.result) {
|
|
135
|
+
if (data.ok && data.result && data.result.length > 0) {
|
|
136
|
+
console.log(`[telegram] Poll got ${data.result.length} updates, lastId=${botState.lastUpdateId}`);
|
|
92
137
|
for (const update of data.result) {
|
|
138
|
+
if (update.update_id <= botState.lastUpdateId) continue;
|
|
93
139
|
botState.lastUpdateId = update.update_id;
|
|
94
140
|
if (update.message?.text) {
|
|
141
|
+
console.log(`[telegram] Processing msg ${update.message.message_id}: ${update.message.text.slice(0, 30)}`);
|
|
95
142
|
await handleMessage(update.message);
|
|
96
143
|
}
|
|
97
144
|
}
|
|
98
145
|
}
|
|
99
|
-
} catch
|
|
100
|
-
|
|
101
|
-
if (!isNetworkError) {
|
|
102
|
-
console.error('[telegram] Poll error:', err);
|
|
103
|
-
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Network errors during sleep/wake — silent
|
|
104
148
|
}
|
|
105
149
|
|
|
106
|
-
botState.
|
|
150
|
+
botState.pollActive = false;
|
|
151
|
+
// Only continue polling if this is still the current generation
|
|
152
|
+
if (botState.polling && myGeneration === botState.pollGeneration) schedulePoll(1000);
|
|
107
153
|
}
|
|
108
154
|
|
|
109
155
|
// ─── Message Handler ─────────────────────────────────────────
|
|
@@ -119,6 +165,16 @@ async function handleMessage(msg: any) {
|
|
|
119
165
|
}
|
|
120
166
|
|
|
121
167
|
// Message received (logged silently)
|
|
168
|
+
// Dedup: skip if we already processed this message
|
|
169
|
+
const msgId = msg.message_id;
|
|
170
|
+
if (botState.processedMsgIds.has(msgId)) return;
|
|
171
|
+
botState.processedMsgIds.add(msgId);
|
|
172
|
+
// Keep set size bounded
|
|
173
|
+
if (botState.processedMsgIds.size > 200) {
|
|
174
|
+
const oldest = [...botState.processedMsgIds].slice(0, 100);
|
|
175
|
+
oldest.forEach(id => botState.processedMsgIds.delete(id));
|
|
176
|
+
}
|
|
177
|
+
|
|
122
178
|
const text: string = msg.text.trim();
|
|
123
179
|
const replyTo = msg.reply_to_message?.message_id;
|
|
124
180
|
|
|
@@ -232,28 +288,30 @@ async function handleMessage(msg: any) {
|
|
|
232
288
|
await sendProjectList(chatId);
|
|
233
289
|
break;
|
|
234
290
|
case '/watch':
|
|
235
|
-
await handleWatch(chatId, args[0], args[1]);
|
|
236
|
-
break;
|
|
237
|
-
case '/watchers':
|
|
238
291
|
case '/w':
|
|
239
|
-
|
|
292
|
+
if (args.length > 0) {
|
|
293
|
+
await handleWatch(chatId, args[0], args[1]);
|
|
294
|
+
} else {
|
|
295
|
+
await sendWatcherList(chatId);
|
|
296
|
+
}
|
|
240
297
|
break;
|
|
241
298
|
case '/unwatch':
|
|
242
299
|
await handleUnwatch(chatId, args[0]);
|
|
243
300
|
break;
|
|
301
|
+
case '/docs':
|
|
302
|
+
case '/doc':
|
|
303
|
+
await handleDocs(chatId, args.join(' '));
|
|
304
|
+
break;
|
|
244
305
|
case '/peek':
|
|
306
|
+
case '/sessions':
|
|
307
|
+
case '/s':
|
|
245
308
|
if (args.length > 0) {
|
|
246
309
|
await handlePeek(chatId, args[0], args[1]);
|
|
247
310
|
} else {
|
|
248
311
|
await startPeekSelection(chatId);
|
|
249
312
|
}
|
|
250
313
|
break;
|
|
251
|
-
case '/docs':
|
|
252
|
-
case '/doc':
|
|
253
|
-
await handleDocs(chatId, args.join(' '));
|
|
254
|
-
break;
|
|
255
314
|
case '/note':
|
|
256
|
-
case '/docs_write':
|
|
257
315
|
await handleDocsWrite(chatId, args.join(' '));
|
|
258
316
|
break;
|
|
259
317
|
case '/cancel':
|
|
@@ -301,23 +359,19 @@ async function handleMessage(msg: any) {
|
|
|
301
359
|
async function sendHelp(chatId: number) {
|
|
302
360
|
await send(chatId,
|
|
303
361
|
`🤖 Forge\n\n` +
|
|
304
|
-
`📋 /
|
|
305
|
-
`/tasks
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
`/
|
|
311
|
-
|
|
312
|
-
`👀 /peek [project] [sessionId] — session summary\n` +
|
|
313
|
-
`📖 /docs — docs session summary\n` +
|
|
314
|
-
`/docs <filename> — view doc file\n` +
|
|
315
|
-
`📝 /note — quick note to docs claude\n\n` +
|
|
362
|
+
`📋 /task — create task (interactive)\n` +
|
|
363
|
+
`/tasks — task list\n\n` +
|
|
364
|
+
`👀 /sessions — session summary (select project)\n` +
|
|
365
|
+
`📖 /docs — docs summary / view file\n` +
|
|
366
|
+
`📝 /note — quick note to docs\n\n` +
|
|
367
|
+
`👁 /watch <project> — monitor session\n` +
|
|
368
|
+
`/watch — list watchers\n` +
|
|
369
|
+
`/unwatch <id> — stop\n\n` +
|
|
316
370
|
`🔧 /cancel <id> /retry <id>\n` +
|
|
371
|
+
`/sessions — browse sessions\n` +
|
|
317
372
|
`/projects — list projects\n\n` +
|
|
318
|
-
`🌐 /tunnel —
|
|
319
|
-
`/tunnel_start
|
|
320
|
-
`/tunnel_stop — stop tunnel\n` +
|
|
373
|
+
`🌐 /tunnel — status\n` +
|
|
374
|
+
`/tunnel_start / /tunnel_stop\n` +
|
|
321
375
|
`/tunnel_password <pw> — get login password\n\n` +
|
|
322
376
|
`Reply number to select`
|
|
323
377
|
);
|
|
@@ -629,39 +683,16 @@ async function handlePeek(chatId: number, projectArg?: string, sessionArg?: stri
|
|
|
629
683
|
contextLen += line.length;
|
|
630
684
|
}
|
|
631
685
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (apiKey) {
|
|
637
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
638
|
-
method: 'POST',
|
|
639
|
-
headers: {
|
|
640
|
-
'Content-Type': 'application/json',
|
|
641
|
-
'x-api-key': apiKey,
|
|
642
|
-
'anthropic-version': '2023-06-01',
|
|
643
|
-
},
|
|
644
|
-
body: JSON.stringify({
|
|
645
|
-
model: 'claude-haiku-4-5-20251001',
|
|
646
|
-
max_tokens: 500,
|
|
647
|
-
messages: [{
|
|
648
|
-
role: 'user',
|
|
649
|
-
content: `Summarize this Claude Code session in 2-3 sentences. What was the user trying to do? What's the current status? Answer in the same language as the session content.\n\n${contextEntries.join('\n')}`,
|
|
650
|
-
}],
|
|
651
|
-
}),
|
|
652
|
-
});
|
|
653
|
-
if (res.ok) {
|
|
654
|
-
const data = await res.json();
|
|
655
|
-
summary = data.content?.[0]?.text || '';
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
} catch {}
|
|
686
|
+
const telegramModel = loadSettings().telegramModel || 'sonnet';
|
|
687
|
+
const summary = contextEntries.length > 3
|
|
688
|
+
? await aiSummarize(contextEntries.join('\n'), 'Summarize this Claude Code session in 2-3 sentences. What was the user working on? What is the current status? Answer in the same language as the content.')
|
|
689
|
+
: '';
|
|
659
690
|
|
|
660
691
|
// Format output
|
|
661
|
-
const header = `📋 ${project.name} / ${session.sessionId.slice(0, 8)}\n${entries.length} entries${session.gitBranch ? ` • ${session.gitBranch}` : ''}`;
|
|
692
|
+
const header = `📋 ${project.name} / ${session.sessionId.slice(0, 8)}\n${entries.length} entries${session.gitBranch ? ` • ${session.gitBranch}` : ''}${summary ? ` • AI: ${telegramModel}` : ''}`;
|
|
662
693
|
|
|
663
694
|
const summaryBlock = summary
|
|
664
|
-
? `\n\n📝 Summary:\n${summary}`
|
|
695
|
+
? `\n\n📝 Summary (${telegramModel}):\n${summary}`
|
|
665
696
|
: '';
|
|
666
697
|
|
|
667
698
|
const rawBlock = `\n\n--- Recent ---\n${recentRaw.join('\n\n')}`;
|
|
@@ -1009,6 +1040,47 @@ async function handleTunnelPassword(chatId: number, password?: string, userMsgId
|
|
|
1009
1040
|
}
|
|
1010
1041
|
}
|
|
1011
1042
|
|
|
1043
|
+
// ─── AI Summarize (using Claude Code subscription) ───────────
|
|
1044
|
+
|
|
1045
|
+
async function aiSummarize(content: string, instruction: string): Promise<string> {
|
|
1046
|
+
try {
|
|
1047
|
+
const settings = loadSettings();
|
|
1048
|
+
const claudePath = settings.claudePath || process.env.CLAUDE_PATH || 'claude';
|
|
1049
|
+
const model = settings.telegramModel || 'sonnet';
|
|
1050
|
+
const { execSync } = require('child_process');
|
|
1051
|
+
const { realpathSync } = require('fs');
|
|
1052
|
+
|
|
1053
|
+
// Resolve claude path
|
|
1054
|
+
let cmd = claudePath;
|
|
1055
|
+
try {
|
|
1056
|
+
const which = execSync(`which ${claudePath}`, { encoding: 'utf-8' }).trim();
|
|
1057
|
+
cmd = realpathSync(which);
|
|
1058
|
+
} catch {}
|
|
1059
|
+
|
|
1060
|
+
const args = ['-p', '--model', model, '--max-turns', '1'];
|
|
1061
|
+
const prompt = `${instruction}\n\nContent:\n${content.slice(0, 8000)}`;
|
|
1062
|
+
|
|
1063
|
+
let execCmd: string;
|
|
1064
|
+
if (cmd.endsWith('.js') || cmd.endsWith('.mjs')) {
|
|
1065
|
+
execCmd = `${process.execPath} ${cmd} ${args.join(' ')}`;
|
|
1066
|
+
} else {
|
|
1067
|
+
execCmd = `${cmd} ${args.join(' ')}`;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const result = execSync(execCmd, {
|
|
1071
|
+
input: prompt,
|
|
1072
|
+
encoding: 'utf-8',
|
|
1073
|
+
timeout: 30000,
|
|
1074
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1075
|
+
env: { ...process.env, CLAUDECODE: undefined },
|
|
1076
|
+
}).trim();
|
|
1077
|
+
|
|
1078
|
+
return result.slice(0, 1000);
|
|
1079
|
+
} catch {
|
|
1080
|
+
return '';
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1012
1084
|
// ─── Docs ────────────────────────────────────────────────────
|
|
1013
1085
|
|
|
1014
1086
|
async function handleDocs(chatId: number, input: string) {
|
|
@@ -1130,38 +1202,14 @@ async function handleDocs(chatId: number, input: string) {
|
|
|
1130
1202
|
} catch {}
|
|
1131
1203
|
|
|
1132
1204
|
const recent = entries.slice(-8).join('\n\n');
|
|
1133
|
-
const header = `📖 Docs: ${docRoot.split('/').pop()}\n📋 Session: ${sessionId.slice(0, 12)}\n`;
|
|
1205
|
+
const header = `📖 Docs: ${docRoot.split('/').pop()}\n📋 Session: ${sessionId.slice(0, 12)}${summary ? ` • AI: ${tModel}` : ''}\n`;
|
|
1134
1206
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const contextText = entries.slice(-15).join('\n');
|
|
1141
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
1142
|
-
method: 'POST',
|
|
1143
|
-
headers: {
|
|
1144
|
-
'Content-Type': 'application/json',
|
|
1145
|
-
'x-api-key': apiKey,
|
|
1146
|
-
'anthropic-version': '2023-06-01',
|
|
1147
|
-
},
|
|
1148
|
-
body: JSON.stringify({
|
|
1149
|
-
model: 'claude-haiku-4-5-20251001',
|
|
1150
|
-
max_tokens: 300,
|
|
1151
|
-
messages: [{
|
|
1152
|
-
role: 'user',
|
|
1153
|
-
content: `Summarize this Claude Code session in 2-3 sentences. What was the user working on? What's the current status? Answer in the same language as the content.\n\n${contextText}`,
|
|
1154
|
-
}],
|
|
1155
|
-
}),
|
|
1156
|
-
});
|
|
1157
|
-
if (res.ok) {
|
|
1158
|
-
const data = await res.json();
|
|
1159
|
-
summary = data.content?.[0]?.text || '';
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
} catch {}
|
|
1207
|
+
const tModel = loadSettings().telegramModel || 'sonnet';
|
|
1208
|
+
const summary = entries.length > 3
|
|
1209
|
+
? await aiSummarize(entries.slice(-15).join('\n'), 'Summarize this Claude Code session in 2-3 sentences. What was the user working on? What is the current status? Answer in the same language as the content.')
|
|
1210
|
+
: '';
|
|
1211
|
+
const summaryBlock = summary ? `\n📝 (${tModel}) ${summary}\n` : '';
|
|
1163
1212
|
|
|
1164
|
-
const summaryBlock = summary ? `\n📝 ${summary}\n` : '';
|
|
1165
1213
|
const fullText = header + summaryBlock + '\n--- Recent ---\n' + recent;
|
|
1166
1214
|
|
|
1167
1215
|
const chunks = splitMessage(fullText, 4000);
|
|
@@ -1365,19 +1413,16 @@ async function setBotCommands(token: string) {
|
|
|
1365
1413
|
headers: { 'Content-Type': 'application/json' },
|
|
1366
1414
|
body: JSON.stringify({
|
|
1367
1415
|
commands: [
|
|
1416
|
+
{ command: 'task', description: 'Create task' },
|
|
1368
1417
|
{ command: 'tasks', description: 'List tasks' },
|
|
1369
|
-
{ command: '
|
|
1370
|
-
{ command: '
|
|
1371
|
-
{ command: '
|
|
1418
|
+
{ command: 'sessions', description: 'Session summary (AI)' },
|
|
1419
|
+
{ command: 'docs', description: 'Docs summary / view file' },
|
|
1420
|
+
{ command: 'note', description: 'Quick note to docs' },
|
|
1421
|
+
{ command: 'watch', description: 'Monitor session / list watchers' },
|
|
1372
1422
|
{ command: 'tunnel', description: 'Tunnel status' },
|
|
1373
1423
|
{ command: 'tunnel_start', description: 'Start tunnel' },
|
|
1374
1424
|
{ command: 'tunnel_stop', description: 'Stop tunnel' },
|
|
1375
1425
|
{ command: 'tunnel_password', description: 'Get login password' },
|
|
1376
|
-
{ command: 'peek', description: 'Session summary (AI + recent)' },
|
|
1377
|
-
{ command: 'docs', description: 'Docs session summary / view file' },
|
|
1378
|
-
{ command: 'note', description: 'Quick note to docs Claude' },
|
|
1379
|
-
{ command: 'watch', description: 'Monitor session' },
|
|
1380
|
-
{ command: 'watchers', description: 'List watchers' },
|
|
1381
1426
|
{ command: 'help', description: 'Show help' },
|
|
1382
1427
|
],
|
|
1383
1428
|
}),
|
package/package.json
CHANGED