@codefilabs/tq 0.0.1 → 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.
@@ -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)"
@@ -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 tq marketplace..."
9
- claude plugin marketplace add kevnk/tq
8
+ echo "Adding codefilabs marketplace..."
9
+ claude plugin marketplace add codefilabs/marketplace
10
10
  echo "Installing tq plugin..."
11
- claude plugin install tq@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/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 kevnk/tq && claude plugin install tq@tq" >&2
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 "tq installed. Crontab example (crontab -e):"
73
+ echo "Add a schedule: key to any queue file in ~/.tq/queues/ to auto-schedule it:"
68
74
  echo ""
69
- echo " 0 9 * * * /opt/homebrew/bin/tq ~/.tq/queues/morning.yaml >> ~/.tq/logs/tq.log 2>&1"
70
- echo " */30 * * * * /opt/homebrew/bin/tq --status ~/.tq/queues/morning.yaml >> ~/.tq/logs/tq.log 2>&1"
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"
@@ -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
- if [[ -z "$TASK_HASH" && -z "$QUEUE_FILE" ]]; then
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.py)
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 in minutes
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="$(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"
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 -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"
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 generate and send
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
- # Pass hash and queue path as arguments to the slash command
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
- # 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
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
- deliver "$SERVICE" "$MESSAGE_TEXT"
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.py)
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'
@@ -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.py)
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.py)
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
- # Output as tab-separated: chat_id<TAB>message_id<TAB>text
104
- messages.append(f"{chat_id}\t{message_id}\t{text}")
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
- # --- Spawn tq --prompt for each message ---
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 [[ -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"
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"
@@ -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", or wants to manage Claude prompts running
7
- in tmux sessions via the tq CLI tool. Triggers on phrases like "queue", "tq", "task queue",
8
- "tmux queue", "scheduled claude tasks".
9
- version: 1.0.0
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 batches Claude prompts into YAML queue files and spawns each as an independent tmux session.
21
- Tasks are idempotent — running `tq` again skips `done` and live `running` tasks.
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