@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codefilabs/tq",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A lightweight task queue runner that spawns Claude AI tasks as independent tmux sessions",
5
5
  "keywords": [
6
6
  "claude",
@@ -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"
@@ -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
- trap 'rm -f "$CONFIG_SCRIPT"' EXIT
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
- messages.append(f"{chat_id}\t{message_id}\t{reply_to}\t{text}")
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
- MESSAGES="$(python3 "$PROCESS_SCRIPT" "$UPDATES" "$USER_ID" "$OFFSET_FILE")"
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
- # --- Check if orchestrator is running ---
120
- ORCHESTRATOR_ACTIVE=0
121
- if tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
122
- ORCHESTRATOR_ACTIVE=1
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
- # --- Route messages ---
198
- mkdir -p "$HOME/.tq/workspace"
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
- while IFS=$'\t' read -r CHAT_ID MSG_ID REPLY_TO MSG; do
201
- if [[ -z "$MSG" ]]; then
202
- continue
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
- echo "[tq-telegram-poll] message: ${MSG:0:60}"
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
- # React with 👀 to acknowledge receipt
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
- # Check for Telegram commands first
214
- if [[ "$MSG" == /* ]]; then
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
- # --- Routing logic (3-tier) ---
221
-
222
- # Store the latest message ID for reply threading
223
- echo "$MSG_ID" > "$HOME/.tq/conversations/latest-msg-id" 2>/dev/null || true
224
- echo "$CHAT_ID" > "$HOME/.tq/conversations/latest-chat-id" 2>/dev/null || true
225
-
226
- ROUTED=0
227
-
228
- # Tier 1: Telegram reply → deterministic routing via registry
229
- if [[ -n "$REPLY_TO" && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
230
- SLUG="$(tq-converse lookup-msg "$REPLY_TO" 2>/dev/null || true)"
231
- if [[ -n "$SLUG" ]]; then
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
- # Tier 2: Explicit #slug prefix → deterministic routing
245
- if [[ "$ROUTED" == "0" && "$MSG" == \#* && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
246
- # Extract slug from #slug-name prefix
247
- SLUG="$(echo "$MSG" | sed 's/^#\([a-z0-9-]*\).*/\1/')"
248
- ACTUAL_MSG="$(echo "$MSG" | sed 's/^#[a-z0-9-]* *//')"
249
- if [[ -n "$SLUG" && -n "$ACTUAL_MSG" ]]; then
250
- CHILD_SESSION="tq-conv-${SLUG}"
251
- if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
252
- echo "[tq-telegram-poll] explicit routing to: $SLUG"
253
- tq-converse track-msg "$SLUG" "$MSG_ID"
254
- echo "$MSG_ID" > "$HOME/.tq/conversations/sessions/$SLUG/reply-to-msg-id" 2>/dev/null || true
255
- tq-converse route "$SLUG" "$ACTUAL_MSG"
256
- ROUTED=1
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
- # Tier 3: Orchestrator routing (smart, Claude-powered)
262
- if [[ "$ROUTED" == "0" && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
263
- echo "[tq-telegram-poll] routing via orchestrator"
264
- # Format message with metadata for the orchestrator
265
- FORMATTED_MSG="[tq-msg msg_id=${MSG_ID} chat_id=${CHAT_ID}"
266
- if [[ -n "$REPLY_TO" ]]; then
267
- FORMATTED_MSG="${FORMATTED_MSG} reply_to=${REPLY_TO}"
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
- FORMATTED_MSG="${FORMATTED_MSG}]
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
- tq-converse send "$FORMATTED_MSG"
272
- ROUTED=1
273
- fi
290
+ tq-converse send "$FORMATTED_MSG"
291
+ ROUTED=1
292
+ fi
274
293
 
275
- # Fallback: no orchestrator — spawn one-off task (legacy behavior)
276
- if [[ "$ROUTED" == "0" ]]; then
277
- echo "[tq-telegram-poll] no orchestrator — spawning one-off task"
278
- tq --no-chrome --prompt "$MSG"
279
- fi
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