instar 0.7.14 → 0.7.16

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.
@@ -651,13 +651,29 @@ The greeting should be **in the agent's voice** AND explain how Telegram topics
651
651
 
652
652
  Adapt the tone and examples to the agent's personality and role. Keep it warm and practical.
653
653
 
654
- ### Step 5c: Tell the User
654
+ ### Step 5c: Install Auto-Start
655
655
 
656
- After the server is running and the greeting is sent:
656
+ After the server starts, install auto-start so the agent comes back on login:
657
+
658
+ ```bash
659
+ npx instar autostart install --dir <project_dir>
660
+ ```
661
+
662
+ This creates a macOS LaunchAgent or Linux systemd service. The agent will start automatically whenever the user logs in — nothing to remember.
663
+
664
+ ### Step 5d: Tell the User
665
+
666
+ After the server is running, auto-start is installed, and the greeting is sent:
657
667
 
658
668
  > "All done! [Agent name] just messaged you in the Lifeline topic on Telegram. From here on, that's your primary channel — just talk to your agent there."
659
669
  >
660
- > "As long as your computer is running the Instar server, your agent is available."
670
+ > "I've set up auto-start — your agent will come back automatically when you log in. As long as your computer is on and awake, Telegram just works."
671
+
672
+ If auto-start install failed, explain the fallback:
673
+
674
+ > "Your agent runs on this computer. If your computer restarts, you'll need to run `instar server start` to bring it back."
675
+
676
+ Keep it matter-of-fact, not alarming.
661
677
 
662
678
  **Do NOT present a list of CLI commands or next steps.** The setup wizard's job is done. The user's next action is opening Telegram and replying to their agent.
663
679
 
@@ -0,0 +1,11 @@
1
+ > Why do I have a folder named ".vercel" in my project?
2
+ The ".vercel" folder is created when you link a directory to a Vercel project.
3
+
4
+ > What does the "project.json" file contain?
5
+ The "project.json" file contains:
6
+ - The ID of the Vercel project that you linked ("projectId")
7
+ - The ID of the user or team your Vercel project is owned by ("orgId")
8
+
9
+ > Should I commit the ".vercel" folder?
10
+ No, you should not share the ".vercel" folder with anyone.
11
+ Upon creation, it will be automatically added to your ".gitignore" file.
@@ -0,0 +1 @@
1
+ {"projectId":"prj_evM5LcItYL3IAmw8zNvEPGrHeaya","orgId":"team_dHctwIDcV3X9ydapQlCPHFGI","projectName":"claude-agent-kit"}
package/dist/cli.js CHANGED
@@ -547,5 +547,82 @@ program
547
547
  console.log();
548
548
  }
549
549
  });
