@codefilabs/tq 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,23 @@ 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"
9
+ DAEMON_MODE=0
10
+ POLL_TIMEOUT=0
11
+
12
+ if [[ "${1:-}" == "--daemon" ]]; then
13
+ DAEMON_MODE=1
14
+ POLL_TIMEOUT=30
15
+ fi
8
16
 
9
17
  # --- Read config ---
10
18
  if [[ ! -f "$CONFIG_FILE" ]]; then
11
19
  exit 0
12
20
  fi
13
21
 
14
- CONFIG_SCRIPT=$(mktemp /tmp/tq-poll-config-XXXXXX.py)
15
- trap 'rm -f "$CONFIG_SCRIPT"' EXIT
22
+ CONFIG_SCRIPT=$(mktemp /tmp/tq-poll-config-XXXXXX)
23
+ PROCESS_SCRIPT=$(mktemp /tmp/tq-poll-process-XXXXXX)
24
+ trap 'rm -f "$CONFIG_SCRIPT" "$PROCESS_SCRIPT"' EXIT
16
25
 
17
26
  cat > "$CONFIG_SCRIPT" <<'PYEOF'
18
27
  import sys, os, re, json
@@ -51,27 +60,6 @@ print(json.dumps({
51
60
  }))
52
61
  PYEOF
53
62
 
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
63
  cat > "$PROCESS_SCRIPT" <<'PYEOF'
76
64
  import sys, json
77
65
 
@@ -100,8 +88,11 @@ for update in results:
100
88
  if text:
101
89
  chat_id = msg['chat']['id']
102
90
  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}")
91
+ # Extract reply_to_message_id if present
92
+ reply_to = msg.get('reply_to_message', {}).get('message_id', '')
93
+ # Output: chat_id<TAB>message_id<TAB>reply_to<TAB>text
94
+ # Use '-' placeholder for empty reply_to (bash read collapses consecutive tabs)
95
+ messages.append(f"{chat_id}\t{message_id}\t{reply_to or '-'}\t{text}")
105
96
 
106
97
  if new_offset is not None:
107
98
  with open(offset_file, 'w') as f:
@@ -111,19 +102,210 @@ for m in messages:
111
102
  print(m)
112
103
  PYEOF
113
104
 
