@codefilabs/tq 0.1.0 → 0.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/package.json +1 -1
- package/scripts/tq-install.sh +53 -3
- package/scripts/tq-telegram-poll +125 -95
package/package.json
CHANGED
package/scripts/tq-install.sh
CHANGED
|
@@ -67,6 +67,59 @@ mkdir -p ~/.tq/queues ~/.tq/logs ~/.tq/config
|
|
|
67
67
|
# Use $INSTALL_DIR directly — PATH may not reflect the just-created symlink yet
|
|
68
68
|
"$INSTALL_DIR/tq-cron-sync" --interval 20
|
|
69
69
|
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Step 4: Set up Telegram long-poll daemon via launchd (if bot is configured)
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
PLIST_LABEL="com.codefi.tq-telegram"
|
|
74
|
+
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
|
|
75
|
+
|
|
76
|
+
if [[ -f "$HOME/.tq/config/message.yaml" ]] && grep -q 'bot_token' "$HOME/.tq/config/message.yaml" 2>/dev/null; then
|
|
77
|
+
# Remove old cron-based poll entries (replaced by launchd daemon)
|
|
78
|
+
CRONTAB_CURRENT="$(crontab -l 2>/dev/null || true)"
|
|
79
|
+
if echo "$CRONTAB_CURRENT" | grep -q 'tq-telegram-poll'; then
|
|
80
|
+
echo "$CRONTAB_CURRENT" | grep -v 'tq-telegram-poll' | grep -v 'tq-telegram-watchdog' | crontab -
|
|
81
|
+
echo " removed cron-based telegram poll (replaced by launchd daemon)"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
cat > "$PLIST_PATH" <<PLISTEOF
|
|
85
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
86
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
87
|
+
<plist version="1.0">
|
|
88
|
+
<dict>
|
|
89
|
+
<key>Label</key>
|
|
90
|
+
<string>${PLIST_LABEL}</string>
|
|
91
|
+
<key>ProgramArguments</key>
|
|
92
|
+
<array>
|
|
93
|
+
<string>${INSTALL_DIR}/tq-telegram-poll</string>
|
|
94
|
+
<string>--daemon</string>
|
|
95
|
+
</array>
|
|
96
|
+
<key>RunAtLoad</key>
|
|
97
|
+
<true/>
|
|
98
|
+
<key>KeepAlive</key>
|
|
99
|
+
<true/>
|
|
100
|
+
<key>StandardOutPath</key>
|
|
101
|
+
<string>${HOME}/.tq/logs/tq-telegram.log</string>
|
|
102
|
+
<key>StandardErrorPath</key>
|
|
103
|
+
<string>${HOME}/.tq/logs/tq-telegram.log</string>
|
|
104
|
+
<key>EnvironmentVariables</key>
|
|
105
|
+
<dict>
|
|
106
|
+
<key>PATH</key>
|
|
107
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
108
|
+
</dict>
|
|
109
|
+
</dict>
|
|
110
|
+
</plist>
|
|
111
|
+
PLISTEOF
|
|
112
|
+
|
|
113
|
+
# (Re)load the daemon
|
|
114
|
+
launchctl bootout "gui/$(id -u)/${PLIST_LABEL}" 2>/dev/null || true
|
|
115
|
+
launchctl bootstrap "gui/$(id -u)" "$PLIST_PATH"
|
|
116
|
+
echo " telegram daemon started (launchd: ${PLIST_LABEL})"
|
|
117
|
+
else
|
|
118
|
+
echo ""
|
|
119
|
+
echo "Telegram bot not configured yet. After running tq-setup, re-run this"
|
|
120
|
+
echo "installer to start the long-poll daemon automatically."
|
|
121
|
+
fi
|
|
122
|
+
|
|
70
123
|
echo ""
|
|
71
124
|
echo "tq installed. Cron schedules are managed automatically."
|
|
72
125
|
echo ""
|
|
@@ -84,9 +137,6 @@ echo " tq-setup"
|
|
|
84
137
|
echo ""
|
|
85
138
|
echo "Or from Claude Code: /setup-telegram"
|
|
86
139
|
echo ""
|
|
87
|
-
echo "To relay Telegram messages as tq tasks, add to crontab:"
|
|
88
|
-
echo " * * * * * /opt/homebrew/bin/tq-telegram-poll >> ~/.tq/logs/tq-telegram.log 2>&1"
|
|
89
|
-
echo ""
|
|
90
140
|
echo "Conversation mode (interactive Telegram <-> Claude Code):"
|
|
91
141
|
echo " tq-converse start [--cwd /path/to/project]"
|
|
92
142
|
echo " Or send /converse from Telegram"
|
package/scripts/tq-telegram-poll
CHANGED
|
@@ -6,6 +6,13 @@ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
|
6
6
|
CONFIG_FILE="$HOME/.tq/config/message.yaml"
|
|
7
7
|
OFFSET_FILE="$HOME/.tq/telegram-poll-offset"
|
|
8
8
|
ORCHESTRATOR_SESSION="tq-orchestrator"
|
|
9
|
+
DAEMON_MODE=0
|
|
10
|
+
POLL_TIMEOUT=0
|
|
11
|
+
|
|
12
|
+
if [[ "${1:-}" == "--daemon" ]]; then
|
|
13
|
+
DAEMON_MODE=1
|
|
14
|
+
POLL_TIMEOUT=30
|
|
15
|
+
fi
|
|
9
16
|
|
|
10
17
|
# --- Read config ---
|
|
11
18
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
@@ -13,7 +20,8 @@ if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
|
13
20
|
fi
|
|
14
21
|
|
|
15
22
|
CONFIG_SCRIPT=$(mktemp /tmp/tq-poll-config-XXXXXX)
|
|
16
|
-
|
|
23
|
+
PROCESS_SCRIPT=$(mktemp /tmp/tq-poll-process-XXXXXX)
|
|
24
|
+
trap 'rm -f "$CONFIG_SCRIPT" "$PROCESS_SCRIPT"' EXIT
|
|
17
25
|
|
|
18
26
|
cat > "$CONFIG_SCRIPT" <<'PYEOF'
|
|
19
27
|
import sys, os, re, json
|
|
@@ -52,27 +60,6 @@ print(json.dumps({
|
|
|
52
60
|
}))
|
|
53
61
|
PYEOF
|
|
54
62
|
|
|
55
|
-
CONFIG_JSON="$(python3 "$CONFIG_SCRIPT" "$CONFIG_FILE")"
|
|
56
|
-
BOT_TOKEN="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('bot_token',''))" "$CONFIG_JSON")"
|
|
57
|
-
USER_ID="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('user_id',''))" "$CONFIG_JSON")"
|
|
58
|
-
|
|
59
|
-
if [[ -z "$BOT_TOKEN" || -z "$USER_ID" ]]; then
|
|
60
|
-
exit 0
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
# --- Read offset ---
|
|
64
|
-
OFFSET=0
|
|
65
|
-
if [[ -f "$OFFSET_FILE" ]]; then
|
|
66
|
-
OFFSET="$(cat "$OFFSET_FILE")"
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
# --- Fetch updates ---
|
|
70
|
-
UPDATES="$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=${OFFSET}&limit=10&timeout=0" 2>&1)"
|
|
71
|
-
|
|
72
|
-
# --- Process updates (extract msg_id, chat_id, reply_to_msg_id, text) ---
|
|
73
|
-
PROCESS_SCRIPT=$(mktemp /tmp/tq-poll-process-XXXXXX)
|
|
74
|
-
trap 'rm -f "$CONFIG_SCRIPT" "$PROCESS_SCRIPT"' EXIT
|
|
75
|
-
|
|
76
63
|
cat > "$PROCESS_SCRIPT" <<'PYEOF'
|
|
77
64
|
import sys, json
|
|
78
65
|
|
|
@@ -104,7 +91,8 @@ for update in results:
|
|
|
104
91
|
# Extract reply_to_message_id if present
|
|
105
92
|
reply_to = msg.get('reply_to_message', {}).get('message_id', '')
|
|
106
93
|
# Output: chat_id<TAB>message_id<TAB>reply_to<TAB>text
|
|
107
|
-
|
|
94
|
+
# Use '-' placeholder for empty reply_to (bash read collapses consecutive tabs)
|
|
95
|
+
messages.append(f"{chat_id}\t{message_id}\t{reply_to or '-'}\t{text}")
|
|
108
96
|
|
|
109
97
|
if new_offset is not None:
|
|
110
98
|
with open(offset_file, 'w') as f:
|
|
@@ -114,12 +102,18 @@ for m in messages:
|
|
|
114
102
|
print(m)
|
|
115
103
|
PYEOF
|
|
116
104
|
|
|
117
|
-
|
|
105
|
+
CONFIG_JSON="$(python3 "$CONFIG_SCRIPT" "$CONFIG_FILE")"
|
|
106
|
+
BOT_TOKEN="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('bot_token',''))" "$CONFIG_JSON")"
|
|
107
|
+
USER_ID="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('user_id',''))" "$CONFIG_JSON")"
|
|
108
|
+
|
|
109
|
+
if [[ -z "$BOT_TOKEN" || -z "$USER_ID" ]]; then
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
118
112
|
|
|
119
|
-
# ---
|
|
120
|
-
|
|
121
|
-
if
|
|
122
|
-
|
|
113
|
+
# --- Read offset ---
|
|
114
|
+
OFFSET=0
|
|
115
|
+
if [[ -f "$OFFSET_FILE" ]]; then
|
|
116
|
+
OFFSET="$(cat "$OFFSET_FILE")"
|
|
123
117
|
fi
|
|
124
118
|
|
|
125
119
|
# --- Handle Telegram commands ---
|
|
@@ -194,88 +188,124 @@ handle_command() {
|
|
|
194
188
|
return 1
|
|
195
189
|
}
|
|
196
190
|
|
|
197
|
-
# ---
|
|
198
|
-
|
|
191
|
+
# --- poll_once: fetch and process a single batch of updates ---
|
|
192
|
+
poll_once() {
|
|
193
|
+
local UPDATES MESSAGES
|
|
194
|
+
|
|
195
|
+
UPDATES="$(curl -s --max-time $((POLL_TIMEOUT + 10)) "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=${OFFSET}&limit=10&timeout=${POLL_TIMEOUT}" 2>&1)"
|
|
196
|
+
|
|
197
|
+
MESSAGES="$(python3 "$PROCESS_SCRIPT" "$UPDATES" "$USER_ID" "$OFFSET_FILE")"
|
|
199
198
|
|
|
200
|
-
|
|
201
|
-
if [[ -
|
|
202
|
-
|
|
199
|
+
# Re-read offset (python may have updated it)
|
|
200
|
+
if [[ -f "$OFFSET_FILE" ]]; then
|
|
201
|
+
OFFSET="$(cat "$OFFSET_FILE")"
|
|
203
202
|
fi
|
|
204
203
|
|
|
205
|
-
|
|
204
|
+
if [[ -z "$MESSAGES" ]]; then
|
|
205
|
+
return 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Check if orchestrator is running
|
|
209
|
+
ORCHESTRATOR_ACTIVE=0
|
|
210
|
+
if tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
|
|
211
|
+
ORCHESTRATOR_ACTIVE=1
|
|
212
|
+
fi
|
|
206
213
|
|
|
207
|
-
|
|
208
|
-
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setMessageReaction" \
|
|
209
|
-
-H "Content-Type: application/json" \
|
|
210
|
-
--data-raw "{\"chat_id\":${CHAT_ID},\"message_id\":${MSG_ID},\"reaction\":[{\"type\":\"emoji\",\"emoji\":\"👀\"}]}" \
|
|
211
|
-
> /dev/null 2>&1 || true
|
|
214
|
+
mkdir -p "$HOME/.tq/workspace"
|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if handle_command "$MSG" "$CHAT_ID"; then
|
|
216
|
+
while IFS=$'\t' read -r CHAT_ID MSG_ID REPLY_TO MSG; do
|
|
217
|
+
if [[ -z "$MSG" ]]; then
|
|
216
218
|
continue
|
|
217
219
|
fi
|
|
218
|
-
fi
|
|
219
220
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
CHILD_SESSION="tq-conv-${SLUG}"
|
|
233
|
-
if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
|
|
234
|
-
echo "[tq-telegram-poll] reply-routing to: $SLUG (via msg $REPLY_TO)"
|
|
235
|
-
tq-converse track-msg "$SLUG" "$MSG_ID"
|
|
236
|
-
# Store msg_id for this session's reply threading
|
|
237
|
-
echo "$MSG_ID" > "$HOME/.tq/conversations/sessions/$SLUG/reply-to-msg-id" 2>/dev/null || true
|
|
238
|
-
tq-converse route "$SLUG" "$MSG"
|
|
239
|
-
ROUTED=1
|
|
221
|
+
echo "[tq-telegram-poll] message: ${MSG:0:60}"
|
|
222
|
+
|
|
223
|
+
# React with 👀 to acknowledge receipt
|
|
224
|
+
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setMessageReaction" \
|
|
225
|
+
-H "Content-Type: application/json" \
|
|
226
|
+
--data-raw "{\"chat_id\":${CHAT_ID},\"message_id\":${MSG_ID},\"reaction\":[{\"type\":\"emoji\",\"emoji\":\"👀\"}]}" \
|
|
227
|
+
> /dev/null 2>&1 || true
|
|
228
|
+
|
|
229
|
+
# Check for Telegram commands first
|
|
230
|
+
if [[ "$MSG" == /* ]]; then
|
|
231
|
+
if handle_command "$MSG" "$CHAT_ID"; then
|
|
232
|
+
continue
|
|
240
233
|
fi
|
|
241
234
|
fi
|
|
242
|
-
fi
|
|
243
235
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
236
|
+
# --- Routing logic (3-tier) ---
|
|
237
|
+
|
|
238
|
+
# Store the latest message ID for reply threading
|
|
239
|
+
echo "$MSG_ID" > "$HOME/.tq/conversations/latest-msg-id" 2>/dev/null || true
|
|
240
|
+
echo "$CHAT_ID" > "$HOME/.tq/conversations/latest-chat-id" 2>/dev/null || true
|
|
241
|
+
|
|
242
|
+
ROUTED=0
|
|
243
|
+
|
|
244
|
+
# Normalize placeholder
|
|
245
|
+
[[ "$REPLY_TO" == "-" ]] && REPLY_TO=""
|
|
246
|
+
|
|
247
|
+
# Tier 1: Telegram reply → deterministic routing via registry
|
|
248
|
+
if [[ -n "$REPLY_TO" && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
|
|
249
|
+
SLUG="$(tq-converse lookup-msg "$REPLY_TO" 2>/dev/null || true)"
|
|
250
|
+
if [[ -n "$SLUG" ]]; then
|
|
251
|
+
CHILD_SESSION="tq-conv-${SLUG}"
|
|
252
|
+
if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
|
|
253
|
+
echo "[tq-telegram-poll] reply-routing to: $SLUG (via msg $REPLY_TO)"
|
|
254
|
+
tq-converse track-msg "$SLUG" "$MSG_ID"
|
|
255
|
+
# Store msg_id for this session's reply threading
|
|
256
|
+
echo "$MSG_ID" > "$HOME/.tq/conversations/sessions/$SLUG/reply-to-msg-id" 2>/dev/null || true
|
|
257
|
+
tq-converse route "$SLUG" "$MSG"
|
|
258
|
+
ROUTED=1
|
|
259
|
+
fi
|
|
257
260
|
fi
|
|
258
261
|
fi
|
|
259
|
-
fi
|
|
260
262
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
# Tier 2: Explicit #slug prefix → deterministic routing
|
|
264
|
+
if [[ "$ROUTED" == "0" && "$MSG" == \#* && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
|
|
265
|
+
# Extract slug from #slug-name prefix
|
|
266
|
+
SLUG="$(echo "$MSG" | sed 's/^#\([a-z0-9-]*\).*/\1/')"
|
|
267
|
+
ACTUAL_MSG="$(echo "$MSG" | sed 's/^#[a-z0-9-]* *//')"
|
|
268
|
+
if [[ -n "$SLUG" && -n "$ACTUAL_MSG" ]]; then
|
|
269
|
+
CHILD_SESSION="tq-conv-${SLUG}"
|
|
270
|
+
if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
|
|
271
|
+
echo "[tq-telegram-poll] explicit routing to: $SLUG"
|
|
272
|
+
tq-converse track-msg "$SLUG" "$MSG_ID"
|
|
273
|
+
echo "$MSG_ID" > "$HOME/.tq/conversations/sessions/$SLUG/reply-to-msg-id" 2>/dev/null || true
|
|
274
|
+
tq-converse route "$SLUG" "$ACTUAL_MSG"
|
|
275
|
+
ROUTED=1
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
268
278
|
fi
|
|
269
|
-
|
|
279
|
+
|
|
280
|
+
# Tier 3: Orchestrator routing (smart, Claude-powered)
|
|
281
|
+
if [[ "$ROUTED" == "0" && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
|
|
282
|
+
echo "[tq-telegram-poll] routing via orchestrator"
|
|
283
|
+
# Format message with metadata for the orchestrator
|
|
284
|
+
FORMATTED_MSG="[tq-msg msg_id=${MSG_ID} chat_id=${CHAT_ID}"
|
|
285
|
+
if [[ -n "$REPLY_TO" ]]; then
|
|
286
|
+
FORMATTED_MSG="${FORMATTED_MSG} reply_to=${REPLY_TO}"
|
|
287
|
+
fi
|
|
288
|
+
FORMATTED_MSG="${FORMATTED_MSG}]
|
|
270
289
|
${MSG}"
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
290
|
+
tq-converse send "$FORMATTED_MSG"
|
|
291
|
+
ROUTED=1
|
|
292
|
+
fi
|
|
274
293
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
294
|
+
# Fallback: no orchestrator — spawn one-off task (legacy behavior)
|
|
295
|
+
if [[ "$ROUTED" == "0" ]]; then
|
|
296
|
+
echo "[tq-telegram-poll] no orchestrator — spawning one-off task"
|
|
297
|
+
tq --no-chrome --prompt "$MSG"
|
|
298
|
+
fi
|
|
280
299
|
|
|
281
|
-
done <<< "$MESSAGES"
|
|
300
|
+
done <<< "$MESSAGES" || true
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# --- Main: single run or daemon loop ---
|
|
304
|
+
if [[ "$DAEMON_MODE" == "1" ]]; then
|
|
305
|
+
echo "[tq-telegram-poll] daemon started (long-poll timeout=${POLL_TIMEOUT}s)"
|
|
306
|
+
while true; do
|
|
307
|
+
poll_once || true
|
|
308
|
+
done
|
|
309
|
+
else
|
|
310
|
+
poll_once
|
|
311
|
+
fi
|