550
+ // ── Auto-Start ───────────────────────────────────────────────────
551
+ const autostartCmd = program
552
+ .command('autostart')
553
+ .description('Manage auto-start on login (agent starts when you log into your computer)');
554
+ autostartCmd
555
+ .command('install')
556
+ .description('Install auto-start so your agent starts on login')
557
+ .option('-d, --dir <path>', 'Project directory')
558
+ .action(async (opts) => {
559
+ const { loadConfig } = await import('./core/Config.js');
560
+ const { installAutoStart } = await import('./commands/setup.js');
561
+ const config = loadConfig(opts.dir);
562
+ const hasTelegram = config.messaging?.some((m) => m.type === 'telegram') ?? false;
563
+ const installed = installAutoStart(config.projectName, config.projectDir, hasTelegram);
564
+ if (installed) {
565
+ console.log(pc.green(`Auto-start installed for "${config.projectName}".`));
566
+ console.log(pc.dim('Your agent will start automatically when you log in.'));
567
+ }
568
+ else {
569
+ console.log(pc.red('Failed to install auto-start.'));
570
+ console.log(pc.dim(`Platform: ${process.platform} — auto-start supports macOS and Linux.`));
571
+ }
572
+ });
573
+ autostartCmd
574
+ .command('uninstall')
575
+ .description('Remove auto-start')
576
+ .option('-d, --dir <path>', 'Project directory')
577
+ .action(async (opts) => {
578
+ const { loadConfig } = await import('./core/Config.js');
579
+ const { uninstallAutoStart } = await import('./commands/setup.js');
580
+ const config = loadConfig(opts.dir);
581
+ const removed = uninstallAutoStart(config.projectName);
582
+ if (removed) {
583
+ console.log(pc.green(`Auto-start removed for "${config.projectName}".`));
584
+ }
585
+ else {
586
+ console.log(pc.yellow('No auto-start found to remove.'));
587
+ }
588
+ });
589
+ autostartCmd
590
+ .command('status')
591
+ .description('Check if auto-start is installed')
592
+ .option('-d, --dir <path>', 'Project directory')
593
+ .action(async (opts) => {
594
+ const { loadConfig } = await import('./core/Config.js');
595
+ const config = loadConfig(opts.dir);
596
+ const os = await import('node:os');
597
+ const fs = await import('node:fs');
598
+ const path = await import('node:path');
599
+ if (process.platform === 'darwin') {
600
+ const label = `ai.instar.${config.projectName}`;
601
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
602
+ if (fs.existsSync(plistPath)) {
603
+ console.log(pc.green(`Auto-start is installed (macOS LaunchAgent: ${label})`));
604
+ console.log(pc.dim(` Plist: ${plistPath}`));
605
+ }
606
+ else {
607
+ console.log(pc.yellow('Auto-start is not installed.'));
608
+ console.log(pc.dim(' Install with: instar autostart install'));
609
+ }
610
+ }
611
+ else if (process.platform === 'linux') {
612
+ const serviceName = `instar-${config.projectName}.service`;
613
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', serviceName);
614
+ if (fs.existsSync(servicePath)) {
615
+ console.log(pc.green(`Auto-start is installed (systemd user service: ${serviceName})`));
616
+ console.log(pc.dim(` Service: ${servicePath}`));
617
+ }
618
+ else {
619
+ console.log(pc.yellow('Auto-start is not installed.'));
620
+ console.log(pc.dim(' Install with: instar autostart install'));
621
+ }
622
+ }
623
+ else {
624
+ console.log(pc.yellow(`Auto-start is not supported on ${process.platform}.`));
625
+ }
626
+ });
550
627
  program.parse();
551
628
  //# sourceMappingURL=cli.js.map
@@ -21,4 +21,16 @@
21
21
  export declare function runSetup(opts?: {
22
22
  classic?: boolean;
23
23
  }): Promise<void>;
24
+ /**
25
+ * Install auto-start so the agent's lifeline process starts on login.
26
+ * macOS: LaunchAgent plist in ~/Library/LaunchAgents/
27
+ * Linux: systemd user service in ~/.config/systemd/user/
28
+ *
29
+ * Returns true if auto-start was installed successfully.
30
+ */
31
+ export declare function installAutoStart(projectName: string, projectDir: string, hasTelegram: boolean): boolean;
32
+ /**
33
+ * Remove auto-start for a project.
34
+ */
35
+ export declare function uninstallAutoStart(projectName: string): boolean;
24
36
  //# sourceMappingURL=setup.d.ts.map
@@ -17,6 +17,7 @@
17
17
  import { execFileSync, spawn } from 'node:child_process';
18
18
  import { randomUUID } from 'node:crypto';
19
19
  import fs from 'node:fs';
20
+ import os from 'node:os';
20
21
  import path from 'node:path';
21
22
  import pc from 'picocolors';
22
23
  import { input, confirm, select, number } from '@inquirer/prompts';
