ping-a-human 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
@@ -98,6 +98,19 @@ The server reads configuration with this precedence:
98
98
  The bot token is a secret — it is never logged or echoed. All diagnostics go to **stderr**; **stdout**
99
99
  is reserved for the MCP JSON-RPC channel.
100
100
 
101
+ ## Use it in pi
102
+
103
+ Using the [pi](https://pi.dev) coding agent? One command wires ping-a-human into
104
+ pi's hooks for notifications, human-in-the-loop approval, a `/ping` command, and a
105
+ global `pah` CLI:
106
+
107
+ ```bash
108
+ npx ping-a-human-pi
109
+ ```
110
+
111
+ See the [`ping-a-human-pi`](./ping-a-human-pi) package or the
112
+ [pi guide](https://github.com/startriseio/ping-a-human/tree/main/ping-a-human-pi).
113
+
101
114
  ## Local development
102
115
 
103
116
  ```bash
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Telegram bot commands (text beginning with "/", e.g. "/start", "/help") are
3
+ * client/protocol messages, never a human's answer to a free-text question.
4
+ * The most common offender is the "/start" Telegram auto-sends when a user
5
+ * first opens the bot, which would otherwise be returned as the reply.
6
+ */
7
+ function isBotCommand(text) {
8
+ return /^\/[A-Za-z0-9_]+(@\w+)?(\s|$)/.test(text.trim());
9
+ }
1
10
  /**
2
11
  * Telegram implementation of the Channel interface using the Bot API over
3
12
  * plain HTTPS (Node's built-in fetch). No third-party Telegram SDK.
@@ -58,6 +67,15 @@ export class TelegramChannel {
58
67
  }
59
68
  async awaitReply(options) {
60
69
  const deadline = Date.now() + options.timeoutMs;
70
+ // Anchor: only accept replies that arrive AFTER the question was sent.
71
+ // Telegram message ids are monotonically increasing per chat, so any
72
+ // update whose message predates the question (e.g. a queued `/start` from
73
+ // first opening the bot) must be ignored — otherwise it would be wrongly
74
+ // returned as the human's answer. Honors AwaitReplyOptions.sinceRef.
75
+ const sinceMessageId = options.sinceRef != null ? Number(options.sinceRef.id) : undefined;
76
+ const isStale = (messageId) => sinceMessageId != null &&
77
+ messageId != null &&
78
+ messageId <= sinceMessageId;
61
79
  while (Date.now() < deadline) {
62
80
  const remainingMs = deadline - Date.now();
63
81
  // Don't long-poll longer than the time we have left.
@@ -73,6 +91,13 @@ export class TelegramChannel {
73
91
  // Free-text reply.
74
92
  const msg = update.message;
75
93
  if (msg?.text && this.fromConfiguredChat(msg.chat)) {
94
+ // Skip backlog that predates the question (e.g. a stale `/start`).
95
+ if (isStale(msg.message_id))
96
+ continue;
97
+ // Bot commands ("/start", "/help", ...) are never valid answers to a
98
+ // free-text question; treat them as noise and keep waiting.
99
+ if (isBotCommand(msg.text))
100
+ continue;
76
101
  return {
77
102
  status: "answered",
78
103
  answer: msg.text,
@@ -81,7 +106,7 @@ export class TelegramChannel {
81
106
  }
82
107
  // Inline-button tap.
83
108
  const cb = update.callback_query;
84
- if (cb) {
109
+ if (cb && !isStale(cb.message?.message_id)) {
85
110
  // Best-effort: clear the client's loading spinner.
86
111
  try {
87
112
  await this.api("answerCallbackQuery", { callback_query_id: cb.id });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-a-human",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "An MCP server that adds a human-in-the-loop step to any AI pipeline by reaching a human on their own messaging app (Telegram first).",
5
5
  "type": "module",
6
6
  "bin": {