pi-telegram-notifier 0.1.2 → 0.1.3
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
CHANGED
package/extensions/index.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
5
|
-
import { pairChat, saveChatId } from '
|
|
6
|
-
import { registerNotifications } from '
|
|
5
|
+
import { pairChat, saveChatId } from '#src/telegram';
|
|
6
|
+
import { registerNotifications } from '#src/notification';
|
|
7
7
|
|
|
8
8
|
const agentDir = () => process.env.PI_CODING_AGENT_DIR || join(homedir(), '.pi', 'agent');
|
|
9
9
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-telegram-notifier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Telegram notifications for final Pi agent responses and provider balances",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": { "node": ">=22.19.0" },
|
|
8
8
|
"keywords": ["pi-package", "pi", "telegram", "notifications"],
|
|
9
9
|
"files": ["extensions", "src", "README.md", "LICENSE", "docs/providers.md"],
|
|
10
|
-
"
|
|
10
|
+
"imports": { "#src/*": "./src/*.ts" },
|
|
11
|
+
"scripts": { "test": "node --test", "typecheck": "tsc --noEmit" },
|
|
11
12
|
"pi": { "extensions": ["./extensions/index.ts"] },
|
|
12
13
|
"peerDependencies": { "@earendil-works/pi-coding-agent": "*" },
|
|
14
|
+
"devDependencies": { "@types/node": "22.20.4", "typescript": "5.9.3" },
|
|
13
15
|
"repository": { "type": "git", "url": "git+https://github.com/Gybra/pi-telegram-notifier.git" }
|
|
14
16
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { hostname } from 'node:os';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { ExtensionAPI, SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { loadChatId, sendTelegram } from '#src/telegram';
|
|
4
|
+
import { getQuota } from '#src/quota';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type FinalResponse = { text: string; provider: string; modelId: string };
|
|
7
|
+
|
|
8
|
+
export function finalResponse(branch: SessionEntry[], startLeafId: string | null): FinalResponse | undefined {
|
|
6
9
|
const index = startLeafId === null ? -1 : branch.findIndex((entry) => entry.id === startLeafId);
|
|
7
10
|
if (index < 0 && startLeafId !== null) return undefined;
|
|
8
11
|
const latest = branch.slice(index + 1).filter((entry) => entry.type === 'message').at(-1)?.message;
|
|
@@ -12,12 +15,12 @@ export function finalResponse(branch, startLeafId) {
|
|
|
12
15
|
return { text, provider: latest.provider, modelId: latest.model };
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
export function formatNotification(timestamp, response, quota) {
|
|
18
|
+
export function formatNotification(timestamp: string, response: FinalResponse, quota: string): string {
|
|
16
19
|
return `${timestamp}\nDevice: ${hostname()}\nProvider: ${response.provider}\n${quota}\n\n${response.text}`;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
export function registerNotifications(pi, agentDir, token, fetchImpl = fetch) {
|
|
20
|
-
let startLeafId;
|
|
22
|
+
export function registerNotifications(pi: ExtensionAPI, agentDir: string, token: string | undefined, fetchImpl: typeof fetch = fetch): void {
|
|
23
|
+
let startLeafId: string | null | undefined;
|
|
21
24
|
pi.on('agent_start', (_event, ctx) => {
|
|
22
25
|
startLeafId = ctx.sessionManager.getLeafId();
|
|
23
26
|
});
|
|
@@ -27,7 +30,7 @@ export function registerNotifications(pi, agentDir, token, fetchImpl = fetch) {
|
|
|
27
30
|
startLeafId = undefined;
|
|
28
31
|
const chatId = loadChatId(agentDir);
|
|
29
32
|
if (!chatId || !response) return;
|
|
30
|
-
let quota;
|
|
33
|
+
let quota: string;
|
|
31
34
|
try {
|
|
32
35
|
const model = ctx.modelRegistry?.find(response.provider, response.modelId);
|
|
33
36
|
quota = await getQuota(model, ctx.modelRegistry, fetchImpl);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
|
|
1
3
|
const UNAVAILABLE = 'Quota unavailable';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
type Model = NonNullable<ReturnType<ModelRegistry['find']>>;
|
|
6
|
+
|
|
7
|
+
const providers: Record<string, { origin: string; url: string }> = {
|
|
4
8
|
openrouter: { origin: 'https://openrouter.ai', url: 'https://openrouter.ai/api/v1/credits' },
|
|
5
9
|
'openai-codex': { origin: 'https://chatgpt.com', url: 'https://chatgpt.com/backend-api/wham/usage' },
|
|
6
10
|
deepseek: { origin: 'https://api.deepseek.com', url: 'https://api.deepseek.com/user/balance' },
|
|
@@ -12,19 +16,19 @@ const providers = {
|
|
|
12
16
|
xai: { origin: 'https://api.x.ai', url: 'https://cli-chat-proxy.grok.com/v1/billing?format=credits' },
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
function remainingPercent(used) {
|
|
19
|
+
function remainingPercent(used: unknown): number | undefined {
|
|
16
20
|
return typeof used === 'number' && Number.isFinite(used) && used >= 0 && used <= 100 ? 100 - used : undefined;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
export async function getQuota(model, modelRegistry, fetchImpl = fetch) {
|
|
23
|
+
export async function getQuota(model: Model | undefined, modelRegistry: Pick<ModelRegistry, 'getApiKeyAndHeaders' | 'isUsingOAuth'> | undefined, fetchImpl: typeof fetch = fetch): Promise<string> {
|
|
20
24
|
const provider = model && providers[model.provider];
|
|
21
25
|
if (!provider) return UNAVAILABLE;
|
|
22
26
|
try {
|
|
23
27
|
if (new URL(model.baseUrl).origin !== provider.origin) return UNAVAILABLE;
|
|
24
|
-
if (['anthropic', 'xai'].includes(model.provider) && !modelRegistry
|
|
25
|
-
const auth = await modelRegistry
|
|
26
|
-
if (!auth
|
|
27
|
-
const headers = { Authorization: ['zai', 'zai-coding-cn'].includes(model.provider) ? auth.apiKey : `Bearer ${auth.apiKey}` };
|
|
28
|
+
if (['anthropic', 'xai'].includes(model.provider) && !modelRegistry?.isUsingOAuth(model)) return UNAVAILABLE;
|
|
29
|
+
const auth = await modelRegistry?.getApiKeyAndHeaders(model);
|
|
30
|
+
if (!auth?.ok || !auth.apiKey || (auth.baseUrl && new URL(auth.baseUrl).origin !== provider.origin)) return UNAVAILABLE;
|
|
31
|
+
const headers: Record<string, string> = { Authorization: ['zai', 'zai-coding-cn'].includes(model.provider) ? auth.apiKey : `Bearer ${auth.apiKey}` };
|
|
28
32
|
if (model.provider === 'anthropic') {
|
|
29
33
|
headers['anthropic-beta'] = 'oauth-2025-04-20';
|
|
30
34
|
headers['anthropic-version'] = '2023-06-01';
|
|
@@ -43,7 +47,7 @@ export async function getQuota(model, modelRegistry, fetchImpl = fetch) {
|
|
|
43
47
|
const data = await response.json();
|
|
44
48
|
if (['zai', 'zai-coding-cn'].includes(model.provider)) {
|
|
45
49
|
if (data.success === false || ![undefined, 0, 200].includes(data.code)) return UNAVAILABLE;
|
|
46
|
-
const used = data.data?.limits?.find((limit) => limit.type === 'TOKENS_LIMIT')?.percentage;
|
|
50
|
+
const used = data.data?.limits?.find((limit: { type: string; percentage: number }) => limit.type === 'TOKENS_LIMIT')?.percentage;
|
|
47
51
|
const remaining = remainingPercent(used);
|
|
48
52
|
return remaining === undefined ? UNAVAILABLE : `Remaining quota: 5h ${remaining}%`;
|
|
49
53
|
}
|
|
@@ -85,7 +89,7 @@ export async function getQuota(model, modelRegistry, fetchImpl = fetch) {
|
|
|
85
89
|
}
|
|
86
90
|
if (model.provider === 'deepseek') {
|
|
87
91
|
if (!Array.isArray(data.balance_infos) || !data.balance_infos.length) return UNAVAILABLE;
|
|
88
|
-
const balances = data.balance_infos.map(({ currency, total_balance }) => {
|
|
92
|
+
const balances = data.balance_infos.map(({ currency, total_balance }: { currency: string; total_balance: string }) => {
|
|
89
93
|
if (!['CNY', 'USD'].includes(currency) || typeof total_balance !== 'string' ||
|
|
90
94
|
!/^\d+(?:\.\d+)?$/.test(total_balance)) return undefined;
|
|
91
95
|
return `${total_balance} ${currency}`;
|
|
@@ -2,18 +2,18 @@ import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
4
|
|
|
5
|
-
const stateFile = (agentDir) => join(agentDir, 'pi-telegram-notifier.json');
|
|
5
|
+
const stateFile = (agentDir: string) => join(agentDir, 'pi-telegram-notifier.json');
|
|
6
6
|
|
|
7
|
-
export function loadChatId(agentDir) {
|
|
7
|
+
export function loadChatId(agentDir: string): number | undefined {
|
|
8
8
|
try {
|
|
9
|
-
const id = JSON.parse(readFileSync(stateFile(agentDir), 'utf8')).chatId;
|
|
10
|
-
return Number.isSafeInteger(id) && id > 0 ? id : undefined;
|
|
9
|
+
const id: unknown = JSON.parse(readFileSync(stateFile(agentDir), 'utf8')).chatId;
|
|
10
|
+
return typeof id === 'number' && Number.isSafeInteger(id) && id > 0 ? id : undefined;
|
|
11
11
|
} catch {
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function saveChatId(agentDir, chatId) {
|
|
16
|
+
export function saveChatId(agentDir: string, chatId: number): void {
|
|
17
17
|
if (!Number.isSafeInteger(chatId) || chatId <= 0) throw new Error('Invalid private chat ID');
|
|
18
18
|
mkdirSync(agentDir, { recursive: true, mode: 0o700 });
|
|
19
19
|
const temporary = `${stateFile(agentDir)}.${randomBytes(8).toString('hex')}`;
|
|
@@ -21,7 +21,7 @@ export function saveChatId(agentDir, chatId) {
|
|
|
21
21
|
renameSync(temporary, stateFile(agentDir));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function pairChat(token, code, fetchImpl = fetch, now = Date.now) {
|
|
24
|
+
export async function pairChat(token: string, code: string, fetchImpl: typeof fetch = fetch, now: () => number = Date.now): Promise<number> {
|
|
25
25
|
const expires = now() + 300_000;
|
|
26
26
|
let offset = 0;
|
|
27
27
|
while (now() < expires) {
|
|
@@ -51,9 +51,9 @@ export async function pairChat(token, code, fetchImpl = fetch, now = Date.now) {
|
|
|
51
51
|
throw new Error('Telegram pairing code expired');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export async function sendTelegram(token, chatId, text, fetchImpl = fetch) {
|
|
54
|
+
export async function sendTelegram(token: string, chatId: number, text: string, fetchImpl: typeof fetch = fetch): Promise<void> {
|
|
55
55
|
let part = '';
|
|
56
|
-
const chunks = [];
|
|
56
|
+
const chunks: string[] = [];
|
|
57
57
|
for (const character of text) {
|
|
58
58
|
if (part.length + character.length > 4096) {
|
|
59
59
|
chunks.push(part);
|