@codefilabs/tq 0.0.2 → 0.1.0
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 +100 -3
- package/package.json +4 -1
- package/scripts/tq +126 -17
- package/scripts/tq-converse +726 -0
- package/scripts/tq-cron-sync +92 -0
- package/scripts/tq-install.sh +23 -9
- package/scripts/tq-message +96 -27
- package/scripts/tq-setup +1 -1
- package/scripts/tq-telegram-poll +167 -15
- package/skills/tq/SKILL.md +42 -6
- package/skills/tq/references/session-naming.md +16 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Ensure homebrew binaries are available (cron has minimal PATH)
|
|
5
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
6
|
+
|
|
7
|
+
INTERVAL=20
|
|
8
|
+
|
|
9
|
+
while [[ $# -gt 0 ]]; do
|
|
10
|
+
case "${1:-}" in
|
|
11
|
+
--interval)
|
|
12
|
+
if [[ $# -lt 2 ]]; then
|
|
13
|
+
echo "Error: --interval requires a value" >&2; exit 1
|
|
14
|
+
fi
|
|
15
|
+
INTERVAL="$2"; shift; shift ;;
|
|
16
|
+
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
QUEUES_DIR="${HOME}/.tq/queues"
|
|
21
|
+
LOGS_DIR="${HOME}/.tq/logs"
|
|
22
|
+
mkdir -p "$QUEUES_DIR" "$LOGS_DIR"
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Scan ~/.tq/queues/*.yaml for schedule: keys via embedded Python temp file
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
SCAN_SCRIPT=$(mktemp /tmp/tq-scan-XXXXXX)
|
|
28
|
+
CRONTAB_NEW=$(mktemp /tmp/tq-crontab-XXXXXX)
|
|
29
|
+
trap 'rm -f "$SCAN_SCRIPT" "$CRONTAB_NEW"' EXIT
|
|
30
|
+
|
|
31
|
+
cat > "$SCAN_SCRIPT" <<'PYEOF'
|
|
32
|
+
import sys, os, re, json, glob
|
|
33
|
+
|
|
34
|
+
queues_dir = sys.argv[1]
|
|
35
|
+
results = []
|
|
36
|
+
|
|
37
|
+
for yaml_path in sorted(glob.glob(os.path.join(queues_dir, '*.yaml'))):
|
|
38
|
+
name = os.path.basename(yaml_path)[:-5] # strip .yaml
|
|
39
|
+
schedule = None
|
|
40
|
+
try:
|
|
41
|
+
with open(yaml_path) as f:
|
|
42
|
+
for line in f:
|
|
43
|
+
m = re.match(r'^schedule:\s*["\']?([^"\'#\n]+?)["\']?\s*$', line)
|
|
44
|
+
if m:
|
|
45
|
+
schedule = m.group(1).strip()
|
|
46
|
+
break
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
if schedule:
|
|
50
|
+
results.append({'name': name, 'schedule': schedule, 'path': yaml_path})
|
|
51
|
+
|
|
52
|
+
for r in results:
|
|
53
|
+
print(json.dumps(r))
|
|
54
|
+
PYEOF
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Detect installed tq binary path
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
if [[ -x "/opt/homebrew/bin/tq" ]]; then
|
|
60
|
+
TQ_BIN="/opt/homebrew/bin/tq"
|
|
61
|
+
SELF_BIN="/opt/homebrew/bin/tq-cron-sync"
|
|
62
|
+
elif [[ -x "/usr/local/bin/tq" ]]; then
|
|
63
|
+
TQ_BIN="/usr/local/bin/tq"
|
|
64
|
+
SELF_BIN="/usr/local/bin/tq-cron-sync"
|
|
65
|
+
else
|
|
66
|
+
echo "Error: tq not found in /opt/homebrew/bin or /usr/local/bin" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Read existing crontab, strip all tq-managed lines into temp file
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
{ crontab -l 2>/dev/null || true; } | grep -v '# tq-managed:' > "$CRONTAB_NEW" || true
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Append new managed lines for each scheduled queue
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
while IFS= read -r JSON_LINE; do
|
|
79
|
+
[[ -z "$JSON_LINE" ]] && continue
|
|
80
|
+
NAME="$(python3 -c "import sys,json; print(json.loads(sys.argv[1])['name'])" "$JSON_LINE")"
|
|
81
|
+
SCHEDULE="$(python3 -c "import sys,json; print(json.loads(sys.argv[1])['schedule'])" "$JSON_LINE")"
|
|
82
|
+
YAML_PATH="$(python3 -c "import sys,json; print(json.loads(sys.argv[1])['path'])" "$JSON_LINE")"
|
|
83
|
+
echo "${SCHEDULE} ${TQ_BIN} ${YAML_PATH} >> ${LOGS_DIR}/tq.log 2>&1 # tq-managed:${NAME}:run" >> "$CRONTAB_NEW"
|
|
84
|
+
echo "*/30 * * * * ${TQ_BIN} --status ${YAML_PATH} >> ${LOGS_DIR}/tq.log 2>&1 # tq-managed:${NAME}:status" >> "$CRONTAB_NEW"
|
|
85
|
+
done < <(python3 "$SCAN_SCRIPT" "$QUEUES_DIR")
|
|
86
|
+
|
|
87
|
+
# Self-watcher entry
|
|
88
|
+
echo "*/${INTERVAL} * * * * ${SELF_BIN} >> ${LOGS_DIR}/tq-cron-sync.log 2>&1 # tq-managed:tq-cron-sync" >> "$CRONTAB_NEW"
|
|
89
|
+
|
|
90
|
+
# Write merged crontab
|
|
91
|
+
crontab "$CRONTAB_NEW"
|
|
92
|
+
echo "tq-cron-sync: crontab synced ($(grep -c '# tq-managed:' "$CRONTAB_NEW" || true) managed entries)"
|
package/scripts/tq-install.sh
CHANGED
|
@@ -5,10 +5,10 @@ set -euo pipefail
|
|
|
5
5
|
# Step 1: Register marketplace and install Claude plugin
|
|
6
6
|
# ---------------------------------------------------------------------------
|
|
7
7
|
if command -v claude &>/dev/null; then
|
|
8
|
-
echo "Adding
|
|
9
|
-
claude plugin marketplace add
|
|
8
|
+
echo "Adding codefilabs marketplace..."
|
|
9
|
+
claude plugin marketplace add codefilabs/marketplace
|
|
10
10
|
echo "Installing tq plugin..."
|
|
11
|
-
claude plugin install tq@
|
|
11
|
+
claude plugin install tq@codefilabs
|
|
12
12
|
else
|
|
13
13
|
echo "Warning: 'claude' CLI not found — skipping plugin registration." >&2
|
|
14
14
|
echo " Install Claude Code first: https://claude.ai/code" >&2
|
|
@@ -24,11 +24,11 @@ elif [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then
|
|
|
24
24
|
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
25
25
|
else
|
|
26
26
|
# Running via curl | bash — find the installed plugin cache
|
|
27
|
-
CACHE_DIR="$HOME/.claude/plugins/cache/tq/
|
|
27
|
+
CACHE_DIR="$HOME/.claude/plugins/cache/tq/codefilabs"
|
|
28
28
|
PLUGIN_ROOT="$(ls -d "$CACHE_DIR"/*/ 2>/dev/null | sort -V | tail -1)"
|
|
29
29
|
if [[ -z "${PLUGIN_ROOT:-}" ]]; then
|
|
30
30
|
echo "Error: could not locate tq plugin files in $CACHE_DIR" >&2
|
|
31
|
-
echo " Try running: claude plugin marketplace add
|
|
31
|
+
echo " Try running: claude plugin marketplace add codefilabs/marketplace && claude plugin install tq@codefilabs" >&2
|
|
32
32
|
exit 1
|
|
33
33
|
fi
|
|
34
34
|
fi
|
|
@@ -48,7 +48,7 @@ else
|
|
|
48
48
|
exit 1
|
|
49
49
|
fi
|
|
50
50
|
|
|
51
|
-
for SCRIPT in tq tq-message tq-setup tq-telegram-poll tq-telegram-watchdog; do
|
|
51
|
+
for SCRIPT in tq tq-message tq-setup tq-telegram-poll tq-telegram-watchdog tq-cron-sync tq-converse; do
|
|
52
52
|
SRC="$PLUGIN_ROOT/scripts/$SCRIPT"
|
|
53
53
|
DEST="$INSTALL_DIR/$SCRIPT"
|
|
54
54
|
if [[ -L "$DEST" ]]; then
|
|
@@ -63,11 +63,21 @@ done
|
|
|
63
63
|
|
|
64
64
|
mkdir -p ~/.tq/queues ~/.tq/logs ~/.tq/config
|
|
65
65
|
|
|
66
|
+
# Run initial cron sync to pick up any existing queue schedules
|
|
67
|
+
# Use $INSTALL_DIR directly — PATH may not reflect the just-created symlink yet
|
|
68
|
+
"$INSTALL_DIR/tq-cron-sync" --interval 20
|
|
69
|
+
|
|
70
|
+
echo ""
|
|
71
|
+
echo "tq installed. Cron schedules are managed automatically."
|
|
66
72
|
echo ""
|
|
67
|
-
echo "
|
|
73
|
+
echo "Add a schedule: key to any queue file in ~/.tq/queues/ to auto-schedule it:"
|
|
68
74
|
echo ""
|
|
69
|
-
echo " 0 9 * *
|
|
70
|
-
echo "
|
|
75
|
+
echo " schedule: \"0 9 * * *\" # runs daily at 9am"
|
|
76
|
+
echo " cwd: /path/to/project"
|
|
77
|
+
echo " tasks: ..."
|
|
78
|
+
echo ""
|
|
79
|
+
echo "tq-cron-sync runs every 20 minutes and syncs all queue schedules to crontab."
|
|
80
|
+
echo "To change the sync interval: tq-cron-sync --interval <minutes>"
|
|
71
81
|
echo ""
|
|
72
82
|
echo "To configure Telegram notifications:"
|
|
73
83
|
echo " tq-setup"
|
|
@@ -76,3 +86,7 @@ echo "Or from Claude Code: /setup-telegram"
|
|
|
76
86
|
echo ""
|
|
77
87
|
echo "To relay Telegram messages as tq tasks, add to crontab:"
|
|
78
88
|
echo " * * * * * /opt/homebrew/bin/tq-telegram-poll >> ~/.tq/logs/tq-telegram.log 2>&1"
|
|
89
|
+
echo ""
|
|
90
|
+
echo "Conversation mode (interactive Telegram <-> Claude Code):"
|
|
91
|
+
echo " tq-converse start [--cwd /path/to/project]"
|
|
92
|
+
echo " Or send /converse from Telegram"
|
package/scripts/tq-message
CHANGED
|
@@ -8,6 +8,7 @@ QUEUE_FILE=""
|
|
|
8
8
|
MESSAGE_TEXT=""
|
|
9
9
|
SESSION=""
|
|
10
10
|
EXPLICIT_STATE_FILE=""
|
|
11
|
+
REPLY_TO_MSG_ID=""
|
|
11
12
|
|
|
12
13
|
while [[ $# -gt 0 ]]; do
|
|
13
14
|
case "${1:-}" in
|
|
@@ -16,15 +17,18 @@ while [[ $# -gt 0 ]]; do
|
|
|
16
17
|
--message) MESSAGE_TEXT="${2:-}"; shift 2 ;;
|
|
17
18
|
--session) SESSION="${2:-}"; shift 2 ;;
|
|
18
19
|
--state-file) EXPLICIT_STATE_FILE="${2:-}"; shift 2 ;;
|
|
20
|
+
--reply-to) REPLY_TO_MSG_ID="${2:-}"; shift 2 ;;
|
|
19
21
|
--) shift; break ;;
|
|
20
22
|
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
|
21
23
|
*) break ;;
|
|
22
24
|
esac
|
|
23
25
|
done
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
# Allow --message without --task/--queue (conversation mode)
|
|
28
|
+
if [[ -z "$TASK_HASH" && -z "$QUEUE_FILE" && -z "$MESSAGE_TEXT" ]]; then
|
|
26
29
|
echo "Usage: tq-message --task <hash> --queue <file.yaml> [--message <text>] [--session <name>]" >&2
|
|
27
30
|
echo " tq-message --queue <file.yaml> [--message <text>]" >&2
|
|
31
|
+
echo " tq-message --message <text> [--reply-to <msg_id>]" >&2
|
|
28
32
|
exit 1
|
|
29
33
|
fi
|
|
30
34
|
|
|
@@ -36,7 +40,7 @@ if [[ -n "$QUEUE_FILE" ]]; then
|
|
|
36
40
|
fi
|
|
37
41
|
fi
|
|
38
42
|
|
|
39
|
-
CONFIG_SCRIPT=$(mktemp /tmp/tq-message-config-XXXXXX
|
|
43
|
+
CONFIG_SCRIPT=$(mktemp /tmp/tq-message-config-XXXXXX)
|
|
40
44
|
trap 'rm -f "$CONFIG_SCRIPT"' EXIT
|
|
41
45
|
|
|
42
46
|
cat > "$CONFIG_SCRIPT" <<'PYEOF'
|
|
@@ -165,21 +169,25 @@ build_message() {
|
|
|
165
169
|
if [[ -f "$state_file" ]]; then
|
|
166
170
|
status="$(grep '^status=' "$state_file" | cut -d= -f2)"
|
|
167
171
|
started="$(grep '^started=' "$state_file" | cut -d= -f2)"
|
|
172
|
+
completed_epoch="$(grep '^completed=' "$state_file" | cut -d= -f2)"
|
|
168
173
|
else
|
|
169
174
|
status="done"
|
|
170
175
|
started=""
|
|
176
|
+
completed_epoch=""
|
|
171
177
|
fi
|
|
172
178
|
|
|
173
|
-
# Calculate duration
|
|
179
|
+
# Calculate duration using completed epoch (avoids timezone skew from ISO string parsing)
|
|
174
180
|
duration=""
|
|
175
181
|
if [[ -n "$started" ]]; then
|
|
176
|
-
start_epoch="$(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
elapsed=$((
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
start_epoch="$(date -j -f "%Y-%m-%dT%H:%M:%S" "${started}" "+%s" 2>/dev/null || echo "")"
|
|
183
|
+
end_epoch="${completed_epoch:-$(date +%s)}"
|
|
184
|
+
if [[ -n "$start_epoch" && -n "$end_epoch" ]]; then
|
|
185
|
+
elapsed=$(( end_epoch - start_epoch ))
|
|
186
|
+
if (( elapsed > 0 )); then
|
|
187
|
+
duration="${elapsed}s"
|
|
188
|
+
if (( elapsed >= 60 )); then
|
|
189
|
+
duration="$(( elapsed / 60 ))m $(( elapsed % 60 ))s"
|
|
190
|
+
fi
|
|
183
191
|
fi
|
|
184
192
|
fi
|
|
185
193
|
fi
|
|
@@ -271,16 +279,50 @@ send_telegram() {
|
|
|
271
279
|
local token="$1"
|
|
272
280
|
local chat_id="$2"
|
|
273
281
|
local text="$3"
|
|
282
|
+
local reply_to="${4:-}"
|
|
283
|
+
|
|
284
|
+
local curl_args=()
|
|
285
|
+
curl_args+=(-s -X POST "https://api.telegram.org/bot${token}/sendMessage")
|
|
286
|
+
curl_args+=(-d "chat_id=${chat_id}")
|
|
287
|
+
curl_args+=(--data-urlencode "text=${text}")
|
|
288
|
+
curl_args+=(-d "parse_mode=Markdown")
|
|
289
|
+
|
|
290
|
+
# Add reply threading if a reply_to message ID is provided
|
|
291
|
+
if [[ -n "$reply_to" ]]; then
|
|
292
|
+
curl_args+=(-d "reply_to_message_id=${reply_to}")
|
|
293
|
+
curl_args+=(-d "allow_sending_without_reply=true")
|
|
294
|
+
fi
|
|
274
295
|
|
|
275
296
|
local response
|
|
276
|
-
response="$(curl
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
297
|
+
response="$(curl "${curl_args[@]}" 2>&1)"
|
|
298
|
+
|
|
299
|
+
# Extract the sent message ID for reply threading tracking
|
|
300
|
+
local sent_msg_id
|
|
301
|
+
sent_msg_id="$(echo "$response" | python3 -c "
|
|
302
|
+
import sys, json
|
|
303
|
+
try:
|
|
304
|
+
d = json.load(sys.stdin)
|
|
305
|
+
if d.get('ok'):
|
|
306
|
+
print(d.get('result', {}).get('message_id', ''))
|
|
307
|
+
else:
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
except:
|
|
310
|
+
sys.exit(1)
|
|
311
|
+
" 2>/dev/null || true)"
|
|
312
|
+
|
|
313
|
+
if [[ -n "$sent_msg_id" ]]; then
|
|
314
|
+
echo "[tq-message] sent via Telegram (msg_id=$sent_msg_id)"
|
|
315
|
+
# Track outgoing message ID in registry for reply threading
|
|
316
|
+
if [[ -n "$sent_msg_id" ]] && command -v tq-converse &>/dev/null; then
|
|
317
|
+
# Try to detect which slug this message belongs to
|
|
318
|
+
local slug_file="$HOME/.tq/conversations/latest-reply-slug"
|
|
319
|
+
if [[ -f "$slug_file" ]]; then
|
|
320
|
+
local slug
|
|
321
|
+
slug="$(cat "$slug_file")"
|
|
322
|
+
tq-converse track-msg "$slug" "$sent_msg_id" 2>/dev/null || true
|
|
323
|
+
rm -f "$slug_file"
|
|
324
|
+
fi
|
|
325
|
+
fi
|
|
284
326
|
else
|
|
285
327
|
echo "[tq-message] Telegram error: $response" >&2
|
|
286
328
|
exit 1
|
|
@@ -290,29 +332,56 @@ send_telegram() {
|
|
|
290
332
|
deliver() {
|
|
291
333
|
local service="$1"
|
|
292
334
|
local msg="$2"
|
|
335
|
+
local reply_to="${3:-}"
|
|
293
336
|
case "$service" in
|
|
294
|
-
telegram) send_telegram "$TG_TOKEN" "$TG_CHAT" "$msg" ;;
|
|
337
|
+
telegram) send_telegram "$TG_TOKEN" "$TG_CHAT" "$msg" "$reply_to" ;;
|
|
295
338
|
*) echo "[tq-message] unknown service: $service" >&2; exit 1 ;;
|
|
296
339
|
esac
|
|
297
340
|
}
|
|
298
341
|
|
|
299
|
-
# Summary mode with a live session: ask Claude to
|
|
342
|
+
# Summary mode with a live session: ask Claude to write summary via slash command
|
|
300
343
|
if [[ "$CONTENT" == "summary" && -n "$SESSION" && -z "$MESSAGE_TEXT" ]]; then
|
|
301
344
|
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
302
|
-
|
|
345
|
+
HANDSHAKE_FILE="/tmp/tq-summary-${TASK_HASH}.txt"
|
|
346
|
+
rm -f "$HANDSHAKE_FILE"
|
|
347
|
+
|
|
348
|
+
# Tell Claude to generate summary (slash command calls tq-message --message which writes handshake file + delivers)
|
|
303
349
|
tmux send-keys -t "$SESSION" "/tq-message ${TASK_HASH} ${QUEUE_FILE}" Enter
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
350
|
+
|
|
351
|
+
# Poll for the handshake file (max 90 seconds, check every 3s)
|
|
352
|
+
WAITED=0
|
|
353
|
+
while [[ ! -f "$HANDSHAKE_FILE" && "$WAITED" -lt 90 ]]; do
|
|
354
|
+
sleep 3
|
|
355
|
+
WAITED=$(( WAITED + 3 ))
|
|
356
|
+
done
|
|
357
|
+
|
|
358
|
+
if [[ -f "$HANDSHAKE_FILE" ]]; then
|
|
359
|
+
# Slash command already delivered the message — just clean up
|
|
360
|
+
rm -f "$HANDSHAKE_FILE"
|
|
361
|
+
exit 0
|
|
362
|
+
else
|
|
363
|
+
# Timed out — fall back to details
|
|
364
|
+
MESSAGE_TEXT="$(build_message "details" "$TASK_HASH" "$STATE_FILE" "$PROMPT_FILE" "")"
|
|
365
|
+
fi
|
|
309
366
|
else
|
|
310
367
|
# Session gone — fall back to details
|
|
311
368
|
MESSAGE_TEXT="$(build_message "details" "$TASK_HASH" "$STATE_FILE" "$PROMPT_FILE" "")"
|
|
312
369
|
fi
|
|
313
370
|
fi
|
|
314
371
|
|
|
372
|
+
# Write handshake file for summary polling (if applicable)
|
|
373
|
+
if [[ -n "$MESSAGE_TEXT" && -n "$TASK_HASH" ]]; then
|
|
374
|
+
echo "$MESSAGE_TEXT" > "/tmp/tq-summary-${TASK_HASH}.txt"
|
|
375
|
+
fi
|
|
376
|
+
|
|
315
377
|
# Deliver message if we have one
|
|
316
378
|
if [[ -n "$MESSAGE_TEXT" ]]; then
|
|
317
|
-
|
|
379
|
+
# If no explicit reply-to, check for latest-msg-id (conversation mode)
|
|
380
|
+
if [[ -z "$REPLY_TO_MSG_ID" ]]; then
|
|
381
|
+
LATEST_MSG_FILE="$HOME/.tq/conversations/latest-msg-id"
|
|
382
|
+
if [[ -f "$LATEST_MSG_FILE" ]]; then
|
|
383
|
+
REPLY_TO_MSG_ID="$(cat "$LATEST_MSG_FILE")"
|
|
384
|
+
fi
|
|
385
|
+
fi
|
|
386
|
+
deliver "$SERVICE" "$MESSAGE_TEXT" "$REPLY_TO_MSG_ID"
|
|
318
387
|
fi
|
package/scripts/tq-setup
CHANGED
|
@@ -32,7 +32,7 @@ read -r _IGNORED
|
|
|
32
32
|
|
|
33
33
|
UPDATES="$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=0&limit=10&timeout=0" 2>&1)"
|
|
34
34
|
|
|
35
|
-
DISCOVER_SCRIPT=$(mktemp /tmp/tq-setup-discover-XXXXXX
|
|
35
|
+
DISCOVER_SCRIPT=$(mktemp /tmp/tq-setup-discover-XXXXXX)
|
|
36
36
|
trap 'rm -f "$DISCOVER_SCRIPT"' EXIT
|
|
37
37
|
|
|
38
38
|
cat > "$DISCOVER_SCRIPT" <<'PYEOF'
|
package/scripts/tq-telegram-poll
CHANGED
|
@@ -5,13 +5,14 @@ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
|
5
5
|
|
|
6
6
|
CONFIG_FILE="$HOME/.tq/config/message.yaml"
|
|
7
7
|
OFFSET_FILE="$HOME/.tq/telegram-poll-offset"
|
|
8
|
+
ORCHESTRATOR_SESSION="tq-orchestrator"
|
|
8
9
|
|
|
9
10
|
# --- Read config ---
|
|
10
11
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
11
12
|
exit 0
|
|
12
13
|
fi
|
|
13
14
|
|
|
14
|
-
CONFIG_SCRIPT=$(mktemp /tmp/tq-poll-config-XXXXXX
|
|
15
|
+
CONFIG_SCRIPT=$(mktemp /tmp/tq-poll-config-XXXXXX)
|
|
15
16
|
trap 'rm -f "$CONFIG_SCRIPT"' EXIT
|
|
16
17
|
|
|
17
18
|
cat > "$CONFIG_SCRIPT" <<'PYEOF'
|
|
@@ -68,8 +69,8 @@ fi
|
|
|
68
69
|
# --- Fetch updates ---
|
|
69
70
|
UPDATES="$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=${OFFSET}&limit=10&timeout=0" 2>&1)"
|
|
70
71
|
|
|
71
|
-
# --- Process updates ---
|
|
72
|
-
PROCESS_SCRIPT=$(mktemp /tmp/tq-poll-process-XXXXXX
|
|
72
|
+
# --- Process updates (extract msg_id, chat_id, reply_to_msg_id, text) ---
|
|
73
|
+
PROCESS_SCRIPT=$(mktemp /tmp/tq-poll-process-XXXXXX)
|
|
73
74
|
trap 'rm -f "$CONFIG_SCRIPT" "$PROCESS_SCRIPT"' EXIT
|
|
74
75
|
|
|
75
76
|
cat > "$PROCESS_SCRIPT" <<'PYEOF'
|
|
@@ -100,8 +101,10 @@ for update in results:
|
|
|
100
101
|
if text:
|
|
101
102
|
chat_id = msg['chat']['id']
|
|
102
103
|
message_id = msg['message_id']
|
|
103
|
-
#
|
|
104
|
-
|
|
104
|
+
# Extract reply_to_message_id if present
|
|
105
|
+
reply_to = msg.get('reply_to_message', {}).get('message_id', '')
|
|
106
|
+
# 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}")
|
|
105
108
|
|
|
106
109
|
if new_offset is not None:
|
|
107
110
|
with open(offset_file, 'w') as f:
|
|
@@ -113,17 +116,166 @@ PYEOF
|
|
|
113
116
|
|
|
114
117
|
MESSAGES="$(python3 "$PROCESS_SCRIPT" "$UPDATES" "$USER_ID" "$OFFSET_FILE")"
|
|
115
118
|
|
|
116
|
-
# ---
|
|
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
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# --- Handle Telegram commands ---
|
|
126
|
+
handle_command() {
|
|
127
|
+
local cmd="$1"
|
|
128
|
+
local chat_id="$2"
|
|
129
|
+
|
|
130
|
+
case "$cmd" in
|
|
131
|
+
/converse|/converse\ *)
|
|
132
|
+
if [[ "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
|
|
133
|
+
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
134
|
+
-d "chat_id=${chat_id}" \
|
|
135
|
+
--data-urlencode "text=Orchestrator already running. Just send messages — they'll be routed automatically." \
|
|
136
|
+
> /dev/null 2>&1 || true
|
|
137
|
+
else
|
|
138
|
+
if command -v tq-converse &>/dev/null; then
|
|
139
|
+
tq-converse start 2>&1 || true
|
|
140
|
+
ORCHESTRATOR_ACTIVE=1
|
|
141
|
+
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
142
|
+
-d "chat_id=${chat_id}" \
|
|
143
|
+
--data-urlencode "text=Orchestrator started. Send messages and they'll be routed to the right conversation." \
|
|
144
|
+
> /dev/null 2>&1 || true
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
return 0
|
|
148
|
+
;;
|
|
149
|
+
/stop)
|
|
150
|
+
if command -v tq-converse &>/dev/null; then
|
|
151
|
+
tq-converse stop 2>&1 || true
|
|
152
|
+
ORCHESTRATOR_ACTIVE=0
|
|
153
|
+
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
154
|
+
-d "chat_id=${chat_id}" \
|
|
155
|
+
--data-urlencode "text=Orchestrator stopped." \
|
|
156
|
+
> /dev/null 2>&1 || true
|
|
157
|
+
fi
|
|
158
|
+
return 0
|
|
159
|
+
;;
|
|
160
|
+
/stop\ *)
|
|
161
|
+
local SLUG="${cmd#/stop }"
|
|
162
|
+
if command -v tq-converse &>/dev/null; then
|
|
163
|
+
tq-converse stop "$SLUG" 2>&1 || true
|
|
164
|
+
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
165
|
+
-d "chat_id=${chat_id}" \
|
|
166
|
+
--data-urlencode "text=Session '$SLUG' stopped." \
|
|
167
|
+
> /dev/null 2>&1 || true
|
|
168
|
+
fi
|
|
169
|
+
return 0
|
|
170
|
+
;;
|
|
171
|
+
/status)
|
|
172
|
+
if command -v tq-converse &>/dev/null; then
|
|
173
|
+
local STATUS_TEXT
|
|
174
|
+
STATUS_TEXT="$(tq-converse status 2>&1 || true)"
|
|
175
|
+
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
176
|
+
-d "chat_id=${chat_id}" \
|
|
177
|
+
--data-urlencode "text=${STATUS_TEXT}" \
|
|
178
|
+
> /dev/null 2>&1 || true
|
|
179
|
+
fi
|
|
180
|
+
return 0
|
|
181
|
+
;;
|
|
182
|
+
/list)
|
|
183
|
+
if command -v tq-converse &>/dev/null; then
|
|
184
|
+
local LIST_TEXT
|
|
185
|
+
LIST_TEXT="$(tq-converse list 2>&1 || true)"
|
|
186
|
+
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
187
|
+
-d "chat_id=${chat_id}" \
|
|
188
|
+
--data-urlencode "text=${LIST_TEXT}" \
|
|
189
|
+
> /dev/null 2>&1 || true
|
|
190
|
+
fi
|
|
191
|
+
return 0
|
|
192
|
+
;;
|
|
193
|
+
esac
|
|
194
|
+
return 1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# --- Route messages ---
|
|
117
198
|
mkdir -p "$HOME/.tq/workspace"
|
|
118
199
|
|
|
119
|
-
while IFS=$'\t' read -r CHAT_ID MSG_ID MSG; do
|
|
120
|
-
if [[ -
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
200
|
+
while IFS=$'\t' read -r CHAT_ID MSG_ID REPLY_TO MSG; do
|
|
201
|
+
if [[ -z "$MSG" ]]; then
|
|
202
|
+
continue
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
echo "[tq-telegram-poll] message: ${MSG:0:60}"
|
|
206
|
+
|
|
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
|
|
212
|
+
|
|
213
|
+
# Check for Telegram commands first
|
|
214
|
+
if [[ "$MSG" == /* ]]; then
|
|
215
|
+
if handle_command "$MSG" "$CHAT_ID"; then
|
|
216
|
+
continue
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
|
|
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
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
fi
|
|
243
|
+
|
|
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
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
128
259
|
fi
|
|
260
|
+
|
|
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}"
|
|
268
|
+
fi
|
|
269
|
+
FORMATTED_MSG="${FORMATTED_MSG}]
|
|
270
|
+
${MSG}"
|
|
271
|
+
tq-converse send "$FORMATTED_MSG"
|
|
272
|
+
ROUTED=1
|
|
273
|
+
fi
|
|
274
|
+
|
|
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
|
|
280
|
+
|
|
129
281
|
done <<< "$MESSAGES"
|
package/skills/tq/SKILL.md
CHANGED
|
@@ -3,10 +3,11 @@ name: tq
|
|
|
3
3
|
description: >
|
|
4
4
|
This skill should be used when the user asks to "add to queue", "run queue", "queue these tasks",
|
|
5
5
|
"schedule with tq", "tq status", "check task queue", "create a tq queue", "set up cron for tq",
|
|
6
|
-
"run claude in background", "batch prompts in tmux",
|
|
7
|
-
|
|
8
|
-
"tmux queue", "scheduled claude tasks"
|
|
9
|
-
|
|
6
|
+
"run claude in background", "batch prompts in tmux", "start a conversation", "converse via telegram",
|
|
7
|
+
"telegram conversation mode", or wants to manage Claude prompts running in tmux sessions via the
|
|
8
|
+
tq CLI tool. Triggers on phrases like "queue", "tq", "task queue", "tmux queue", "scheduled claude tasks",
|
|
9
|
+
"conversation mode", "telegram chat", "converse".
|
|
10
|
+
version: 1.1.0
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# tq — Claude Task Queue Runner
|
|
@@ -17,8 +18,9 @@ Installed to PATH via `/install`: `/opt/homebrew/bin/tq`
|
|
|
17
18
|
|
|
18
19
|
## Overview
|
|
19
20
|
|
|
20
|
-
tq
|
|
21
|
-
|
|
21
|
+
tq manages Claude Code sessions via tmux in two modes:
|
|
22
|
+
1. **Queue mode** — batches prompts into YAML queue files, spawns each as an independent tmux session. Idempotent — running `tq` again skips `done` and live `running` tasks.
|
|
23
|
+
2. **Conversation mode** — persistent interactive Claude Code sessions orchestrated via Telegram. An orchestrator routes messages to the right conversation, creating new sessions or resuming existing ones.
|
|
22
24
|
|
|
23
25
|
## Queue File Format
|
|
24
26
|
|
|
@@ -59,6 +61,9 @@ Statuses: `pending` → `running` → `done`
|
|
|
59
61
|
| `/jobs [filter]` | List all scheduled tq cron jobs |
|
|
60
62
|
| `/health [queue]` | System-wide diagnostics |
|
|
61
63
|
| `/install` | Symlink tq binaries to PATH |
|
|
64
|
+
| `/converse [start\|stop\|status]` | Manage Telegram conversation sessions |
|
|
65
|
+
| `/tq-reply` | Send response back to Telegram (conversation mode) |
|
|
66
|
+
| `/setup-telegram` | Configure Telegram bot token and notifications |
|
|
62
67
|
|
|
63
68
|
## CLI Usage
|
|
64
69
|
|
|
@@ -102,6 +107,37 @@ The Claude extension stores the browser name as `bridgeDisplayName` in the exten
|
|
|
102
107
|
- Right-click the Claude extension icon in the Chrome toolbar → **Options**
|
|
103
108
|
- Or open the sidepanel and look for a settings/gear icon with a name field
|
|
104
109
|
|
|
110
|
+
## Conversation Mode
|
|
111
|
+
|
|
112
|
+
Start an orchestrator: `tq-converse start` or send `/converse` from Telegram.
|
|
113
|
+
|
|
114
|
+
The orchestrator routes incoming Telegram messages to the appropriate conversation session:
|
|
115
|
+
- Telegram reply to a known message → routes to that session automatically
|
|
116
|
+
- `#slug message` → routes to the named session
|
|
117
|
+
- New topic → orchestrator spawns a new session with a descriptive slug
|
|
118
|
+
|
|
119
|
+
Each conversation is a persistent Claude Code interactive session in its own tmux window.
|
|
120
|
+
Child sessions use `/tq-reply` to send responses back to Telegram as threaded replies.
|
|
121
|
+
|
|
122
|
+
### Conversation CLI
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
tq-converse start # start orchestrator
|
|
126
|
+
tq-converse spawn <slug> --cwd <dir> # new conversation session
|
|
127
|
+
tq-converse route <slug> <message> # send to a session
|
|
128
|
+
tq-converse list # list active sessions
|
|
129
|
+
tq-converse stop [<slug>] # stop session or orchestrator
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Telegram Commands
|
|
133
|
+
|
|
134
|
+
| Command | Purpose |
|
|
135
|
+
|---------|---------|
|
|
136
|
+
| `/converse` | Start the orchestrator |
|
|
137
|
+
| `/stop [slug]` | Stop orchestrator or a specific session |
|
|
138
|
+
| `/status` | Show all sessions |
|
|
139
|
+
| `/list` | List active conversations |
|
|
140
|
+
|
|
105
141
|
## Additional Resources
|
|
106
142
|
|
|
107
143
|
- **`references/session-naming.md`** — Session/window name generation algorithm and examples
|