@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,726 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
5
+
6
+ CONVERSE_DIR="$HOME/.tq/conversations"
7
+ REGISTRY="$CONVERSE_DIR/registry.json"
8
+ ORCHESTRATOR_SESSION="tq-orchestrator"
9
+
10
+ mkdir -p "$CONVERSE_DIR"
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Registry helpers (embedded Python for JSON manipulation)
14
+ # ---------------------------------------------------------------------------
15
+ ensure_registry() {
16
+ if [[ ! -f "$REGISTRY" ]]; then
17
+ echo '{"sessions":{},"messages":{}}' > "$REGISTRY"
18
+ fi
19
+ }
20
+
21
+ registry_op() {
22
+ ensure_registry
23
+ local OP="$1"; shift
24
+ REGISTRY_SCRIPT=$(mktemp /tmp/tq-registry-XXXXXX)
25
+ trap 'rm -f "$REGISTRY_SCRIPT"' EXIT
26
+
27
+ cat > "$REGISTRY_SCRIPT" <<'PYEOF'
28
+ import sys, os, json
29
+
30
+ registry_path = sys.argv[1]
31
+ op = sys.argv[2]
32
+
33
+ with open(registry_path) as f:
34
+ reg = json.load(f)
35
+
36
+ sessions = reg.setdefault('sessions', {})
37
+ messages = reg.setdefault('messages', {})
38
+
39
+ def save():
40
+ with open(registry_path, 'w') as f:
41
+ json.dump(reg, f, indent=2)
42
+
43
+ if op == 'list':
44
+ for slug, info in sessions.items():
45
+ status = info.get('status', 'unknown')
46
+ desc = info.get('description', '')[:60]
47
+ tmux = info.get('tmux', '')
48
+ print(f"{slug}\t{status}\t{tmux}\t{desc}")
49
+
50
+ elif op == 'get':
51
+ slug = sys.argv[3]
52
+ if slug in sessions:
53
+ print(json.dumps(sessions[slug]))
54
+ else:
55
+ sys.exit(1)
56
+
57
+ elif op == 'set':
58
+ slug = sys.argv[3]
59
+ data = json.loads(sys.argv[4])
60
+ sessions[slug] = data
61
+ save()
62
+
63
+ elif op == 'update-field':
64
+ slug = sys.argv[3]
65
+ field = sys.argv[4]
66
+ value = sys.argv[5]
67
+ if slug in sessions:
68
+ sessions[slug][field] = value
69
+ save()
70
+
71
+ elif op == 'remove':
72
+ slug = sys.argv[3]
73
+ sessions.pop(slug, None)
74
+ # Clean up message mappings for this slug
75
+ messages_to_remove = [mid for mid, s in messages.items() if s == slug]
76
+ for mid in messages_to_remove:
77
+ del messages[mid]
78
+ save()
79
+
80
+ elif op == 'track-msg':
81
+ slug = sys.argv[3]
82
+ msg_id = sys.argv[4]
83
+ messages[msg_id] = slug
84
+ save()
85
+
86
+ elif op == 'lookup-msg':
87
+ msg_id = sys.argv[3]
88
+ slug = messages.get(msg_id, '')
89
+ if slug:
90
+ print(slug)
91
+ else:
92
+ sys.exit(1)
93
+
94
+ elif op == 'dump':
95
+ print(json.dumps(reg, indent=2))
96
+
97
+ elif op == 'slugs':
98
+ for slug in sessions:
99
+ print(slug)
100
+
101
+ elif op == 'active-slugs':
102
+ for slug, info in sessions.items():
103
+ if info.get('status') == 'active':
104
+ print(slug)
105
+
106
+ elif op == 'summary':
107
+ # Output slug\tdescription for all active sessions (for orchestrator context)
108
+ for slug, info in sessions.items():
109
+ if info.get('status') == 'active':
110
+ desc = info.get('description', '(no description)')
111
+ cwd = info.get('cwd', '')
112
+ print(f"{slug}\t{desc}\t{cwd}")
113
+
114
+ PYEOF
115
+
116
+ python3 "$REGISTRY_SCRIPT" "$REGISTRY" "$OP" "$@"
117
+ }
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Auth capture (shared by orchestrator and child sessions)
121
+ # ---------------------------------------------------------------------------
122
+ capture_auth() {
123
+ local AUTH_SCRIPT
124
+ AUTH_SCRIPT=$(mktemp /tmp/tq-auth-XXXXXX)
125
+
126
+ cat > "$AUTH_SCRIPT" <<'PYEOF'
127
+ import sys, os, json, subprocess
128
+ captured = {}
129
+ try:
130
+ result = subprocess.run(
131
+ ['security', 'find-generic-password', '-s', 'Claude Code-credentials', '-a', os.environ.get('USER', ''), '-w'],
132
+ capture_output=True, text=True
133
+ )
134
+ if result.returncode == 0 and result.stdout.strip():
135
+ creds = json.loads(result.stdout.strip())
136
+ oauth = creds.get('claudeAiOauth', {})
137
+ if oauth.get('accessToken'):
138
+ captured['CLAUDE_CODE_OAUTH_KEY'] = oauth['accessToken']
139
+ except Exception:
140
+ pass
141
+ for k in ['CLAUDE_CODE_OAUTH_KEY', 'ANTHROPIC_API_KEY']:
142
+ if k not in captured and k in os.environ:
143
+ captured[k] = os.environ[k]
144
+ for k, v in captured.items():
145
+ print(f"{k}={v}")
146
+ PYEOF
147
+
148
+ python3 "$AUTH_SCRIPT" 2>/dev/null || true
149
+ rm -f "$AUTH_SCRIPT"
150
+ }
151
+
152
+ # Inject auth env vars into a tmux session:window
153
+ inject_auth() {
154
+ local TARGET="$1"
155
+ local AUTH_ENV
156
+ AUTH_ENV="$(capture_auth)"
157
+ while IFS='=' read -r KEY VAL; do
158
+ if [[ -n "$KEY" && -n "$VAL" ]]; then
159
+ tmux send-keys -t "$TARGET" "export $KEY='$VAL'" Enter
160
+ fi
161
+ done <<< "$AUTH_ENV"
162
+ }
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # Write settings.json with hooks for a session
166
+ # ---------------------------------------------------------------------------
167
+ write_session_settings() {
168
+ local HOOKS_DIR="$1"
169
+ local SETTINGS_FILE="$2"
170
+ local SLUG="${3:-}"
171
+
172
+ mkdir -p "$HOOKS_DIR"
173
+
174
+ # Stop hook
175
+ cat > "$HOOKS_DIR/on-stop.sh" <<HOOKEOF
176
+ #!/usr/bin/env bash
177
+ set -euo pipefail
178
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
179
+
180
+ SLUG="$SLUG"
181
+
182
+ # Mark session inactive in registry
183
+ if [[ -n "\$SLUG" ]] && command -v tq-converse &>/dev/null; then
184
+ tq-converse update-status "\$SLUG" stopped
185
+ fi
186
+
187
+ if command -v tq-message &>/dev/null; then
188
+ if [[ -n "\$SLUG" ]]; then
189
+ tq-message --message "[\$SLUG] Session ended."
190
+ else
191
+ tq-message --message "Orchestrator session ended."
192
+ fi
193
+ fi
194
+ HOOKEOF
195
+ chmod +x "$HOOKS_DIR/on-stop.sh"
196
+
197
+ # Notification hook
198
+ cat > "$HOOKS_DIR/on-notification.sh" <<'HOOKEOF'
199
+ #!/usr/bin/env bash
200
+ set -euo pipefail
201
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
202
+ NOTIFICATION="${1:-Claude Code notification}"
203
+ if command -v tq-message &>/dev/null; then
204
+ tq-message --message "[notification] $NOTIFICATION"
205
+ fi
206
+ HOOKEOF
207
+ chmod +x "$HOOKS_DIR/on-notification.sh"
208
+
209
+ cat > "$SETTINGS_FILE" <<JSONEOF
210
+ {
211
+ "hooks": {
212
+ "Stop": [
213
+ {
214
+ "hooks": [
215
+ {
216
+ "type": "command",
217
+ "command": "$HOOKS_DIR/on-stop.sh"
218
+ }
219
+ ]
220
+ }
221
+ ],
222
+ "Notification": [
223
+ {
224
+ "hooks": [
225
+ {
226
+ "type": "command",
227
+ "command": "$HOOKS_DIR/on-notification.sh"
228
+ }
229
+ ]
230
+ }
231
+ ]
232
+ }
233
+ }
234
+ JSONEOF
235
+ }
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Usage
239
+ # ---------------------------------------------------------------------------
240
+ usage() {
241
+ cat <<'EOF'
242
+ Usage: tq-converse <command> [options]
243
+
244
+ Session Management:
245
+ start Start the orchestrator (auto-converse mode)
246
+ stop [<slug>] Stop a session (or orchestrator if no slug)
247
+ status Show all sessions
248
+ list List active conversation slugs
249
+
250
+ Orchestrator Commands (called by orchestrator Claude or tq-telegram-poll):
251
+ spawn <slug> [options] Create a new child conversation session
252
+ route <slug> <message> Send a message to a child session
253
+ send <message> Send a message to the orchestrator
254
+
255
+ Registry:
256
+ track-msg <slug> <msg-id> Register a Telegram message ID to a session
257
+ lookup-msg <msg-id> Find which session owns a Telegram message ID
258
+ update-status <slug> <status> Update session status in registry
259
+ registry Dump the full registry
260
+
261
+ spawn options:
262
+ --cwd <dir> Working directory for the session
263
+ --desc <description> Human-readable description
264
+ --msg-id <id> Telegram message ID that triggered this session
265
+ EOF
266
+ exit 1
267
+ }
268
+
269
+ COMMAND="${1:-}"
270
+ shift || true
271
+
272
+ case "$COMMAND" in
273
+ start|stop|status|list|spawn|route|send|track-msg|lookup-msg|update-status|registry) ;;
274
+ *) usage ;;
275
+ esac
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # start — launch the orchestrator session
279
+ # ---------------------------------------------------------------------------
280
+ if [[ "$COMMAND" == "start" ]]; then
281
+ if tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
282
+ echo "Orchestrator is already running."
283
+ echo " Attach: tmux attach -t $ORCHESTRATOR_SESSION"
284
+ exit 0
285
+ fi
286
+
287
+ ORCH_DIR="$CONVERSE_DIR/orchestrator"
288
+ mkdir -p "$ORCH_DIR/hooks"
289
+
290
+ # Write orchestrator instructions
291
+ cat > "$ORCH_DIR/.tq-orchestrator.md" <<'ORCHEOF'
292
+ # tq Orchestrator <!-- tq-orchestrator -->
293
+
294
+ You are the **tq conversation orchestrator**. You manage multiple Claude Code
295
+ conversation sessions on behalf of a user who messages you via Telegram.
296
+
297
+ ## How Messages Arrive
298
+
299
+ Messages are injected into your session in this format:
300
+ ```
301
+ [tq-msg msg_id=<ID> chat_id=<CHAT_ID>]
302
+ <message text>
303
+ ```
304
+
305
+ Or if the user replied to a previous message:
306
+ ```
307
+ [tq-msg msg_id=<ID> chat_id=<CHAT_ID> reply_to=<REPLY_MSG_ID>]
308
+ <message text>
309
+ ```
310
+
311
+ ## Your Job
312
+
313
+ For each incoming message:
314
+
315
+ 1. **Read the session registry** to see active conversations:
316
+ ```bash
317
+ tq-converse list
318
+ ```
319
+ For more detail:
320
+ ```bash
321
+ tq-converse registry
322
+ ```
323
+
324
+ 2. **Decide routing**:
325
+ - If the message is clearly a continuation of an existing conversation → route to it
326
+ - If the message is a new topic → spawn a new session
327
+ - If ambiguous → make your best judgment based on the descriptions
328
+
329
+ 3. **Execute**:
330
+
331
+ **To route to an existing session:**
332
+ ```bash
333
+ tq-converse route <slug> '<message text>'
334
+ ```
335
+ Then confirm to the user via /tq-reply:
336
+ `[→ slug-name] Routed your message.`
337
+
338
+ **To spawn a new session:**
339
+ ```bash
340
+ tq-converse spawn <slug> --cwd <dir> --desc '<description>' --msg-id <MSG_ID>
341
+ ```
342
+ Then route the first message:
343
+ ```bash
344
+ tq-converse route <slug> '<message text>'
345
+ ```
346
+ Then tell the user via /tq-reply:
347
+ `[new: slug-name] Started conversation: <description>`
348
+
349
+ 4. **Track the Telegram message ID** so reply-threading works:
350
+ ```bash
351
+ tq-converse track-msg <slug> <MSG_ID>
352
+ ```
353
+
354
+ ## Slug Naming
355
+
356
+ Pick short, descriptive kebab-case slugs (2-4 words):
357
+ - `fix-auth-bug`
358
+ - `refactor-api`
359
+ - `update-docs`
360
+ - `morning-review`
361
+
362
+ ## Rules
363
+
364
+ - Always route or spawn — never answer the user's question yourself
365
+ - Keep /tq-reply confirmations very short (one line)
366
+ - If the user says something like "in the auth thread" or "#fix-auth-bug", route to that slug
367
+ - If a user asks to stop a session: `tq-converse stop <slug>`
368
+ - If a user asks for status: run `tq-converse status` and share via /tq-reply
369
+ - Default cwd for new sessions: `$HOME/.tq/workspace` unless the message implies a specific project
370
+ ORCHEOF
371
+
372
+ # Write settings with hooks
373
+ SETTINGS_FILE="$ORCH_DIR/settings.json"
374
+ write_session_settings "$ORCH_DIR/hooks" "$SETTINGS_FILE" ""
375
+
376
+ ensure_registry
377
+
378
+ # Start tmux session
379
+ tmux start-server
380
+ tmux new-session -d -s "$ORCHESTRATOR_SESSION" -n "orchestrator"
381
+
382
+ inject_auth "$ORCHESTRATOR_SESSION:orchestrator"
383
+
384
+ tmux send-keys -t "$ORCHESTRATOR_SESSION:orchestrator" "cd '$ORCH_DIR'" Enter
385
+
386
+ tmux send-keys -t "$ORCHESTRATOR_SESSION:orchestrator" \
387
+ "claude --settings '$SETTINGS_FILE' --dangerously-skip-permissions" Enter
388
+
389
+ # Wait for Claude to start, then prime with instructions
390
+ sleep 3
391
+ tmux send-keys -t "$ORCHESTRATOR_SESSION:orchestrator" \
392
+ "Read .tq-orchestrator.md — those are your instructions. Acknowledge with 'ready'." Enter
393
+
394
+ echo "Orchestrator started: $ORCHESTRATOR_SESSION"
395
+ echo " Attach: tmux attach -t $ORCHESTRATOR_SESSION"
396
+ echo ""
397
+ echo "Send /converse from Telegram to enable auto-routing."
398
+ echo "All messages will be routed to the appropriate conversation."
399
+ exit 0
400
+ fi
401
+
402
+ # ---------------------------------------------------------------------------
403
+ # spawn — create a new child conversation session
404
+ # ---------------------------------------------------------------------------
405
+ if [[ "$COMMAND" == "spawn" ]]; then
406
+ SLUG="${1:-}"
407
+ shift || true
408
+
409
+ if [[ -z "$SLUG" ]]; then
410
+ echo "Usage: tq-converse spawn <slug> [--cwd <dir>] [--desc <desc>] [--msg-id <id>]" >&2
411
+ exit 1
412
+ fi
413
+
414
+ CHILD_CWD="$HOME/.tq/workspace"
415
+ DESCRIPTION=""
416
+ MSG_ID=""
417
+
418
+ while [[ $# -gt 0 ]]; do
419
+ case "${1:-}" in
420
+ --cwd) CHILD_CWD="${2:-}"; shift 2 ;;
421
+ --desc) DESCRIPTION="${2:-}"; shift 2 ;;
422
+ --msg-id) MSG_ID="${2:-}"; shift 2 ;;
423
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
424
+ esac
425
+ done
426
+
427
+ CHILD_SESSION="tq-conv-${SLUG}"
428
+
429
+ # Check if already exists
430
+ if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
431
+ echo "Session '$CHILD_SESSION' already exists."
432
+ exit 0
433
+ fi
434
+
435
+ # Create conversation directory
436
+ CONV_DIR="$CONVERSE_DIR/sessions/$SLUG"
437
+ mkdir -p "$CONV_DIR/inbox" "$CONV_DIR/outbox" "$CONV_DIR/hooks"
438
+
439
+ # Write conversation-mode instructions
440
+ cat > "$CONV_DIR/.tq-converse.md" <<CONVEOF
441
+ # tq Conversation: $SLUG <!-- tq-conversation-mode -->
442
+
443
+ You are in **Telegram conversation mode** for the conversation **$SLUG**.
444
+ Messages you receive are from a user chatting via Telegram.
445
+
446
+ ## Response Protocol
447
+
448
+ 1. Process each message normally — read files, write code, answer questions.
449
+ 2. After completing your response, **always** use the \`/tq-reply\` slash command
450
+ to send your response back to the user on Telegram.
451
+ 3. Keep responses concise — Telegram has a 4096 character limit per message.
452
+
453
+ ## Important
454
+
455
+ - Your conversation slug is: **$SLUG**
456
+ - Before calling /tq-reply, write your slug to the marker file:
457
+ \`echo "$SLUG" > $CONV_DIR/current-slug\`
458
+ - The user sees ONLY what you send via /tq-reply. Everything else stays in tmux.
459
+ - Lead with the answer, not the reasoning.
460
+ - Use plain text or basic Markdown (*bold*, _italic_, \`code\`).
461
+ CONVEOF
462
+
463
+ # Write current slug marker (so /tq-reply knows which session this is)
464
+ echo "$SLUG" > "$CONV_DIR/current-slug"
465
+
466
+ # Write settings with hooks
467
+ SETTINGS_FILE="$CONV_DIR/settings.json"
468
+ write_session_settings "$CONV_DIR/hooks" "$SETTINGS_FILE" "$SLUG"
469
+
470
+ # Register in registry
471
+ ensure_registry
472
+ CREATED="$(date -u +%Y-%m-%dT%H:%M:%S)"
473
+ registry_op set "$SLUG" "$(python3 -c "
474
+ import json, sys
475
+ print(json.dumps({
476
+ 'description': sys.argv[1],
477
+ 'tmux': sys.argv[2],
478
+ 'cwd': sys.argv[3],
479
+ 'conv_dir': sys.argv[4],
480
+ 'created': sys.argv[5],
481
+ 'last_active': sys.argv[5],
482
+ 'status': 'active'
483
+ }))
484
+ " "$DESCRIPTION" "$CHILD_SESSION" "$CHILD_CWD" "$CONV_DIR" "$CREATED")"
485
+
486
+ # Track triggering message if provided
487
+ if [[ -n "$MSG_ID" ]]; then
488
+ registry_op track-msg "$SLUG" "$MSG_ID"
489
+ fi
490
+
491
+ # Start tmux session
492
+ mkdir -p "$CHILD_CWD"
493
+ tmux start-server
494
+ tmux new-session -d -s "$CHILD_SESSION" -n "$SLUG"
495
+
496
+ inject_auth "$CHILD_SESSION:$SLUG"
497
+
498
+ tmux send-keys -t "$CHILD_SESSION:$SLUG" "cd '$CHILD_CWD'" Enter
499
+
500
+ tmux send-keys -t "$CHILD_SESSION:$SLUG" \
501
+ "claude --settings '$SETTINGS_FILE' --dangerously-skip-permissions" Enter
502
+
503
+ sleep 3
504
+ tmux send-keys -t "$CHILD_SESSION:$SLUG" \
505
+ "Read $CONV_DIR/.tq-converse.md — those are your instructions. Acknowledge with 'ready'." Enter
506
+
507
+ echo "Spawned conversation: $SLUG ($CHILD_SESSION)"
508
+ echo " CWD: $CHILD_CWD"
509
+ echo " Desc: $DESCRIPTION"
510
+ exit 0
511
+ fi
512
+
513
+ # ---------------------------------------------------------------------------
514
+ # route — send a message to a child conversation session
515
+ # ---------------------------------------------------------------------------
516
+ if [[ "$COMMAND" == "route" ]]; then
517
+ SLUG="${1:-}"
518
+ shift || true
519
+ MESSAGE="$*"
520
+
521
+ if [[ -z "$SLUG" || -z "$MESSAGE" ]]; then
522
+ echo "Usage: tq-converse route <slug> <message>" >&2
523
+ exit 1
524
+ fi
525
+
526
+ CHILD_SESSION="tq-conv-${SLUG}"
527
+
528
+ if ! tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
529
+ echo "Session '$CHILD_SESSION' is not running." >&2
530
+ exit 1
531
+ fi
532
+
533
+ # Log the incoming message
534
+ CONV_DIR="$CONVERSE_DIR/sessions/$SLUG"
535
+ mkdir -p "$CONV_DIR/inbox"
536
+ TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
537
+ echo "$MESSAGE" > "$CONV_DIR/inbox/${TIMESTAMP}.txt"
538
+
539
+ # Update last_active in registry
540
+ registry_op update-field "$SLUG" "last_active" "$(date -u +%Y-%m-%dT%H:%M:%S)" 2>/dev/null || true
541
+
542
+ # Write to temp file and use tmux load-buffer for reliable injection
543
+ MSG_TMPFILE=$(mktemp /tmp/tq-converse-msg-XXXXXX)
544
+ printf '%s' "$MESSAGE" > "$MSG_TMPFILE"
545
+
546
+ tmux load-buffer "$MSG_TMPFILE"
547
+ tmux paste-buffer -t "$CHILD_SESSION:$SLUG"
548
+ sleep 0.2
549
+ tmux send-keys -t "$CHILD_SESSION:$SLUG" Enter
550
+
551
+ rm -f "$MSG_TMPFILE"
552
+
553
+ echo "Routed to $SLUG"
554
+ exit 0
555
+ fi
556
+
557
+ # ---------------------------------------------------------------------------
558
+ # send — send a message to the orchestrator
559
+ # ---------------------------------------------------------------------------
560
+ if [[ "$COMMAND" == "send" ]]; then
561
+ MESSAGE="$*"
562
+ if [[ -z "$MESSAGE" ]]; then
563
+ echo "Usage: tq-converse send <message>" >&2
564
+ exit 1
565
+ fi
566
+
567
+ if ! tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
568
+ echo "Orchestrator is not running. Start with: tq-converse start" >&2
569
+ exit 1
570
+ fi
571
+
572
+ MSG_TMPFILE=$(mktemp /tmp/tq-converse-msg-XXXXXX)
573
+ printf '%s' "$MESSAGE" > "$MSG_TMPFILE"
574
+
575
+ tmux load-buffer "$MSG_TMPFILE"
576
+ tmux paste-buffer -t "$ORCHESTRATOR_SESSION:orchestrator"
577
+ sleep 0.2
578
+ tmux send-keys -t "$ORCHESTRATOR_SESSION:orchestrator" Enter
579
+
580
+ rm -f "$MSG_TMPFILE"
581
+
582
+ echo "Sent to orchestrator."
583
+ exit 0
584
+ fi
585
+
586
+ # ---------------------------------------------------------------------------
587
+ # stop — stop a session or the orchestrator
588
+ # ---------------------------------------------------------------------------
589
+ if [[ "$COMMAND" == "stop" ]]; then
590
+ SLUG="${1:-}"
591
+
592
+ if [[ -z "$SLUG" ]]; then
593
+ # Stop orchestrator
594
+ if tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
595
+ tmux send-keys -t "$ORCHESTRATOR_SESSION" "/exit" Enter
596
+ sleep 2
597
+ if tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
598
+ tmux kill-session -t "$ORCHESTRATOR_SESSION"
599
+ fi
600
+ echo "Orchestrator stopped."
601
+ else
602
+ echo "Orchestrator is not running."
603
+ fi
604
+ exit 0
605
+ fi
606
+
607
+ # Stop a specific child session
608
+ CHILD_SESSION="tq-conv-${SLUG}"
609
+ if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
610
+ tmux send-keys -t "$CHILD_SESSION" "/exit" Enter
611
+ sleep 2
612
+ if tmux has-session -t "$CHILD_SESSION" 2>/dev/null; then
613
+ tmux kill-session -t "$CHILD_SESSION"
614
+ fi
615
+ echo "Session '$SLUG' stopped."
616
+ else
617
+ echo "Session '$SLUG' is not running."
618
+ fi
619
+
620
+ # Update registry
621
+ registry_op update-field "$SLUG" "status" "stopped" 2>/dev/null || true
622
+ exit 0
623
+ fi
624
+
625
+ # ---------------------------------------------------------------------------
626
+ # status — show orchestrator and all sessions
627
+ # ---------------------------------------------------------------------------
628
+ if [[ "$COMMAND" == "status" ]]; then
629
+ echo "=== Orchestrator ==="
630
+ if tmux has-session -t "$ORCHESTRATOR_SESSION" 2>/dev/null; then
631
+ echo " Running: yes"
632
+ echo " Attach: tmux attach -t $ORCHESTRATOR_SESSION"
633
+ else
634
+ echo " Running: no"
635
+ fi
636
+ echo ""
637
+
638
+ echo "=== Conversations ==="
639
+ ensure_registry
640
+
641
+ SESSIONS="$(registry_op list 2>/dev/null || true)"
642
+ if [[ -z "$SESSIONS" ]]; then
643
+ echo " (none)"
644
+ else
645
+ printf " %-20s %-10s %-25s %s\n" "SLUG" "STATUS" "TMUX" "DESCRIPTION"
646
+ printf " %-20s %-10s %-25s %s\n" "----" "------" "----" "-----------"
647
+ while IFS=$'\t' read -r SLUG STATUS TMUX DESC; do
648
+ # Check actual tmux status
649
+ LIVE="$STATUS"
650
+ if [[ "$STATUS" == "active" ]] && ! tmux has-session -t "$TMUX" 2>/dev/null; then
651
+ LIVE="dead"
652
+ registry_op update-field "$SLUG" "status" "stopped" 2>/dev/null || true
653
+ fi
654
+ printf " %-20s %-10s %-25s %s\n" "$SLUG" "$LIVE" "$TMUX" "$DESC"
655
+ done <<< "$SESSIONS"
656
+ fi
657
+ exit 0
658
+ fi
659
+
660
+ # ---------------------------------------------------------------------------
661
+ # list — list active session slugs (compact, for orchestrator consumption)
662
+ # ---------------------------------------------------------------------------
663
+ if [[ "$COMMAND" == "list" ]]; then
664
+ ensure_registry
665
+ SUMMARY="$(registry_op summary 2>/dev/null || true)"
666
+ if [[ -z "$SUMMARY" ]]; then
667
+ echo "(no active conversations)"
668
+ else
669
+ printf "%-20s %-50s %s\n" "SLUG" "DESCRIPTION" "CWD"
670
+ printf "%-20s %-50s %s\n" "----" "-----------" "---"
671
+ while IFS=$'\t' read -r SLUG DESC CWD; do
672
+ printf "%-20s %-50s %s\n" "$SLUG" "$DESC" "$CWD"
673
+ done <<< "$SUMMARY"
674
+ fi
675
+ exit 0
676
+ fi
677
+
678
+ # ---------------------------------------------------------------------------
679
+ # track-msg — register a Telegram message ID to a session slug
680
+ # ---------------------------------------------------------------------------
681
+ if [[ "$COMMAND" == "track-msg" ]]; then
682
+ SLUG="${1:-}"
683
+ MSG_ID="${2:-}"
684
+ if [[ -z "$SLUG" || -z "$MSG_ID" ]]; then
685
+ echo "Usage: tq-converse track-msg <slug> <msg-id>" >&2
686
+ exit 1
687
+ fi
688
+ registry_op track-msg "$SLUG" "$MSG_ID"
689
+ exit 0
690
+ fi
691
+
692
+ # ---------------------------------------------------------------------------
693
+ # lookup-msg — find which session owns a Telegram message ID
694
+ # ---------------------------------------------------------------------------
695
+ if [[ "$COMMAND" == "lookup-msg" ]]; then
696
+ MSG_ID="${1:-}"
697
+ if [[ -z "$MSG_ID" ]]; then
698
+ echo "Usage: tq-converse lookup-msg <msg-id>" >&2
699
+ exit 1
700
+ fi
701
+ registry_op lookup-msg "$MSG_ID"
702
+ exit 0
703
+ fi
704
+
705
+ # ---------------------------------------------------------------------------
706
+ # update-status — update session status in registry
707
+ # ---------------------------------------------------------------------------
708
+ if [[ "$COMMAND" == "update-status" ]]; then
709
+ SLUG="${1:-}"
710
+ STATUS="${2:-}"
711
+ if [[ -z "$SLUG" || -z "$STATUS" ]]; then
712
+ echo "Usage: tq-converse update-status <slug> <status>" >&2
713
+ exit 1
714
+ fi
715
+ registry_op update-field "$SLUG" "status" "$STATUS"
716
+ exit 0
717
+ fi
718
+
719
+ # ---------------------------------------------------------------------------
720
+ # registry — dump the full registry
721
+ # ---------------------------------------------------------------------------
722
+ if [[ "$COMMAND" == "registry" ]]; then
723
+ ensure_registry
724
+ registry_op dump
725
+ exit 0
726
+ fi