@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,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
|