pi-telegram-notifier 0.1.0 → 0.1.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 CHANGED
@@ -1,21 +1,20 @@
1
1
  # pi-telegram-notifier
2
2
 
3
- Send the **final** response of each Pi agent run to a paired private Telegram chat. Each notification contains a UTC timestamp, provider, full response text, and the **actual** remaining provider balance when it can be queried. Telegram metadata (provider and quota) is in English; the agent's response is forwarded verbatim in its original language. Intermediate tool calls do not trigger messages.
3
+ Send the **final** response of each Pi agent run to a paired private Telegram chat. Each notification contains a UTC timestamp, device hostname, provider, full response text, and the **actual** remaining provider balance when it can be queried. Telegram metadata (provider and quota) is in English; the agent's response is forwarded verbatim in its original language. Intermediate tool calls do not trigger messages.
4
4
 
5
5
  ## Install and configure
6
6
 
7
- Requires Node.js 22.19+ and [Pi](https://pi.dev). Create a Telegram bot using [@BotFather](https://t.me/BotFather), then set its token **outside the repository** before starting Pi:
7
+ Requires Node.js 22.19+ and [Pi](https://pi.dev). Install the published package, create a Telegram bot using [@BotFather](https://t.me/BotFather), and set its token **outside the repository** before starting Pi:
8
8
 
9
9
  ```sh
10
+ pi install npm:pi-telegram-notifier
10
11
  export PI_TELEGRAM_BOT_TOKEN='your-bot-token'
11
- pi -e . # test this checkout as a Pi package
12
- # Later, after public release: pi install npm:pi-telegram-notifier
13
- # Or: pi install git:github.com/Gybra/pi-telegram-notifier
12
+ pi
14
13
  ```
15
14
 
16
- In Pi run `/telegram-pair`. Within five minutes send **`/start <code>`** to your bot in a private Telegram chat using the displayed one-time code. The extension stores only the numeric chat ID in `~/.pi/agent/pi-telegram-notifier.json` (or `$PI_CODING_AGENT_DIR/pi-telegram-notifier.json`), with mode 0600. To switch chats, run `/telegram-pair` again; the existing chat stays paired if pairing fails. The bot needs to receive messages via `getUpdates` during pairing: a webhook or another updates consumer must be disconnected first. There is no always-on polling after pairing.
15
+ If Pi is already running, run `/reload` after installing the package (restart Pi if you just set the token). If you previously installed a local checkout, run `pi list`, then `pi remove /absolute/path/to/pi-telegram-notifier` to avoid loading both copies. To get future releases, run `pi update npm:pi-telegram-notifier`. In Pi run `/telegram-pair`. Within five minutes send **`/start <code>`** to your bot in a private Telegram chat using the displayed one-time code. The extension stores only the numeric chat ID in `~/.pi/agent/pi-telegram-notifier.json` (or `$PI_CODING_AGENT_DIR/pi-telegram-notifier.json`), with mode 0600. To switch chats, run `/telegram-pair` again; the existing chat stays paired if pairing fails. The bot needs to receive messages via `getUpdates` during pairing: a webhook or another updates consumer must be disconnected first. There is no always-on polling after pairing.
17
16
 
18
- **Privacy:** final agent responses are sent in full to Telegram. They can contain sensitive text; only use this package if that is acceptable for your projects and Telegram chat. Bot and provider tokens are not stored in the package or sent as message text. Telegram Bot API uses the bot token in the request URL; do not log outgoing URLs. Keep your shell environment and Pi credentials private. A lost or compromised Telegram bot token must be revoked with BotFather.
17
+ **Privacy:** final agent responses and the device hostname are sent to Telegram. Responses can contain sensitive text; only use this package if that is acceptable for your projects and Telegram chat. Bot and provider tokens are not stored in the package or sent as message text. Telegram Bot API uses the bot token in the request URL; do not log outgoing URLs. Keep your shell environment and Pi credentials private. A lost or compromised Telegram bot token must be revoked with BotFather.
19
18
 
20
19
  ## Quota coverage
21
20
 
@@ -44,7 +43,7 @@ Telegram has a message-length limit; long responses are delivered in consecutive
44
43
  ```sh
45
44
  npm test
46
45
  npm pack --dry-run --json
47
- pi -e . --help
46
+ pi --no-extensions -e . --help
48
47
  ```
49
48
 
50
- Read [AGENTS.md](AGENTS.md) before editing. Open contributions with tests for behavior changes and official provider documentation for balance integrations. MIT licensed; see [LICENSE](LICENSE). CI runs tests and packaging checks. Publishing is deliberately manual: create the public GitHub repository, then publish the npm package with the `pi-package` keyword to make it eligible for the [Pi package gallery](https://pi.dev/packages). No action here pushes, publishes, or handles real credentials.
49
+ Read [AGENTS.md](AGENTS.md) before editing. Open contributions with tests for behavior changes and official provider documentation for balance integrations. MIT licensed; see [LICENSE](LICENSE). CI runs tests and packaging checks. The published npm package includes the `pi-package` keyword for the [Pi package gallery](https://pi.dev/packages). New versions are released manually; CI does not publish packages or handle real credentials.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-telegram-notifier",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Telegram notifications for final Pi agent responses and provider balances",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,3 +1,4 @@
1
+ import { hostname } from 'node:os';
1
2
  import { loadChatId, sendTelegram } from './telegram.js';
2
3
  import { getQuota } from './quota.js';
3
4
 
@@ -12,7 +13,7 @@ export function finalResponse(branch, startLeafId) {
12
13
  }
13
14
 
14
15
  export function formatNotification(timestamp, response, quota) {
15
- return `${timestamp}\nProvider: ${response.provider}\n${quota}\n\n${response.text}`;
16
+ return `${timestamp}\nDevice: ${hostname()}\nProvider: ${response.provider}\n${quota}\n\n${response.text}`;
16
17
  }
17
18
 
18
19
  export function registerNotifications(pi, agentDir, token, fetchImpl = fetch) {
@@ -26,9 +27,14 @@ export function registerNotifications(pi, agentDir, token, fetchImpl = fetch) {
26
27
  startLeafId = undefined;
27
28
  const chatId = loadChatId(agentDir);
28
29
  if (!chatId || !response) return;
30
+ let quota;
29
31
  try {
30
32
  const model = ctx.modelRegistry?.find(response.provider, response.modelId);
31
- const quota = await getQuota(model, ctx.modelRegistry, fetchImpl);
33
+ quota = await getQuota(model, ctx.modelRegistry, fetchImpl);
34
+ } catch {
35
+ quota = 'Quota unavailable';
36
+ }
37
+ try {
32
38
  await sendTelegram(token, chatId,
33
39
  formatNotification(new Date().toISOString(), response, quota), fetchImpl);
34
40
  } catch {
package/src/quota.js CHANGED
@@ -1,15 +1,15 @@
1
1
  const UNAVAILABLE = 'Quota unavailable';
2
2
 
3
3
  const providers = {
4
- openrouter: ['https://openrouter.ai', 'https://openrouter.ai/api/v1/credits'],
5
- 'openai-codex': ['https://chatgpt.com', 'https://chatgpt.com/backend-api/wham/usage'],
6
- deepseek: ['https://api.deepseek.com', 'https://api.deepseek.com/user/balance'],
7
- moonshotai: ['https://api.moonshot.ai', 'https://api.moonshot.ai/v1/users/me/balance'],
8
- 'moonshotai-cn': ['https://api.moonshot.cn', 'https://api.moonshot.cn/v1/users/me/balance'],
9
- zai: ['https://api.z.ai', 'https://api.z.ai/api/monitor/usage/quota/limit'],
10
- 'zai-coding-cn': ['https://open.bigmodel.cn', 'https://open.bigmodel.cn/api/monitor/usage/quota/limit'],
11
- anthropic: ['https://api.anthropic.com', 'https://api.anthropic.com/api/oauth/usage'],
12
- xai: ['https://api.x.ai', 'https://cli-chat-proxy.grok.com/v1/billing?format=credits'],
4
+ openrouter: { origin: 'https://openrouter.ai', url: 'https://openrouter.ai/api/v1/credits' },
5
+ 'openai-codex': { origin: 'https://chatgpt.com', url: 'https://chatgpt.com/backend-api/wham/usage' },
6
+ deepseek: { origin: 'https://api.deepseek.com', url: 'https://api.deepseek.com/user/balance' },
7
+ moonshotai: { origin: 'https://api.moonshot.ai', url: 'https://api.moonshot.ai/v1/users/me/balance' },
8
+ 'moonshotai-cn': { origin: 'https://api.moonshot.cn', url: 'https://api.moonshot.cn/v1/users/me/balance' },
9
+ zai: { origin: 'https://api.z.ai', url: 'https://api.z.ai/api/monitor/usage/quota/limit' },
10
+ 'zai-coding-cn': { origin: 'https://open.bigmodel.cn', url: 'https://open.bigmodel.cn/api/monitor/usage/quota/limit' },
11
+ anthropic: { origin: 'https://api.anthropic.com', url: 'https://api.anthropic.com/api/oauth/usage' },
12
+ xai: { origin: 'https://api.x.ai', url: 'https://cli-chat-proxy.grok.com/v1/billing?format=credits' },
13
13
  };
14
14
 
15
15
  function remainingPercent(used) {
@@ -20,10 +20,10 @@ export async function getQuota(model, modelRegistry, fetchImpl = fetch) {
20
20
  const provider = model && providers[model.provider];
21
21
  if (!provider) return UNAVAILABLE;
22
22
  try {
23
- if (new URL(model.baseUrl).origin !== provider[0]) return UNAVAILABLE;
23
+ if (new URL(model.baseUrl).origin !== provider.origin) return UNAVAILABLE;
24
24
  if (['anthropic', 'xai'].includes(model.provider) && !modelRegistry.isUsingOAuth(model)) return UNAVAILABLE;
25
25
  const auth = await modelRegistry.getApiKeyAndHeaders(model);
26
- if (!auth.ok || !auth.apiKey || (auth.baseUrl && new URL(auth.baseUrl).origin !== provider[0])) return UNAVAILABLE;
26
+ if (!auth.ok || !auth.apiKey || (auth.baseUrl && new URL(auth.baseUrl).origin !== provider.origin)) return UNAVAILABLE;
27
27
  const headers = { Authorization: ['zai', 'zai-coding-cn'].includes(model.provider) ? auth.apiKey : `Bearer ${auth.apiKey}` };
28
28
  if (model.provider === 'anthropic') {
29
29
  headers['anthropic-beta'] = 'oauth-2025-04-20';
@@ -35,7 +35,7 @@ export async function getQuota(model, modelRegistry, fetchImpl = fetch) {
35
35
  if (typeof accountId !== 'string' || !accountId) return UNAVAILABLE;
36
36
  headers['ChatGPT-Account-Id'] = accountId;
37
37
  }
38
- const response = await fetchImpl(provider[1], {
38
+ const response = await fetchImpl(provider.url, {
39
39
  headers,
40
40
  signal: AbortSignal.timeout(10_000),
41
41
  });
@@ -62,7 +62,7 @@ export async function getQuota(model, modelRegistry, fetchImpl = fetch) {
62
62
  if (remaining === undefined && config.creditUsagePercent === undefined) {
63
63
  const { currentPeriod, productUsage } = config;
64
64
  if (Date.parse(currentPeriod.start) < Date.parse(currentPeriod.end) &&
65
- (productUsage === undefined || (Array.isArray(productUsage) && productUsage.every((item) => item?.usagePercent === 0)))) remaining = 100;
65
+ Array.isArray(productUsage) && productUsage.length > 0 && productUsage.every((item) => item?.usagePercent === 0)) remaining = 100;
66
66
  }
67
67
  return remaining === undefined ? UNAVAILABLE : `Remaining quota: 7d ${remaining}%`;
68
68
  }