@codefilabs/tq 0.0.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.
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
5
+
6
+ TASK_HASH=""
7
+ QUEUE_FILE=""
8
+ MESSAGE_TEXT=""
9
+ SESSION=""
10
+ EXPLICIT_STATE_FILE=""
11
+
12
+ while [[ $# -gt 0 ]]; do
13
+ case "${1:-}" in
14
+ --task) TASK_HASH="${2:-}"; shift 2 ;;
15
+ --queue) QUEUE_FILE="${2:-}"; shift 2 ;;
16
+ --message) MESSAGE_TEXT="${2:-}"; shift 2 ;;
17
+ --session) SESSION="${2:-}"; shift 2 ;;
18
+ --state-file) EXPLICIT_STATE_FILE="${2:-}"; shift 2 ;;
19
+ --) shift; break ;;
20
+ -*) echo "Unknown flag: $1" >&2; exit 1 ;;
21
+ *) break ;;
22
+ esac
23
+ done
24
+
25
+ if [[ -z "$TASK_HASH" && -z "$QUEUE_FILE" ]]; then
26
+ echo "Usage: tq-message --task <hash> --queue <file.yaml> [--message <text>] [--session <name>]" >&2
27
+ echo " tq-message --queue <file.yaml> [--message <text>]" >&2
28
+ exit 1
29
+ fi
30
+
31
+ if [[ -n "$QUEUE_FILE" ]]; then
32
+ QUEUE_FILE="$(realpath "$QUEUE_FILE")"
33
+ if [[ ! -f "$QUEUE_FILE" ]]; then
34
+ echo "Error: queue file not found: $QUEUE_FILE" >&2
35
+ exit 1
36
+ fi
37
+ fi
38
+
39
+ CONFIG_SCRIPT=$(mktemp /tmp/tq-message-config-XXXXXX.py)
40
+ trap 'rm -f "$CONFIG_SCRIPT"' EXIT
41
+
42
+ cat > "$CONFIG_SCRIPT" <<'PYEOF'
43
+ import sys, os, re, json
44
+
45
+ def parse_flat_block(text, top_key):
46
+ """Extract key: value pairs from an indented block under top_key."""
47
+ lines = text.split('\n')
48
+ in_block = False
49
+ block_indent = None
50
+ result = {}
51
+ for line in lines:
52
+ m = re.match(r'^' + re.escape(top_key) + r':\s*$', line)
53
+ if m:
54
+ in_block = True
55
+ block_indent = None
56
+ continue
57
+ if in_block:
58
+ if not line.strip():
59
+ continue
60
+ cur = len(line) - len(line.lstrip())
61
+ if block_indent is None:
62
+ block_indent = cur
63
+ if cur < block_indent:
64
+ break
65
+ kv = re.match(r'^\s+(\w+):\s*(.+)$', line)
66
+ if kv:
67
+ result[kv.group(1)] = kv.group(2).strip().strip('"\'')
68
+ return result
69
+
70
+ def parse_top_key(text, key):
71
+ """Extract a single top-level key: value."""
72
+ m = re.search(r'^' + re.escape(key) + r':\s*(.+)$', text, re.MULTILINE)
73
+ return m.group(1).strip().strip('"\'') if m else ''
74
+
75
+ # --- 1. Global config: ~/.tq/config/message.yaml ---
76
+ config = {}
77
+ global_path = os.path.expanduser('~/.tq/config/message.yaml')
78
+ if os.path.exists(global_path):
79
+ with open(global_path) as f:
80
+ g = f.read()
81
+ config['service'] = parse_top_key(g, 'default_service')
82
+ config['content'] = parse_top_key(g, 'content')
83
+ tg = parse_flat_block(g, 'telegram')
84
+ if tg.get('bot_token'): config['telegram_bot_token'] = tg['bot_token']
85
+ if tg.get('chat_id'): config['telegram_chat_id'] = tg['chat_id']
86
+ if tg.get('user_id'): config['telegram_chat_id'] = tg['user_id']
87
+ sl = parse_flat_block(g, 'slack')
88
+ if sl.get('webhook'): config['slack_webhook'] = sl['webhook']
89
+
90
+ # --- 2. Queue YAML message: block ---
91
+ queue_file = sys.argv[1] if len(sys.argv) > 1 else ''
92
+ if queue_file and os.path.exists(queue_file):
93
+ with open(queue_file) as f:
94
+ q = f.read()
95
+ msg = parse_flat_block(q, 'message')
96
+ if msg.get('service'): config['service'] = msg['service']
97
+ if msg.get('content'): config['content'] = msg['content']
98
+ if msg.get('chat_id'): config['telegram_chat_id'] = msg['chat_id']
99
+ if msg.get('webhook'): config['slack_webhook'] = msg['webhook']
100
+
101
+ # --- 3. Env var overrides ---
102
+ if os.environ.get('TQ_MESSAGE_SERVICE'): config['service'] = os.environ['TQ_MESSAGE_SERVICE']
103
+ if os.environ.get('TQ_MESSAGE_CONTENT'): config['content'] = os.environ['TQ_MESSAGE_CONTENT']
104
+ if os.environ.get('TQ_TELEGRAM_BOT_TOKEN'): config['telegram_bot_token'] = os.environ['TQ_TELEGRAM_BOT_TOKEN']
105
+ if os.environ.get('TQ_TELEGRAM_CHAT_ID'): config['telegram_chat_id'] = os.environ['TQ_TELEGRAM_CHAT_ID']
106
+ if os.environ.get('TQ_SLACK_WEBHOOK'): config['slack_webhook'] = os.environ['TQ_SLACK_WEBHOOK']
107
+
108
+ # Defaults
109
+ if not config.get('service'): config['service'] = 'telegram'
110
+ if not config.get('content'): config['content'] = 'summary'
111
+
112
+ print(json.dumps(config))
113
+ PYEOF
114
+
115
+ CONFIG_JSON="$(python3 "$CONFIG_SCRIPT" "${QUEUE_FILE:-}")"
116
+
117
+ SERVICE="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('service',''))" "$CONFIG_JSON")"
118
+ CONTENT="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('content',''))" "$CONFIG_JSON")"
119
+ TG_TOKEN="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('telegram_bot_token',''))" "$CONFIG_JSON")"
120
+ TG_CHAT="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('telegram_chat_id',''))" "$CONFIG_JSON")"
121
+
122
+ if [[ -z "$SERVICE" ]] || { [[ "$SERVICE" == "telegram" ]] && [[ -z "$TG_TOKEN" || -z "$TG_CHAT" ]]; }; then
123
+ # No messaging configured — exit silently
124
+ exit 0
125
+ fi
126
+
127
+ # Resolve state dir from queue file and task hash
128
+ if [[ -n "$QUEUE_FILE" ]]; then
129
+ QUEUE_DIR="$(dirname "$QUEUE_FILE")"
130
+ QUEUE_BASENAME="$(basename "$QUEUE_FILE" .yaml)"
131
+ STATE_DIR="$QUEUE_DIR/.tq/$QUEUE_BASENAME"
132
+ else
133
+ QUEUE_DIR=""
134
+ QUEUE_BASENAME=""
135
+ STATE_DIR=""
136
+ fi
137
+ if [[ -n "$TASK_HASH" && -n "$STATE_DIR" ]]; then
138
+ STATE_FILE="$STATE_DIR/$TASK_HASH"
139
+ PROMPT_FILE="$STATE_DIR/$TASK_HASH.prompt"
140
+ elif [[ -n "$EXPLICIT_STATE_FILE" ]]; then
141
+ STATE_FILE="$EXPLICIT_STATE_FILE"
142
+ PROMPT_FILE="${EXPLICIT_STATE_FILE}.prompt"
143
+ else
144
+ STATE_FILE=""
145
+ PROMPT_FILE=""
146
+ fi
147
+
148
+ build_message() {
149
+ local content_type="$1"
150
+ local hash="$2"
151
+ local state_file="$3"
152
+ local prompt_file="$4"
153
+ local session="$5"
154
+
155
+ local first_line status started duration msg
156
+
157
+ if [[ -f "$prompt_file" ]]; then
158
+ first_line="$(head -1 "$prompt_file")"
159
+ elif [[ -f "$state_file" ]]; then
160
+ first_line="$(grep '^prompt=' "$state_file" | cut -d= -f2-)"
161
+ else
162
+ first_line="(unknown task)"
163
+ fi
164
+
165
+ if [[ -f "$state_file" ]]; then
166
+ status="$(grep '^status=' "$state_file" | cut -d= -f2)"
167
+ started="$(grep '^started=' "$state_file" | cut -d= -f2)"
168
+ else
169
+ status="done"
170
+ started=""
171
+ fi
172
+
173
+ # Calculate duration in minutes
174
+ duration=""
175
+ if [[ -n "$started" ]]; then
176
+ start_epoch="$(python3 -c "import datetime; print(int(datetime.datetime.fromisoformat('${started}').timestamp()))" 2>/dev/null || echo "")"
177
+ if [[ -n "$start_epoch" ]]; then
178
+ now_epoch="$(date +%s)"
179
+ elapsed=$(( now_epoch - start_epoch ))
180
+ duration="${elapsed}s"
181
+ if (( elapsed >= 60 )); then
182
+ duration="$(( elapsed / 60 ))m $(( elapsed % 60 ))s"
183
+ fi
184
+ fi
185
+ fi
186
+
187
+ case "$content_type" in
188
+ status)
189
+ msg="tq: task ${status}
190
+ ${first_line:0:100}
191
+ ${duration:+Duration: $duration}"
192
+ ;;
193
+ details)
194
+ msg="tq: task ${status} [${hash}]
195
+ Prompt: ${first_line:0:200}
196
+ ${duration:+Duration: $duration}"
197
+ ;;
198
+ log)
199
+ if [[ -n "$session" ]] && tmux has-session -t "$session" 2>/dev/null; then
200
+ local pane_text
201
+ pane_text="$(tmux capture-pane -t "$session" -p -S -200 2>/dev/null || echo "(could not capture pane)")"
202
+ msg="tq: task ${status} [${hash}]
203
+ ${first_line:0:80}
204
+
205
+ Last output:
206
+ \`\`\`
207
+ ${pane_text: -1500}
208
+ \`\`\`"
209
+ else
210
+ msg="tq: task ${status} [${hash}]
211
+ ${first_line:0:80}
212
+ (session no longer active — log unavailable)"
213
+ fi
214
+ ;;
215
+ *)
216
+ msg="(unknown content type: $content_type)"
217
+ ;;
218
+ esac
219
+
220
+ echo "$msg"
221
+ }
222
+
223
+ build_queue_message() {
224
+ local queue_basename="$1"
225
+ local state_dir="$2"
226
+ local total=0 done_count=0
227
+
228
+ if [[ -d "$state_dir" ]]; then
229
+ for sf in "$state_dir"/*; do
230
+ [[ -f "$sf" ]] || continue
231
+ [[ "$sf" == *.prompt ]] && continue
232
+ [[ "$sf" == *.launch.py ]] && continue
233
+ [[ "$(basename "$sf")" == .queue-notified ]] && continue
234
+ total=$(( total + 1 ))
235
+ local s
236
+ s="$(grep '^status=' "$sf" | cut -d= -f2)"
237
+ [[ "$s" == "done" ]] && done_count=$(( done_count + 1 ))
238
+ done
239
+ fi
240
+
241
+ echo "tq: queue complete
242
+ ${queue_basename}.yaml (${done_count}/${total} tasks done)"
243
+ }
244
+
245
+ # If --message provided directly (e.g. from /tq-message slash command), skip generation
246
+ if [[ -z "$MESSAGE_TEXT" ]]; then
247
+ # Queue-level notification (no specific task)
248
+ if [[ -z "$TASK_HASH" && -n "$QUEUE_BASENAME" ]]; then
249
+ MESSAGE_TEXT="$(build_queue_message "$QUEUE_BASENAME" "$STATE_DIR")"
250
+ else
251
+ case "$CONTENT" in
252
+ summary)
253
+ # summary mode: handled by send-keys in Task 4
254
+ # if no session available, fall back to details
255
+ if [[ -z "$SESSION" ]]; then
256
+ MESSAGE_TEXT="$(build_message "details" "$TASK_HASH" "$STATE_FILE" "$PROMPT_FILE" "")"
257
+ fi
258
+ ;;
259
+ status|details|log)
260
+ MESSAGE_TEXT="$(build_message "$CONTENT" "$TASK_HASH" "$STATE_FILE" "$PROMPT_FILE" "$SESSION")"
261
+ ;;
262
+ *)
263
+ echo "Error: unknown content type: $CONTENT" >&2
264
+ exit 1
265
+ ;;
266
+ esac
267
+ fi
268
+ fi
269
+
270
+ send_telegram() {
271
+ local token="$1"
272
+ local chat_id="$2"
273
+ local text="$3"
274
+
275
+ local response
276
+ response="$(curl -s -X POST \
277
+ "https://api.telegram.org/bot${token}/sendMessage" \
278
+ -d "chat_id=${chat_id}" \
279
+ --data-urlencode "text=${text}" \
280
+ -d "parse_mode=Markdown" 2>&1)"
281
+
282
+ if echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('ok') else 1)" 2>/dev/null; then
283
+ echo "[tq-message] sent via Telegram"
284
+ else
285
+ echo "[tq-message] Telegram error: $response" >&2
286
+ exit 1
287
+ fi
288
+ }
289
+
290
+ deliver() {
291
+ local service="$1"
292
+ local msg="$2"
293
+ case "$service" in
294
+ telegram) send_telegram "$TG_TOKEN" "$TG_CHAT" "$msg" ;;
295
+ *) echo "[tq-message] unknown service: $service" >&2; exit 1 ;;
296
+ esac
297
+ }
298
+
299
+ # Summary mode with a live session: ask Claude to generate and send
300
+ if [[ "$CONTENT" == "summary" && -n "$SESSION" && -z "$MESSAGE_TEXT" ]]; then
301
+ if tmux has-session -t "$SESSION" 2>/dev/null; then
302
+ # Pass hash and queue path as arguments to the slash command
303
+ tmux send-keys -t "$SESSION" "/tq-message ${TASK_HASH} ${QUEUE_FILE}" Enter
304
+ # Give Claude time to summarize and call back tq-message --message
305
+ sleep 45
306
+ # Close the session
307
+ tmux send-keys -t "$SESSION" "" Enter
308
+ exit 0
309
+ else
310
+ # Session gone — fall back to details
311
+ MESSAGE_TEXT="$(build_message "details" "$TASK_HASH" "$STATE_FILE" "$PROMPT_FILE" "")"
312
+ fi
313
+ fi
314
+
315
+ # Deliver message if we have one
316
+ if [[ -n "$MESSAGE_TEXT" ]]; then
317
+ deliver "$SERVICE" "$MESSAGE_TEXT"
318
+ fi
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
5
+
6
+ CONFIG_FILE="$HOME/.tq/config/message.yaml"
7
+
8
+ # --- Overwrite guard ---
9
+ if [[ -f "$CONFIG_FILE" ]]; then
10
+ printf '%s already exists.\nOverwrite? [y/N]: ' "$CONFIG_FILE"
11
+ read -r OVERWRITE
12
+ if [[ "${OVERWRITE,,}" != "y" ]]; then
13
+ exit 0
14
+ fi
15
+ fi
16
+
17
+ echo ""
18
+ echo "Setting up Telegram notifications for tq."
19
+ echo ""
20
+ echo "You'll need a bot token from @BotFather (https://t.me/BotFather — send /newbot)."
21
+ echo ""
22
+
23
+ # --- Bot token ---
24
+ printf 'Bot token: '
25
+ read -rs BOT_TOKEN
26
+ echo ""
27
+
28
+ # --- Auto-discover user_id ---
29
+ echo ""
30
+ echo "Send any message to your bot now, then press Enter..."
31
+ read -r _IGNORED
32
+
33
+ UPDATES="$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=0&limit=10&timeout=0" 2>&1)"
34
+
35
+ DISCOVER_SCRIPT=$(mktemp /tmp/tq-setup-discover-XXXXXX.py)
36
+ trap 'rm -f "$DISCOVER_SCRIPT"' EXIT
37
+
38
+ cat > "$DISCOVER_SCRIPT" <<'PYEOF'
39
+ import sys, json
40
+ data = json.loads(sys.argv[1])
41
+ if not data.get('ok'):
42
+ print('ERROR: ' + data.get('description', 'unknown error'))
43
+ sys.exit(1)
44
+ results = data.get('result', [])
45
+ for update in results:
46
+ msg = update.get('message') or update.get('edited_message') or update.get('channel_post')
47
+ if msg:
48
+ from_id = msg.get('from', {}).get('id') or msg.get('sender_chat', {}).get('id')
49
+ if from_id:
50
+ print(str(from_id))
51
+ sys.exit(0)
52
+ print('NONE')
53
+ PYEOF
54
+
55
+ USER_ID_RAW="$(python3 "$DISCOVER_SCRIPT" "$UPDATES")"
56
+
57
+ if [[ "$USER_ID_RAW" == NONE ]]; then
58
+ echo "No message found. Make sure you sent a message to your bot, then try again." >&2
59
+ exit 1
60
+ fi
61
+ if [[ "$USER_ID_RAW" == ERROR* ]]; then
62
+ echo "Telegram error: ${USER_ID_RAW#ERROR: }" >&2
63
+ exit 1
64
+ fi
65
+
66
+ USER_ID="$USER_ID_RAW"
67
+ echo "Found your user ID: $USER_ID"
68
+
69
+ # --- Content type ---
70
+ echo ""
71
+ echo "Content type (what tq sends when a task finishes):"
72
+ echo " status — task name + done/failed + duration [default]"
73
+ echo " summary — Claude writes a 2-3 sentence digest (requires live session)"
74
+ printf 'Content [status]: '
75
+ read -r CONTENT_TYPE
76
+ CONTENT_TYPE="${CONTENT_TYPE:-status}"
77
+ if [[ "$CONTENT_TYPE" != "status" && "$CONTENT_TYPE" != "summary" ]]; then
78
+ echo "Invalid content type '$CONTENT_TYPE' — using 'status'" >&2
79
+ CONTENT_TYPE="status"
80
+ fi
81
+
82
+ # --- Test message ---
83
+ echo ""
84
+ echo "Sending test message..."
85
+
86
+ TEST_RESPONSE="$(curl -s -X POST \
87
+ "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
88
+ -d "chat_id=${USER_ID}" \
89
+ --data-urlencode "text=tq is configured. Notifications are working." \
90
+ -d "parse_mode=Markdown" 2>&1)"
91
+
92
+ if echo "$TEST_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('ok') else 1)" 2>/dev/null; then
93
+ echo "Test message sent. Check your Telegram."
94
+ else
95
+ echo "Error: Telegram rejected the request:" >&2
96
+ echo "$TEST_RESPONSE" >&2
97
+ echo "" >&2
98
+ echo "Config NOT written. Fix the token and try again." >&2
99
+ exit 1
100
+ fi
101
+
102
+ # --- Write config ---
103
+ mkdir -p "$HOME/.tq/config"
104
+ cat > "$CONFIG_FILE" <<EOF
105
+ default_service: telegram
106
+ content: ${CONTENT_TYPE}
107
+
108
+ telegram:
109
+ bot_token: "${BOT_TOKEN}"
110
+ user_id: "${USER_ID}"
111
+ EOF
112
+
113
+ # --- Create workspace ---
114
+ mkdir -p "$HOME/.tq/workspace"
115
+ mkdir -p "$HOME/.tq/logs"
116
+
117
+ echo ""
118
+ echo "Config written to $CONFIG_FILE"
119
+ echo "Workspace: $HOME/.tq/workspace/ (created)"
120
+
121
+ # --- Install crons ---
122
+ POLL_BIN="$(command -v tq-telegram-poll 2>/dev/null || echo "/opt/homebrew/bin/tq-telegram-poll")"
123
+ WATCHDOG_BIN="$(command -v tq-telegram-watchdog 2>/dev/null || echo "/opt/homebrew/bin/tq-telegram-watchdog")"
124
+ POLL_CRON="* * * * * ${POLL_BIN} >> $HOME/.tq/logs/tq-telegram.log 2>&1"
125
+ WATCHDOG_CRON="*/5 * * * * ${WATCHDOG_BIN} >> $HOME/.tq/logs/tq-telegram.log 2>&1"
126
+
127
+ CRONTAB_CURRENT="$(crontab -l 2>/dev/null || true)"
128
+
129
+ CRONTAB_NEW="$CRONTAB_CURRENT"
130
+ if ! echo "$CRONTAB_CURRENT" | grep -qF "tq-telegram-poll"; then
131
+ CRONTAB_NEW="${CRONTAB_NEW}${CRONTAB_NEW:+$'\n'}${POLL_CRON}"
132
+ echo "Cron installed: tq-telegram-poll (every minute)"
133
+ else
134
+ echo "Cron already present: tq-telegram-poll"
135
+ fi
136
+
137
+ if ! echo "$CRONTAB_CURRENT" | grep -qF "tq-telegram-watchdog"; then
138
+ CRONTAB_NEW="${CRONTAB_NEW}${CRONTAB_NEW:+$'\n'}${WATCHDOG_CRON}"
139
+ echo "Cron installed: tq-telegram-watchdog (every 5 minutes)"
140
+ else
141
+ echo "Cron already present: tq-telegram-watchdog"
142
+ fi
143
+
144
+ echo "$CRONTAB_NEW" | crontab -
145
+
146
+ echo ""
147
+ echo "Run tq-setup again at any time to reconfigure."
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
5
+
6
+ CONFIG_FILE="$HOME/.tq/config/message.yaml"
7
+ OFFSET_FILE="$HOME/.tq/telegram-poll-offset"
8
+
9
+ # --- Read config ---
10
+ if [[ ! -f "$CONFIG_FILE" ]]; then
11
+ exit 0
12
+ fi
13
+
14
+ CONFIG_SCRIPT=$(mktemp /tmp/tq-poll-config-XXXXXX.py)
15
+ trap 'rm -f "$CONFIG_SCRIPT"' EXIT
16
+
17
+ cat > "$CONFIG_SCRIPT" <<'PYEOF'
18
+ import sys, os, re, json
19
+
20
+ def parse_flat_block(text, top_key):
21
+ lines = text.split('\n')
22
+ in_block = False
23
+ block_indent = None
24
+ result = {}
25
+ for line in lines:
26
+ if re.match(r'^' + re.escape(top_key) + r':\s*$', line):
27
+ in_block = True
28
+ block_indent = None
29
+ continue
30
+ if in_block:
31
+ if not line.strip():
32
+ continue
33
+ cur = len(line) - len(line.lstrip())
34
+ if block_indent is None:
35
+ block_indent = cur
36
+ if cur < block_indent:
37
+ break
38
+ kv = re.match(r'^\s+(\w+):\s*(.+)$', line)
39
+ if kv:
40
+ result[kv.group(1)] = kv.group(2).strip().strip('"\'')
41
+ return result
42
+
43
+ config_path = sys.argv[1]
44
+ with open(config_path) as f:
45
+ text = f.read()
46
+
47
+ tg = parse_flat_block(text, 'telegram')
48
+ print(json.dumps({
49
+ 'bot_token': tg.get('bot_token', ''),
50
+ 'user_id': tg.get('user_id', ''),
51
+ }))
52
+ PYEOF
53
+
54
+ CONFIG_JSON="$(python3 "$CONFIG_SCRIPT" "$CONFIG_FILE")"
55
+ BOT_TOKEN="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('bot_token',''))" "$CONFIG_JSON")"
56
+ USER_ID="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('user_id',''))" "$CONFIG_JSON")"
57
+
58
+ if [[ -z "$BOT_TOKEN" || -z "$USER_ID" ]]; then
59
+ exit 0
60
+ fi
61
+
62
+ # --- Read offset ---
63
+ OFFSET=0
64
+ if [[ -f "$OFFSET_FILE" ]]; then
65
+ OFFSET="$(cat "$OFFSET_FILE")"
66
+ fi
67
+
68
+ # --- Fetch updates ---
69
+ UPDATES="$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=${OFFSET}&limit=10&timeout=0" 2>&1)"
70
+
71
+ # --- Process updates ---
72
+ PROCESS_SCRIPT=$(mktemp /tmp/tq-poll-process-XXXXXX.py)
73
+ trap 'rm -f "$CONFIG_SCRIPT" "$PROCESS_SCRIPT"' EXIT
74
+
75
+ cat > "$PROCESS_SCRIPT" <<'PYEOF'
76
+ import sys, json
77
+
78
+ data = json.loads(sys.argv[1])
79
+ user_id = int(sys.argv[2])
80
+ offset_file = sys.argv[3]
81
+
82
+ if not data.get('ok'):
83
+ sys.exit(0)
84
+
85
+ results = data.get('result', [])
86
+ new_offset = None
87
+ messages = []
88
+
89
+ for update in results:
90
+ update_id = update['update_id']
91
+ new_offset = update_id + 1
92
+
93
+ msg = update.get('message') or update.get('edited_message')
94
+ if not msg:
95
+ continue
96
+ from_id = msg.get('from', {}).get('id')
97
+ if from_id != user_id:
98
+ continue
99
+ text = msg.get('text', '').strip()
100
+ if text:
101
+ chat_id = msg['chat']['id']
102
+ message_id = msg['message_id']
103
+ # Output as tab-separated: chat_id<TAB>message_id<TAB>text
104
+ messages.append(f"{chat_id}\t{message_id}\t{text}")
105
+
106
+ if new_offset is not None:
107
+ with open(offset_file, 'w') as f:
108
+ f.write(str(new_offset))
109
+
110
+ for m in messages:
111
+ print(m)
112
+ PYEOF
113
+
114
+ MESSAGES="$(python3 "$PROCESS_SCRIPT" "$UPDATES" "$USER_ID" "$OFFSET_FILE")"
115
+
116
+ # --- Spawn tq --prompt for each message ---
117
+ mkdir -p "$HOME/.tq/workspace"
118
+
119
+ while IFS=$'\t' read -r CHAT_ID MSG_ID MSG; do
120
+ if [[ -n "$MSG" ]]; then
121
+ echo "[tq-telegram-poll] prompt: ${MSG:0:60}"
122
+ # React with 👀 to acknowledge receipt
123
+ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setMessageReaction" \
124
+ -H "Content-Type: application/json" \
125
+ --data-raw "{\"chat_id\":${CHAT_ID},\"message_id\":${MSG_ID},\"reaction\":[{\"type\":\"emoji\",\"emoji\":\"👀\"}]}" \
126
+ > /dev/null 2>&1 || true
127
+ tq --prompt "$MSG"
128
+ fi
129
+ done <<< "$MESSAGES"
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
5
+
6
+ CONFIG_FILE="$HOME/.tq/config/message.yaml"
7
+
8
+ # Exit silently if not configured
9
+ if [[ ! -f "$CONFIG_FILE" ]]; then
10
+ exit 0
11
+ fi
12
+
13
+ POLL_BIN="$(command -v tq-telegram-poll 2>/dev/null || true)"
14
+ if [[ -z "$POLL_BIN" ]]; then
15
+ echo "[tq-telegram-watchdog] tq-telegram-poll not found in PATH" >&2
16
+ exit 0
17
+ fi
18
+
19
+ if ! crontab -l 2>/dev/null | grep -qF "tq-telegram-poll"; then
20
+ echo "[tq-telegram-watchdog] poll cron missing — restoring"
21
+ mkdir -p "$HOME/.tq/logs"
22
+ (crontab -l 2>/dev/null; echo "* * * * * ${POLL_BIN} >> $HOME/.tq/logs/tq-telegram.log 2>&1") | crontab -
23
+ fi