instar 0.3.4 → 0.3.5

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.
@@ -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**:
@@ -739,6 +749,17 @@ if [ -d "$INSTAR_DIR/relationships" ]; then
739
749
  fi
740
750
  fi
741
751
  CONTEXT="\${CONTEXT}IMPORTANT: To report bugs or request features, use POST /feedback on your local server. NEVER use gh or GitHub directly.\\n"
752
+
753
+ # Self-discovery: check what capabilities are available
754
+ if [ -f "$INSTAR_DIR/config.json" ]; then
755
+ PORT=$(python3 -c "import json; print(json.load(open('$INSTAR_DIR/config.json')).get('port', 4040))" 2>/dev/null || echo "4040")
756
+ HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:\${PORT}/health" 2>/dev/null)
757
+ if [ "$HEALTH" = "200" ]; then
758
+ CONTEXT="\${CONTEXT}Instar server is running on port \${PORT}. Query your capabilities: curl http://localhost:\${PORT}/capabilities\\n"
759
+ CONTEXT="\${CONTEXT}IMPORTANT: Before claiming you lack a capability, check /capabilities first.\\n"
760
+ fi
761
+ fi
762
+
742
763
  [ -n "$CONTEXT" ] && echo "$CONTEXT"
743
764
  `, { mode: 0o755 });
744
765
  // Dangerous command guard
@@ -268,7 +268,13 @@ export async function startServer(options) {
268
268
  });
269
269
  console.log(pc.green(' Dispatch system enabled'));
270
270
  }
271
- const updateChecker = new UpdateChecker(config.stateDir);
271
+ const updateChecker = new UpdateChecker({
272
+ stateDir: config.stateDir,
273
+ projectDir: config.projectDir,
274
+ port: config.port,
275
+ hasTelegram: config.messaging.some(m => m.type === 'telegram' && m.enabled),
276
+ projectName: config.projectName,
277
+ });
272
278
  // Check for updates on startup
273
279
  updateChecker.check().then(info => {
274
280
  if (info.updateAvailable) {
@@ -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
@@ -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
- constructor(stateDir: string);
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 { refreshHooksAndSettings } from '../commands/init.js';
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
- constructor(stateDir) {
23
- this.stateDir = stateDir;
24
- this.stateFile = path.join(stateDir, 'state', 'update-check.json');
25
- this.rollbackFile = path.join(stateDir, 'state', 'update-rollback.json');
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
- // Refresh hooks and settings — new versions may include new hooks
129
+ }
130
+ // Post-update migration: upgrade hooks, CLAUDE.md, scripts
131
+ let migrationSummary = '';
132
+ if (success && this.migratorConfig) {
115
133
  try {
116
- const projectDir = path.resolve(this.stateDir, '..');
117
- refreshHooksAndSettings(projectDir, this.stateDir);
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
- // Non-critical hooks can be refreshed manually via `instar init`
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}. Hooks refreshed. ${info.changeSummary || 'Restart to use the new version.'}`
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:
@@ -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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",