codeep 3.0.0 → 3.1.1
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/config/index.d.ts +6 -0
- package/dist/config/index.js +1 -0
- package/dist/config/providers.js +4 -2
- package/dist/renderer/App.d.ts +5 -1
- package/dist/renderer/App.js +12 -4
- package/dist/renderer/agentExecution.js +62 -5
- package/dist/renderer/components/Settings.js +11 -0
- package/dist/renderer/index.d.ts +12 -6
- package/dist/renderer/layout.d.ts +7 -0
- package/dist/renderer/layout.js +1 -1
- package/dist/renderer/main.js +46 -2
- package/dist/utils/telegramApproval.d.ts +5 -20
- package/dist/utils/telegramApproval.js +16 -55
- package/dist/utils/telegramCredentials.d.ts +8 -0
- package/dist/utils/telegramCredentials.js +15 -0
- package/dist/utils/telegramInbox.d.ts +93 -0
- package/dist/utils/telegramInbox.js +188 -0
- package/dist/utils/telegramNotify.d.ts +102 -0
- package/dist/utils/telegramNotify.js +201 -0
- package/dist/utils/telegramUpdates.d.ts +71 -0
- package/dist/utils/telegramUpdates.js +175 -0
- package/dist/utils/tokenTracker.js +51 -8
- package/dist/utils/tools.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/config/index.d.ts
CHANGED
|
@@ -88,6 +88,12 @@ export interface ConfigSchema {
|
|
|
88
88
|
* and waiting on an answer that cannot come would hang CI. The bot token
|
|
89
89
|
* lives in the keychain, never here. */
|
|
90
90
|
telegramApproval: boolean;
|
|
91
|
+
/** Whether a message from the configured chat becomes a prompt.
|
|
92
|
+
*
|
|
93
|
+
* Separate from telegramApproval on purpose. Approval lets the phone answer
|
|
94
|
+
* a question the agent already chose to ask; this lets the phone ask one,
|
|
95
|
+
* which is a keyboard attached to this machine. Off unless asked for. */
|
|
96
|
+
telegramInbox: boolean;
|
|
91
97
|
/** The single chat allowed to answer. Not a secret — it identifies a
|
|
92
98
|
* conversation, and it is useless without the token. */
|
|
93
99
|
telegramChatId: string;
|
package/dist/config/index.js
CHANGED
package/dist/config/providers.js
CHANGED
|
@@ -477,7 +477,8 @@ export const PROVIDERS = {
|
|
|
477
477
|
},
|
|
478
478
|
},
|
|
479
479
|
models: [
|
|
480
|
-
{ id: 'claude-fable-5', name: 'Claude Fable 5', description: 'Most capable — hardest reasoning & long-horizon agentic work' },
|
|
480
|
+
{ id: 'claude-fable-5-1', name: 'Claude Fable 5.1', description: 'Most capable — hardest reasoning & long-horizon agentic work' },
|
|
481
|
+
{ id: 'claude-fable-5', name: 'Claude Fable 5', description: 'Superseded by 5.1 — same price, kept for pinned configs' },
|
|
481
482
|
{ id: 'claude-opus-5', name: 'Claude Opus 5', description: 'Complex agentic coding & deep reasoning — the Opus workhorse' },
|
|
482
483
|
{ id: 'claude-sonnet-5', name: 'Claude Sonnet 5', description: 'Best balance of speed and intelligence' },
|
|
483
484
|
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku', description: 'Fastest and most affordable' },
|
|
@@ -528,7 +529,8 @@ export const PROVIDERS = {
|
|
|
528
529
|
// get a working dropdown.
|
|
529
530
|
models: [
|
|
530
531
|
{ id: 'openrouter/auto', name: 'Auto-route', description: 'OpenRouter picks the best model for the task' },
|
|
531
|
-
{ id: 'anthropic/claude-fable-5', name: 'Claude Fable 5', description: 'Anthropic — most capable' },
|
|
532
|
+
{ id: 'anthropic/claude-fable-5-1', name: 'Claude Fable 5.1', description: 'Anthropic — most capable' },
|
|
533
|
+
{ id: 'anthropic/claude-fable-5', name: 'Claude Fable 5', description: 'Anthropic — superseded by 5.1' },
|
|
532
534
|
{ id: 'anthropic/claude-opus-5', name: 'Claude Opus 5', description: 'Anthropic — flagship Opus tier' },
|
|
533
535
|
{ id: 'anthropic/claude-sonnet-5', name: 'Claude Sonnet 5', description: 'Anthropic — balanced' },
|
|
534
536
|
{ id: 'openai/gpt-5.6-sol', name: 'GPT-5.6 Sol', description: 'OpenAI — flagship' },
|
package/dist/renderer/App.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface ConfirmOptions {
|
|
|
20
20
|
onConfirm: () => void;
|
|
21
21
|
onCancel?: () => void;
|
|
22
22
|
}
|
|
23
|
-
export { HunkPickerItem, HunkPickerOptions } from './components/HunkPicker';
|
|
23
|
+
export type { HunkPickerItem, HunkPickerOptions } from './components/HunkPicker';
|
|
24
24
|
import { type HunkPickerOptions } from './components/HunkPicker';
|
|
25
25
|
/**
|
|
26
26
|
* Options for the interactive hunk picker — see components/HunkPicker.ts.
|
|
@@ -492,6 +492,10 @@ export declare class App {
|
|
|
492
492
|
/**
|
|
493
493
|
* Render status bar
|
|
494
494
|
*/
|
|
495
|
+
/**
|
|
496
|
+
* @param canScroll false when the caller renders instead of the transcript
|
|
497
|
+
* rather than above it, so there is nothing on screen for PgDn to move.
|
|
498
|
+
*/
|
|
495
499
|
private renderStatusBar;
|
|
496
500
|
/**
|
|
497
501
|
* Get visible messages (including streaming)
|
package/dist/renderer/App.js
CHANGED
|
@@ -1469,7 +1469,7 @@ export class App {
|
|
|
1469
1469
|
// Input (don't render cursor when menu/settings is open)
|
|
1470
1470
|
this.renderInput(inputLine, width, this.menuOpen || this.settingsOpen);
|
|
1471
1471
|
// Status bar
|
|
1472
|
-
this.renderStatusBar(statusLine, width);
|
|
1472
|
+
this.renderStatusBar(statusLine, width, true);
|
|
1473
1473
|
// Inline menu renders BELOW status bar
|
|
1474
1474
|
if (this.menuOpen && this.menuItems.length > 0) {
|
|
1475
1475
|
this.renderInlineMenu(statusLine + 1, width);
|
|
@@ -1707,7 +1707,7 @@ export class App {
|
|
|
1707
1707
|
this.renderInput(inputY, width, false);
|
|
1708
1708
|
this.renderAgentKeyHints(hintsY, width);
|
|
1709
1709
|
this.screen.horizontalLine(footerDividerY, '─', fg.gray);
|
|
1710
|
-
this.renderStatusBar(statusY, width);
|
|
1710
|
+
this.renderStatusBar(statusY, width, false);
|
|
1711
1711
|
this.screen.render();
|
|
1712
1712
|
}
|
|
1713
1713
|
renderAgentContextRail(dividerX, top, bottom, railWidth, timeline) {
|
|
@@ -2424,7 +2424,11 @@ export class App {
|
|
|
2424
2424
|
/**
|
|
2425
2425
|
* Render status bar
|
|
2426
2426
|
*/
|
|
2427
|
-
|
|
2427
|
+
/**
|
|
2428
|
+
* @param canScroll false when the caller renders instead of the transcript
|
|
2429
|
+
* rather than above it, so there is nothing on screen for PgDn to move.
|
|
2430
|
+
*/
|
|
2431
|
+
renderStatusBar(y, width, canScroll) {
|
|
2428
2432
|
// Clear the line first
|
|
2429
2433
|
this.screen.writeLine(y, '');
|
|
2430
2434
|
if (this.notification) {
|
|
@@ -2441,8 +2445,12 @@ export class App {
|
|
|
2441
2445
|
unseenWhileScrolled: this.unseenWhileScrolled,
|
|
2442
2446
|
isStreaming: this.isStreaming,
|
|
2443
2447
|
isLoading: this.isLoading,
|
|
2448
|
+
canScroll,
|
|
2444
2449
|
});
|
|
2445
|
-
|
|
2450
|
+
// Only the badge replaces the footer. Without the guard a run wide enough
|
|
2451
|
+
// to show the timeline traded its runtime and token counts for a hint that
|
|
2452
|
+
// did nothing.
|
|
2453
|
+
if (canScroll && this.scrollOffset > 0 && this.unseenWhileScrolled > 0) {
|
|
2446
2454
|
this.screen.write(width - rightText.length, y, rightText, PRIMARY_COLOR);
|
|
2447
2455
|
return;
|
|
2448
2456
|
}
|
|
@@ -9,6 +9,9 @@ import { chat } from '../api/index.js';
|
|
|
9
9
|
import { runAgent } from '../utils/agent.js';
|
|
10
10
|
import { TelegramApproval, outcomeForAnswer, describePermissionOutcome } from '../utils/telegramApproval.js';
|
|
11
11
|
import { loadTelegramCredentials } from '../utils/telegramCredentials.js';
|
|
12
|
+
import { composeRunMessages, sendTelegramNotice, shouldNotify } from '../utils/telegramNotify.js';
|
|
13
|
+
import { takeRunFromPhone } from '../utils/telegramInbox.js';
|
|
14
|
+
import { isFlatFeeProvider } from '../config/providers.js';
|
|
12
15
|
import { raceApproval } from '../utils/approvalRace.js';
|
|
13
16
|
import { describeAuditTarget } from '../utils/auditLog.js';
|
|
14
17
|
import { config, autoSaveSession, getCurrentSessionId } from '../config/index.js';
|
|
@@ -167,6 +170,12 @@ export async function executeAgentTask(task, dryRun, ctx) {
|
|
|
167
170
|
const telegramCredentials = confirmationMode === 'dangerous'
|
|
168
171
|
? await loadTelegramCredentials()
|
|
169
172
|
: null;
|
|
173
|
+
// The finish notice does not depend on the confirmation mode — a run with
|
|
174
|
+
// confirmations off is exactly the one you are most likely to walk away
|
|
175
|
+
// from. Reuse the credentials already read above when there are any, so
|
|
176
|
+
// this costs a second keychain round-trip only when there are not.
|
|
177
|
+
const noticeCredentials = telegramCredentials ?? await loadTelegramCredentials();
|
|
178
|
+
const runStartedAt = Date.now();
|
|
170
179
|
const onRequestPermission = confirmationMode === 'dangerous'
|
|
171
180
|
? async (toolCall) => {
|
|
172
181
|
// `parameters.command` is the binary alone — `git`, not `git status`.
|
|
@@ -411,12 +420,31 @@ export async function executeAgentTask(task, dryRun, ctx) {
|
|
|
411
420
|
// Report stats to codeep.dev (fire-and-forget, only if github_id is set)
|
|
412
421
|
const { getCurrentVersion } = await import('../utils/update.js');
|
|
413
422
|
const sessionId = getCurrentSessionId();
|
|
414
|
-
// Auto-name from task if no display name set yet
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
423
|
+
// Auto-name from the task if no display name is set yet.
|
|
424
|
+
//
|
|
425
|
+
// The derived name is kept in a local rather than read back off ctx.
|
|
426
|
+
// makeCtx() copies sessionDisplayName by value, so setSessionDisplayName
|
|
427
|
+
// updates the module's variable while this object keeps the undefined it
|
|
428
|
+
// was built with — and reading it one line after calling the setter always
|
|
429
|
+
// returned nothing. Everything downstream then fell back to the session id,
|
|
430
|
+
// so a run reported itself to the dashboard, and announced itself on
|
|
431
|
+
// Telegram, as "session-2026-09-02-ddc1f13c" instead of its task.
|
|
432
|
+
const shortLabel = (text) => {
|
|
433
|
+
const words = text.replace(/\s+/g, ' ').trim().split(' ').slice(0, 5).join(' ');
|
|
434
|
+
return words.length > 48 ? words.slice(0, 45) + '…' : words;
|
|
435
|
+
};
|
|
436
|
+
// What THIS run was asked to do. The session name below is the first task's
|
|
437
|
+
// and stays put, which is right for a session and wrong for one run inside
|
|
438
|
+
// it: the second task in a session would otherwise announce itself on
|
|
439
|
+
// Telegram under the first one's name.
|
|
440
|
+
const runLabel = shortLabel(task) || sessionId;
|
|
441
|
+
let displayName = ctx.sessionDisplayName;
|
|
442
|
+
if (!displayName) {
|
|
443
|
+
displayName = shortLabel(task);
|
|
444
|
+
ctx.setSessionDisplayName?.(displayName);
|
|
418
445
|
}
|
|
419
|
-
|
|
446
|
+
if (!displayName)
|
|
447
|
+
displayName = sessionId;
|
|
420
448
|
syncSession({
|
|
421
449
|
sessionId,
|
|
422
450
|
sessionName: displayName,
|
|
@@ -428,6 +456,35 @@ export async function executeAgentTask(task, dryRun, ctx) {
|
|
|
428
456
|
// even if the user switched model mid-session. Only this run's delta
|
|
429
457
|
// (since tokenReportStart) is reported; the cumulative store is preserved.
|
|
430
458
|
const costBreakdown = getCostBreakdown(tokenReportStart);
|
|
459
|
+
// Told once the run is over, and only when it ran long enough that you
|
|
460
|
+
// could plausibly have stopped watching. Awaited so the process does not
|
|
461
|
+
// exit from under the request, but never allowed to fail the run.
|
|
462
|
+
if (noticeCredentials) {
|
|
463
|
+
const elapsedMs = Date.now() - runStartedAt;
|
|
464
|
+
// Consumed once per run either way, so a phone-started run cannot leave
|
|
465
|
+
// the flag set for whatever the terminal does next.
|
|
466
|
+
const fromPhone = takeRunFromPhone();
|
|
467
|
+
// The one-minute threshold exists so a phone is not buzzed about work you
|
|
468
|
+
// watched finish. It has no business gating a run the phone itself asked
|
|
469
|
+
// for: that answer was wanted whether it took ten seconds or ten minutes,
|
|
470
|
+
// and withholding it leaves "Started —" as the last word.
|
|
471
|
+
if (fromPhone || shouldNotify(elapsedMs, true)) {
|
|
472
|
+
const payPerUse = costBreakdown.filter(entry => !isFlatFeeProvider(entry.provider));
|
|
473
|
+
// Usually one message. An answer past Telegram's limit continues into
|
|
474
|
+
// further ones rather than being cut at the first — awaited in turn so
|
|
475
|
+
// they arrive in the order they were written.
|
|
476
|
+
const messages = composeRunMessages({
|
|
477
|
+
task: runLabel,
|
|
478
|
+
elapsedMs,
|
|
479
|
+
answer: fromPhone ? result.finalResponse : undefined,
|
|
480
|
+
tokens: costBreakdown.reduce((sum, e) => sum + e.promptTokens + e.completionTokens, 0),
|
|
481
|
+
costUsd: payPerUse.reduce((sum, e) => sum + e.estimatedCost, 0),
|
|
482
|
+
});
|
|
483
|
+
for (const message of messages) {
|
|
484
|
+
await sendTelegramNotice(noticeCredentials, message).catch(() => false);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
431
488
|
const sharedFields = {
|
|
432
489
|
sessionId,
|
|
433
490
|
sessionName: displayName,
|
|
@@ -136,6 +136,17 @@ export const SETTINGS = [
|
|
|
136
136
|
{ value: false, label: 'OFF' },
|
|
137
137
|
],
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
key: 'telegramInbox',
|
|
141
|
+
label: 'Start tasks from Telegram',
|
|
142
|
+
getValue: () => config.get('telegramInbox') === true,
|
|
143
|
+
type: 'select',
|
|
144
|
+
// Booleans, for the same reason as the row above.
|
|
145
|
+
options: [
|
|
146
|
+
{ value: true, label: 'ON' },
|
|
147
|
+
{ value: false, label: 'OFF' },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
139
150
|
{
|
|
140
151
|
key: 'telegramChatId',
|
|
141
152
|
label: 'Telegram chat ID',
|
package/dist/renderer/index.d.ts
CHANGED
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
* with diff-based rendering for flicker-free updates.
|
|
7
7
|
*/
|
|
8
8
|
export { cursor, screen, fg, bg, style, styled, stripAnsi, visibleLength, truncate, wordWrap } from './ansi';
|
|
9
|
-
export { Screen
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
9
|
+
export { Screen } from './Screen';
|
|
10
|
+
export type { Cell } from './Screen';
|
|
11
|
+
export { Input, LineEditor } from './Input';
|
|
12
|
+
export type { KeyEvent, KeyHandler } from './Input';
|
|
13
|
+
export { App } from './App';
|
|
14
|
+
export type { AppOptions, Message } from './App';
|
|
15
|
+
export { createBox, centerBox } from './components/Box';
|
|
16
|
+
export type { BoxStyle, BoxOptions } from './components/Box';
|
|
17
|
+
export { renderModal, renderHelpModal, renderListModal } from './components/Modal';
|
|
18
|
+
export type { ModalOptions } from './components/Modal';
|
|
14
19
|
export { helpCategories, keyboardShortcuts } from './components/Help';
|
|
15
|
-
export { renderStatusScreen
|
|
20
|
+
export { renderStatusScreen } from './components/Status';
|
|
21
|
+
export type { StatusInfo } from './components/Status';
|
|
@@ -145,6 +145,13 @@ export declare function statusBarRightHint(args: {
|
|
|
145
145
|
unseenWhileScrolled: number;
|
|
146
146
|
isStreaming: boolean;
|
|
147
147
|
isLoading: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* False while something else owns the whole screen — the agent timeline
|
|
150
|
+
* takes it over and returns before the transcript is drawn at all, so
|
|
151
|
+
* scrolling changes an offset nothing reads. Offering PgDn there asks for a
|
|
152
|
+
* keypress that does nothing and leaves the reader hunting for the key.
|
|
153
|
+
*/
|
|
154
|
+
canScroll: boolean;
|
|
148
155
|
}): string;
|
|
149
156
|
/** The panel that currently owns keyboard focus, in priority order. */
|
|
150
157
|
export type ActivePanel = 'pasteInfo' | 'permission' | 'sessionPicker' | 'confirm' | 'status' | 'help' | 'settings' | 'search' | 'export' | 'logout' | 'login' | 'menu' | 'autocomplete' | 'hunkPicker' | 'chat';
|
package/dist/renderer/layout.js
CHANGED
|
@@ -213,7 +213,7 @@ export function formatTokenCount(tokens) {
|
|
|
213
213
|
* scrolled up — otherwise the hint depends on whether work is in flight.
|
|
214
214
|
*/
|
|
215
215
|
export function statusBarRightHint(args) {
|
|
216
|
-
if (args.scrollOffset > 0 && args.unseenWhileScrolled > 0) {
|
|
216
|
+
if (args.canScroll && args.scrollOffset > 0 && args.unseenWhileScrolled > 0) {
|
|
217
217
|
return `↓ ${args.unseenWhileScrolled} new · PgDn `;
|
|
218
218
|
}
|
|
219
219
|
return args.isStreaming || args.isLoading ? 'Esc to stop ' : '/help · ↑↓ history ';
|
package/dist/renderer/main.js
CHANGED
|
@@ -22,6 +22,10 @@ import { expandFileAndFolderMentions, expandGitMentions } from '../utils/mention
|
|
|
22
22
|
import { expandWebMentions } from '../utils/webFetch.js';
|
|
23
23
|
import { handleCommand as dispatchCommand } from './commands.js';
|
|
24
24
|
import { logAppError } from '../utils/logger.js';
|
|
25
|
+
import { loadTelegramInboxCredentials } from '../utils/telegramCredentials.js';
|
|
26
|
+
import { attachTelegramInbox } from '../utils/telegramInbox.js';
|
|
27
|
+
import { sharedUpdates } from '../utils/telegramUpdates.js';
|
|
28
|
+
import { sendTelegramNotice } from '../utils/telegramNotify.js';
|
|
25
29
|
import { executeAgentTask, runAgentTask, } from './agentExecution.js';
|
|
26
30
|
// ─── Global state ─────────────────────────────────────────────────────────────
|
|
27
31
|
let projectPath = process.cwd();
|
|
@@ -50,6 +54,8 @@ export function deriveSessionName(message) {
|
|
|
50
54
|
return words.length > 48 ? words.slice(0, 45) + '…' : words;
|
|
51
55
|
}
|
|
52
56
|
let isAgentRunningFlag = false;
|
|
57
|
+
/** Set once the phone is allowed to send instructions; null when it is not. */
|
|
58
|
+
let telegramInbox = null;
|
|
53
59
|
let agentAbortController = null;
|
|
54
60
|
let pendingInteractiveContext = null;
|
|
55
61
|
// ─── Context factory ──────────────────────────────────────────────────────────
|
|
@@ -66,8 +72,14 @@ function makeCtx() {
|
|
|
66
72
|
isAgentRunning: () => isAgentRunningFlag,
|
|
67
73
|
// A finished run may have switched branches — drop the cache so the next
|
|
68
74
|
// header render re-reads it.
|
|
69
|
-
setAgentRunning: (v) => {
|
|
70
|
-
|
|
75
|
+
setAgentRunning: (v) => {
|
|
76
|
+
isAgentRunningFlag = v;
|
|
77
|
+
if (!v) {
|
|
78
|
+
gitBranchCache = null;
|
|
79
|
+
// A run just ended: hand over anything the phone sent while it was busy.
|
|
80
|
+
telegramInbox?.drain();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
71
83
|
setAbortController: (ctrl) => { agentAbortController = ctrl; },
|
|
72
84
|
formatAddedFilesContext,
|
|
73
85
|
handleCommand: (cmd, args) => dispatchCommand(cmd, args, makeCtx()),
|
|
@@ -749,6 +761,38 @@ Commands (in chat):
|
|
|
749
761
|
welcomeLines.push(' /help · Ctrl+L clear · Esc cancel');
|
|
750
762
|
app.addMessage({ role: 'welcome', content: welcomeLines.join('\n') });
|
|
751
763
|
app.start();
|
|
764
|
+
// Let the phone send instructions, if it has been switched on. Started after
|
|
765
|
+
// app.start() so a prompt that arrives immediately has somewhere to land.
|
|
766
|
+
void (async () => {
|
|
767
|
+
const credentials = await loadTelegramInboxCredentials();
|
|
768
|
+
if (!credentials)
|
|
769
|
+
return;
|
|
770
|
+
telegramInbox = attachTelegramInbox(credentials, {
|
|
771
|
+
isBusy: () => isAgentRunningFlag,
|
|
772
|
+
submit: (text) => {
|
|
773
|
+
// Through the same door the input box uses, and shown in the transcript
|
|
774
|
+
// the same way — a run started from the phone must not be invisible to
|
|
775
|
+
// whoever is sitting at the terminal.
|
|
776
|
+
app.addMessage({ role: 'user', content: text });
|
|
777
|
+
app.notify('Telegram: running an instruction from your phone');
|
|
778
|
+
app.setLoading(true);
|
|
779
|
+
void handleSubmit(text).catch(err => {
|
|
780
|
+
app.notify(`Error: ${err.message}`);
|
|
781
|
+
app.setLoading(false);
|
|
782
|
+
});
|
|
783
|
+
},
|
|
784
|
+
reply: (text) => { void sendTelegramNotice(credentials, text); },
|
|
785
|
+
});
|
|
786
|
+
// Say it once when the poll starts failing, and once when it recovers. A
|
|
787
|
+
// webhook left on the bot, or a revoked token, otherwise looks exactly like
|
|
788
|
+
// a phone nobody has messaged.
|
|
789
|
+
sharedUpdates(credentials.botToken).observe(({ ok, detail }) => {
|
|
790
|
+
if (ok)
|
|
791
|
+
app.notify(`Telegram: ${detail}`);
|
|
792
|
+
else
|
|
793
|
+
app.notifyWarn(`Telegram: ${detail}`);
|
|
794
|
+
});
|
|
795
|
+
})();
|
|
752
796
|
// Spawn MCP servers in the background. They register against the fixed
|
|
753
797
|
// session id `codeep-tui` that runAgentTask passes into runAgent's
|
|
754
798
|
// `mcpSessionId` — so the agent picks up `.codeep/mcp_servers.json`
|
|
@@ -27,12 +27,9 @@
|
|
|
27
27
|
*/
|
|
28
28
|
/** What the phone sent back. Mirrors the three buttons the desktop offers. */
|
|
29
29
|
export type TelegramAnswer = 'run' | 'skip' | 'cancel';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/** The single chat allowed to answer. Anything else is ignored. */
|
|
34
|
-
chatID: string;
|
|
35
|
-
}
|
|
30
|
+
import { type TelegramCredentials } from './telegramUpdates';
|
|
31
|
+
export type { TelegramCredentials } from './telegramUpdates';
|
|
32
|
+
export { nextOffset } from './telegramUpdates';
|
|
36
33
|
/**
|
|
37
34
|
* The message text.
|
|
38
35
|
*
|
|
@@ -64,17 +61,6 @@ export declare function parseCallbackData(data: string): {
|
|
|
64
61
|
/** The keyboard sent with the question. Shape pinned by a test — a renamed
|
|
65
62
|
* callback_data field would leave three buttons that silently do nothing. */
|
|
66
63
|
export declare function buildKeyboard(token: string): Record<string, unknown>;
|
|
67
|
-
/**
|
|
68
|
-
* Advance the long-poll offset past everything just handled.
|
|
69
|
-
*
|
|
70
|
-
* Telegram redelivers any update that has not been acknowledged by a higher
|
|
71
|
-
* offset, so getting this wrong means the same tap arrives forever. Exported
|
|
72
|
-
* because it is off-by-one-shaped and cheaper to test than to debug against a
|
|
73
|
-
* live bot.
|
|
74
|
-
*/
|
|
75
|
-
export declare function nextOffset(current: number, updates: {
|
|
76
|
-
update_id?: unknown;
|
|
77
|
-
}[]): number;
|
|
78
64
|
/**
|
|
79
65
|
* Telegram's three buttons, in the terms the agent's gate speaks.
|
|
80
66
|
*
|
|
@@ -104,8 +90,8 @@ export declare function describeApiError(status: number, json: Record<string, un
|
|
|
104
90
|
export declare class TelegramApproval {
|
|
105
91
|
private readonly credentials;
|
|
106
92
|
private outstanding;
|
|
107
|
-
|
|
108
|
-
private
|
|
93
|
+
/** Set while a question is open; called to stop listening once it closes. */
|
|
94
|
+
private unlisten;
|
|
109
95
|
/** Called once when the question could not be put at all. Not for a missing
|
|
110
96
|
* answer — only for a failure to ask. */
|
|
111
97
|
private readonly onProblem?;
|
|
@@ -135,6 +121,5 @@ export declare class TelegramApproval {
|
|
|
135
121
|
private post;
|
|
136
122
|
private sendQuestion;
|
|
137
123
|
private edit;
|
|
138
|
-
private poll;
|
|
139
124
|
private handle;
|
|
140
125
|
}
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* one side is findable on the other.
|
|
27
27
|
*/
|
|
28
28
|
const ANSWERS = ['run', 'skip', 'cancel'];
|
|
29
|
+
import { sharedUpdates } from './telegramUpdates.js';
|
|
30
|
+
export { nextOffset } from './telegramUpdates.js';
|
|
29
31
|
/** Longest command we put in a message. Telegram caps at 4096 for the whole
|
|
30
32
|
* text; this keeps room for the heading and the fences, and a command longer
|
|
31
33
|
* than this is not something anyone reads off a phone anyway. */
|
|
@@ -99,22 +101,6 @@ export function buildKeyboard(token) {
|
|
|
99
101
|
]],
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Advance the long-poll offset past everything just handled.
|
|
104
|
-
*
|
|
105
|
-
* Telegram redelivers any update that has not been acknowledged by a higher
|
|
106
|
-
* offset, so getting this wrong means the same tap arrives forever. Exported
|
|
107
|
-
* because it is off-by-one-shaped and cheaper to test than to debug against a
|
|
108
|
-
* live bot.
|
|
109
|
-
*/
|
|
110
|
-
export function nextOffset(current, updates) {
|
|
111
|
-
let out = current;
|
|
112
|
-
for (const update of updates) {
|
|
113
|
-
if (typeof update.update_id === 'number')
|
|
114
|
-
out = Math.max(out, update.update_id + 1);
|
|
115
|
-
}
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
104
|
/**
|
|
119
105
|
* Telegram's three buttons, in the terms the agent's gate speaks.
|
|
120
106
|
*
|
|
@@ -173,15 +159,13 @@ export function describeApiError(status, json) {
|
|
|
173
159
|
}
|
|
174
160
|
// ─── The client ───────────────────────────────────────────────────────────────
|
|
175
161
|
const API = 'https://api.telegram.org';
|
|
176
|
-
/**
|
|
177
|
-
const
|
|
178
|
-
/** Local ceiling, comfortably past the server's own. */
|
|
179
|
-
const REQUEST_TIMEOUT_MS = (POLL_SECONDS + 10) * 1000;
|
|
162
|
+
/** Sending a message or editing one — no long poll lives here any more. */
|
|
163
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
180
164
|
export class TelegramApproval {
|
|
181
165
|
credentials;
|
|
182
166
|
outstanding = null;
|
|
183
|
-
|
|
184
|
-
|
|
167
|
+
/** Set while a question is open; called to stop listening once it closes. */
|
|
168
|
+
unlisten = null;
|
|
185
169
|
/** Called once when the question could not be put at all. Not for a missing
|
|
186
170
|
* answer — only for a failure to ask. */
|
|
187
171
|
onProblem;
|
|
@@ -212,7 +196,8 @@ export class TelegramApproval {
|
|
|
212
196
|
return;
|
|
213
197
|
settled = true;
|
|
214
198
|
this.outstanding = null;
|
|
215
|
-
this.
|
|
199
|
+
this.unlisten?.();
|
|
200
|
+
this.unlisten = null;
|
|
216
201
|
resolve(answer);
|
|
217
202
|
};
|
|
218
203
|
this.outstanding = { token, messageID, resolve: finish };
|
|
@@ -223,7 +208,10 @@ export class TelegramApproval {
|
|
|
223
208
|
}
|
|
224
209
|
signal.addEventListener('abort', () => finish(null), { once: true });
|
|
225
210
|
}
|
|
226
|
-
|
|
211
|
+
// Listen on the bot's one poller rather than opening a second. Two
|
|
212
|
+
// cursors on the same bot silently eat each other's updates.
|
|
213
|
+
this.unlisten = sharedUpdates(this.credentials.botToken)
|
|
214
|
+
.subscribe('callback_query', callback => this.handle(callback));
|
|
227
215
|
});
|
|
228
216
|
}
|
|
229
217
|
/**
|
|
@@ -235,7 +223,8 @@ export class TelegramApproval {
|
|
|
235
223
|
if (!pending)
|
|
236
224
|
return;
|
|
237
225
|
this.outstanding = null;
|
|
238
|
-
this.
|
|
226
|
+
this.unlisten?.();
|
|
227
|
+
this.unlisten = null;
|
|
239
228
|
pending.resolve(null);
|
|
240
229
|
await this.edit(pending.messageID, `Answered in the terminal — ${decidedInTerminal}.`);
|
|
241
230
|
}
|
|
@@ -289,32 +278,6 @@ export class TelegramApproval {
|
|
|
289
278
|
text,
|
|
290
279
|
});
|
|
291
280
|
}
|
|
292
|
-
async poll() {
|
|
293
|
-
if (this.polling)
|
|
294
|
-
return;
|
|
295
|
-
this.polling = true;
|
|
296
|
-
while (this.polling && this.outstanding) {
|
|
297
|
-
const json = await this.post('getUpdates', {
|
|
298
|
-
offset: this.updateOffset,
|
|
299
|
-
timeout: POLL_SECONDS,
|
|
300
|
-
allowed_updates: ['callback_query'],
|
|
301
|
-
});
|
|
302
|
-
if (!this.polling || !this.outstanding)
|
|
303
|
-
return;
|
|
304
|
-
const updates = Array.isArray(json?.result) ? json.result : [];
|
|
305
|
-
this.updateOffset = nextOffset(this.updateOffset, updates);
|
|
306
|
-
for (const update of updates) {
|
|
307
|
-
if (update.callback_query)
|
|
308
|
-
await this.handle(update.callback_query);
|
|
309
|
-
if (!this.outstanding)
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
// Telegram's long poll already blocks server-side; this pause only covers
|
|
313
|
-
// the error case, so a failing network cannot spin the loop.
|
|
314
|
-
if (updates.length === 0)
|
|
315
|
-
await sleep(1000);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
281
|
async handle(callback) {
|
|
319
282
|
const pending = this.outstanding;
|
|
320
283
|
if (!pending)
|
|
@@ -328,7 +291,8 @@ export class TelegramApproval {
|
|
|
328
291
|
if (!parsed || parsed.token !== pending.token)
|
|
329
292
|
return;
|
|
330
293
|
this.outstanding = null;
|
|
331
|
-
this.
|
|
294
|
+
this.unlisten?.();
|
|
295
|
+
this.unlisten = null;
|
|
332
296
|
const id = callback.id;
|
|
333
297
|
if (typeof id === 'string')
|
|
334
298
|
await this.post('answerCallbackQuery', { callback_query_id: id });
|
|
@@ -342,6 +306,3 @@ function randomToken() {
|
|
|
342
306
|
function capitalise(value) {
|
|
343
307
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
344
308
|
}
|
|
345
|
-
function sleep(ms) {
|
|
346
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
347
|
-
}
|
|
@@ -22,3 +22,11 @@ export declare function hasTelegramToken(): Promise<boolean>;
|
|
|
22
22
|
* both would otherwise stall a run waiting for an answer that was never asked.
|
|
23
23
|
*/
|
|
24
24
|
export declare function loadTelegramCredentials(): Promise<TelegramCredentials | null>;
|
|
25
|
+
/**
|
|
26
|
+
* The same pair, for the inbox, behind its own switch.
|
|
27
|
+
*
|
|
28
|
+
* Not folded into the call above: someone can want the phone to be told a run
|
|
29
|
+
* finished without wanting the phone to be able to start one, and the reverse.
|
|
30
|
+
* One flag serving both would make turning either off turn both off.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadTelegramInboxCredentials(): Promise<TelegramCredentials | null>;
|
|
@@ -43,6 +43,21 @@ export async function loadTelegramCredentials() {
|
|
|
43
43
|
// would be live while the settings row read OFF.
|
|
44
44
|
if (config.get('telegramApproval') !== true)
|
|
45
45
|
return null;
|
|
46
|
+
return readCredentials();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The same pair, for the inbox, behind its own switch.
|
|
50
|
+
*
|
|
51
|
+
* Not folded into the call above: someone can want the phone to be told a run
|
|
52
|
+
* finished without wanting the phone to be able to start one, and the reverse.
|
|
53
|
+
* One flag serving both would make turning either off turn both off.
|
|
54
|
+
*/
|
|
55
|
+
export async function loadTelegramInboxCredentials() {
|
|
56
|
+
if (config.get('telegramInbox') !== true)
|
|
57
|
+
return null;
|
|
58
|
+
return readCredentials();
|
|
59
|
+
}
|
|
60
|
+
async function readCredentials() {
|
|
46
61
|
const chatID = String(config.get('telegramChatId') || '').trim();
|
|
47
62
|
if (!chatID)
|
|
48
63
|
return null;
|