codeep 2.25.0 → 3.1.0
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 +14 -0
- package/dist/config/index.js +3 -0
- package/dist/config/providers.js +4 -2
- package/dist/renderer/App.d.ts +31 -1
- package/dist/renderer/App.js +60 -5
- package/dist/renderer/agentExecution.js +120 -25
- package/dist/renderer/commands/registry.js +1 -0
- package/dist/renderer/commands.js +34 -0
- package/dist/renderer/components/Settings.js +31 -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/approvalRace.d.ts +33 -0
- package/dist/utils/approvalRace.js +58 -0
- package/dist/utils/telegramApproval.d.ts +125 -0
- package/dist/utils/telegramApproval.js +308 -0
- package/dist/utils/telegramCredentials.d.ts +32 -0
- package/dist/utils/telegramCredentials.js +68 -0
- package/dist/utils/telegramInbox.d.ts +93 -0
- package/dist/utils/telegramInbox.js +188 -0
- package/dist/utils/telegramNotify.d.ts +76 -0
- package/dist/utils/telegramNotify.js +132 -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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One long poll, one offset, both kinds of update.
|
|
3
|
+
*
|
|
4
|
+
* Telegram's getUpdates has a single cursor per bot. `offset` confirms every
|
|
5
|
+
* update older than itself **regardless of `allowed_updates`** — that parameter
|
|
6
|
+
* only filters what comes back in the response, not what the call acknowledges.
|
|
7
|
+
* So two pollers on one bot do not coexist: each advances the cursor past
|
|
8
|
+
* updates the other never saw, and both start losing traffic silently. An
|
|
9
|
+
* approval tapped on the phone would simply not register, with nothing anywhere
|
|
10
|
+
* to say why.
|
|
11
|
+
*
|
|
12
|
+
* Hence this. Everything that wants updates subscribes here, the loop asks for
|
|
13
|
+
* every type any subscriber could want, and dispatch happens locally where
|
|
14
|
+
* losing one is impossible.
|
|
15
|
+
*/
|
|
16
|
+
const API = 'https://api.telegram.org';
|
|
17
|
+
/** Server-side long-poll window. The request blocks for up to this long. */
|
|
18
|
+
const POLL_SECONDS = 25;
|
|
19
|
+
/** Local ceiling, comfortably past the server's own. */
|
|
20
|
+
const REQUEST_TIMEOUT_MS = (POLL_SECONDS + 10) * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Pause after a poll that brought nothing back.
|
|
23
|
+
*
|
|
24
|
+
* Telegram's long poll already blocks server-side for POLL_SECONDS, so in
|
|
25
|
+
* normal running this never fires. It exists for the case where the request
|
|
26
|
+
* returns immediately — a network failure, or a server that ignored the
|
|
27
|
+
* timeout — which without it turns this into a loop that hammers the API as
|
|
28
|
+
* fast as the connection allows.
|
|
29
|
+
*/
|
|
30
|
+
const IDLE_PAUSE_MS = 1000;
|
|
31
|
+
/**
|
|
32
|
+
* Where the cursor goes after a batch.
|
|
33
|
+
*
|
|
34
|
+
* Never backwards: a retry that returns an older batch, or a response with ids
|
|
35
|
+
* this build does not understand, must not re-deliver what was already handled.
|
|
36
|
+
*/
|
|
37
|
+
export function nextOffset(current, updates) {
|
|
38
|
+
let out = current;
|
|
39
|
+
for (const update of updates) {
|
|
40
|
+
if (typeof update.update_id === 'number')
|
|
41
|
+
out = Math.max(out, update.update_id + 1);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** Every kind this loop asks for, so one cursor can serve every subscriber. */
|
|
46
|
+
export const POLLED_KINDS = ['callback_query', 'message'];
|
|
47
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
48
|
+
export class TelegramUpdates {
|
|
49
|
+
botToken;
|
|
50
|
+
handlers = new Map();
|
|
51
|
+
offset = 0;
|
|
52
|
+
running = false;
|
|
53
|
+
idlePauseMs;
|
|
54
|
+
observer = null;
|
|
55
|
+
/** Only the first failure of a streak is reported, then the recovery. */
|
|
56
|
+
failing = false;
|
|
57
|
+
constructor(botToken, idlePauseMs = IDLE_PAUSE_MS) {
|
|
58
|
+
this.botToken = botToken;
|
|
59
|
+
this.idlePauseMs = idlePauseMs;
|
|
60
|
+
}
|
|
61
|
+
/** Watch the health of the poll itself, separately from its payload. */
|
|
62
|
+
observe(observer) {
|
|
63
|
+
this.observer = observer;
|
|
64
|
+
}
|
|
65
|
+
report(ok, detail) {
|
|
66
|
+
if (ok === !this.failing)
|
|
67
|
+
return; // nothing changed; stay quiet
|
|
68
|
+
this.failing = !ok;
|
|
69
|
+
this.observer?.({ ok, detail });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Listen for one kind of update. Returns the function that stops listening.
|
|
73
|
+
*
|
|
74
|
+
* The loop runs while anyone is listening and stops when the last subscriber
|
|
75
|
+
* leaves, so a CLI with the inbox switched off never opens a connection.
|
|
76
|
+
*/
|
|
77
|
+
subscribe(kind, handler) {
|
|
78
|
+
let set = this.handlers.get(kind);
|
|
79
|
+
if (!set) {
|
|
80
|
+
set = new Set();
|
|
81
|
+
this.handlers.set(kind, set);
|
|
82
|
+
}
|
|
83
|
+
set.add(handler);
|
|
84
|
+
if (!this.running)
|
|
85
|
+
void this.loop();
|
|
86
|
+
return () => {
|
|
87
|
+
set.delete(handler);
|
|
88
|
+
if (this.subscriberCount() === 0)
|
|
89
|
+
this.running = false;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
subscriberCount() {
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const set of this.handlers.values())
|
|
95
|
+
total += set.size;
|
|
96
|
+
return total;
|
|
97
|
+
}
|
|
98
|
+
async loop() {
|
|
99
|
+
if (this.running)
|
|
100
|
+
return;
|
|
101
|
+
this.running = true;
|
|
102
|
+
while (this.running && this.subscriberCount() > 0) {
|
|
103
|
+
const json = await this.getUpdates();
|
|
104
|
+
if (!this.running)
|
|
105
|
+
break;
|
|
106
|
+
const updates = Array.isArray(json?.result) ? json.result : [];
|
|
107
|
+
// Advance before dispatching. A handler that throws must not make the
|
|
108
|
+
// loop re-read the same update forever.
|
|
109
|
+
this.offset = nextOffset(this.offset, updates);
|
|
110
|
+
for (const update of updates) {
|
|
111
|
+
for (const kind of POLLED_KINDS) {
|
|
112
|
+
const payload = update[kind];
|
|
113
|
+
if (payload === undefined)
|
|
114
|
+
continue;
|
|
115
|
+
for (const handler of this.handlers.get(kind) ?? []) {
|
|
116
|
+
try {
|
|
117
|
+
await handler(payload);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// One subscriber's failure is not the others' problem, and is
|
|
121
|
+
// certainly not a reason to stop reading the bot.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Covers an empty batch as well as a failed request: both mean the loop
|
|
127
|
+
// would otherwise come straight back with nothing to do.
|
|
128
|
+
if (updates.length === 0)
|
|
129
|
+
await sleep(this.idlePauseMs);
|
|
130
|
+
}
|
|
131
|
+
this.running = false;
|
|
132
|
+
}
|
|
133
|
+
async getUpdates() {
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(`${API}/bot${this.botToken}/getUpdates`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
offset: this.offset,
|
|
140
|
+
timeout: POLL_SECONDS,
|
|
141
|
+
allowed_updates: POLLED_KINDS,
|
|
142
|
+
}),
|
|
143
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
144
|
+
});
|
|
145
|
+
const json = await response.json().catch(() => null);
|
|
146
|
+
if (!response.ok || json?.ok === false) {
|
|
147
|
+
const description = typeof json?.description === 'string' ? json.description : `HTTP ${response.status}`;
|
|
148
|
+
this.report(false, description);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
this.report(true, 'reading updates again');
|
|
152
|
+
return json;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.report(false, error?.message || 'could not reach Telegram');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The one poller per bot token.
|
|
162
|
+
*
|
|
163
|
+
* Approvals are constructed per dangerous tool call and the inbox lives for the
|
|
164
|
+
* whole session; both must reach the same cursor, so the instance is keyed by
|
|
165
|
+
* token rather than owned by either.
|
|
166
|
+
*/
|
|
167
|
+
const shared = new Map();
|
|
168
|
+
export function sharedUpdates(botToken) {
|
|
169
|
+
let instance = shared.get(botToken);
|
|
170
|
+
if (!instance) {
|
|
171
|
+
instance = new TelegramUpdates(botToken);
|
|
172
|
+
shared.set(botToken, instance);
|
|
173
|
+
}
|
|
174
|
+
return instance;
|
|
175
|
+
}
|
|
@@ -22,6 +22,7 @@ const MODEL_CONTEXT_WINDOWS = {
|
|
|
22
22
|
'gpt-5.4': 1_050_000,
|
|
23
23
|
'gpt-5.4-mini': 400_000,
|
|
24
24
|
// Anthropic
|
|
25
|
+
'claude-fable-5-1': 1_000_000,
|
|
25
26
|
'claude-fable-5': 1_000_000,
|
|
26
27
|
'claude-opus-5': 1_000_000,
|
|
27
28
|
'claude-sonnet-4-6': 1_000_000,
|
|
@@ -97,10 +98,11 @@ const MODEL_PRICING = {
|
|
|
97
98
|
'gpt-5.4': { inputPer1M: 2.50, outputPer1M: 15.00 },
|
|
98
99
|
'gpt-5.4-mini': { inputPer1M: 0.75, outputPer1M: 4.50 },
|
|
99
100
|
// Anthropic
|
|
101
|
+
'claude-fable-5-1': { inputPer1M: 10.00, outputPer1M: 50.00 },
|
|
100
102
|
'claude-fable-5': { inputPer1M: 10.00, outputPer1M: 50.00 },
|
|
101
103
|
'claude-opus-5': { inputPer1M: 5.00, outputPer1M: 25.00 },
|
|
102
104
|
'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00 },
|
|
103
|
-
'claude-sonnet-5': { inputPer1M:
|
|
105
|
+
'claude-sonnet-5': { inputPer1M: 2.00, outputPer1M: 10.00 },
|
|
104
106
|
'claude-haiku-4-5-20251001': { inputPer1M: 1.00, outputPer1M: 5.00 },
|
|
105
107
|
// DeepSeek (cache-miss input pricing)
|
|
106
108
|
// DeepSeek moved to peak / off-peak billing on 2026-08-16, with off-peak at
|
|
@@ -206,11 +208,18 @@ export function recordTokenUsage(usage, model, provider, actualCostUsd) {
|
|
|
206
208
|
*/
|
|
207
209
|
export function extractOpenAIUsage(data) {
|
|
208
210
|
if (data?.usage) {
|
|
209
|
-
// OpenAI-protocol `prompt_tokens` is INCLUSIVE of cached prompt tokens
|
|
210
|
-
// (DeepSeek/OpenAI report cache hits in prompt_tokens_details.cached_tokens).
|
|
211
|
+
// OpenAI-protocol `prompt_tokens` is INCLUSIVE of cached prompt tokens.
|
|
211
212
|
// Surface them so getCostBreakdown bills cache reads at the discounted
|
|
212
213
|
// rate instead of the full cache-miss input rate.
|
|
213
|
-
|
|
214
|
+
//
|
|
215
|
+
// Two shapes are in the wild. OpenAI, DeepSeek and Qwen's text models nest
|
|
216
|
+
// it under prompt_tokens_details; Kimi returns it at the top level, and
|
|
217
|
+
// Alibaba's own docs say some Qwen models still do and will be migrated
|
|
218
|
+
// later. Reading only the nested form zeroed every Kimi cache hit, so the
|
|
219
|
+
// cached portion of a run billed at the full cache-miss rate — five times
|
|
220
|
+
// what it costs — with nothing anywhere to say so.
|
|
221
|
+
const nested = data.usage.prompt_tokens_details?.cached_tokens;
|
|
222
|
+
const cached = (typeof nested === 'number' ? nested : data.usage.cached_tokens) || 0;
|
|
214
223
|
return {
|
|
215
224
|
promptTokens: data.usage.prompt_tokens || 0,
|
|
216
225
|
completionTokens: data.usage.completion_tokens || 0,
|
|
@@ -242,6 +251,36 @@ export function extractAnthropicUsage(data) {
|
|
|
242
251
|
}
|
|
243
252
|
return null;
|
|
244
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* What a provider charges to read a cached token, as a fraction of its own
|
|
256
|
+
* cache-miss input rate.
|
|
257
|
+
*
|
|
258
|
+
* 0.1 is Anthropic's ratio and used to be applied to everyone. Kimi lists
|
|
259
|
+
* $0.19 against a $0.95 cache-miss rate, and Alibaba prices Qwen's implicit
|
|
260
|
+
* cache at 20% of input — both 0.2, so every cached token on those two was
|
|
261
|
+
* billed at half what it actually costs.
|
|
262
|
+
*/
|
|
263
|
+
/**
|
|
264
|
+
* Models whose cache-read rate is not their provider's usual one.
|
|
265
|
+
*
|
|
266
|
+
* Fable 5.1 reads a cached token at 0.025× the base input price, where every
|
|
267
|
+
* other Anthropic model charges 0.1×. Checked before the provider map, because
|
|
268
|
+
* this is a property of the model and not of the account it runs under.
|
|
269
|
+
*/
|
|
270
|
+
const MODEL_CACHE_READ_RATE = {
|
|
271
|
+
'claude-fable-5-1': 0.025,
|
|
272
|
+
};
|
|
273
|
+
const CACHE_READ_RATE = {
|
|
274
|
+
'kimi': 0.2,
|
|
275
|
+
'kimi-api': 0.2,
|
|
276
|
+
'qwen': 0.2,
|
|
277
|
+
'qwen-api': 0.2,
|
|
278
|
+
'qwen-cn': 0.2,
|
|
279
|
+
'qwen-cn-api': 0.2,
|
|
280
|
+
'qwen-token-plan': 0.2,
|
|
281
|
+
};
|
|
282
|
+
/** Anthropic's ratio, and the safest guess for a provider we have not priced. */
|
|
283
|
+
const DEFAULT_CACHE_READ_RATE = 0.1;
|
|
245
284
|
/**
|
|
246
285
|
* Get cost breakdown grouped by provider/model.
|
|
247
286
|
*
|
|
@@ -269,16 +308,20 @@ export function getCostBreakdown(startIndex = 0) {
|
|
|
269
308
|
else {
|
|
270
309
|
const pricing = MODEL_PRICING[record.model];
|
|
271
310
|
if (pricing) {
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
311
|
+
// Cache writes bill at 1.25× the base input rate (Anthropic's, and the
|
|
312
|
+
// only provider here that charges for them at all); cache reads bill
|
|
313
|
+
// at whatever fraction the provider charges. The remaining (uncached)
|
|
314
|
+
// prompt tokens bill at the standard 1.0× rate.
|
|
275
315
|
const cacheCreate = record.cacheCreationTokens ?? 0;
|
|
276
316
|
const cacheRead = record.cacheReadTokens ?? 0;
|
|
317
|
+
const cacheReadRate = MODEL_CACHE_READ_RATE[record.model]
|
|
318
|
+
?? CACHE_READ_RATE[record.provider?.trim().toLowerCase()]
|
|
319
|
+
?? DEFAULT_CACHE_READ_RATE;
|
|
277
320
|
const uncachedPrompt = Math.max(0, record.promptTokens - cacheCreate - cacheRead);
|
|
278
321
|
existing.estimatedCost +=
|
|
279
322
|
(uncachedPrompt / 1_000_000) * pricing.inputPer1M
|
|
280
323
|
+ (cacheCreate / 1_000_000) * pricing.inputPer1M * 1.25
|
|
281
|
-
+ (cacheRead / 1_000_000) * pricing.inputPer1M *
|
|
324
|
+
+ (cacheRead / 1_000_000) * pricing.inputPer1M * cacheReadRate
|
|
282
325
|
+ (record.completionTokens / 1_000_000) * pricing.outputPer1M;
|
|
283
326
|
}
|
|
284
327
|
}
|
package/dist/utils/tools.js
CHANGED
|
@@ -57,7 +57,12 @@ export const AGENT_TOOLS = {
|
|
|
57
57
|
},
|
|
58
58
|
execute_command: {
|
|
59
59
|
name: 'execute_command',
|
|
60
|
-
|
|
60
|
+
// Naming a few examples read as an exhaustive list: the agent refused
|
|
61
|
+
// `sleep`, which IS on the allowlist, explaining that its tool was
|
|
62
|
+
// "limited to package managers and version control". It believed the
|
|
63
|
+
// description over its own capability. Say the shape instead, and let
|
|
64
|
+
// the runtime refuse — it already answers with a specific reason.
|
|
65
|
+
description: 'Execute a shell command. A safety allowlist applies and covers far more than build tooling — package managers, git, test runners, and ordinary utilities such as ls, cat, grep, find, echo, date, sleep and curl. Do not assume a command is forbidden: try it, and a refusal will say so.',
|
|
61
66
|
parameters: {
|
|
62
67
|
command: { type: 'string', description: 'The command to run (e.g., npm, git, node)', required: true },
|
|
63
68
|
args: { type: 'array', description: 'Command arguments as array (e.g., ["install", "lodash"])', required: false },
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "
|
|
1
|
+
export declare const VERSION = "3.1.0";
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
|
|
2
2
|
// Baked from package.json at build time so the bun-compiled binary reports
|
|
3
3
|
// the right version (it has no package.json on disk to read at runtime).
|
|
4
|
-
export const VERSION = '
|
|
4
|
+
export const VERSION = '3.1.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|