instar 0.1.0
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/settings.local.json +7 -0
- package/.claude/skills/setup-wizard/skill.md +343 -0
- package/.github/workflows/ci.yml +78 -0
- package/CLAUDE.md +82 -0
- package/README.md +194 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +141 -0
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +568 -0
- package/dist/commands/job.d.ts +20 -0
- package/dist/commands/job.js +84 -0
- package/dist/commands/server.d.ts +19 -0
- package/dist/commands/server.js +273 -0
- package/dist/commands/setup.d.ts +24 -0
- package/dist/commands/setup.js +865 -0
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.js +114 -0
- package/dist/commands/user.d.ts +17 -0
- package/dist/commands/user.js +53 -0
- package/dist/core/Config.d.ts +16 -0
- package/dist/core/Config.js +144 -0
- package/dist/core/Prerequisites.d.ts +28 -0
- package/dist/core/Prerequisites.js +159 -0
- package/dist/core/RelationshipManager.d.ts +73 -0
- package/dist/core/RelationshipManager.js +318 -0
- package/dist/core/SessionManager.d.ts +89 -0
- package/dist/core/SessionManager.js +326 -0
- package/dist/core/StateManager.d.ts +28 -0
- package/dist/core/StateManager.js +96 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/types.js +8 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +23 -0
- package/dist/messaging/TelegramAdapter.d.ts +73 -0
- package/dist/messaging/TelegramAdapter.js +288 -0
- package/dist/monitoring/HealthChecker.d.ts +38 -0
- package/dist/monitoring/HealthChecker.js +148 -0
- package/dist/scaffold/bootstrap.d.ts +21 -0
- package/dist/scaffold/bootstrap.js +110 -0
- package/dist/scaffold/templates.d.ts +34 -0
- package/dist/scaffold/templates.js +187 -0
- package/dist/scheduler/JobLoader.d.ts +18 -0
- package/dist/scheduler/JobLoader.js +70 -0
- package/dist/scheduler/JobScheduler.d.ts +111 -0
- package/dist/scheduler/JobScheduler.js +402 -0
- package/dist/server/AgentServer.d.ts +40 -0
- package/dist/server/AgentServer.js +73 -0
- package/dist/server/middleware.d.ts +12 -0
- package/dist/server/middleware.js +50 -0
- package/dist/server/routes.d.ts +25 -0
- package/dist/server/routes.js +224 -0
- package/dist/users/UserManager.d.ts +45 -0
- package/dist/users/UserManager.js +113 -0
- package/docs/dawn-audit-report.md +412 -0
- package/docs/positioning-vs-openclaw.md +246 -0
- package/package.json +52 -0
- package/src/cli.ts +169 -0
- package/src/commands/init.ts +654 -0
- package/src/commands/job.ts +110 -0
- package/src/commands/server.ts +325 -0
- package/src/commands/setup.ts +958 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/user.ts +71 -0
- package/src/core/Config.ts +161 -0
- package/src/core/Prerequisites.ts +187 -0
- package/src/core/RelationshipManager.ts +366 -0
- package/src/core/SessionManager.ts +385 -0
- package/src/core/StateManager.ts +121 -0
- package/src/core/types.ts +320 -0
- package/src/index.ts +58 -0
- package/src/messaging/TelegramAdapter.ts +365 -0
- package/src/monitoring/HealthChecker.ts +172 -0
- package/src/scaffold/bootstrap.ts +122 -0
- package/src/scaffold/templates.ts +204 -0
- package/src/scheduler/JobLoader.ts +85 -0
- package/src/scheduler/JobScheduler.ts +476 -0
- package/src/server/AgentServer.ts +93 -0
- package/src/server/middleware.ts +58 -0
- package/src/server/routes.ts +278 -0
- package/src/templates/default-jobs.json +47 -0
- package/src/templates/hooks/compaction-recovery.sh +23 -0
- package/src/templates/hooks/dangerous-command-guard.sh +35 -0
- package/src/templates/hooks/grounding-before-messaging.sh +22 -0
- package/src/templates/hooks/session-start.sh +37 -0
- package/src/templates/hooks/settings-template.json +45 -0
- package/src/templates/scripts/health-watchdog.sh +63 -0
- package/src/templates/scripts/telegram-reply.sh +54 -0
- package/src/users/UserManager.ts +129 -0
- package/tests/e2e/lifecycle.test.ts +376 -0
- package/tests/fixtures/test-repo/CLAUDE.md +3 -0
- package/tests/fixtures/test-repo/README.md +1 -0
- package/tests/helpers/setup.ts +209 -0
- package/tests/integration/fresh-install.test.ts +218 -0
- package/tests/integration/scheduler-basic.test.ts +109 -0
- package/tests/integration/server-full.test.ts +284 -0
- package/tests/integration/session-lifecycle.test.ts +181 -0
- package/tests/unit/Config.test.ts +22 -0
- package/tests/unit/HealthChecker.test.ts +168 -0
- package/tests/unit/JobLoader.test.ts +151 -0
- package/tests/unit/JobScheduler.test.ts +267 -0
- package/tests/unit/Prerequisites.test.ts +59 -0
- package/tests/unit/RelationshipManager.test.ts +345 -0
- package/tests/unit/StateManager.test.ts +143 -0
- package/tests/unit/TelegramAdapter.test.ts +165 -0
- package/tests/unit/UserManager.test.ts +131 -0
- package/tests/unit/bootstrap.test.ts +28 -0
- package/tests/unit/commands.test.ts +138 -0
- package/tests/unit/middleware.test.ts +92 -0
- package/tests/unit/relationship-routes.test.ts +131 -0
- package/tests/unit/scaffold-templates.test.ts +132 -0
- package/tests/unit/server.test.ts +163 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
- package/vitest.e2e.config.ts +9 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `instar job add|list` — Manage scheduled jobs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import { loadConfig, ensureStateDir } from '../core/Config.js';
|
|
8
|
+
import { loadJobs, validateJob } from '../scheduler/JobLoader.js';
|
|
9
|
+
import { StateManager } from '../core/StateManager.js';
|
|
10
|
+
import type { JobDefinition, JobPriority, ModelTier } from '../core/types.js';
|
|
11
|
+
|
|
12
|
+
interface JobAddOptions {
|
|
13
|
+
slug: string;
|
|
14
|
+
name: string;
|
|
15
|
+
schedule: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
priority?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
execute?: string;
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function addJob(options: JobAddOptions): Promise<void> {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
ensureStateDir(config.stateDir);
|
|
27
|
+
|
|
28
|
+
const jobsFile = config.scheduler.jobsFile;
|
|
29
|
+
let jobs: JobDefinition[] = [];
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(jobsFile)) {
|
|
32
|
+
jobs = JSON.parse(fs.readFileSync(jobsFile, 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check for duplicate slug
|
|
36
|
+
if (jobs.some(j => j.slug === options.slug)) {
|
|
37
|
+
console.log(pc.red(`Job with slug "${options.slug}" already exists.`));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const newJob: JobDefinition = {
|
|
42
|
+
slug: options.slug,
|
|
43
|
+
name: options.name,
|
|
44
|
+
description: options.description || options.name,
|
|
45
|
+
schedule: options.schedule,
|
|
46
|
+
priority: (options.priority || 'medium') as JobPriority,
|
|
47
|
+
expectedDurationMinutes: 5,
|
|
48
|
+
model: (options.model || 'sonnet') as ModelTier,
|
|
49
|
+
enabled: options.enabled !== false,
|
|
50
|
+
execute: {
|
|
51
|
+
type: (options.type || 'prompt') as 'skill' | 'prompt' | 'script',
|
|
52
|
+
value: options.execute || `Run the ${options.name} job`,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Validate before saving
|
|
57
|
+
try {
|
|
58
|
+
validateJob(newJob);
|
|
59
|
+
} catch (err: any) {
|
|
60
|
+
console.log(pc.red(`Invalid job: ${err.message}`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
jobs.push(newJob);
|
|
65
|
+
fs.writeFileSync(jobsFile, JSON.stringify(jobs, null, 2));
|
|
66
|
+
|
|
67
|
+
console.log(pc.green(`Job "${options.name}" (${options.slug}) added.`));
|
|
68
|
+
console.log(` Schedule: ${options.schedule}`);
|
|
69
|
+
console.log(` Priority: ${newJob.priority}`);
|
|
70
|
+
console.log(` Model: ${newJob.model}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function listJobs(_options: { dir?: string }): Promise<void> {
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
|
|
76
|
+
let jobs: JobDefinition[];
|
|
77
|
+
try {
|
|
78
|
+
jobs = loadJobs(config.scheduler.jobsFile);
|
|
79
|
+
} catch {
|
|
80
|
+
console.log(pc.dim('No jobs configured.'));
|
|
81
|
+
console.log(`Add one: ${pc.cyan("instar job add --slug health-check --name 'Health Check' --schedule '0 */4 * * *'")}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (jobs.length === 0) {
|
|
86
|
+
console.log(pc.dim('No jobs configured.'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const state = new StateManager(config.stateDir);
|
|
91
|
+
const enabled = jobs.filter(j => j.enabled);
|
|
92
|
+
|
|
93
|
+
console.log(pc.bold(`Jobs (${enabled.length} enabled / ${jobs.length} total):\n`));
|
|
94
|
+
|
|
95
|
+
for (const job of jobs) {
|
|
96
|
+
const icon = job.enabled ? pc.green('●') : pc.dim('○');
|
|
97
|
+
const jobState = state.getJobState(job.slug);
|
|
98
|
+
const lastRun = jobState?.lastRun
|
|
99
|
+
? new Date(jobState.lastRun).toLocaleString()
|
|
100
|
+
: pc.dim('never');
|
|
101
|
+
const failures = jobState?.consecutiveFailures
|
|
102
|
+
? pc.red(` (${jobState.consecutiveFailures} failures)`)
|
|
103
|
+
: '';
|
|
104
|
+
|
|
105
|
+
console.log(` ${icon} ${pc.bold(job.slug)} — ${job.name}`);
|
|
106
|
+
console.log(` Schedule: ${job.schedule} | Priority: ${job.priority} | Model: ${job.model}`);
|
|
107
|
+
console.log(` Last run: ${lastRun}${failures}`);
|
|
108
|
+
console.log(` Execute: ${job.execute.type}:${job.execute.value}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `instar server start|stop` — Manage the persistent agent server.
|
|
3
|
+
*
|
|
4
|
+
* Start launches the server in a tmux session (background) or foreground.
|
|
5
|
+
* Stop kills the server tmux session.
|
|
6
|
+
*
|
|
7
|
+
* When Telegram is configured, wires up message routing:
|
|
8
|
+
* topic message → find/spawn session → inject message → session replies via [telegram:N]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import pc from 'picocolors';
|
|
15
|
+
import { loadConfig, ensureStateDir, detectTmuxPath } from '../core/Config.js';
|
|
16
|
+
import { SessionManager } from '../core/SessionManager.js';
|
|
17
|
+
import { StateManager } from '../core/StateManager.js';
|
|
18
|
+
import { JobScheduler } from '../scheduler/JobScheduler.js';
|
|
19
|
+
import { AgentServer } from '../server/AgentServer.js';
|
|
20
|
+
import { TelegramAdapter } from '../messaging/TelegramAdapter.js';
|
|
21
|
+
import { RelationshipManager } from '../core/RelationshipManager.js';
|
|
22
|
+
import type { Message } from '../core/types.js';
|
|
23
|
+
|
|
24
|
+
interface StartOptions {
|
|
25
|
+
foreground?: boolean;
|
|
26
|
+
dir?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Respawn a session for a topic, including thread history in the bootstrap.
|
|
31
|
+
* This prevents "thread drift" where respawned sessions lose context.
|
|
32
|
+
*/
|
|
33
|
+
async function respawnSessionForTopic(
|
|
34
|
+
sessionManager: SessionManager,
|
|
35
|
+
telegram: TelegramAdapter,
|
|
36
|
+
targetSession: string,
|
|
37
|
+
topicId: number,
|
|
38
|
+
latestMessage?: string,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
console.log(`[telegram→session] Session "${targetSession}" needs respawn for topic ${topicId}`);
|
|
41
|
+
|
|
42
|
+
const msg = latestMessage || 'Session respawned — send a message to continue.';
|
|
43
|
+
|
|
44
|
+
// Fetch thread history for context
|
|
45
|
+
let historyLines: string[] = [];
|
|
46
|
+
try {
|
|
47
|
+
const history = telegram.getTopicHistory(topicId, 20);
|
|
48
|
+
if (history.length > 0) {
|
|
49
|
+
historyLines.push(`--- Thread History (last ${history.length} messages) ---`);
|
|
50
|
+
historyLines.push(`IMPORTANT: Read this history carefully before taking any action.`);
|
|
51
|
+
historyLines.push(`Your task is to continue THIS conversation, not start something new.`);
|
|
52
|
+
historyLines.push(``);
|
|
53
|
+
for (const m of history) {
|
|
54
|
+
const sender = m.fromUser ? 'User' : 'Agent';
|
|
55
|
+
const ts = m.timestamp ? new Date(m.timestamp).toISOString().slice(11, 19) : '??:??';
|
|
56
|
+
const text = (m.text || '').slice(0, 300);
|
|
57
|
+
historyLines.push(`[${ts}] ${sender}: ${text}`);
|
|
58
|
+
}
|
|
59
|
+
historyLines.push(``);
|
|
60
|
+
historyLines.push(`--- End Thread History ---`);
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(`[telegram→session] Failed to fetch thread history:`, err);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Always keep the user's message inline — never hide it behind a file reference.
|
|
67
|
+
// Only thread history goes into a file for supplementary context.
|
|
68
|
+
let bootstrapMessage: string;
|
|
69
|
+
|
|
70
|
+
if (historyLines.length > 0) {
|
|
71
|
+
const historyContent = historyLines.join('\n');
|
|
72
|
+
const tmpDir = '/tmp/instar-telegram';
|
|
73
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
74
|
+
const filepath = path.join(tmpDir, `history-${topicId}-${Date.now()}.txt`);
|
|
75
|
+
fs.writeFileSync(filepath, historyContent);
|
|
76
|
+
|
|
77
|
+
bootstrapMessage = [
|
|
78
|
+
`[telegram:${topicId}] ${msg}`,
|
|
79
|
+
``,
|
|
80
|
+
`(Session was respawned. Thread history is at ${filepath} — read it for context. Then RESPOND to the user's message above via Telegram relay.)`,
|
|
81
|
+
].join('\n');
|
|
82
|
+
} else {
|
|
83
|
+
bootstrapMessage = `[telegram:${topicId}] ${msg}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const storedName = telegram.getTopicName(topicId);
|
|
87
|
+
const topicName = storedName || targetSession;
|
|
88
|
+
const newSessionName = await sessionManager.spawnInteractiveSession(bootstrapMessage, topicName);
|
|
89
|
+
|
|
90
|
+
telegram.registerTopicSession(topicId, newSessionName);
|
|
91
|
+
await telegram.sendToTopic(topicId, `Session respawned.`);
|
|
92
|
+
console.log(`[telegram→session] Respawned "${newSessionName}" for topic ${topicId}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Wire up Telegram message routing: topic messages → Claude sessions.
|
|
97
|
+
* This is the core handler that makes Telegram topics work like sessions.
|
|
98
|
+
*/
|
|
99
|
+
function wireTelegramRouting(
|
|
100
|
+
telegram: TelegramAdapter,
|
|
101
|
+
sessionManager: SessionManager,
|
|
102
|
+
): void {
|
|
103
|
+
telegram.onTopicMessage = (msg: Message) => {
|
|
104
|
+
const topicId = (msg.metadata?.messageThreadId as number) ?? null;
|
|
105
|
+
if (!topicId) return;
|
|
106
|
+
|
|
107
|
+
const text = msg.content;
|
|
108
|
+
|
|
109
|
+
// Handle /new command — spawn a new session with its own topic
|
|
110
|
+
const newMatch = text.match(/^\/new(?:\s+(.+))?$/);
|
|
111
|
+
if (newMatch) {
|
|
112
|
+
const sessionName = newMatch[1]?.trim() || null;
|
|
113
|
+
const topicName = sessionName || `session-${new Date().toISOString().slice(5, 16).replace('T', '-').replace(':', '')}`;
|
|
114
|
+
|
|
115
|
+
(async () => {
|
|
116
|
+
try {
|
|
117
|
+
const topic = await telegram.createForumTopic(topicName, 9367192); // Green
|
|
118
|
+
const newSession = await sessionManager.spawnInteractiveSession(
|
|
119
|
+
`[telegram:${topic.topicId}] New session started.`,
|
|
120
|
+
topicName,
|
|
121
|
+
);
|
|
122
|
+
telegram.registerTopicSession(topic.topicId, newSession);
|
|
123
|
+
await telegram.sendToTopic(topic.topicId, `Session created. I'm here.`);
|
|
124
|
+
await telegram.sendToTopic(topicId, `New session created: "${topicName}" — check the new topic above.`);
|
|
125
|
+
console.log(`[telegram] Spawned session "${newSession}" for new topic ${topic.topicId}`);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`[telegram] /new failed:`, err);
|
|
128
|
+
await telegram.sendToTopic(topicId, `Failed to spawn session: ${err instanceof Error ? err.message : String(err)}`).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Route message to corresponding session
|
|
135
|
+
const targetSession = telegram.getSessionForTopic(topicId);
|
|
136
|
+
|
|
137
|
+
if (targetSession) {
|
|
138
|
+
// Session is mapped — check if it's alive, inject or respawn
|
|
139
|
+
if (sessionManager.isSessionAlive(targetSession)) {
|
|
140
|
+
console.log(`[telegram→session] Injecting into ${targetSession}: "${text.slice(0, 80)}"`);
|
|
141
|
+
sessionManager.injectTelegramMessage(targetSession, topicId, text);
|
|
142
|
+
} else {
|
|
143
|
+
// Session died — respawn with thread history
|
|
144
|
+
respawnSessionForTopic(sessionManager, telegram, targetSession, topicId, text).catch(err => {
|
|
145
|
+
console.error(`[telegram→session] Respawn failed:`, err);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// No session mapped — auto-spawn one
|
|
150
|
+
console.log(`[telegram→session] No session for topic ${topicId}, auto-spawning...`);
|
|
151
|
+
const storedName = telegram.getTopicName(topicId) || `topic-${topicId}`;
|
|
152
|
+
|
|
153
|
+
// Always keep the user's message inline — never hide it behind a file reference
|
|
154
|
+
const bootstrapMessage = [
|
|
155
|
+
`[telegram:${topicId}] ${text}`,
|
|
156
|
+
``,
|
|
157
|
+
`(This session was auto-created for a Telegram topic. Respond to the user's message above via Telegram relay.)`,
|
|
158
|
+
].join('\n');
|
|
159
|
+
|
|
160
|
+
sessionManager.spawnInteractiveSession(bootstrapMessage, storedName).then((newSessionName) => {
|
|
161
|
+
telegram.registerTopicSession(topicId, newSessionName);
|
|
162
|
+
telegram.sendToTopic(topicId, `Session auto-created. I'm here.`).catch(() => {});
|
|
163
|
+
console.log(`[telegram→session] Auto-spawned "${newSessionName}" for topic ${topicId}`);
|
|
164
|
+
}).catch((err) => {
|
|
165
|
+
console.error(`[telegram→session] Auto-spawn failed:`, err);
|
|
166
|
+
telegram.sendToTopic(topicId, `Failed to create session: ${err instanceof Error ? err.message : String(err)}`).catch(() => {});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Ensure the Agent Attention topic exists — the agent's direct line to the user.
|
|
174
|
+
* Created once on first server start, persisted in state.
|
|
175
|
+
*/
|
|
176
|
+
async function ensureAgentAttentionTopic(
|
|
177
|
+
telegram: TelegramAdapter,
|
|
178
|
+
state: StateManager,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const existingTopicId = state.get<number>('agent-attention-topic');
|
|
181
|
+
if (existingTopicId) {
|
|
182
|
+
console.log(` Agent Attention topic: ${existingTopicId}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const topic = await telegram.createForumTopic(
|
|
188
|
+
'Agent Attention',
|
|
189
|
+
9367192, // Green — direct line to user
|
|
190
|
+
);
|
|
191
|
+
state.set('agent-attention-topic', topic.topicId);
|
|
192
|
+
await telegram.sendToTopic(topic.topicId,
|
|
193
|
+
`This is your agent's direct line to you.\n\nInfrastructure issues, proactive observations, relationship insights, and anything that doesn't fit into another topic will appear here.`
|
|
194
|
+
);
|
|
195
|
+
console.log(pc.green(` Created Agent Attention topic: ${topic.topicId}`));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(` Failed to create Agent Attention topic: ${err}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function startServer(options: StartOptions): Promise<void> {
|
|
202
|
+
const config = loadConfig(options.dir);
|
|
203
|
+
ensureStateDir(config.stateDir);
|
|
204
|
+
|
|
205
|
+
const serverSessionName = `${config.projectName}-server`;
|
|
206
|
+
|
|
207
|
+
if (options.foreground) {
|
|
208
|
+
// Run in foreground — useful for development
|
|
209
|
+
console.log(pc.bold(`Starting instar server for ${pc.cyan(config.projectName)}`));
|
|
210
|
+
console.log(` Port: ${config.port}`);
|
|
211
|
+
console.log(` State: ${config.stateDir}`);
|
|
212
|
+
console.log();
|
|
213
|
+
|
|
214
|
+
const state = new StateManager(config.stateDir);
|
|
215
|
+
const sessionManager = new SessionManager(config.sessions, state);
|
|
216
|
+
const relationships = new RelationshipManager(config.relationships);
|
|
217
|
+
console.log(pc.green(` Relationships loaded: ${relationships.getAll().length} tracked`));
|
|
218
|
+
|
|
219
|
+
let scheduler: JobScheduler | undefined;
|
|
220
|
+
if (config.scheduler.enabled) {
|
|
221
|
+
scheduler = new JobScheduler(config.scheduler, sessionManager, state);
|
|
222
|
+
scheduler.start();
|
|
223
|
+
console.log(pc.green(' Scheduler started'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Set up Telegram if configured
|
|
227
|
+
let telegram: TelegramAdapter | undefined;
|
|
228
|
+
const telegramConfig = config.messaging.find(m => m.type === 'telegram' && m.enabled);
|
|
229
|
+
if (telegramConfig) {
|
|
230
|
+
telegram = new TelegramAdapter(telegramConfig.config as any, config.stateDir);
|
|
231
|
+
await telegram.start();
|
|
232
|
+
console.log(pc.green(' Telegram connected'));
|
|
233
|
+
|
|
234
|
+
// Wire up topic → session routing
|
|
235
|
+
wireTelegramRouting(telegram, sessionManager);
|
|
236
|
+
console.log(pc.green(' Telegram message routing active'));
|
|
237
|
+
|
|
238
|
+
if (scheduler) {
|
|
239
|
+
scheduler.setMessenger(telegram);
|
|
240
|
+
scheduler.setTelegram(telegram);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Ensure Agent Attention topic exists (the agent's direct line to the user)
|
|
244
|
+
ensureAgentAttentionTopic(telegram, state).catch(err => {
|
|
245
|
+
console.error(`[server] Failed to ensure Agent Attention topic: ${err}`);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
sessionManager.startMonitoring();
|
|
250
|
+
if (scheduler) {
|
|
251
|
+
sessionManager.on('sessionComplete', (session) => {
|
|
252
|
+
scheduler!.processQueue();
|
|
253
|
+
scheduler!.notifyJobComplete(session.id, session.tmuxSession);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships });
|
|
258
|
+
await server.start();
|
|
259
|
+
|
|
260
|
+
// Graceful shutdown
|
|
261
|
+
const shutdown = async () => {
|
|
262
|
+
console.log('\nShutting down...');
|
|
263
|
+
scheduler?.stop();
|
|
264
|
+
if (telegram) await telegram.stop();
|
|
265
|
+
sessionManager.stopMonitoring();
|
|
266
|
+
await server.stop();
|
|
267
|
+
process.exit(0);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
process.on('SIGINT', shutdown);
|
|
271
|
+
process.on('SIGTERM', shutdown);
|
|
272
|
+
} else {
|
|
273
|
+
// Run in tmux background session
|
|
274
|
+
const tmuxPath = detectTmuxPath();
|
|
275
|
+
if (!tmuxPath) {
|
|
276
|
+
console.log(pc.red('tmux not found. Use --foreground to run without tmux.'));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check if already running
|
|
281
|
+
try {
|
|
282
|
+
execSync(`${tmuxPath} has-session -t '=${serverSessionName}' 2>/dev/null`);
|
|
283
|
+
console.log(pc.yellow(`Server already running in tmux session: ${serverSessionName}`));
|
|
284
|
+
console.log(` Attach with: tmux attach -t '=${serverSessionName}'`);
|
|
285
|
+
return;
|
|
286
|
+
} catch {
|
|
287
|
+
// Not running — good
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Get the path to the CLI entry point
|
|
291
|
+
const cliPath = new URL('../cli.js', import.meta.url).pathname;
|
|
292
|
+
|
|
293
|
+
const nodeCmd = `node '${cliPath}' server start --foreground`;
|
|
294
|
+
const cmd = `${tmuxPath} new-session -d -s '${serverSessionName}' -c '${config.projectDir}' '${nodeCmd}'`;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
execSync(cmd);
|
|
298
|
+
console.log(pc.green(`Server started in tmux session: ${pc.bold(serverSessionName)}`));
|
|
299
|
+
console.log(` Port: ${config.port}`);
|
|
300
|
+
console.log(` Attach: tmux attach -t '=${serverSessionName}'`);
|
|
301
|
+
console.log(` Health: curl http://localhost:${config.port}/health`);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.log(pc.red(`Failed to start server: ${err}`));
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function stopServer(options: { dir?: string }): Promise<void> {
|
|
310
|
+
const config = loadConfig(options.dir);
|
|
311
|
+
const serverSessionName = `${config.projectName}-server`;
|
|
312
|
+
const tmuxPath = detectTmuxPath();
|
|
313
|
+
|
|
314
|
+
if (!tmuxPath) {
|
|
315
|
+
console.log(pc.red('tmux not found'));
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
execSync(`${tmuxPath} kill-session -t '=${serverSessionName}'`);
|
|
321
|
+
console.log(pc.green(`Server stopped (killed tmux session: ${serverSessionName})`));
|
|
322
|
+
} catch {
|
|
323
|
+
console.log(pc.yellow(`No server running (no tmux session: ${serverSessionName})`));
|
|
324
|
+
}
|
|
325
|
+
}
|