instar 0.3.4 → 0.3.6
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/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +142 -8
- package/dist/commands/server.js +27 -17
- package/dist/commands/setup.js +10 -0
- package/dist/core/PostUpdateMigrator.d.ts +65 -0
- package/dist/core/PostUpdateMigrator.js +347 -0
- package/dist/core/SessionManager.d.ts +4 -1
- package/dist/core/SessionManager.js +58 -12
- package/dist/core/UpdateChecker.d.ts +13 -1
- package/dist/core/UpdateChecker.js +35 -11
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/scaffold/templates.js +10 -0
- package/dist/server/routes.js +98 -0
- package/package.json +1 -1
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
|
@@ -432,6 +432,16 @@ This routes feedback to the Instar maintainers automatically. Valid types: \`bug
|
|
|
432
432
|
|
|
433
433
|
**Scripts** — Create shell/python scripts in \`.claude/scripts/\` for reusable capabilities.
|
|
434
434
|
|
|
435
|
+
### Self-Discovery (Know Before You Claim)
|
|
436
|
+
|
|
437
|
+
Before EVER saying "I don't have", "I can't", or "this isn't available" — check what actually exists:
|
|
438
|
+
|
|
439
|
+
\`\`\`bash
|
|
440
|
+
curl http://localhost:${port}/capabilities
|
|
441
|
+
\`\`\`
|
|
442
|
+
|
|
443
|
+
This returns your full capability matrix: scripts, hooks, Telegram status, jobs, relationships, and more. It is the source of truth about what you can do. **Never hallucinate about missing capabilities — verify first.**
|
|
444
|
+
|
|
435
445
|
### How to Build New Capabilities
|
|
436
446
|
|
|
437
447
|
When a user asks for something you can't do yet, **build it**:
|
|
@@ -626,13 +636,15 @@ If everything looks healthy, exit silently. Only report issues.`,
|
|
|
626
636
|
* Refresh hooks, Claude settings, and CLAUDE.md for an existing installation.
|
|
627
637
|
* Called after updates to ensure new hooks and documentation are installed.
|
|
628
638
|
* Re-writes all hook files (idempotent), merges new hooks into settings,
|
|
629
|
-
*
|
|
639
|
+
* appends any missing sections to CLAUDE.md, and installs scripts for
|
|
640
|
+
* configured integrations (e.g., Telegram relay).
|
|
630
641
|
*/
|
|
631
642
|
export function refreshHooksAndSettings(projectDir, stateDir) {
|
|
632
643
|
installHooks(stateDir);
|
|
633
644
|
installClaudeSettings(projectDir);
|
|
634
645
|
refreshClaudeMd(projectDir, stateDir);
|
|
635
646
|
refreshJobs(stateDir);
|
|
647
|
+
refreshScripts(projectDir, stateDir);
|
|
636
648
|
}
|
|
637
649
|
/**
|
|
638
650
|
* Merge new default jobs into existing jobs.json without overwriting user changes.
|
|
@@ -665,21 +677,116 @@ function refreshJobs(stateDir) {
|
|
|
665
677
|
}
|
|
666
678
|
catch { /* don't break on errors */ }
|
|
667
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
|
+
}
|
|
668
779
|
/**
|
|
669
780
|
* Append missing sections to CLAUDE.md without overwriting user customizations.
|
|
670
781
|
* Reads config.json for port, checks for known section headers, appends if missing.
|
|
782
|
+
* Also adds Telegram relay section if Telegram is configured.
|
|
671
783
|
*/
|
|
672
784
|
function refreshClaudeMd(projectDir, stateDir) {
|
|
673
785
|
const claudeMdPath = path.join(projectDir, 'CLAUDE.md');
|
|
674
786
|
if (!fs.existsSync(claudeMdPath))
|
|
675
787
|
return;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
try {
|
|
679
|
-
const config = JSON.parse(fs.readFileSync(path.join(stateDir, 'config.json'), 'utf-8'));
|
|
680
|
-
port = config.port || 4321;
|
|
681
|
-
}
|
|
682
|
-
catch { /* use default port */ }
|
|
788
|
+
const config = readConfig(stateDir);
|
|
789
|
+
const port = config?.port || 4040;
|
|
683
790
|
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
684
791
|
const additions = [];
|
|
685
792
|
// Check for Self-Diagnosis section
|
|
@@ -709,6 +816,22 @@ curl -s -X POST http://localhost:${port}/feedback \\
|
|
|
709
816
|
Types: \`bug\`, \`feature\`, \`improvement\`, \`question\`
|
|
710
817
|
|
|
711
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.
|
|
712
835
|
`);
|
|
713
836
|
}
|
|
714
837
|
if (additions.length > 0) {
|
|
@@ -739,6 +862,17 @@ if [ -d "$INSTAR_DIR/relationships" ]; then
|
|
|
739
862
|
fi
|
|
740
863
|
fi
|
|
741
864
|
CONTEXT="\${CONTEXT}IMPORTANT: To report bugs or request features, use POST /feedback on your local server. NEVER use gh or GitHub directly.\\n"
|
|
865
|
+
|
|
866
|
+
# Self-discovery: check what capabilities are available
|
|
867
|
+
if [ -f "$INSTAR_DIR/config.json" ]; then
|
|
868
|
+
PORT=$(python3 -c "import json; print(json.load(open('$INSTAR_DIR/config.json')).get('port', 4040))" 2>/dev/null || echo "4040")
|
|
869
|
+
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:\${PORT}/health" 2>/dev/null)
|
|
870
|
+
if [ "$HEALTH" = "200" ]; then
|
|
871
|
+
CONTEXT="\${CONTEXT}Instar server is running on port \${PORT}. Query your capabilities: curl http://localhost:\${PORT}/capabilities\\n"
|
|
872
|
+
CONTEXT="\${CONTEXT}IMPORTANT: Before claiming you lack a capability, check /capabilities first.\\n"
|
|
873
|
+
fi
|
|
874
|
+
fi
|
|
875
|
+
|
|
742
876
|
[ -n "$CONTEXT" ] && echo "$CONTEXT"
|
|
743
877
|
`, { mode: 0o755 });
|
|
744
878
|
// Dangerous command guard
|
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);
|
|
@@ -268,7 +272,13 @@ export async function startServer(options) {
|
|
|
268
272
|
});
|
|
269
273
|
console.log(pc.green(' Dispatch system enabled'));
|
|
270
274
|
}
|
|
271
|
-
const updateChecker = new UpdateChecker(
|
|
275
|
+
const updateChecker = new UpdateChecker({
|
|
276
|
+
stateDir: config.stateDir,
|
|
277
|
+
projectDir: config.projectDir,
|
|
278
|
+
port: config.port,
|
|
279
|
+
hasTelegram: config.messaging.some(m => m.type === 'telegram' && m.enabled),
|
|
280
|
+
projectName: config.projectName,
|
|
281
|
+
});
|
|
272
282
|
// Check for updates on startup
|
|
273
283
|
updateChecker.check().then(info => {
|
|
274
284
|
if (info.updateAvailable) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -798,6 +798,16 @@ This routes feedback to the Instar maintainers automatically. Valid types: \`bug
|
|
|
798
798
|
|
|
799
799
|
**Scripts** — Create shell/python scripts in \`.claude/scripts/\` for reusable capabilities.
|
|
800
800
|
|
|
801
|
+
### Self-Discovery (Know Before You Claim)
|
|
802
|
+
|
|
803
|
+
Before EVER saying "I don't have", "I can't", or "this isn't available" — check what actually exists:
|
|
804
|
+
|
|
805
|
+
\`\`\`bash
|
|
806
|
+
curl http://localhost:${port}/capabilities
|
|
807
|
+
\`\`\`
|
|
808
|
+
|
|
809
|
+
This returns your full capability matrix: scripts, hooks, Telegram status, jobs, relationships, and more. It is the source of truth about what you can do. **Never hallucinate about missing capabilities — verify first.**
|
|
810
|
+
|
|
801
811
|
### How to Build New Capabilities
|
|
802
812
|
|
|
803
813
|
When a user asks for something you can't do yet, **build it**:
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Update Migrator — the "intelligence download" layer.
|
|
3
|
+
*
|
|
4
|
+
* When an agent installs a new version of instar, updating the npm
|
|
5
|
+
* package only changes the server code. But the agent's local awareness
|
|
6
|
+
* lives in project files: CLAUDE.md, hooks, scripts.
|
|
7
|
+
*
|
|
8
|
+
* This migrator bridges that gap. After every successful update, it:
|
|
9
|
+
* 1. Re-installs hooks with the latest templates (behavioral upgrades)
|
|
10
|
+
* 2. Patches CLAUDE.md with any new sections (awareness upgrades)
|
|
11
|
+
* 3. Installs any new scripts (capability upgrades)
|
|
12
|
+
* 4. Returns a human-readable migration report
|
|
13
|
+
*
|
|
14
|
+
* Design principles:
|
|
15
|
+
* - Additive only: never remove or modify existing user customizations
|
|
16
|
+
* - Hooks are overwritten (they're generated infrastructure, not user-edited)
|
|
17
|
+
* - CLAUDE.md sections are appended only if missing (check by heading)
|
|
18
|
+
* - Scripts are installed only if missing (never overwrite user modifications)
|
|
19
|
+
*/
|
|
20
|
+
export interface MigrationResult {
|
|
21
|
+
/** What was upgraded */
|
|
22
|
+
upgraded: string[];
|
|
23
|
+
/** What was already up to date */
|
|
24
|
+
skipped: string[];
|
|
25
|
+
/** Any errors that occurred (non-fatal) */
|
|
26
|
+
errors: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface MigratorConfig {
|
|
29
|
+
projectDir: string;
|
|
30
|
+
stateDir: string;
|
|
31
|
+
port: number;
|
|
32
|
+
hasTelegram: boolean;
|
|
33
|
+
projectName: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class PostUpdateMigrator {
|
|
36
|
+
private config;
|
|
37
|
+
constructor(config: MigratorConfig);
|
|
38
|
+
/**
|
|
39
|
+
* Run all post-update migrations. Safe to call multiple times —
|
|
40
|
+
* each migration is idempotent.
|
|
41
|
+
*/
|
|
42
|
+
migrate(): MigrationResult;
|
|
43
|
+
/**
|
|
44
|
+
* Re-install hooks with the latest templates.
|
|
45
|
+
* Hooks are generated infrastructure — always overwrite.
|
|
46
|
+
*/
|
|
47
|
+
private migrateHooks;
|
|
48
|
+
/**
|
|
49
|
+
* Patch CLAUDE.md with any new sections that don't exist yet.
|
|
50
|
+
* Only adds — never modifies or removes existing content.
|
|
51
|
+
*/
|
|
52
|
+
private migrateClaudeMd;
|
|
53
|
+
/**
|
|
54
|
+
* Install any new scripts that don't exist yet.
|
|
55
|
+
* Never overwrites existing scripts (user may have customized them).
|
|
56
|
+
*/
|
|
57
|
+
private migrateScripts;
|
|
58
|
+
private getSessionStartHook;
|
|
59
|
+
private getDangerousCommandGuard;
|
|
60
|
+
private getGroundingBeforeMessaging;
|
|
61
|
+
private getCompactionRecovery;
|
|
62
|
+
private getTelegramReplyScript;
|
|
63
|
+
private getHealthWatchdog;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=PostUpdateMigrator.d.ts.map
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Update Migrator — the "intelligence download" layer.
|
|
3
|
+
*
|
|
4
|
+
* When an agent installs a new version of instar, updating the npm
|
|
5
|
+
* package only changes the server code. But the agent's local awareness
|
|
6
|
+
* lives in project files: CLAUDE.md, hooks, scripts.
|
|
7
|
+
*
|
|
8
|
+
* This migrator bridges that gap. After every successful update, it:
|
|
9
|
+
* 1. Re-installs hooks with the latest templates (behavioral upgrades)
|
|
10
|
+
* 2. Patches CLAUDE.md with any new sections (awareness upgrades)
|
|
11
|
+
* 3. Installs any new scripts (capability upgrades)
|
|
12
|
+
* 4. Returns a human-readable migration report
|
|
13
|
+
*
|
|
14
|
+
* Design principles:
|
|
15
|
+
* - Additive only: never remove or modify existing user customizations
|
|
16
|
+
* - Hooks are overwritten (they're generated infrastructure, not user-edited)
|
|
17
|
+
* - CLAUDE.md sections are appended only if missing (check by heading)
|
|
18
|
+
* - Scripts are installed only if missing (never overwrite user modifications)
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
export class PostUpdateMigrator {
|
|
23
|
+
config;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run all post-update migrations. Safe to call multiple times —
|
|
29
|
+
* each migration is idempotent.
|
|
30
|
+
*/
|
|
31
|
+
migrate() {
|
|
32
|
+
const result = {
|
|
33
|
+
upgraded: [],
|
|
34
|
+
skipped: [],
|
|
35
|
+
errors: [],
|
|
36
|
+
};
|
|
37
|
+
this.migrateHooks(result);
|
|
38
|
+
this.migrateClaudeMd(result);
|
|
39
|
+
this.migrateScripts(result);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Re-install hooks with the latest templates.
|
|
44
|
+
* Hooks are generated infrastructure — always overwrite.
|
|
45
|
+
*/
|
|
46
|
+
migrateHooks(result) {
|
|
47
|
+
const hooksDir = path.join(this.config.stateDir, 'hooks');
|
|
48
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
49
|
+
try {
|
|
50
|
+
// Session start hook — the most important one for self-discovery
|
|
51
|
+
fs.writeFileSync(path.join(hooksDir, 'session-start.sh'), this.getSessionStartHook(), { mode: 0o755 });
|
|
52
|
+
result.upgraded.push('hooks/session-start.sh (capability awareness)');
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
result.errors.push(`session-start.sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(path.join(hooksDir, 'dangerous-command-guard.sh'), this.getDangerousCommandGuard(), { mode: 0o755 });
|
|
59
|
+
result.upgraded.push('hooks/dangerous-command-guard.sh');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
result.errors.push(`dangerous-command-guard.sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(path.join(hooksDir, 'grounding-before-messaging.sh'), this.getGroundingBeforeMessaging(), { mode: 0o755 });
|
|
66
|
+
result.upgraded.push('hooks/grounding-before-messaging.sh');
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
result.errors.push(`grounding-before-messaging.sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
fs.writeFileSync(path.join(hooksDir, 'compaction-recovery.sh'), this.getCompactionRecovery(), { mode: 0o755 });
|
|
73
|
+
result.upgraded.push('hooks/compaction-recovery.sh');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
result.errors.push(`compaction-recovery.sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Patch CLAUDE.md with any new sections that don't exist yet.
|
|
81
|
+
* Only adds — never modifies or removes existing content.
|
|
82
|
+
*/
|
|
83
|
+
migrateClaudeMd(result) {
|
|
84
|
+
const claudeMdPath = path.join(this.config.projectDir, 'CLAUDE.md');
|
|
85
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
86
|
+
result.skipped.push('CLAUDE.md (not found — will be created on next init)');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let content;
|
|
90
|
+
try {
|
|
91
|
+
content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
result.errors.push(`CLAUDE.md read: ${err instanceof Error ? err.message : String(err)}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let patched = false;
|
|
98
|
+
const port = this.config.port;
|
|
99
|
+
// Self-Discovery section
|
|
100
|
+
if (!content.includes('Self-Discovery') && !content.includes('/capabilities')) {
|
|
101
|
+
const section = `
|
|
102
|
+
### Self-Discovery (Know Before You Claim)
|
|
103
|
+
|
|
104
|
+
Before EVER saying "I don't have", "I can't", or "this isn't available" — check what actually exists:
|
|
105
|
+
|
|
106
|
+
\`\`\`bash
|
|
107
|
+
curl http://localhost:${port}/capabilities
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
This returns your full capability matrix: scripts, hooks, Telegram status, jobs, relationships, and more. It is the source of truth about what you can do. **Never hallucinate about missing capabilities — verify first.**
|
|
111
|
+
`;
|
|
112
|
+
// Insert before "### How to Build" or "### Building New" if present, otherwise append
|
|
113
|
+
const insertPoint = content.indexOf('### How to Build New Capabilities');
|
|
114
|
+
const insertPoint2 = content.indexOf('### Building New Capabilities');
|
|
115
|
+
const target = insertPoint >= 0 ? insertPoint : (insertPoint2 >= 0 ? insertPoint2 : -1);
|
|
116
|
+
if (target >= 0) {
|
|
117
|
+
content = content.slice(0, target) + section + '\n' + content.slice(target);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
content += '\n' + section;
|
|
121
|
+
}
|
|
122
|
+
patched = true;
|
|
123
|
+
result.upgraded.push('CLAUDE.md: added Self-Discovery section');
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
result.skipped.push('CLAUDE.md: Self-Discovery section already present');
|
|
127
|
+
}
|
|
128
|
+
// Telegram Relay section — add if Telegram is configured but section is missing
|
|
129
|
+
if (this.config.hasTelegram && !content.includes('Telegram Relay') && !content.includes('telegram-reply')) {
|
|
130
|
+
const section = `
|
|
131
|
+
## Telegram Relay
|
|
132
|
+
|
|
133
|
+
When user input starts with \`[telegram:N]\` (e.g., \`[telegram:26] hello\`), the message came from a user via Telegram topic N. **After responding**, relay your response back:
|
|
134
|
+
|
|
135
|
+
\`\`\`bash
|
|
136
|
+
cat <<'EOF' | .claude/scripts/telegram-reply.sh N
|
|
137
|
+
Your response text here
|
|
138
|
+
EOF
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
Strip the \`[telegram:N]\` prefix before interpreting the message. Respond naturally, then relay. Only relay your conversational text — not tool output or internal reasoning.
|
|
142
|
+
`;
|
|
143
|
+
content += '\n' + section;
|
|
144
|
+
patched = true;
|
|
145
|
+
result.upgraded.push('CLAUDE.md: added Telegram Relay section');
|
|
146
|
+
}
|
|
147
|
+
if (patched) {
|
|
148
|
+
try {
|
|
149
|
+
fs.writeFileSync(claudeMdPath, content);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
result.errors.push(`CLAUDE.md write: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Install any new scripts that don't exist yet.
|
|
158
|
+
* Never overwrites existing scripts (user may have customized them).
|
|
159
|
+
*/
|
|
160
|
+
migrateScripts(result) {
|
|
161
|
+
const scriptsDir = path.join(this.config.projectDir, '.claude', 'scripts');
|
|
162
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
163
|
+
// Telegram reply script — install if Telegram configured and script missing
|
|
164
|
+
if (this.config.hasTelegram) {
|
|
165
|
+
const scriptPath = path.join(scriptsDir, 'telegram-reply.sh');
|
|
166
|
+
if (!fs.existsSync(scriptPath)) {
|
|
167
|
+
try {
|
|
168
|
+
fs.writeFileSync(scriptPath, this.getTelegramReplyScript(), { mode: 0o755 });
|
|
169
|
+
result.upgraded.push('scripts/telegram-reply.sh (Telegram outbound relay)');
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
result.errors.push(`telegram-reply.sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
result.skipped.push('scripts/telegram-reply.sh (already exists)');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Health watchdog — install if missing
|
|
180
|
+
const watchdogPath = path.join(scriptsDir, 'health-watchdog.sh');
|
|
181
|
+
if (!fs.existsSync(watchdogPath)) {
|
|
182
|
+
try {
|
|
183
|
+
fs.writeFileSync(watchdogPath, this.getHealthWatchdog(), { mode: 0o755 });
|
|
184
|
+
result.upgraded.push('scripts/health-watchdog.sh');
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
result.errors.push(`health-watchdog.sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
result.skipped.push('scripts/health-watchdog.sh (already exists)');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ── Hook Templates ─────────────────────────────────────────────────
|
|
195
|
+
getSessionStartHook() {
|
|
196
|
+
return `#!/bin/bash
|
|
197
|
+
# Session start hook — injects identity context when a new Claude session begins.
|
|
198
|
+
INSTAR_DIR="\${CLAUDE_PROJECT_DIR:-.}/.instar"
|
|
199
|
+
CONTEXT=""
|
|
200
|
+
if [ -f "$INSTAR_DIR/AGENT.md" ]; then
|
|
201
|
+
CONTEXT="\${CONTEXT}Your identity file is at .instar/AGENT.md — read it if you need to remember who you are.\\n"
|
|
202
|
+
fi
|
|
203
|
+
if [ -f "$INSTAR_DIR/USER.md" ]; then
|
|
204
|
+
CONTEXT="\${CONTEXT}Your user context is at .instar/USER.md — read it to know who you're working with.\\n"
|
|
205
|
+
fi
|
|
206
|
+
if [ -f "$INSTAR_DIR/MEMORY.md" ]; then
|
|
207
|
+
CONTEXT="\${CONTEXT}Your persistent memory is at .instar/MEMORY.md — check it for past learnings.\\n"
|
|
208
|
+
fi
|
|
209
|
+
if [ -d "$INSTAR_DIR/relationships" ]; then
|
|
210
|
+
REL_COUNT=$(ls -1 "$INSTAR_DIR/relationships"/*.json 2>/dev/null | wc -l | tr -d ' ')
|
|
211
|
+
if [ "$REL_COUNT" -gt "0" ]; then
|
|
212
|
+
CONTEXT="\${CONTEXT}You have \${REL_COUNT} tracked relationships in .instar/relationships/.\\n"
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# Self-discovery: check what capabilities are available
|
|
217
|
+
if [ -f "$INSTAR_DIR/config.json" ]; then
|
|
218
|
+
PORT=$(python3 -c "import json; print(json.load(open('$INSTAR_DIR/config.json')).get('port', 4040))" 2>/dev/null || echo "4040")
|
|
219
|
+
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:\${PORT}/health" 2>/dev/null)
|
|
220
|
+
if [ "$HEALTH" = "200" ]; then
|
|
221
|
+
CONTEXT="\${CONTEXT}Instar server is running on port \${PORT}. Query your capabilities: curl http://localhost:\${PORT}/capabilities\\n"
|
|
222
|
+
CONTEXT="\${CONTEXT}IMPORTANT: Before claiming you lack a capability, check /capabilities first.\\n"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
[ -n "$CONTEXT" ] && echo "$CONTEXT"
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
getDangerousCommandGuard() {
|
|
230
|
+
return `#!/bin/bash
|
|
231
|
+
# Dangerous command guard — blocks destructive operations.
|
|
232
|
+
INPUT="$1"
|
|
233
|
+
for pattern in "rm -rf /" "rm -rf ~" "rm -rf \\." "git push --force" "git push -f" "git reset --hard" "git clean -fd" "DROP TABLE" "DROP DATABASE" "TRUNCATE" "DELETE FROM" "> /dev/sda" "mkfs\\." "dd if=" ":(){:|:&};:"; do
|
|
234
|
+
if echo "$INPUT" | grep -qi "$pattern"; then
|
|
235
|
+
echo "BLOCKED: Potentially destructive command detected: $pattern"
|
|
236
|
+
echo "If you genuinely need to run this, ask the user for explicit confirmation first."
|
|
237
|
+
exit 2
|
|
238
|
+
fi
|
|
239
|
+
done
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
getGroundingBeforeMessaging() {
|
|
243
|
+
return `#!/bin/bash
|
|
244
|
+
# Grounding before messaging — Security Through Identity.
|
|
245
|
+
INPUT="$1"
|
|
246
|
+
if echo "$INPUT" | grep -qE "(telegram-reply|send-email|send-message|POST.*/telegram/reply)"; then
|
|
247
|
+
INSTAR_DIR="\${CLAUDE_PROJECT_DIR:-.}/.instar"
|
|
248
|
+
if [ -f "$INSTAR_DIR/AGENT.md" ]; then
|
|
249
|
+
echo "Before sending this message, remember who you are."
|
|
250
|
+
echo "Re-read .instar/AGENT.md if you haven't recently."
|
|
251
|
+
echo "Security Through Identity: An agent that knows itself is harder to compromise."
|
|
252
|
+
fi
|
|
253
|
+
fi
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
getCompactionRecovery() {
|
|
257
|
+
return `#!/bin/bash
|
|
258
|
+
# Compaction recovery — re-injects identity when Claude's context compresses.
|
|
259
|
+
INSTAR_DIR="\${CLAUDE_PROJECT_DIR:-.}/.instar"
|
|
260
|
+
if [ -f "$INSTAR_DIR/AGENT.md" ]; then
|
|
261
|
+
AGENT_NAME=$(head -5 "$INSTAR_DIR/AGENT.md" | grep -iE "name|I am|My name" | head -1)
|
|
262
|
+
[ -n "$AGENT_NAME" ] && echo "Identity reminder: $AGENT_NAME"
|
|
263
|
+
echo "Read .instar/AGENT.md and .instar/MEMORY.md to restore full context."
|
|
264
|
+
fi
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
getTelegramReplyScript() {
|
|
268
|
+
const port = this.config.port;
|
|
269
|
+
return `#!/bin/bash
|
|
270
|
+
# telegram-reply.sh — Send a message back to a Telegram topic via instar server.
|
|
271
|
+
#
|
|
272
|
+
# Usage:
|
|
273
|
+
# .claude/scripts/telegram-reply.sh TOPIC_ID "message text"
|
|
274
|
+
# echo "message text" | .claude/scripts/telegram-reply.sh TOPIC_ID
|
|
275
|
+
# cat <<'EOF' | .claude/scripts/telegram-reply.sh TOPIC_ID
|
|
276
|
+
# Multi-line message here
|
|
277
|
+
# EOF
|
|
278
|
+
|
|
279
|
+
TOPIC_ID="$1"
|
|
280
|
+
shift
|
|
281
|
+
|
|
282
|
+
if [ -z "$TOPIC_ID" ]; then
|
|
283
|
+
echo "Usage: telegram-reply.sh TOPIC_ID [message]" >&2
|
|
284
|
+
exit 1
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# Read message from args or stdin
|
|
288
|
+
if [ $# -gt 0 ]; then
|
|
289
|
+
MSG="$*"
|
|
290
|
+
else
|
|
291
|
+
MSG="$(cat)"
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [ -z "$MSG" ]; then
|
|
295
|
+
echo "No message provided" >&2
|
|
296
|
+
exit 1
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
PORT="\${INSTAR_PORT:-${port}}"
|
|
300
|
+
|
|
301
|
+
# Escape for JSON
|
|
302
|
+
JSON_MSG=$(printf '%s' "$MSG" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
|
|
303
|
+
if [ -z "$JSON_MSG" ]; then
|
|
304
|
+
JSON_MSG="$(printf '%s' "$MSG" | sed 's/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/"/\\\\\\\\"/g' | sed ':a;N;$!ba;s/\\\\n/\\\\\\\\n/g')"
|
|
305
|
+
JSON_MSG="\\"$JSON_MSG\\""
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
RESPONSE=$(curl -s -w "\\n%{http_code}" -X POST "http://localhost:\${PORT}/telegram/reply/\${TOPIC_ID}" \\
|
|
309
|
+
-H 'Content-Type: application/json' \\
|
|
310
|
+
-d "{\\"text\\":\${JSON_MSG}}")
|
|
311
|
+
|
|
312
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
313
|
+
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
314
|
+
|
|
315
|
+
if [ "$HTTP_CODE" = "200" ]; then
|
|
316
|
+
echo "Sent $(echo "$MSG" | wc -c | tr -d ' ') chars to topic $TOPIC_ID"
|
|
317
|
+
else
|
|
318
|
+
echo "Failed (HTTP $HTTP_CODE): $BODY" >&2
|
|
319
|
+
exit 1
|
|
320
|
+
fi
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
getHealthWatchdog() {
|
|
324
|
+
const port = this.config.port;
|
|
325
|
+
const projectName = this.config.projectName;
|
|
326
|
+
const escapedProjectDir = this.config.projectDir.replace(/'/g, "'\\''");
|
|
327
|
+
return `#!/bin/bash
|
|
328
|
+
# health-watchdog.sh — Monitor instar server and auto-recover.
|
|
329
|
+
# Install as cron: */5 * * * * '${path.join(this.config.projectDir, '.claude/scripts/health-watchdog.sh').replace(/'/g, "'\\''")}'
|
|
330
|
+
|
|
331
|
+
PORT="${port}"
|
|
332
|
+
SERVER_SESSION="${projectName}-server"
|
|
333
|
+
PROJECT_DIR='${escapedProjectDir}'
|
|
334
|
+
TMUX_PATH=$(which tmux 2>/dev/null || echo "/opt/homebrew/bin/tmux")
|
|
335
|
+
|
|
336
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:\${PORT}/health" 2>/dev/null)
|
|
337
|
+
if [ "$HTTP_CODE" = "200" ]; then exit 0; fi
|
|
338
|
+
|
|
339
|
+
echo "[\$(date -Iseconds)] Server not responding. Restarting..."
|
|
340
|
+
$TMUX_PATH kill-session -t "=\${SERVER_SESSION}" 2>/dev/null
|
|
341
|
+
sleep 2
|
|
342
|
+
cd "$PROJECT_DIR" && npx instar server start
|
|
343
|
+
echo "[\$(date -Iseconds)] Server restart initiated"
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=PostUpdateMigrator.js.map
|
|
@@ -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) {
|
|
@@ -17,11 +17,23 @@ export interface RollbackResult {
|
|
|
17
17
|
restoredVersion: string;
|
|
18
18
|
message: string;
|
|
19
19
|
}
|
|
20
|
+
export interface UpdateCheckerConfig {
|
|
21
|
+
stateDir: string;
|
|
22
|
+
/** Required for post-update migrations */
|
|
23
|
+
projectDir?: string;
|
|
24
|
+
/** Server port for capability URLs in migrated files */
|
|
25
|
+
port?: number;
|
|
26
|
+
/** Whether Telegram is configured */
|
|
27
|
+
hasTelegram?: boolean;
|
|
28
|
+
/** Project name for migrated files */
|
|
29
|
+
projectName?: string;
|
|
30
|
+
}
|
|
20
31
|
export declare class UpdateChecker {
|
|
21
32
|
private stateDir;
|
|
22
33
|
private stateFile;
|
|
23
34
|
private rollbackFile;
|
|
24
|
-
|
|
35
|
+
private migratorConfig;
|
|
36
|
+
constructor(config: string | UpdateCheckerConfig);
|
|
25
37
|
/**
|
|
26
38
|
* Check npm for the latest version, fetch changelog, and compare to installed.
|
|
27
39
|
*/
|
|
@@ -13,16 +13,31 @@
|
|
|
13
13
|
import { execFile } from 'node:child_process';
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
-
import {
|
|
16
|
+
import { PostUpdateMigrator } from './PostUpdateMigrator.js';
|
|
17
17
|
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/SageMindAI/instar/releases';
|
|
18
18
|
export class UpdateChecker {
|
|
19
19
|
stateDir;
|
|
20
20
|
stateFile;
|
|
21
21
|
rollbackFile;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
migratorConfig;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
// Backwards-compatible: accept plain string (stateDir) or config object
|
|
25
|
+
if (typeof config === 'string') {
|
|
26
|
+
this.stateDir = config;
|
|
27
|
+
this.migratorConfig = null;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.stateDir = config.stateDir;
|
|
31
|
+
this.migratorConfig = config.projectDir ? {
|
|
32
|
+
projectDir: config.projectDir,
|
|
33
|
+
stateDir: config.stateDir,
|
|
34
|
+
port: config.port ?? 4040,
|
|
35
|
+
hasTelegram: config.hasTelegram ?? false,
|
|
36
|
+
projectName: config.projectName ?? 'agent',
|
|
37
|
+
} : null;
|
|
38
|
+
}
|
|
39
|
+
this.stateFile = path.join(this.stateDir, 'state', 'update-check.json');
|
|
40
|
+
this.rollbackFile = path.join(this.stateDir, 'state', 'update-rollback.json');
|
|
26
41
|
}
|
|
27
42
|
/**
|
|
28
43
|
* Check npm for the latest version, fetch changelog, and compare to installed.
|
|
@@ -111,13 +126,22 @@ export class UpdateChecker {
|
|
|
111
126
|
// Save rollback info on successful update
|
|
112
127
|
if (success) {
|
|
113
128
|
this.saveRollbackInfo(previousVersion, newVersion);
|
|
114
|
-
|
|
129
|
+
}
|
|
130
|
+
// Post-update migration: upgrade hooks, CLAUDE.md, scripts
|
|
131
|
+
let migrationSummary = '';
|
|
132
|
+
if (success && this.migratorConfig) {
|
|
115
133
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
134
|
+
const migrator = new PostUpdateMigrator(this.migratorConfig);
|
|
135
|
+
const migration = migrator.migrate();
|
|
136
|
+
if (migration.upgraded.length > 0) {
|
|
137
|
+
migrationSummary = ` Intelligence download: ${migration.upgraded.length} files upgraded (${migration.upgraded.join(', ')}).`;
|
|
138
|
+
}
|
|
139
|
+
if (migration.errors.length > 0) {
|
|
140
|
+
migrationSummary += ` Migration warnings: ${migration.errors.join('; ')}.`;
|
|
141
|
+
}
|
|
118
142
|
}
|
|
119
|
-
catch {
|
|
120
|
-
|
|
143
|
+
catch (err) {
|
|
144
|
+
migrationSummary = ` Post-update migration failed: ${err instanceof Error ? err.message : String(err)}.`;
|
|
121
145
|
}
|
|
122
146
|
}
|
|
123
147
|
return {
|
|
@@ -125,7 +149,7 @@ export class UpdateChecker {
|
|
|
125
149
|
previousVersion,
|
|
126
150
|
newVersion,
|
|
127
151
|
message: success
|
|
128
|
-
? `Updated from v${previousVersion} to v${newVersion}
|
|
152
|
+
? `Updated from v${previousVersion} to v${newVersion}.${migrationSummary} ${info.changeSummary || 'Restart to use the new version.'}`
|
|
129
153
|
: `Update command ran but version didn't change (still v${previousVersion}). May need manual intervention.`,
|
|
130
154
|
restartNeeded: success,
|
|
131
155
|
healthCheck: 'skipped', // Can't check health until after restart
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export { RelationshipManager } from './core/RelationshipManager.js';
|
|
|
9
9
|
export { FeedbackManager } from './core/FeedbackManager.js';
|
|
10
10
|
export { DispatchManager } from './core/DispatchManager.js';
|
|
11
11
|
export { UpdateChecker } from './core/UpdateChecker.js';
|
|
12
|
-
export type { RollbackResult } from './core/UpdateChecker.js';
|
|
12
|
+
export type { RollbackResult, UpdateCheckerConfig } from './core/UpdateChecker.js';
|
|
13
|
+
export { PostUpdateMigrator } from './core/PostUpdateMigrator.js';
|
|
14
|
+
export type { MigrationResult, MigratorConfig } from './core/PostUpdateMigrator.js';
|
|
13
15
|
export { loadConfig, detectTmuxPath, detectClaudePath, detectProjectDir, ensureStateDir } from './core/Config.js';
|
|
14
16
|
export { UserManager } from './users/UserManager.js';
|
|
15
17
|
export { JobScheduler } from './scheduler/JobScheduler.js';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { RelationshipManager } from './core/RelationshipManager.js';
|
|
|
10
10
|
export { FeedbackManager } from './core/FeedbackManager.js';
|
|
11
11
|
export { DispatchManager } from './core/DispatchManager.js';
|
|
12
12
|
export { UpdateChecker } from './core/UpdateChecker.js';
|
|
13
|
+
export { PostUpdateMigrator } from './core/PostUpdateMigrator.js';
|
|
13
14
|
export { loadConfig, detectTmuxPath, detectClaudePath, detectProjectDir, ensureStateDir } from './core/Config.js';
|
|
14
15
|
// Users
|
|
15
16
|
export { UserManager } from './users/UserManager.js';
|
|
@@ -155,6 +155,16 @@ This routes feedback to the Instar maintainers automatically. Valid types: \`bug
|
|
|
155
155
|
|
|
156
156
|
**Scripts** — Reusable capabilities in \`.claude/scripts/\`.
|
|
157
157
|
|
|
158
|
+
### Self-Discovery (Know Before You Claim)
|
|
159
|
+
|
|
160
|
+
Before EVER saying "I don't have", "I can't", or "this isn't available" — check what actually exists:
|
|
161
|
+
|
|
162
|
+
\`\`\`bash
|
|
163
|
+
curl http://localhost:${port}/capabilities
|
|
164
|
+
\`\`\`
|
|
165
|
+
|
|
166
|
+
This returns your full capability matrix: scripts, hooks, Telegram status, jobs, relationships, and more. It is the source of truth about what you can do. **Never hallucinate about missing capabilities — verify first.**
|
|
167
|
+
|
|
158
168
|
### Building New Capabilities
|
|
159
169
|
|
|
160
170
|
When asked for something I can't do yet, I build it:
|
package/dist/server/routes.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { Router } from 'express';
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
10
12
|
import { rateLimiter } from './middleware.js';
|
|
11
13
|
// Validation patterns for route parameters
|
|
12
14
|
const SESSION_NAME_RE = /^[a-zA-Z0-9_-]{1,200}$/;
|
|
@@ -60,6 +62,102 @@ export function createRoutes(ctx) {
|
|
|
60
62
|
scheduler: schedulerStatus,
|
|
61
63
|
});
|
|
62
64
|
});
|
|
65
|
+
// ── Capabilities (Self-Discovery) ──────────────────────────────
|
|
66
|
+
//
|
|
67
|
+
// Returns a structured self-portrait of what this agent has available.
|
|
68
|
+
// Agents should query this at session start rather than guessing
|
|
69
|
+
// about what infrastructure exists.
|
|
70
|
+
router.get('/capabilities', (_req, res) => {
|
|
71
|
+
const projectDir = ctx.config.projectDir;
|
|
72
|
+
const stateDir = ctx.config.stateDir;
|
|
73
|
+
// Identity files
|
|
74
|
+
const identityFiles = {
|
|
75
|
+
'AGENT.md': fs.existsSync(path.join(stateDir, 'AGENT.md')),
|
|
76
|
+
'USER.md': fs.existsSync(path.join(stateDir, 'USER.md')),
|
|
77
|
+
'MEMORY.md': fs.existsSync(path.join(stateDir, 'MEMORY.md')),
|
|
78
|
+
};
|
|
79
|
+
// Scripts
|
|
80
|
+
const scriptsDir = path.join(projectDir, '.claude', 'scripts');
|
|
81
|
+
let scripts = [];
|
|
82
|
+
if (fs.existsSync(scriptsDir)) {
|
|
83
|
+
try {
|
|
84
|
+
scripts = fs.readdirSync(scriptsDir).filter(f => !f.startsWith('.'));
|
|
85
|
+
}
|
|
86
|
+
catch { /* permission error, etc. */ }
|
|
87
|
+
}
|
|
88
|
+
// Hooks
|
|
89
|
+
const hooksDir = path.join(stateDir, 'hooks');
|
|
90
|
+
let hooks = [];
|
|
91
|
+
if (fs.existsSync(hooksDir)) {
|
|
92
|
+
try {
|
|
93
|
+
hooks = fs.readdirSync(hooksDir).filter(f => !f.startsWith('.'));
|
|
94
|
+
}
|
|
95
|
+
catch { /* permission error, etc. */ }
|
|
96
|
+
}
|
|
97
|
+
// Telegram
|
|
98
|
+
const hasTelegramConfig = ctx.config.messaging.some(m => m.type === 'telegram' && m.enabled);
|
|
99
|
+
const hasTelegramReplyScript = scripts.includes('telegram-reply.sh');
|
|
100
|
+
const telegram = {
|
|
101
|
+
configured: hasTelegramConfig,
|
|
102
|
+
replyScript: hasTelegramReplyScript,
|
|
103
|
+
adapter: !!ctx.telegram,
|
|
104
|
+
bidirectional: hasTelegramConfig && hasTelegramReplyScript && !!ctx.telegram,
|
|
105
|
+
};
|
|
106
|
+
// Jobs
|
|
107
|
+
let jobCount = 0;
|
|
108
|
+
let jobSlugs = [];
|
|
109
|
+
if (ctx.scheduler) {
|
|
110
|
+
const jobs = ctx.scheduler.getJobs();
|
|
111
|
+
jobCount = jobs.length;
|
|
112
|
+
jobSlugs = jobs.map(j => j.slug);
|
|
113
|
+
}
|
|
114
|
+
// Relationships
|
|
115
|
+
const relationshipsDir = ctx.config.relationships?.relationshipsDir;
|
|
116
|
+
let relationshipCount = 0;
|
|
117
|
+
if (relationshipsDir && fs.existsSync(relationshipsDir)) {
|
|
118
|
+
try {
|
|
119
|
+
relationshipCount = fs.readdirSync(relationshipsDir)
|
|
120
|
+
.filter(f => f.endsWith('.json')).length;
|
|
121
|
+
}
|
|
122
|
+
catch { /* ignore */ }
|
|
123
|
+
}
|
|
124
|
+
// Users
|
|
125
|
+
let userCount = 0;
|
|
126
|
+
const usersFile = path.join(stateDir, 'users.json');
|
|
127
|
+
if (fs.existsSync(usersFile)) {
|
|
128
|
+
try {
|
|
129
|
+
const users = JSON.parse(fs.readFileSync(usersFile, 'utf-8'));
|
|
130
|
+
if (Array.isArray(users))
|
|
131
|
+
userCount = users.length;
|
|
132
|
+
}
|
|
133
|
+
catch { /* ignore */ }
|
|
134
|
+
}
|
|
135
|
+
res.json({
|
|
136
|
+
project: ctx.config.projectName,
|
|
137
|
+
version: ctx.config.version || '0.0.0',
|
|
138
|
+
port: ctx.config.port,
|
|
139
|
+
identity: identityFiles,
|
|
140
|
+
scripts,
|
|
141
|
+
hooks,
|
|
142
|
+
telegram,
|
|
143
|
+
scheduler: {
|
|
144
|
+
enabled: ctx.config.scheduler.enabled,
|
|
145
|
+
jobCount,
|
|
146
|
+
jobSlugs,
|
|
147
|
+
},
|
|
148
|
+
relationships: {
|
|
149
|
+
enabled: !!ctx.config.relationships,
|
|
150
|
+
count: relationshipCount,
|
|
151
|
+
},
|
|
152
|
+
feedback: {
|
|
153
|
+
enabled: !!ctx.config.feedback?.enabled,
|
|
154
|
+
},
|
|
155
|
+
users: {
|
|
156
|
+
count: userCount,
|
|
157
|
+
},
|
|
158
|
+
monitoring: ctx.config.monitoring,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
63
161
|
// ── Sessions ────────────────────────────────────────────────────
|
|
64
162
|
// Literal routes BEFORE parameterized routes to avoid capture
|
|
65
163
|
router.get('/sessions/tmux', (_req, res) => {
|