@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.
- package/README.md +296 -0
- package/package.json +41 -0
- package/scripts/tq +419 -0
- package/scripts/tq-install.sh +78 -0
- package/scripts/tq-message +318 -0
- package/scripts/tq-setup +147 -0
- package/scripts/tq-telegram-poll +129 -0
- package/scripts/tq-telegram-watchdog +23 -0
- package/skills/tq/SKILL.md +108 -0
- package/skills/tq/references/cron-expressions.md +67 -0
- package/skills/tq/references/session-naming.md +50 -0
|
@@ -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
|
package/scripts/tq-setup
ADDED
|
@@ -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
|