114
- MESSAGES="$(python3 "$PROCESS_SCRIPT" "$UPDATES" "$USER_ID" "$OFFSET_FILE")"
105
+ CONFIG_JSON="$(python3 "$CONFIG_SCRIPT" "$CONFIG_FILE")"
106
+ BOT_TOKEN="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('bot_token',''))" "$CONFIG_JSON")"
107
+ USER_ID="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('user_id',''))" "$CONFIG_JSON")"
108
+
109
+ if [[ -z "$BOT_TOKEN" || -z "$USER_ID" ]]; then
110
+ exit 0
111
+ fi
112
+
113
+ # --- Read offset ---
114
+ OFFSET=0
115
+ if [[ -f "$OFFSET_FILE" ]]; then
116
+ OFFSET="$(cat "$OFFSET_FILE")"
117
+ fi
118
+
119
+ # --- Handle Telegram commands ---
120
+ handle_command() {
121
+ local cmd="$1"
122
+ local chat_id="$2"
123
+
124
+ case "$cmd" in
125
+ /converse|/converse\ *)
126
+ if [[ "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
127
+ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
128
+ -d "chat_id=${chat_id}" \
129
+ --data-urlencode "text=Orchestrator already running. Just send messages — they'll be routed automatically." \
130
+ > /dev/null 2>&1 || true
131
+ else
132
+ if command -v tq-converse &>/dev/null; then
133
+ tq-converse start 2>&1 || true
134
+ ORCHESTRATOR_ACTIVE=1
135
+ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
136
+ -d "chat_id=${chat_id}" \
137
+ --data-urlencode "text=Orchestrator started. Send messages and they'll be routed to the right conversation." \
138
+ > /dev/null 2>&1 || true
139
+ fi
140
+ fi
141
+ return 0
142
+ ;;
143
+ /stop)
144
+ if command -v tq-converse &>/dev/null; then
145
+ tq-converse stop 2>&1 || true
146
+ ORCHESTRATOR_ACTIVE=0
147
+ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
148
+ -d "chat_id=${chat_id}" \
149
+ --data-urlencode "text=Orchestrator stopped." \
150
+ > /dev/null 2>&1 || true
151
+ fi
152
+ return 0
153
+ ;;
154
+ /stop\ *)
155
+ local SLUG="${cmd#/stop }"
156
+ if command -v tq-converse &>/dev/null; then
157
+ tq-converse stop "$SLUG" 2>&1 || true
158
+ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
159
+ -d "chat_id=${chat_id}" \
160
+ --data-urlencode "text=Session '$SLUG' stopped." \
161
+ > /dev/null 2>&1 || true
162
+ fi
163
+ return 0
164
+ ;;
165
+ /status)
166
+ if command -v tq-converse &>/dev/null; then
167
+ local STATUS_TEXT
168
+ STATUS_TEXT="$(tq-converse status 2>&1 || true)"
169
+ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
170
+ -d "chat_id=${chat_id}" \
171
+ --data-urlencode "text=${STATUS_TEXT}" \
172
+ > /dev/null 2>&1 || true
173
+ fi
174
+ return 0
175
+ ;;
176
+ /list)
177
+ if command -v tq-converse &>/dev/null; then
178
+ local LIST_TEXT
179
+ LIST_TEXT="$(tq-converse list 2>&1 || true)"
180
+ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
181
+ -d "chat_id=${chat_id}" \
182
+ --data-urlencode "text=${LIST_TEXT}" \
183
+ > /dev/null 2>&1 || true
184
+ fi
185
+ return 0
186
+ ;;
187
+ esac
188
+ return 1
189
+ }
190
+
191
+ # --- poll_once: fetch and process a single batch of updates ---
192
+ poll_once() {
193
+ local UPDATES MESSAGES
194
+
195
+ UPDATES="$(curl -s --max-time $((POLL_TIMEOUT + 10)) "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=${OFFSET}&limit=10&timeout=${POLL_TIMEOUT}" 2>&1)"
196
+
197
+ MESSAGES="$(python3 "$PROCESS_SCRIPT" "$UPDATES" "$USER_ID" "$OFFSET_FILE")"
198
+
199
+ # Re-read offset (python may have updated it)
200
+ if [[ -f "$OFFSET_FILE" ]]; then
201
+ OFFSET="$(cat "$OFFSET_FILE")"
202
+ fi
203
+
204
+ if [[ -z "$MESSAGES" ]]; then
205
+ return 0
206
+ fi
207
+
208
+ # Check if orchestrator is running
209
+ ORCHESTRATOR_ACTIVE=0
210
+ if tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
211
+ ORCHESTRATOR_ACTIVE=1
212
+ fi
213
+
214
+ mkdir -p "$HOME/.tq/workspace"
115
215
 
116
- # --- Spawn tq --prompt for each message ---
117
- mkdir -p "$HOME/.tq/workspace"
216
+ while IFS=$'\t' read -r CHAT_ID MSG_ID REPLY_TO MSG; do
217
+ if [[ -z "$MSG" ]]; then
218
+ continue
219
+ fi
220
+
221
+ echo "[tq-telegram-poll] message: ${MSG:0:60}"
118
222
 
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
223
  # React with 👀 to acknowledge receipt
123
224
  curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setMessageReaction" \
124
225
  -H "Content-Type: application/json" \
125
226
  --data-raw "{\"chat_id\":${CHAT_ID},\"message_id\":${MSG_ID},\"reaction\":[{\"type\":\"emoji\",\"emoji\":\"👀\"}]}" \
126
227
  > /dev/null 2>&1 || true
127
- tq --prompt "$MSG"
128
- fi
129
- done <<< "$MESSAGES"
228
+
229
+ # Check for Telegram commands first
230
+ if [[ "$MSG" == /* ]]; then
231
+ if handle_command "$MSG" "$CHAT_ID"; then
232
+ continue
233
+ fi
234
+ fi
235
+
236
+ # --- Routing logic (3-tier) ---
237
+
238
+ # Store the latest message ID for reply threading
239
+ echo "$MSG_ID" > "$HOME/.tq/conversations/latest-msg-id" 2>/dev/null || true
240
+ echo "$CHAT_ID" > "$HOME/.tq/conversations/latest-chat-id" 2>/dev/null || true
241
+
242
+ ROUTED=0
243
+
244
+ # Normalize placeholder
245
+ [[ "$REPLY_TO" == "-" ]] && REPLY_TO=""
246
+
247
+ # Tier 1: Telegram reply → deterministic routing via registry
248
+ if [[ -n "$REPLY_TO" && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
249
+ SLUG="$(tq-converse lookup-msg "$REPLY_TO" 2>/dev/null || true)"
250
+ if [[ -n "$SLUG" ]]; then
251
+ CHILD_SESSION="tq-conv-${SLUG}"
252
+ if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
253
+ echo "[tq-telegram-poll] reply-routing to: $SLUG (via msg $REPLY_TO)"
254
+ tq-converse track-msg "$SLUG" "$MSG_ID"
255
+ # Store msg_id for this session's reply threading
256
+ echo "$MSG_ID" > "$HOME/.tq/conversations/sessions/$SLUG/reply-to-msg-id" 2>/dev/null || true
257
+ tq-converse route "$SLUG" "$MSG"
258
+ ROUTED=1
259
+ fi
260
+ fi
261
+ fi
262
+
263
+ # Tier 2: Explicit #slug prefix → deterministic routing
264
+ if [[ "$ROUTED" == "0" && "$MSG" == \#* && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
265
+ # Extract slug from #slug-name prefix
266
+ SLUG="$(echo "$MSG" | sed 's/^#\([a-z0-9-]*\).*/\1/')"
267
+ ACTUAL_MSG="$(echo "$MSG" | sed 's/^#[a-z0-9-]* *//')"
268
+ if [[ -n "$SLUG" && -n "$ACTUAL_MSG" ]]; then
269
+ CHILD_SESSION="tq-conv-${SLUG}"
270
+ if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
271
+ echo "[tq-telegram-poll] explicit routing to: $SLUG"
272
+ tq-converse track-msg "$SLUG" "$MSG_ID"
273
+ echo "$MSG_ID" > "$HOME/.tq/conversations/sessions/$SLUG/reply-to-msg-id" 2>/dev/null || true
274
+ tq-converse route "$SLUG" "$ACTUAL_MSG"
275
+ ROUTED=1
276
+ fi
277
+ fi
278
+ fi
279
+
280
+ # Tier 3: Orchestrator routing (smart, Claude-powered)
281
+ if [[ "$ROUTED" == "0" && "$ORCHESTRATOR_ACTIVE" == "1" ]]; then
282
+ echo "[tq-telegram-poll] routing via orchestrator"
283
+ # Format message with metadata for the orchestrator
284
+ FORMATTED_MSG="[tq-msg msg_id=${MSG_ID} chat_id=${CHAT_ID}"
285
+ if [[ -n "$REPLY_TO" ]]; then
286
+ FORMATTED_MSG="${FORMATTED_MSG} reply_to=${REPLY_TO}"
287
+ fi
288
+ FORMATTED_MSG="${FORMATTED_MSG}]
289
+ ${MSG}"
290
+ tq-converse send "$FORMATTED_MSG"
291
+ ROUTED=1
292
+ fi
293
+
294
+ # Fallback: no orchestrator — spawn one-off task (legacy behavior)
295
+ if [[ "$ROUTED" == "0" ]]; then
296
+ echo "[tq-telegram-poll] no orchestrator — spawning one-off task"
297
+ tq --no-chrome --prompt "$MSG"
298
+ fi
299
+
300
+ done <<< "$MESSAGES" || true
301
+ }
302
+
303
+ # --- Main: single run or daemon loop ---
304
+ if [[ "$DAEMON_MODE" == "1" ]]; then
305
+ echo "[tq-telegram-poll] daemon started (long-poll timeout=${POLL_TIMEOUT}s)"
306
+ while true; do
307
+ poll_once || true
308
+ done
309
+ else
310
+ poll_once
311
+ fi
@@ -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
@@ -48,3 +48,19 @@ WINDOW="$(echo "$PROMPT" | awk '{print $1" "$2}' \
48
48
  - Epoch suffix prevents collision when the same prompt is re-queued after a reset
49
49
  - tmux session names must not contain `.` or `:` — the character replacement handles this
50
50
  - The `-` separator between base and epoch is always present; if the base ends with `-`, it gets stripped first to avoid `--`
51
+
52
+ ## Conversation Mode Session Names
53
+
54
+ Conversation sessions use a different naming scheme — slug-based, no epoch suffix:
55
+
56
+ | Type | Pattern | Example |
57
+ |------|---------|---------|
58
+ | Orchestrator | `tq-orchestrator` (fixed) | `tq-orchestrator` |
59
+ | Child session | `tq-conv-<slug>` | `tq-conv-fix-auth` |
60
+ | Child window | `<slug>` | `fix-auth` |
61
+
62
+ Slugs are short kebab-case names (2-4 words) chosen by the orchestrator Claude when a
63
+ new conversation is created. Examples: `fix-auth-bug`, `refactor-api`, `update-docs`.
64
+
65
+ Since there is no epoch suffix, conversation session names are unique by slug. Starting
66
+ a session with an already-active slug will reuse the existing session.