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.
- package/.claude/skills/setup-wizard/skill.md +19 -3
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dist/cli.js +77 -0
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.js +234 -1
- package/dist/lifeline/TelegramLifeline.d.ts +9 -0
- package/dist/lifeline/TelegramLifeline.js +82 -2
- package/dist/messaging/TelegramAdapter.d.ts +15 -0
- package/dist/messaging/TelegramAdapter.js +93 -0
- package/package.json +1 -1
|
@@ -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:
|
|
654
|
+
### Step 5c: Install Auto-Start
|
|
655
655
|
|
|
656
|
-
After the server
|
|
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
|
|
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
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/setup.js
CHANGED
|
@@ -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(
|
|
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, '&')
|
|
801
|
+
.replace(/</g, '<')
|
|
802
|
+
.replace(/>/g, '>')
|
|
803
|
+
.replace(/"/g, '"')
|
|
804
|
+
.replace(/'/g, ''');
|
|
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
|
|
304
|
-
|
|
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
|
*/
|