instar 0.3.5 → 0.3.7
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/.claude/skills/setup-wizard/skill.md +89 -2
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +132 -8
- package/dist/commands/server.js +20 -16
- package/dist/core/PostUpdateMigrator.d.ts +5 -0
- package/dist/core/PostUpdateMigrator.js +45 -0
- package/dist/core/SessionManager.d.ts +4 -1
- package/dist/core/SessionManager.js +58 -12
- package/package.json +1 -1
|
@@ -262,7 +262,95 @@ For **Project Agents**: Telegram is strongly recommended but optional. The agent
|
|
|
262
262
|
|
|
263
263
|
If the user declines, that's their choice — but make the tradeoff clear in one sentence.
|
|
264
264
|
|
|
265
|
-
|
|
265
|
+
#### Browser-Automated Setup (Default)
|
|
266
|
+
|
|
267
|
+
**You have Playwright browser automation available.** Use it to do ALL of this for the user. They just need to be logged into Telegram Web.
|
|
268
|
+
|
|
269
|
+
Tell the user:
|
|
270
|
+
> "I'll set up Telegram for you automatically using the browser. Just make sure you're logged into web.telegram.org. I'll handle the bot creation, group setup, and everything else."
|
|
271
|
+
|
|
272
|
+
Then ask:
|
|
273
|
+
> "Are you logged into web.telegram.org?"
|
|
274
|
+
|
|
275
|
+
If yes, proceed with full browser automation. If no, tell them to log in first and wait.
|
|
276
|
+
|
|
277
|
+
**The automated flow:**
|
|
278
|
+
|
|
279
|
+
1. **Navigate to web.telegram.org** using Playwright:
|
|
280
|
+
```
|
|
281
|
+
mcp__playwright__browser_navigate({ url: "https://web.telegram.org/a/" })
|
|
282
|
+
```
|
|
283
|
+
Take a snapshot to verify the user is logged in (look for the chat list, search bar, etc.). If you see a login/QR code screen, tell the user they need to log in first and wait.
|
|
284
|
+
|
|
285
|
+
2. **Create a bot via @BotFather**:
|
|
286
|
+
- Take a snapshot, find the search input, click it
|
|
287
|
+
- Type "BotFather" in the search bar
|
|
288
|
+
- Take a snapshot, find @BotFather in the results, click it
|
|
289
|
+
- Take a snapshot, find the message input area
|
|
290
|
+
- If you see a "Start" button, click it. Otherwise type `/start` and press Enter
|
|
291
|
+
- Wait 2 seconds for BotFather to respond
|
|
292
|
+
- Type `/newbot` and press Enter
|
|
293
|
+
- Wait 2 seconds for BotFather to ask for a name
|
|
294
|
+
- Type the bot display name (use the project name, e.g., "My Project Agent") and press Enter
|
|
295
|
+
- Wait 2 seconds for BotFather to ask for a username
|
|
296
|
+
- Type the bot username (e.g., `myproject_agent_bot` — must end in "bot", use lowercase + underscores) and press Enter
|
|
297
|
+
- Wait 3 seconds for BotFather to respond with the token
|
|
298
|
+
- Take a snapshot and extract the bot token from BotFather's response. The token looks like `7123456789:AAHn3-xYz_example`. Look for text containing a colon between a number and alphanumeric characters.
|
|
299
|
+
- **CRITICAL: Store the token** — you'll need it for config.json
|
|
300
|
+
|
|
301
|
+
3. **Create a group**:
|
|
302
|
+
- Take a snapshot of the main Telegram screen
|
|
303
|
+
- Find and click the "New Message" / compose / pencil button (usually bottom-left area of chat list)
|
|
304
|
+
- Take a snapshot, find "New Group" option, click it
|
|
305
|
+
- In the "Add Members" search, type the bot username you just created
|
|
306
|
+
- Take a snapshot, find the bot in results, click to select it
|
|
307
|
+
- Find and click the "Next" / arrow button to proceed
|
|
308
|
+
- Type the group name (use the project name, e.g., "My Project")
|
|
309
|
+
- Find and click "Create" / checkmark button
|
|
310
|
+
- Wait 2 seconds for the group to be created
|
|
311
|
+
|
|
312
|
+
4. **Enable Topics**:
|
|
313
|
+
- Take a snapshot of the new group chat
|
|
314
|
+
- Click on the group name/header at the top to open group info
|
|
315
|
+
- Take a snapshot, find the Edit / pencil button, click it
|
|
316
|
+
- Take a snapshot, look for "Topics" toggle and enable it
|
|
317
|
+
- If you don't see Topics directly, look for "Group Type" or "Chat Type" first — changing this may reveal the Topics toggle
|
|
318
|
+
- Find and click Save / checkmark
|
|
319
|
+
- Wait 2 seconds
|
|
320
|
+
|
|
321
|
+
5. **Make bot admin**:
|
|
322
|
+
- Take a snapshot of the group info or edit screen
|
|
323
|
+
- Navigate to Administrators section (may need to click group name first, then Edit)
|
|
324
|
+
- Click "Add Admin" or "Add Administrator"
|
|
325
|
+
- Search for your bot username
|
|
326
|
+
- Take a snapshot, find the bot, click to select
|
|
327
|
+
- Click Save / Done to confirm admin rights
|
|
328
|
+
- Wait 2 seconds
|
|
329
|
+
|
|
330
|
+
6. **Detect chat ID**:
|
|
331
|
+
- Type "hello" in the group chat and send it (this triggers the bot to see the group)
|
|
332
|
+
- Wait 3 seconds for the message to propagate to the bot
|
|
333
|
+
- Use Bash to call the Telegram Bot API:
|
|
334
|
+
```bash
|
|
335
|
+
curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates?offset=-1" > /dev/null
|
|
336
|
+
curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates?timeout=5"
|
|
337
|
+
```
|
|
338
|
+
- Parse the response to find `chat.id` where `chat.type` is "supergroup" or "group"
|
|
339
|
+
- If auto-detection fails, try once more (send another message, wait, call API again)
|
|
340
|
+
|
|
341
|
+
**Browser automation tips:**
|
|
342
|
+
- **Always take a snapshot** before interacting. Telegram Web's UI changes frequently.
|
|
343
|
+
- **Use `mcp__playwright__browser_snapshot`** to see the accessibility tree (more reliable than screenshots for finding elements).
|
|
344
|
+
- **Use `mcp__playwright__browser_click`** with element refs from the snapshot.
|
|
345
|
+
- **Use `mcp__playwright__browser_type`** to type text into inputs. For the Telegram message input, you may need to find the message input ref and use `submit: true` to send.
|
|
346
|
+
- **Wait 2-3 seconds** after each action for Telegram to process. Use `mcp__playwright__browser_wait_for({ time: 2 })`.
|
|
347
|
+
- **If an element isn't found**, take a fresh snapshot — Telegram may have changed the view.
|
|
348
|
+
- **Telegram Web uses version "a"** (web.telegram.org/a/) — this is the React-based client.
|
|
349
|
+
- **If something goes wrong**, tell the user what happened and offer to retry that step or fall back to manual instructions.
|
|
350
|
+
|
|
351
|
+
#### Manual Fallback
|
|
352
|
+
|
|
353
|
+
If Playwright tools are not available, or if browser automation fails, fall back to the manual walkthrough:
|
|
266
354
|
|
|
267
355
|
1. **Create a bot** via @BotFather on Telegram:
|
|
268
356
|
- Open https://web.telegram.org
|
|
@@ -276,7 +364,6 @@ Walk through setup step by step:
|
|
|
276
364
|
|
|
277
365
|
3. **Enable Topics**:
|
|
278
366
|
- Open group info, Edit, turn on Topics
|
|
279
|
-
- This gives you separate threads (like Slack channels)
|
|
280
367
|
|
|
281
368
|
4. **Make bot admin**:
|
|
282
369
|
- Group info, Edit, Administrators, Add your bot
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -42,7 +42,8 @@ export declare function initProject(options: InitOptions): Promise<void>;
|
|
|
42
42
|
* Refresh hooks, Claude settings, and CLAUDE.md for an existing installation.
|
|
43
43
|
* Called after updates to ensure new hooks and documentation are installed.
|
|
44
44
|
* Re-writes all hook files (idempotent), merges new hooks into settings,
|
|
45
|
-
*
|
|
45
|
+
* appends any missing sections to CLAUDE.md, and installs scripts for
|
|
46
|
+
* configured integrations (e.g., Telegram relay).
|
|
46
47
|
*/
|
|
47
48
|
export declare function refreshHooksAndSettings(projectDir: string, stateDir: string): void;
|
|
48
49
|
export {};
|
package/dist/commands/init.js
CHANGED
|
@@ -636,13 +636,15 @@ If everything looks healthy, exit silently. Only report issues.`,
|
|
|
636
636
|
* Refresh hooks, Claude settings, and CLAUDE.md for an existing installation.
|
|
637
637
|
* Called after updates to ensure new hooks and documentation are installed.
|
|
638
638
|
* Re-writes all hook files (idempotent), merges new hooks into settings,
|
|
639
|
-
*
|
|
639
|
+
* appends any missing sections to CLAUDE.md, and installs scripts for
|
|
640
|
+
* configured integrations (e.g., Telegram relay).
|
|
640
641
|
*/
|
|
641
642
|
export function refreshHooksAndSettings(projectDir, stateDir) {
|
|
642
643
|
installHooks(stateDir);
|
|
643
644
|
installClaudeSettings(projectDir);
|
|
644
645
|
refreshClaudeMd(projectDir, stateDir);
|
|
645
646
|
refreshJobs(stateDir);
|
|
647
|
+
refreshScripts(projectDir, stateDir);
|
|
646
648
|
}
|
|
647
649
|
/**
|
|
648
650
|
* Merge new default jobs into existing jobs.json without overwriting user changes.
|
|
@@ -675,21 +677,116 @@ function refreshJobs(stateDir) {
|
|
|
675
677
|
}
|
|
676
678
|
catch { /* don't break on errors */ }
|
|
677
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Read config.json from state dir, returning parsed config or null.
|
|
682
|
+
*/
|
|
683
|
+
function readConfig(stateDir) {
|
|
684
|
+
try {
|
|
685
|
+
return JSON.parse(fs.readFileSync(path.join(stateDir, 'config.json'), 'utf-8'));
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Check if Telegram is configured in config.json.
|
|
693
|
+
*/
|
|
694
|
+
function isTelegramConfigured(stateDir) {
|
|
695
|
+
const config = readConfig(stateDir);
|
|
696
|
+
if (!config)
|
|
697
|
+
return false;
|
|
698
|
+
const messaging = config.messaging;
|
|
699
|
+
return !!messaging?.some(m => m.type === 'telegram' && m.enabled);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Install scripts for configured integrations (e.g., Telegram relay).
|
|
703
|
+
* Called during refresh to ensure scripts exist for all configured integrations.
|
|
704
|
+
*/
|
|
705
|
+
function refreshScripts(projectDir, stateDir) {
|
|
706
|
+
const config = readConfig(stateDir);
|
|
707
|
+
if (!config)
|
|
708
|
+
return;
|
|
709
|
+
const port = config.port || 4040;
|
|
710
|
+
// Install telegram-reply.sh if Telegram is configured
|
|
711
|
+
if (isTelegramConfigured(stateDir)) {
|
|
712
|
+
installTelegramRelay(projectDir, port);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Install the Telegram relay script that Claude uses to send responses
|
|
717
|
+
* back to Telegram topics via the instar server API.
|
|
718
|
+
*/
|
|
719
|
+
function installTelegramRelay(projectDir, port) {
|
|
720
|
+
const scriptsDir = path.join(projectDir, '.claude', 'scripts');
|
|
721
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
722
|
+
const scriptContent = `#!/bin/bash
|
|
723
|
+
# telegram-reply.sh — Send a message back to a Telegram topic via instar server.
|
|
724
|
+
#
|
|
725
|
+
# Usage:
|
|
726
|
+
# .claude/scripts/telegram-reply.sh TOPIC_ID "message text"
|
|
727
|
+
# echo "message text" | .claude/scripts/telegram-reply.sh TOPIC_ID
|
|
728
|
+
# cat <<'EOF' | .claude/scripts/telegram-reply.sh TOPIC_ID
|
|
729
|
+
# Multi-line message here
|
|
730
|
+
# EOF
|
|
731
|
+
|
|
732
|
+
TOPIC_ID="$1"
|
|
733
|
+
shift
|
|
734
|
+
|
|
735
|
+
if [ -z "$TOPIC_ID" ]; then
|
|
736
|
+
echo "Usage: telegram-reply.sh TOPIC_ID [message]" >&2
|
|
737
|
+
exit 1
|
|
738
|
+
fi
|
|
739
|
+
|
|
740
|
+
# Read message from args or stdin
|
|
741
|
+
if [ $# -gt 0 ]; then
|
|
742
|
+
MSG="$*"
|
|
743
|
+
else
|
|
744
|
+
MSG="$(cat)"
|
|
745
|
+
fi
|
|
746
|
+
|
|
747
|
+
if [ -z "$MSG" ]; then
|
|
748
|
+
echo "No message provided" >&2
|
|
749
|
+
exit 1
|
|
750
|
+
fi
|
|
751
|
+
|
|
752
|
+
PORT="\${INSTAR_PORT:-${port}}"
|
|
753
|
+
|
|
754
|
+
# Escape for JSON
|
|
755
|
+
JSON_MSG=$(printf '%s' "$MSG" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
|
|
756
|
+
if [ -z "$JSON_MSG" ]; then
|
|
757
|
+
# Fallback if python3 not available: basic escape
|
|
758
|
+
JSON_MSG="$(printf '%s' "$MSG" | sed 's/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/"/\\\\\\\\"/g' | sed ':a;N;$!ba;s/\\\\n/\\\\\\\\n/g')"
|
|
759
|
+
JSON_MSG="\\"$JSON_MSG\\""
|
|
760
|
+
fi
|
|
761
|
+
|
|
762
|
+
RESPONSE=$(curl -s -w "\\n%{http_code}" -X POST "http://localhost:\${PORT}/telegram/reply/\${TOPIC_ID}" \\
|
|
763
|
+
-H 'Content-Type: application/json' \\
|
|
764
|
+
-d "{\\"text\\":\${JSON_MSG}}")
|
|
765
|
+
|
|
766
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
767
|
+
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
768
|
+
|
|
769
|
+
if [ "$HTTP_CODE" = "200" ]; then
|
|
770
|
+
echo "Sent $(echo "$MSG" | wc -c | tr -d ' ') chars to topic $TOPIC_ID"
|
|
771
|
+
else
|
|
772
|
+
echo "Failed (HTTP $HTTP_CODE): $BODY" >&2
|
|
773
|
+
exit 1
|
|
774
|
+
fi
|
|
775
|
+
`;
|
|
776
|
+
const scriptPath = path.join(scriptsDir, 'telegram-reply.sh');
|
|
777
|
+
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
778
|
+
}
|
|
678
779
|
/**
|
|
679
780
|
* Append missing sections to CLAUDE.md without overwriting user customizations.
|
|
680
781
|
* Reads config.json for port, checks for known section headers, appends if missing.
|
|
782
|
+
* Also adds Telegram relay section if Telegram is configured.
|
|
681
783
|
*/
|
|
682
784
|
function refreshClaudeMd(projectDir, stateDir) {
|
|
683
785
|
const claudeMdPath = path.join(projectDir, 'CLAUDE.md');
|
|
684
786
|
if (!fs.existsSync(claudeMdPath))
|
|
685
787
|
return;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
const config = JSON.parse(fs.readFileSync(path.join(stateDir, 'config.json'), 'utf-8'));
|
|
690
|
-
port = config.port || 4321;
|
|
691
|
-
}
|
|
692
|
-
catch { /* use default port */ }
|
|
788
|
+
const config = readConfig(stateDir);
|
|
789
|
+
const port = config?.port || 4040;
|
|
693
790
|
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
694
791
|
const additions = [];
|
|
695
792
|
// Check for Self-Diagnosis section
|
|
@@ -719,6 +816,22 @@ curl -s -X POST http://localhost:${port}/feedback \\
|
|
|
719
816
|
Types: \`bug\`, \`feature\`, \`improvement\`, \`question\`
|
|
720
817
|
|
|
721
818
|
**Do not wait for the user to notice.** If a hook throws an error, report it. If a job fails, report it. If the server returns unexpected data, report it. You are not just using instar — you are part of its immune system.
|
|
819
|
+
`);
|
|
820
|
+
}
|
|
821
|
+
// Check for Telegram Relay section (add if Telegram is configured)
|
|
822
|
+
if (isTelegramConfigured(stateDir) && !content.includes('Telegram Relay')) {
|
|
823
|
+
additions.push(`
|
|
824
|
+
## Telegram Relay
|
|
825
|
+
|
|
826
|
+
When user input starts with \`[telegram:N]\`, the message came from a user via Telegram topic N. After responding, relay the response back:
|
|
827
|
+
|
|
828
|
+
\`\`\`bash
|
|
829
|
+
cat <<'EOF' | .claude/scripts/telegram-reply.sh N
|
|
830
|
+
Your response text here
|
|
831
|
+
EOF
|
|
832
|
+
\`\`\`
|
|
833
|
+
|
|
834
|
+
Strip the \`[telegram:N]\` prefix before interpreting the message. Only relay conversational text — not tool output.
|
|
722
835
|
`);
|
|
723
836
|
}
|
|
724
837
|
if (additions.length > 0) {
|
|
@@ -1044,6 +1157,17 @@ function installClaudeSettings(projectDir) {
|
|
|
1044
1157
|
},
|
|
1045
1158
|
];
|
|
1046
1159
|
}
|
|
1160
|
+
// MCP Servers: Playwright for browser automation (used by setup wizard, Telegram setup, etc.)
|
|
1161
|
+
if (!settings.mcpServers) {
|
|
1162
|
+
settings.mcpServers = {};
|
|
1163
|
+
}
|
|
1164
|
+
const mcpServers = settings.mcpServers;
|
|
1165
|
+
if (!mcpServers.playwright) {
|
|
1166
|
+
mcpServers.playwright = {
|
|
1167
|
+
command: 'npx',
|
|
1168
|
+
args: ['-y', '@playwright/mcp@latest'],
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1047
1171
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1048
1172
|
}
|
|
1049
1173
|
//# sourceMappingURL=init.js.map
|
package/dist/commands/server.js
CHANGED
|
@@ -50,20 +50,17 @@ async function respawnSessionForTopic(sessionManager, telegram, targetSession, t
|
|
|
50
50
|
catch (err) {
|
|
51
51
|
console.error(`[telegram→session] Failed to fetch thread history:`, err);
|
|
52
52
|
}
|
|
53
|
-
//
|
|
54
|
-
//
|
|
53
|
+
// Single-line bootstrap to avoid tmux send-keys newline issues.
|
|
54
|
+
// Thread history and context go into temp files for Claude to read.
|
|
55
|
+
const tmpDir = '/tmp/instar-telegram';
|
|
56
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
55
57
|
let bootstrapMessage;
|
|
56
58
|
if (historyLines.length > 0) {
|
|
57
59
|
const historyContent = historyLines.join('\n');
|
|
58
|
-
const tmpDir = '/tmp/instar-telegram';
|
|
59
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
60
60
|
const filepath = path.join(tmpDir, `history-${topicId}-${Date.now()}-${process.pid}.txt`);
|
|
61
61
|
fs.writeFileSync(filepath, historyContent);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
``,
|
|
65
|
-
`(Session was respawned. Thread history is at ${filepath} — read it for context. Then RESPOND to the user's message above via Telegram relay.)`,
|
|
66
|
-
].join('\n');
|
|
62
|
+
// Single-line: user message + file reference for history
|
|
63
|
+
bootstrapMessage = `[telegram:${topicId}] ${msg} (Session respawned. Thread history at ${filepath} — read it for context before responding.)`;
|
|
67
64
|
}
|
|
68
65
|
else {
|
|
69
66
|
bootstrapMessage = `[telegram:${topicId}] ${msg}`;
|
|
@@ -125,15 +122,22 @@ function wireTelegramRouting(telegram, sessionManager) {
|
|
|
125
122
|
// No session mapped — auto-spawn one
|
|
126
123
|
console.log(`[telegram→session] No session for topic ${topicId}, auto-spawning...`);
|
|
127
124
|
const storedName = telegram.getTopicName(topicId) || `topic-${topicId}`;
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
|
|
125
|
+
// Single-line bootstrap to avoid tmux send-keys newline issues.
|
|
126
|
+
// Multi-line context is written to a temp file for Claude to read.
|
|
127
|
+
const contextLines = [
|
|
128
|
+
`This session was auto-created for Telegram topic ${topicId}.`,
|
|
129
|
+
`Respond to the user's message via Telegram relay: cat <<'EOF' | .claude/scripts/telegram-reply.sh ${topicId}`,
|
|
130
|
+
`Your response here`,
|
|
131
|
+
`EOF`,
|
|
132
|
+
];
|
|
133
|
+
const tmpDir = '/tmp/instar-telegram';
|
|
134
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
135
|
+
const ctxPath = path.join(tmpDir, `ctx-${topicId}-${Date.now()}.txt`);
|
|
136
|
+
fs.writeFileSync(ctxPath, contextLines.join('\n'));
|
|
137
|
+
const bootstrapMessage = `[telegram:${topicId}] ${text}`;
|
|
134
138
|
sessionManager.spawnInteractiveSession(bootstrapMessage, storedName).then((newSessionName) => {
|
|
135
139
|
telegram.registerTopicSession(topicId, newSessionName);
|
|
136
|
-
telegram.sendToTopic(topicId, `Session
|
|
140
|
+
telegram.sendToTopic(topicId, `Session created.`).catch(() => { });
|
|
137
141
|
console.log(`[telegram→session] Auto-spawned "${newSessionName}" for topic ${topicId}`);
|
|
138
142
|
}).catch((err) => {
|
|
139
143
|
console.error(`[telegram→session] Auto-spawn failed:`, err);
|
|
@@ -55,6 +55,11 @@ export declare class PostUpdateMigrator {
|
|
|
55
55
|
* Never overwrites existing scripts (user may have customized them).
|
|
56
56
|
*/
|
|
57
57
|
private migrateScripts;
|
|
58
|
+
/**
|
|
59
|
+
* Ensure .claude/settings.json has required MCP servers.
|
|
60
|
+
* Only adds — never removes existing configuration.
|
|
61
|
+
*/
|
|
62
|
+
private migrateSettings;
|
|
58
63
|
private getSessionStartHook;
|
|
59
64
|
private getDangerousCommandGuard;
|
|
60
65
|
private getGroundingBeforeMessaging;
|
|
@@ -37,6 +37,7 @@ export class PostUpdateMigrator {
|
|
|
37
37
|
this.migrateHooks(result);
|
|
38
38
|
this.migrateClaudeMd(result);
|
|
39
39
|
this.migrateScripts(result);
|
|
40
|
+
this.migrateSettings(result);
|
|
40
41
|
return result;
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
@@ -191,6 +192,50 @@ Strip the \`[telegram:N]\` prefix before interpreting the message. Respond natur
|
|
|
191
192
|
result.skipped.push('scripts/health-watchdog.sh (already exists)');
|
|
192
193
|
}
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Ensure .claude/settings.json has required MCP servers.
|
|
197
|
+
* Only adds — never removes existing configuration.
|
|
198
|
+
*/
|
|
199
|
+
migrateSettings(result) {
|
|
200
|
+
const settingsPath = path.join(this.config.projectDir, '.claude', 'settings.json');
|
|
201
|
+
if (!fs.existsSync(settingsPath)) {
|
|
202
|
+
result.skipped.push('.claude/settings.json (not found — will be created on next init)');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
let settings;
|
|
206
|
+
try {
|
|
207
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
result.errors.push(`settings.json read: ${err instanceof Error ? err.message : String(err)}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let patched = false;
|
|
214
|
+
// Playwright MCP server — required for browser automation (Telegram setup, etc.)
|
|
215
|
+
if (!settings.mcpServers) {
|
|
216
|
+
settings.mcpServers = {};
|
|
217
|
+
}
|
|
218
|
+
const mcpServers = settings.mcpServers;
|
|
219
|
+
if (!mcpServers.playwright) {
|
|
220
|
+
mcpServers.playwright = {
|
|
221
|
+
command: 'npx',
|
|
222
|
+
args: ['-y', '@playwright/mcp@latest'],
|
|
223
|
+
};
|
|
224
|
+
patched = true;
|
|
225
|
+
result.upgraded.push('.claude/settings.json: added Playwright MCP server');
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
result.skipped.push('.claude/settings.json: Playwright MCP already configured');
|
|
229
|
+
}
|
|
230
|
+
if (patched) {
|
|
231
|
+
try {
|
|
232
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
result.errors.push(`settings.json write: ${err instanceof Error ? err.message : String(err)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
194
239
|
// ── Hook Templates ─────────────────────────────────────────────────
|
|
195
240
|
getSessionStartHook() {
|
|
196
241
|
return `#!/bin/bash
|
|
@@ -88,11 +88,14 @@ export declare class SessionManager extends EventEmitter {
|
|
|
88
88
|
injectTelegramMessage(tmuxSession: string, topicId: number, text: string): void;
|
|
89
89
|
/**
|
|
90
90
|
* Send text to a tmux session via send-keys.
|
|
91
|
-
*
|
|
91
|
+
* For single-line text, uses -l (literal) flag directly.
|
|
92
|
+
* For multi-line text, writes to a temp file and uses tmux load-buffer/paste-buffer
|
|
93
|
+
* to avoid newlines being interpreted as Enter keypresses.
|
|
92
94
|
*/
|
|
93
95
|
private injectMessage;
|
|
94
96
|
/**
|
|
95
97
|
* Wait for Claude to be ready in a tmux session by polling output.
|
|
98
|
+
* Looks for Claude Code's prompt character (❯) which appears when ready for input.
|
|
96
99
|
*/
|
|
97
100
|
private waitForClaudeReady;
|
|
98
101
|
private tmuxSessionExists;
|
|
@@ -314,9 +314,15 @@ export class SessionManager extends EventEmitter {
|
|
|
314
314
|
this.waitForClaudeReady(tmuxSession).then((ready) => {
|
|
315
315
|
if (ready) {
|
|
316
316
|
this.injectMessage(tmuxSession, initialMessage);
|
|
317
|
+
console.log(`[SessionManager] Injected initial message into "${tmuxSession}" (${initialMessage.length} chars)`);
|
|
317
318
|
}
|
|
318
319
|
else {
|
|
319
|
-
console.error(`[SessionManager] Claude not ready in session "${tmuxSession}"
|
|
320
|
+
console.error(`[SessionManager] Claude not ready in session "${tmuxSession}" — message NOT injected. Session may need manual intervention.`);
|
|
321
|
+
// Still try to inject — Claude might be ready but prompt detection failed
|
|
322
|
+
if (this.tmuxSessionExists(tmuxSession)) {
|
|
323
|
+
console.log(`[SessionManager] Session "${tmuxSession}" still alive — attempting injection anyway`);
|
|
324
|
+
this.injectMessage(tmuxSession, initialMessage);
|
|
325
|
+
}
|
|
320
326
|
}
|
|
321
327
|
}).catch((err) => {
|
|
322
328
|
console.error(`[SessionManager] Error waiting for Claude ready in "${tmuxSession}": ${err}`);
|
|
@@ -346,19 +352,50 @@ export class SessionManager extends EventEmitter {
|
|
|
346
352
|
}
|
|
347
353
|
/**
|
|
348
354
|
* Send text to a tmux session via send-keys.
|
|
349
|
-
*
|
|
355
|
+
* For single-line text, uses -l (literal) flag directly.
|
|
356
|
+
* For multi-line text, writes to a temp file and uses tmux load-buffer/paste-buffer
|
|
357
|
+
* to avoid newlines being interpreted as Enter keypresses.
|
|
350
358
|
*/
|
|
351
359
|
injectMessage(tmuxSession, text) {
|
|
352
360
|
const exactTarget = `=${tmuxSession}:`;
|
|
353
361
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
+
if (text.includes('\n')) {
|
|
363
|
+
// Multi-line: write to temp file, load into tmux buffer, paste into pane.
|
|
364
|
+
// This avoids newlines being treated as Enter keypresses which would
|
|
365
|
+
// fragment the message into multiple Claude prompts.
|
|
366
|
+
const tmpDir = path.join('/tmp', 'instar-inject');
|
|
367
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
368
|
+
const tmpPath = path.join(tmpDir, `msg-${Date.now()}-${process.pid}.txt`);
|
|
369
|
+
fs.writeFileSync(tmpPath, text);
|
|
370
|
+
try {
|
|
371
|
+
execFileSync(this.config.tmuxPath, ['load-buffer', tmpPath], {
|
|
372
|
+
encoding: 'utf-8', timeout: 5000,
|
|
373
|
+
});
|
|
374
|
+
execFileSync(this.config.tmuxPath, ['paste-buffer', '-t', exactTarget, '-p'], {
|
|
375
|
+
encoding: 'utf-8', timeout: 5000,
|
|
376
|
+
});
|
|
377
|
+
// Send Enter to submit
|
|
378
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, 'Enter'], {
|
|
379
|
+
encoding: 'utf-8', timeout: 5000,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
try {
|
|
384
|
+
fs.unlinkSync(tmpPath);
|
|
385
|
+
}
|
|
386
|
+
catch { /* ignore */ }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// Single-line: simple send-keys
|
|
391
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, '-l', text], {
|
|
392
|
+
encoding: 'utf-8', timeout: 5000,
|
|
393
|
+
});
|
|
394
|
+
// Send Enter separately
|
|
395
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, 'Enter'], {
|
|
396
|
+
encoding: 'utf-8', timeout: 5000,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
362
399
|
}
|
|
363
400
|
catch (err) {
|
|
364
401
|
console.error(`[SessionManager] Failed to inject message into ${tmuxSession}: ${err}`);
|
|
@@ -366,20 +403,29 @@ export class SessionManager extends EventEmitter {
|
|
|
366
403
|
}
|
|
367
404
|
/**
|
|
368
405
|
* Wait for Claude to be ready in a tmux session by polling output.
|
|
406
|
+
* Looks for Claude Code's prompt character (❯) which appears when ready for input.
|
|
369
407
|
*/
|
|
370
|
-
async waitForClaudeReady(tmuxSession, timeoutMs =
|
|
408
|
+
async waitForClaudeReady(tmuxSession, timeoutMs = 30000) {
|
|
371
409
|
const start = Date.now();
|
|
372
410
|
// Wait a minimum startup delay before checking (Claude needs time to load)
|
|
373
|
-
await new Promise(r => setTimeout(r,
|
|
411
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
374
412
|
while (Date.now() - start < timeoutMs) {
|
|
413
|
+
if (!this.tmuxSessionExists(tmuxSession)) {
|
|
414
|
+
console.error(`[SessionManager] Session "${tmuxSession}" died during startup`);
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
375
417
|
const output = this.captureOutput(tmuxSession, 10);
|
|
376
418
|
// Check for Claude Code's specific prompt character (❯)
|
|
377
419
|
// Avoid matching generic shell prompts (> and $) which cause false positives
|
|
378
420
|
if (output && output.includes('❯')) {
|
|
421
|
+
console.log(`[SessionManager] Claude ready in "${tmuxSession}" after ${Date.now() - start}ms`);
|
|
379
422
|
return true;
|
|
380
423
|
}
|
|
381
424
|
await new Promise(r => setTimeout(r, 500));
|
|
382
425
|
}
|
|
426
|
+
// Log what we see on timeout for debugging
|
|
427
|
+
const finalOutput = this.captureOutput(tmuxSession, 20);
|
|
428
|
+
console.error(`[SessionManager] Claude not ready in "${tmuxSession}" after ${timeoutMs}ms. Output: ${(finalOutput || '').slice(-200)}`);
|
|
383
429
|
return false;
|
|
384
430
|
}
|
|
385
431
|
tmuxSessionExists(name) {
|