@@ -449,6 +450,12 @@ async function runClassicSetup() {
449
450
  console.log(pc.dim(' Starting server...'));
450
451
  const { startServer } = await import('./server.js');
451
452
  await startServer({ foreground: false });
453
+ // ── Auto-start on login ──────────────────────────────────────────
454
+ const hasTelegram = !!telegramConfig?.chatId;
455
+ const autoStartInstalled = installAutoStart(projectName, projectDir, hasTelegram);
456
+ if (autoStartInstalled) {
457
+ console.log(pc.green(' ✓ Auto-start installed — your agent will start on login.'));
458
+ }
452
459
  if (telegramConfig?.chatId) {
453
460
  // Create the Lifeline topic — the always-available channel
454
461
  let lifelineThreadId = null;
@@ -462,6 +469,19 @@ async function runClassicSetup() {
462
469
  const parsed = JSON.parse(topicResult);
463
470
  if (parsed.ok && parsed.result?.message_thread_id) {
464
471
  lifelineThreadId = parsed.result.message_thread_id;
472
+ // Persist lifelineTopicId back to config.json
473
+ try {
474
+ const configPath = path.join(stateDir, 'config.json');
475
+ const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
476
+ const tgEntry = rawConfig.messaging?.find((m) => m.type === 'telegram');
477
+ if (tgEntry?.config) {
478
+ tgEntry.config.lifelineTopicId = lifelineThreadId;
479
+ const tmpPath = `${configPath}.${process.pid}.tmp`;
480
+ fs.writeFileSync(tmpPath, JSON.stringify(rawConfig, null, 2));
481
+ fs.renameSync(tmpPath, configPath);
482
+ }
483
+ }
484
+ catch { /* non-fatal */ }
465
485
  }
466
486
  }
467
487
  catch {
@@ -480,6 +500,8 @@ async function runClassicSetup() {
480
500
  '- I can proactively create topics when something needs attention',
481
501
  '- Lifeline is always here for anything that doesn\'t fit elsewhere',
482
502
  '',
503
+ '_I run on your computer, so I\'m available as long as it\'s on and awake. If it sleeps, I\'ll pick up messages when it wakes back up._',
504
+ '',
483
505
  'What should we work on first?',
484
506
  ].join('\n');
485
507
  const payload = {
@@ -504,7 +526,16 @@ async function runClassicSetup() {
504
526
  const topicNote = lifelineThreadId ? ' in the Lifeline topic' : '';
505
527
  console.log(pc.bold(` All done! ${projectName} just messaged you${topicNote} on Telegram.`));
506
528
  console.log(pc.dim(' That\'s your primary channel from here on — no terminal needed.'));
507
- console.log(pc.dim(' As long as your computer is running the Instar server, your agent is available.'));
529
+ console.log();
530
+ if (autoStartInstalled) {
531
+ console.log(pc.dim(' Your agent starts automatically when you log in — nothing to remember.'));
532
+ console.log(pc.dim(' As long as your computer is on and awake, Telegram just works.'));
533
+ }
534
+ else {
535
+ console.log(pc.dim(' Your agent runs on this computer. As long as it\'s on and awake,'));
536
+ console.log(pc.dim(' your agent is reachable via Telegram. You\'ll need to run'));
537
+ console.log(pc.dim(` ${pc.cyan('instar server start')} after a reboot.`));
538
+ }
508
539
  }
509
540
  else {
510
541
  console.log();
@@ -570,6 +601,208 @@ function isInstarGlobal() {
570
601
  return false;
571
602
  }
572
603
  }
604
+ // ── Auto-Start on Login ─────────────────────────────────────────
605
+ /**
606
+ * Install auto-start so the agent's lifeline process starts on login.
607
+ * macOS: LaunchAgent plist in ~/Library/LaunchAgents/
608
+ * Linux: systemd user service in ~/.config/systemd/user/
609
+ *
610
+ * Returns true if auto-start was installed successfully.
611
+ */
612
+ export function installAutoStart(projectName, projectDir, hasTelegram) {
613
+ const platform = process.platform;
614
+ if (platform === 'darwin') {
615
+ return installMacOSLaunchAgent(projectName, projectDir, hasTelegram);
616
+ }
617
+ else if (platform === 'linux') {
618
+ return installLinuxSystemdService(projectName, projectDir, hasTelegram);
619
+ }
620
+ else {
621
+ // Windows or other — no auto-start support yet
622
+ return false;
623
+ }
624
+ }
625
+ /**
626
+ * Remove auto-start for a project.
627
+ */
628
+ export function uninstallAutoStart(projectName) {
629
+ const platform = process.platform;
630
+ if (platform === 'darwin') {
631
+ const label = `ai.instar.${projectName}`;
632
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
633
+ // Unload if loaded
634
+ try {
635
+ execFileSync('launchctl', ['bootout', `gui/${process.getuid?.() ?? 501}`, plistPath], { stdio: 'ignore' });
636
+ }
637
+ catch { /* not loaded */ }
638
+ // Remove file
639
+ try {
640
+ fs.unlinkSync(plistPath);
641
+ return true;
642
+ }
643
+ catch {
644
+ return false;
645
+ }
646
+ }
647
+ else if (platform === 'linux') {
648
+ const serviceName = `instar-${projectName}.service`;
649
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', serviceName);
650
+ try {
651
+ execFileSync('systemctl', ['--user', 'disable', serviceName], { stdio: 'ignore' });
652
+ execFileSync('systemctl', ['--user', 'stop', serviceName], { stdio: 'ignore' });
653
+ }
654
+ catch { /* not loaded */ }
655
+ try {
656
+ fs.unlinkSync(servicePath);
657
+ execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
658
+ return true;
659
+ }
660
+ catch {
661
+ return false;
662
+ }
663
+ }
664
+ return false;
665
+ }
666
+ function findNodePath() {
667
+ try {
668
+ return execFileSync('which', ['node'], {
669
+ encoding: 'utf-8',
670
+ stdio: ['pipe', 'pipe', 'pipe'],
671
+ }).trim();
672
+ }
673
+ catch {
674
+ return '/usr/local/bin/node';
675
+ }
676
+ }
677
+ function findInstarCli() {
678
+ // Find the actual instar CLI entry point
679
+ try {
680
+ const globalPath = execFileSync('which', ['instar'], {
681
+ encoding: 'utf-8',
682
+ stdio: ['pipe', 'pipe', 'pipe'],
683
+ }).trim();
684
+ if (globalPath && !globalPath.includes('.npm/_npx')) {
685
+ return globalPath;
686
+ }
687
+ }
688
+ catch { /* not global */ }
689
+ // Fallback: use the dist/cli.js from the npm package
690
+ const cliPath = new URL('../cli.js', import.meta.url).pathname;
691
+ if (fs.existsSync(cliPath)) {
692
+ return cliPath;
693
+ }
694
+ return 'instar';
695
+ }
696
+ function installMacOSLaunchAgent(projectName, projectDir, hasTelegram) {
697
+ const label = `ai.instar.${projectName}`;
698
+ const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
699
+ const plistPath = path.join(launchAgentsDir, `${label}.plist`);
700
+ const logDir = path.join(projectDir, '.instar', 'logs');
701
+ const nodePath = findNodePath();
702
+ const instarCli = findInstarCli();
703
+ // Determine what to start: lifeline if Telegram configured, otherwise just the server
704
+ const command = hasTelegram ? 'lifeline' : 'server';
705
+ const args = hasTelegram
706
+ ? [instarCli, 'lifeline', 'start', '--dir', projectDir]
707
+ : [instarCli, 'server', 'start', '--foreground', '--dir', projectDir];
708
+ // If instar CLI is a node script (not a binary), prepend node
709
+ const isNodeScript = instarCli.endsWith('.js') || instarCli.endsWith('.mjs');
710
+ const programArgs = isNodeScript ? [nodePath, ...args] : args;
711
+ // Build the plist XML
712
+ const argsXml = programArgs.map(a => ` <string>${escapeXml(a)}</string>`).join('\n');
713
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
714
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
715
+ <plist version="1.0">
716
+ <dict>
717
+ <key>Label</key>
718
+ <string>${escapeXml(label)}</string>
719
+ <key>ProgramArguments</key>
720
+ <array>
721
+ ${argsXml}
722
+ </array>
723
+ <key>WorkingDirectory</key>
724
+ <string>${escapeXml(projectDir)}</string>
725
+ <key>RunAtLoad</key>
726
+ <true/>
727
+ <key>KeepAlive</key>
728
+ <true/>
729
+ <key>StandardOutPath</key>
730
+ <string>${escapeXml(path.join(logDir, `${command}-launchd.log`))}</string>
731
+ <key>StandardErrorPath</key>
732
+ <string>${escapeXml(path.join(logDir, `${command}-launchd.err`))}</string>
733
+ <key>EnvironmentVariables</key>
734
+ <dict>
735
+ <key>PATH</key>
736
+ <string>${escapeXml(process.env.PATH || '/usr/local/bin:/usr/bin:/bin')}</string>
737
+ </dict>
738
+ <key>ThrottleInterval</key>
739
+ <integer>10</integer>
740
+ </dict>
741
+ </plist>`;
742
+ try {
743
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
744
+ fs.mkdirSync(logDir, { recursive: true });
745
+ fs.writeFileSync(plistPath, plist);
746
+ // Load the agent
747
+ try {
748
+ // Unload first if already loaded
749
+ execFileSync('launchctl', ['bootout', `gui/${process.getuid?.() ?? 501}`, plistPath], { stdio: 'ignore' });
750
+ }
751
+ catch { /* not loaded yet — fine */ }
752
+ execFileSync('launchctl', ['bootstrap', `gui/${process.getuid?.() ?? 501}`, plistPath], { stdio: 'ignore' });
753
+ return true;
754
+ }
755
+ catch {
756
+ return false;
757
+ }
758
+ }
759
+ function installLinuxSystemdService(projectName, projectDir, hasTelegram) {
760
+ const serviceName = `instar-${projectName}.service`;
761
+ const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
762
+ const servicePath = path.join(serviceDir, serviceName);
763
+ const nodePath = findNodePath();
764
+ const instarCli = findInstarCli();
765
+ const command = hasTelegram ? 'lifeline' : 'server';
766
+ const args = hasTelegram
767
+ ? `${instarCli} lifeline start --dir ${projectDir}`
768
+ : `${instarCli} server start --foreground --dir ${projectDir}`;
769
+ const isNodeScript = instarCli.endsWith('.js') || instarCli.endsWith('.mjs');
770
+ const execStart = isNodeScript ? `${nodePath} ${args}` : args;
771
+ const service = `[Unit]
772
+ Description=Instar Agent - ${projectName}
773
+ After=network.target
774
+
775
+ [Service]
776
+ Type=simple
777
+ ExecStart=${execStart}
778
+ WorkingDirectory=${projectDir}
779
+ Restart=always
780
+ RestartSec=10
781
+ Environment=PATH=${process.env.PATH || '/usr/local/bin:/usr/bin:/bin'}
782
+
783
+ [Install]
784
+ WantedBy=default.target
785
+ `;
786
+ try {
787
+ fs.mkdirSync(serviceDir, { recursive: true });
788
+ fs.writeFileSync(servicePath, service);
789
+ execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
790
+ execFileSync('systemctl', ['--user', 'enable', serviceName], { stdio: 'ignore' });
791
+ execFileSync('systemctl', ['--user', 'start', serviceName], { stdio: 'ignore' });
792
+ return true;
793
+ }
794
+ catch {
795
+ return false;
796
+ }
797
+ }
798
+ function escapeXml(str) {
799
+ return str
800
+ .replace(/&/g, '&amp;')
801
+ .replace(/</g, '&lt;')
802
+ .replace(/>/g, '&gt;')
803
+ .replace(/"/g, '&quot;')
804
+ .replace(/'/g, '&apos;');
805
+ }
573
806
  // ── Prompt Helpers ───────────────────────────────────────────────
574
807
  /**
575
808
  * Full Telegram walkthrough. Returns config or null if skipped.
@@ -29,6 +29,7 @@ export declare class TelegramLifeline {
29
29
  private offsetPath;
30
30
  private stopHeartbeat;
31
31
  private replayInterval;
32
+ private lifelineTopicId;
32
33
  constructor(projectDir?: string);
33
34
  /**
34
35
  * Start the lifeline — begins Telegram polling and server supervision.
@@ -43,6 +44,14 @@ export declare class TelegramLifeline {
43
44
  private handleLifelineCommand;
44
45
  private replayQueue;
45
46
  private notifyServerDown;
47
+ /**
48
+ * Ensure the Lifeline topic exists. Recreates if deleted.
49
+ */
50
+ private ensureLifelineTopic;
51
+ /**
52
+ * Persist the Lifeline topic ID to config.json.
53
+ */
54
+ private persistLifelineTopicId;
46
55
  private sendToTopic;
47
56
  private getUpdates;
48
57
  private apiCall;
@@ -36,6 +36,7 @@ export class TelegramLifeline {
36
36
  offsetPath;
37
37
  stopHeartbeat = null;
38
38
  replayInterval = null;
39
+ lifelineTopicId = null;
39
40
  constructor(projectDir) {
40
41
  this.projectConfig = loadConfig(projectDir);
41
42
  ensureStateDir(this.projectConfig.stateDir);
@@ -81,6 +82,11 @@ export class TelegramLifeline {
81
82
  }
82
83
  catch { /* non-critical */ }
83
84
  this.stopHeartbeat = startHeartbeat(`${this.projectConfig.projectName}-lifeline`);
85
+ // Ensure Lifeline topic exists (auto-recreate if deleted)
86
+ this.lifelineTopicId = await this.ensureLifelineTopic();
87
+ if (this.lifelineTopicId) {
88
+ console.log(pc.green(` Lifeline topic: ${this.lifelineTopicId}`));
89
+ }
84
90
  // Start server supervisor
85
91
  const serverStarted = await this.supervisor.start();
86
92
  if (serverStarted) {
@@ -300,8 +306,82 @@ export class TelegramLifeline {
300
306
  }
301
307
  // ── Notifications ─────────────────────────────────────────
302
308
  async notifyServerDown(reason) {
303
- // Send to General topic (1) since we don't know which topic the user is watching
304
- await this.sendToTopic(1, `Server went down: ${reason}\n\nYour messages will be queued until recovery. Use /lifeline status to check.`).catch(() => { });
309
+ // Send to Lifeline topic if available, otherwise General
310
+ const topicId = this.lifelineTopicId ?? 1;
311
+ await this.sendToTopic(topicId, `Server went down: ${reason}\n\nYour messages will be queued until recovery. Use /lifeline status to check.`).catch(() => { });
312
+ }
313
+ // ── Lifeline Topic ──────────────────────────────────────────
314
+ /**
315
+ * Ensure the Lifeline topic exists. Recreates if deleted.
316
+ */
317
+ async ensureLifelineTopic() {
318
+ const existingId = this.config.lifelineTopicId;
319
+ if (existingId) {
320
+ // Verify it still exists
321
+ try {
322
+ await this.apiCall('sendMessage', {
323
+ chat_id: this.config.chatId,
324
+ message_thread_id: existingId,
325
+ text: '🟢 Lifeline connected.',
326
+ });
327
+ return existingId;
328
+ }
329
+ catch (err) {
330
+ const errStr = String(err);
331
+ if (errStr.includes('thread not found') || errStr.includes('TOPIC_DELETED') ||
332
+ errStr.includes('TOPIC_CLOSED') || errStr.includes('not found')) {
333
+ console.log(`[Lifeline] Topic ${existingId} was deleted — recreating`);
334
+ }
335
+ else {
336
+ // Non-fatal error (network etc.) — assume it still exists
337
+ console.warn(`[Lifeline] Topic check failed (non-fatal): ${err}`);
338
+ return existingId;
339
+ }
340
+ }
341
+ }
342
+ // Create or recreate
343
+ try {
344
+ const result = await this.apiCall('createForumTopic', {
345
+ chat_id: this.config.chatId,
346
+ name: 'Lifeline',
347
+ icon_color: 9367192, // green
348
+ });
349
+ const topicId = result.message_thread_id;
350
+ this.config.lifelineTopicId = topicId;
351
+ this.persistLifelineTopicId(topicId);
352
+ console.log(`[Lifeline] ${existingId ? 'Recreated' : 'Created'} Lifeline topic: ${topicId}`);
353
+ // Send welcome message in new topic
354
+ await this.sendToTopic(topicId, '🟢 Lifeline connected. This topic is always available — even when the server is down.');
355
+ return topicId;
356
+ }
357
+ catch (err) {
358
+ console.error(`[Lifeline] Failed to create Lifeline topic: ${err}`);
359
+ return null;
360
+ }
361
+ }
362
+ /**
363
+ * Persist the Lifeline topic ID to config.json.
364
+ */
365
+ persistLifelineTopicId(topicId) {
366
+ try {
367
+ const configPath = path.join(this.projectConfig.projectDir, '.instar', 'config.json');
368
+ if (fs.existsSync(configPath)) {
369
+ const raw = fs.readFileSync(configPath, 'utf-8');
370
+ const config = JSON.parse(raw);
371
+ if (Array.isArray(config.messaging)) {
372
+ const entry = config.messaging.find((m) => m.type === 'telegram');
373
+ if (entry?.config) {
374
+ entry.config.lifelineTopicId = topicId;
375
+ const tmpPath = `${configPath}.${process.pid}.tmp`;
376
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
377
+ fs.renameSync(tmpPath, configPath);
378
+ }
379
+ }
380
+ }
381
+ }
382
+ catch (err) {
383
+ console.warn(`[Lifeline] Failed to persist lifelineTopicId: ${err}`);
384
+ }
305
385
  }
306
386
  // ── Telegram API ──────────────────────────────────────────
307
387
  async sendToTopic(topicId, text) {
@@ -22,6 +22,8 @@ export interface TelegramConfig {
22
22
  voiceProvider?: string;
23
23
  /** Stall detection timeout in minutes (default: 5, 0 to disable) */
24
24
  stallTimeoutMinutes?: number;
25
+ /** Lifeline topic thread ID — the always-available channel. Auto-recreated if deleted. */
26
+ lifelineTopicId?: number;
25
27
  }
26
28
  export interface SendResult {
27
29
  /** Telegram message ID */
@@ -100,6 +102,19 @@ export declare class TelegramAdapter implements MessagingAdapter {
100
102
  topicId: number;
101
103
  name: string;
102
104
  }>;
105
+ /**
106
+ * Get the Lifeline topic ID (if configured).
107
+ */
108
+ getLifelineTopicId(): number | undefined;
109
+ /**
110
+ * Ensure the Lifeline topic exists. If it was deleted, recreate it.
111
+ * Called on startup and can be called periodically.
112
+ */
113
+ ensureLifelineTopic(): Promise<number | null>;
114
+ /**
115
+ * Persist the Lifeline topic ID back to config.json so it survives restarts.
116
+ */
117
+ private persistLifelineTopicId;
103
118
  /**
104
119
  * Close a forum topic.
105
120
  */
@@ -87,6 +87,8 @@ export class TelegramAdapter {
87
87
  this.polling = true;
88
88
  this.startedAt = new Date();
89
89
  this.consecutivePollErrors = 0;
90
+ // Ensure Lifeline topic exists (auto-recreate if deleted)
91
+ await this.ensureLifelineTopic();
90
92
  console.log(`[telegram] Starting long-polling...`);
91
93
  this.poll();
92
94
  // Start stall detection if configured
@@ -180,6 +182,97 @@ export class TelegramAdapter {
180
182
  console.log(`[telegram] Created forum topic: "${name}" (ID: ${result.message_thread_id})`);
181
183
  return { topicId: result.message_thread_id, name: result.name };
182
184
  }
185
+ /**
186
+ * Get the Lifeline topic ID (if configured).
187
+ */
188
+ getLifelineTopicId() {
189
+ return this.config.lifelineTopicId;
190
+ }
191
+ /**
192
+ * Ensure the Lifeline topic exists. If it was deleted, recreate it.
193
+ * Called on startup and can be called periodically.
194
+ */
195
+ async ensureLifelineTopic() {
196
+ if (!this.config.lifelineTopicId) {
197
+ // No lifeline topic configured — create one
198
+ try {
199
+ const topic = await this.createForumTopic('Lifeline', 9367192); // Green
200
+ this.config.lifelineTopicId = topic.topicId;
201
+ this.persistLifelineTopicId(topic.topicId);
202
+ console.log(`[telegram] Created Lifeline topic: ${topic.topicId}`);
203
+ return topic.topicId;
204
+ }
205
+ catch (err) {
206
+ console.error(`[telegram] Failed to create Lifeline topic: ${err}`);
207
+ return null;
208
+ }
209
+ }
210
+ // Lifeline topic ID exists — verify it's still valid by sending a test
211
+ try {
212
+ await this.apiCall('sendMessage', {
213
+ chat_id: this.config.chatId,
214
+ message_thread_id: this.config.lifelineTopicId,
215
+ text: '🟢 Lifeline connected.',
216
+ });
217
+ console.log(`[telegram] Lifeline topic verified: ${this.config.lifelineTopicId}`);
218
+ return this.config.lifelineTopicId;
219
+ }
220
+ catch (err) {
221
+ const errStr = String(err);
222
+ // Topic was deleted — "message thread not found" or "TOPIC_CLOSED" or similar
223
+ if (errStr.includes('thread not found') || errStr.includes('TOPIC_DELETED') ||
224
+ errStr.includes('TOPIC_CLOSED') || errStr.includes('not found')) {
225
+ console.log(`[telegram] Lifeline topic ${this.config.lifelineTopicId} was deleted — recreating`);
226
+ try {
227
+ const topic = await this.createForumTopic('Lifeline', 9367192);
228
+ this.config.lifelineTopicId = topic.topicId;
229
+ this.persistLifelineTopicId(topic.topicId);
230
+ console.log(`[telegram] Recreated Lifeline topic: ${topic.topicId}`);
231
+ return topic.topicId;
232
+ }
233
+ catch (recreateErr) {
234
+ console.error(`[telegram] Failed to recreate Lifeline topic: ${recreateErr}`);
235
+ return null;
236
+ }
237
+ }
238
+ // Some other error (network, etc.) — don't recreate, just warn
239
+ console.warn(`[telegram] Lifeline topic check failed (non-fatal): ${err}`);
240
+ return this.config.lifelineTopicId;
241
+ }
242
+ }
243
+ /**
244
+ * Persist the Lifeline topic ID back to config.json so it survives restarts.
245
+ */
246
+ persistLifelineTopicId(topicId) {
247
+ try {
248
+ // Find config.json in state dir's parent (stateDir is .instar/state or .instar)
249
+ const candidates = [
250
+ path.join(this.stateDir, '..', 'config.json'),
251
+ path.join(this.stateDir, 'config.json'),
252
+ ];
253
+ for (const configPath of candidates) {
254
+ if (fs.existsSync(configPath)) {
255
+ const raw = fs.readFileSync(configPath, 'utf-8');
256
+ const config = JSON.parse(raw);
257
+ // Find the telegram messaging config and update it
258
+ if (Array.isArray(config.messaging)) {
259
+ const telegramEntry = config.messaging.find((m) => m.type === 'telegram');
260
+ if (telegramEntry?.config) {
261
+ telegramEntry.config.lifelineTopicId = topicId;
262
+ const tmpPath = `${configPath}.${process.pid}.tmp`;
263
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
264
+ fs.renameSync(tmpPath, configPath);
265
+ console.log(`[telegram] Saved lifelineTopicId=${topicId} to config`);
266
+ return;
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+ catch (err) {
273
+ console.warn(`[telegram] Failed to persist lifelineTopicId: ${err}`);
274
+ }
275
+ }
183
276
  /**
184
277
  * Close a forum topic.
185
278
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.7.14",
3
+ "version": "0.7.16",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",