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.
Files changed (115) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.claude/skills/setup-wizard/skill.md +343 -0
  3. package/.github/workflows/ci.yml +78 -0
  4. package/CLAUDE.md +82 -0
  5. package/README.md +194 -0
  6. package/dist/cli.d.ts +18 -0
  7. package/dist/cli.js +141 -0
  8. package/dist/commands/init.d.ts +40 -0
  9. package/dist/commands/init.js +568 -0
  10. package/dist/commands/job.d.ts +20 -0
  11. package/dist/commands/job.js +84 -0
  12. package/dist/commands/server.d.ts +19 -0
  13. package/dist/commands/server.js +273 -0
  14. package/dist/commands/setup.d.ts +24 -0
  15. package/dist/commands/setup.js +865 -0
  16. package/dist/commands/status.d.ts +11 -0
  17. package/dist/commands/status.js +114 -0
  18. package/dist/commands/user.d.ts +17 -0
  19. package/dist/commands/user.js +53 -0
  20. package/dist/core/Config.d.ts +16 -0
  21. package/dist/core/Config.js +144 -0
  22. package/dist/core/Prerequisites.d.ts +28 -0
  23. package/dist/core/Prerequisites.js +159 -0
  24. package/dist/core/RelationshipManager.d.ts +73 -0
  25. package/dist/core/RelationshipManager.js +318 -0
  26. package/dist/core/SessionManager.d.ts +89 -0
  27. package/dist/core/SessionManager.js +326 -0
  28. package/dist/core/StateManager.d.ts +28 -0
  29. package/dist/core/StateManager.js +96 -0
  30. package/dist/core/types.d.ts +279 -0
  31. package/dist/core/types.js +8 -0
  32. package/dist/index.d.ts +18 -0
  33. package/dist/index.js +23 -0
  34. package/dist/messaging/TelegramAdapter.d.ts +73 -0
  35. package/dist/messaging/TelegramAdapter.js +288 -0
  36. package/dist/monitoring/HealthChecker.d.ts +38 -0
  37. package/dist/monitoring/HealthChecker.js +148 -0
  38. package/dist/scaffold/bootstrap.d.ts +21 -0
  39. package/dist/scaffold/bootstrap.js +110 -0
  40. package/dist/scaffold/templates.d.ts +34 -0
  41. package/dist/scaffold/templates.js +187 -0
  42. package/dist/scheduler/JobLoader.d.ts +18 -0
  43. package/dist/scheduler/JobLoader.js +70 -0
  44. package/dist/scheduler/JobScheduler.d.ts +111 -0
  45. package/dist/scheduler/JobScheduler.js +402 -0
  46. package/dist/server/AgentServer.d.ts +40 -0
  47. package/dist/server/AgentServer.js +73 -0
  48. package/dist/server/middleware.d.ts +12 -0
  49. package/dist/server/middleware.js +50 -0
  50. package/dist/server/routes.d.ts +25 -0
  51. package/dist/server/routes.js +224 -0
  52. package/dist/users/UserManager.d.ts +45 -0
  53. package/dist/users/UserManager.js +113 -0
  54. package/docs/dawn-audit-report.md +412 -0
  55. package/docs/positioning-vs-openclaw.md +246 -0
  56. package/package.json +52 -0
  57. package/src/cli.ts +169 -0
  58. package/src/commands/init.ts +654 -0
  59. package/src/commands/job.ts +110 -0
  60. package/src/commands/server.ts +325 -0
  61. package/src/commands/setup.ts +958 -0
  62. package/src/commands/status.ts +125 -0
  63. package/src/commands/user.ts +71 -0
  64. package/src/core/Config.ts +161 -0
  65. package/src/core/Prerequisites.ts +187 -0
  66. package/src/core/RelationshipManager.ts +366 -0
  67. package/src/core/SessionManager.ts +385 -0
  68. package/src/core/StateManager.ts +121 -0
  69. package/src/core/types.ts +320 -0
  70. package/src/index.ts +58 -0
  71. package/src/messaging/TelegramAdapter.ts +365 -0
  72. package/src/monitoring/HealthChecker.ts +172 -0
  73. package/src/scaffold/bootstrap.ts +122 -0
  74. package/src/scaffold/templates.ts +204 -0
  75. package/src/scheduler/JobLoader.ts +85 -0
  76. package/src/scheduler/JobScheduler.ts +476 -0
  77. package/src/server/AgentServer.ts +93 -0
  78. package/src/server/middleware.ts +58 -0
  79. package/src/server/routes.ts +278 -0
  80. package/src/templates/default-jobs.json +47 -0
  81. package/src/templates/hooks/compaction-recovery.sh +23 -0
  82. package/src/templates/hooks/dangerous-command-guard.sh +35 -0
  83. package/src/templates/hooks/grounding-before-messaging.sh +22 -0
  84. package/src/templates/hooks/session-start.sh +37 -0
  85. package/src/templates/hooks/settings-template.json +45 -0
  86. package/src/templates/scripts/health-watchdog.sh +63 -0
  87. package/src/templates/scripts/telegram-reply.sh +54 -0
  88. package/src/users/UserManager.ts +129 -0
  89. package/tests/e2e/lifecycle.test.ts +376 -0
  90. package/tests/fixtures/test-repo/CLAUDE.md +3 -0
  91. package/tests/fixtures/test-repo/README.md +1 -0
  92. package/tests/helpers/setup.ts +209 -0
  93. package/tests/integration/fresh-install.test.ts +218 -0
  94. package/tests/integration/scheduler-basic.test.ts +109 -0
  95. package/tests/integration/server-full.test.ts +284 -0
  96. package/tests/integration/session-lifecycle.test.ts +181 -0
  97. package/tests/unit/Config.test.ts +22 -0
  98. package/tests/unit/HealthChecker.test.ts +168 -0
  99. package/tests/unit/JobLoader.test.ts +151 -0
  100. package/tests/unit/JobScheduler.test.ts +267 -0
  101. package/tests/unit/Prerequisites.test.ts +59 -0
  102. package/tests/unit/RelationshipManager.test.ts +345 -0
  103. package/tests/unit/StateManager.test.ts +143 -0
  104. package/tests/unit/TelegramAdapter.test.ts +165 -0
  105. package/tests/unit/UserManager.test.ts +131 -0
  106. package/tests/unit/bootstrap.test.ts +28 -0
  107. package/tests/unit/commands.test.ts +138 -0
  108. package/tests/unit/middleware.test.ts +92 -0
  109. package/tests/unit/relationship-routes.test.ts +131 -0
  110. package/tests/unit/scaffold-templates.test.ts +132 -0
  111. package/tests/unit/server.test.ts +163 -0
  112. package/tsconfig.json +20 -0
  113. package/vitest.config.ts +9 -0
  114. package/vitest.e2e.config.ts +9 -0
  115. package/vitest.integration.config.ts +9 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Job Scheduler — cron-based job execution engine.
3
+ *
4
+ * Schedules jobs via croner, respects session limits and quota,
5
+ * queues jobs when at capacity, and drains when slots open.
6
+ *
7
+ * Simplified from Dawn's 1400-line scheduler — serial queue,
8
+ * no JSONL discovery, no machine coordination.
9
+ */
10
+ import type { SessionManager } from '../core/SessionManager.js';
11
+ import type { StateManager } from '../core/StateManager.js';
12
+ import type { MessagingAdapter } from '../core/types.js';
13
+ import type { JobDefinition, JobSchedulerConfig, JobPriority } from '../core/types.js';
14
+ import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
15
+ interface QueuedJob {
16
+ slug: string;
17
+ reason: string;
18
+ queuedAt: string;
19
+ }
20
+ interface SchedulerStatus {
21
+ running: boolean;
22
+ paused: boolean;
23
+ jobCount: number;
24
+ enabledJobs: number;
25
+ queueLength: number;
26
+ activeJobSessions: number;
27
+ }
28
+ export declare class JobScheduler {
29
+ private config;
30
+ private sessionManager;
31
+ private state;
32
+ private jobs;
33
+ private cronTasks;
34
+ private queue;
35
+ private running;
36
+ private paused;
37
+ /** Callback to check if quota allows running a job at the given priority */
38
+ canRunJob: (priority: JobPriority) => boolean;
39
+ /** Optional messenger for sending job notifications */
40
+ private messenger;
41
+ /** Optional Telegram adapter for job-topic coupling */
42
+ private telegram;
43
+ constructor(config: JobSchedulerConfig, sessionManager: SessionManager, state: StateManager);
44
+ /**
45
+ * Set a messaging adapter for job completion notifications.
46
+ */
47
+ setMessenger(adapter: MessagingAdapter): void;
48
+ /**
49
+ * Set the Telegram adapter for job-topic coupling.
50
+ * Every job gets its own topic — the user's window into the job.
51
+ */
52
+ setTelegram(adapter: TelegramAdapter): void;
53
+ /**
54
+ * Start the scheduler — load jobs, set up cron tasks, check for missed jobs.
55
+ */
56
+ start(): void;
57
+ /**
58
+ * Stop the scheduler — cancel all cron tasks.
59
+ */
60
+ stop(): void;
61
+ /**
62
+ * Trigger a job by slug. Checks quota, session limits, queues if at capacity.
63
+ */
64
+ triggerJob(slug: string, reason: string): 'triggered' | 'queued' | 'skipped';
65
+ /**
66
+ * Process the queue — dequeue and run next job if a slot is available.
67
+ */
68
+ processQueue(): void;
69
+ /**
70
+ * Pause — cron tasks keep ticking but triggers are skipped.
71
+ */
72
+ pause(): void;
73
+ /**
74
+ * Resume — triggers start executing again.
75
+ */
76
+ resume(): void;
77
+ /**
78
+ * Get scheduler status for the /status endpoint.
79
+ */
80
+ getStatus(): SchedulerStatus;
81
+ /**
82
+ * Get loaded job definitions (for /jobs endpoint).
83
+ */
84
+ getJobs(): JobDefinition[];
85
+ /**
86
+ * Get the current queue.
87
+ */
88
+ getQueue(): QueuedJob[];
89
+ private enqueue;
90
+ private spawnJobSession;
91
+ private buildPrompt;
92
+ private getConsecutiveFailures;
93
+ private getNextRun;
94
+ /**
95
+ * Called when a job's session completes. Captures output and notifies via messenger.
96
+ */
97
+ notifyJobComplete(sessionName: string, tmuxSession: string): Promise<void>;
98
+ /**
99
+ * Ensure every enabled job has a Telegram topic.
100
+ * Creates topics for jobs that don't have one.
101
+ * This is the "job-topic coupling" — every job lives in a topic.
102
+ */
103
+ private ensureJobTopics;
104
+ /**
105
+ * Save a job-topic mapping (used when recreating a deleted topic).
106
+ */
107
+ private saveJobTopicMapping;
108
+ private checkMissedJobs;
109
+ }
110
+ export {};
111
+ //# sourceMappingURL=JobScheduler.d.ts.map
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Job Scheduler — cron-based job execution engine.
3
+ *
4
+ * Schedules jobs via croner, respects session limits and quota,
5
+ * queues jobs when at capacity, and drains when slots open.
6
+ *
7
+ * Simplified from Dawn's 1400-line scheduler — serial queue,
8
+ * no JSONL discovery, no machine coordination.
9
+ */
10
+ import { Cron } from 'croner';
11
+ import { loadJobs } from './JobLoader.js';
12
+ const PRIORITY_ORDER = {
13
+ critical: 0,
14
+ high: 1,
15
+ medium: 2,
16
+ low: 3,
17
+ };
18
+ export class JobScheduler {
19
+ config;
20
+ sessionManager;
21
+ state;
22
+ jobs = [];
23
+ cronTasks = new Map();
24
+ queue = [];
25
+ running = false;
26
+ paused = false;
27
+ /** Callback to check if quota allows running a job at the given priority */
28
+ canRunJob = () => true;
29
+ /** Optional messenger for sending job notifications */
30
+ messenger = null;
31
+ /** Optional Telegram adapter for job-topic coupling */
32
+ telegram = null;
33
+ constructor(config, sessionManager, state) {
34
+ this.config = config;
35
+ this.sessionManager = sessionManager;
36
+ this.state = state;
37
+ }
38
+ /**
39
+ * Set a messaging adapter for job completion notifications.
40
+ */
41
+ setMessenger(adapter) {
42
+ this.messenger = adapter;
43
+ }
44
+ /**
45
+ * Set the Telegram adapter for job-topic coupling.
46
+ * Every job gets its own topic — the user's window into the job.
47
+ */
48
+ setTelegram(adapter) {
49
+ this.telegram = adapter;
50
+ }
51
+ /**
52
+ * Start the scheduler — load jobs, set up cron tasks, check for missed jobs.
53
+ */
54
+ start() {
55
+ if (this.running)
56
+ return;
57
+ this.jobs = loadJobs(this.config.jobsFile);
58
+ this.running = true;
59
+ const enabledJobs = this.jobs.filter(j => j.enabled);
60
+ for (const job of enabledJobs) {
61
+ const task = new Cron(job.schedule, () => {
62
+ this.triggerJob(job.slug, 'scheduled');
63
+ });
64
+ this.cronTasks.set(job.slug, task);
65
+ }
66
+ // Check for missed jobs — any enabled job overdue by >1.5x its interval
67
+ this.checkMissedJobs(enabledJobs);
68
+ // Ensure every job has a Telegram topic (job-topic coupling)
69
+ if (this.telegram) {
70
+ this.ensureJobTopics(enabledJobs).catch(err => {
71
+ console.error(`[scheduler] Failed to ensure job topics: ${err}`);
72
+ });
73
+ }
74
+ this.state.appendEvent({
75
+ type: 'scheduler_start',
76
+ summary: `Scheduler started with ${enabledJobs.length} enabled jobs`,
77
+ timestamp: new Date().toISOString(),
78
+ });
79
+ }
80
+ /**
81
+ * Stop the scheduler — cancel all cron tasks.
82
+ */
83
+ stop() {
84
+ if (!this.running)
85
+ return;
86
+ for (const [, task] of this.cronTasks) {
87
+ task.stop();
88
+ }
89
+ this.cronTasks.clear();
90
+ this.queue = [];
91
+ this.running = false;
92
+ this.state.appendEvent({
93
+ type: 'scheduler_stop',
94
+ summary: 'Scheduler stopped',
95
+ timestamp: new Date().toISOString(),
96
+ });
97
+ }
98
+ /**
99
+ * Trigger a job by slug. Checks quota, session limits, queues if at capacity.
100
+ */
101
+ triggerJob(slug, reason) {
102
+ const job = this.jobs.find(j => j.slug === slug);
103
+ if (!job) {
104
+ throw new Error(`Unknown job: ${slug}`);
105
+ }
106
+ if (this.paused) {
107
+ return 'skipped';
108
+ }
109
+ if (!this.canRunJob(job.priority)) {
110
+ this.state.appendEvent({
111
+ type: 'job_skipped',
112
+ summary: `Job "${slug}" skipped — quota check failed`,
113
+ timestamp: new Date().toISOString(),
114
+ metadata: { slug, reason, priority: job.priority },
115
+ });
116
+ return 'skipped';
117
+ }
118
+ // Check session capacity
119
+ const runningSessions = this.sessionManager.listRunningSessions();
120
+ const jobSessions = runningSessions.filter(s => s.jobSlug);
121
+ if (jobSessions.length >= this.config.maxParallelJobs) {
122
+ this.enqueue(slug, reason);
123
+ return 'queued';
124
+ }
125
+ this.spawnJobSession(job, reason);
126
+ return 'triggered';
127
+ }
128
+ /**
129
+ * Process the queue — dequeue and run next job if a slot is available.
130
+ */
131
+ processQueue() {
132
+ if (this.paused || this.queue.length === 0)
133
+ return;
134
+ const runningSessions = this.sessionManager.listRunningSessions();
135
+ const jobSessions = runningSessions.filter(s => s.jobSlug);
136
+ if (jobSessions.length >= this.config.maxParallelJobs)
137
+ return;
138
+ const next = this.queue.shift();
139
+ if (!next)
140
+ return;
141
+ const job = this.jobs.find(j => j.slug === next.slug);
142
+ if (!job)
143
+ return;
144
+ if (!this.canRunJob(job.priority))
145
+ return;
146
+ this.spawnJobSession(job, `queued:${next.reason}`);
147
+ }
148
+ /**
149
+ * Pause — cron tasks keep ticking but triggers are skipped.
150
+ */
151
+ pause() {
152
+ this.paused = true;
153
+ }
154
+ /**
155
+ * Resume — triggers start executing again.
156
+ */
157
+ resume() {
158
+ this.paused = false;
159
+ this.processQueue();
160
+ }
161
+ /**
162
+ * Get scheduler status for the /status endpoint.
163
+ */
164
+ getStatus() {
165
+ const runningSessions = this.sessionManager.listRunningSessions();
166
+ return {
167
+ running: this.running,
168
+ paused: this.paused,
169
+ jobCount: this.jobs.length,
170
+ enabledJobs: this.jobs.filter(j => j.enabled).length,
171
+ queueLength: this.queue.length,
172
+ activeJobSessions: runningSessions.filter(s => s.jobSlug).length,
173
+ };
174
+ }
175
+ /**
176
+ * Get loaded job definitions (for /jobs endpoint).
177
+ */
178
+ getJobs() {
179
+ return this.jobs;
180
+ }
181
+ /**
182
+ * Get the current queue.
183
+ */
184
+ getQueue() {
185
+ return [...this.queue];
186
+ }
187
+ enqueue(slug, reason) {
188
+ // Don't queue duplicates
189
+ if (this.queue.some(q => q.slug === slug))
190
+ return;
191
+ this.queue.push({ slug, reason, queuedAt: new Date().toISOString() });
192
+ // Sort by priority — critical first
193
+ this.queue.sort((a, b) => {
194
+ const jobA = this.jobs.find(j => j.slug === a.slug);
195
+ const jobB = this.jobs.find(j => j.slug === b.slug);
196
+ return (PRIORITY_ORDER[jobA?.priority ?? 'low']) - (PRIORITY_ORDER[jobB?.priority ?? 'low']);
197
+ });
198
+ }
199
+ spawnJobSession(job, reason) {
200
+ const prompt = this.buildPrompt(job);
201
+ const sessionName = `job-${job.slug}-${Date.now().toString(36)}`;
202
+ this.sessionManager.spawnSession({
203
+ name: sessionName,
204
+ prompt,
205
+ model: job.model,
206
+ jobSlug: job.slug,
207
+ triggeredBy: `scheduler:${reason}`,
208
+ }).then(() => {
209
+ // Update job state on success
210
+ const jobState = {
211
+ slug: job.slug,
212
+ lastRun: new Date().toISOString(),
213
+ consecutiveFailures: 0,
214
+ nextScheduled: this.getNextRun(job.slug),
215
+ };
216
+ this.state.saveJobState(jobState);
217
+ this.state.appendEvent({
218
+ type: 'job_triggered',
219
+ summary: `Job "${job.slug}" triggered (${reason})`,
220
+ sessionId: sessionName,
221
+ timestamp: new Date().toISOString(),
222
+ metadata: { slug: job.slug, reason, model: job.model },
223
+ });
224
+ }).catch((err) => {
225
+ // Track failure
226
+ const failures = this.getConsecutiveFailures(job.slug) + 1;
227
+ const jobState = {
228
+ slug: job.slug,
229
+ lastRun: new Date().toISOString(),
230
+ lastResult: 'failure',
231
+ consecutiveFailures: failures,
232
+ nextScheduled: this.getNextRun(job.slug),
233
+ };
234
+ this.state.saveJobState(jobState);
235
+ this.state.appendEvent({
236
+ type: 'job_error',
237
+ summary: `Job "${job.slug}" failed to spawn: ${err}`,
238
+ timestamp: new Date().toISOString(),
239
+ metadata: { slug: job.slug, consecutiveFailures: failures },
240
+ });
241
+ });
242
+ }
243
+ buildPrompt(job) {
244
+ switch (job.execute.type) {
245
+ case 'skill':
246
+ return `/${job.execute.value}${job.execute.args ? ' ' + job.execute.args : ''}`;
247
+ case 'prompt':
248
+ return job.execute.value;
249
+ case 'script':
250
+ return `Run this script: ${job.execute.value}${job.execute.args ? ' ' + job.execute.args : ''}`;
251
+ }
252
+ }
253
+ getConsecutiveFailures(slug) {
254
+ return this.state.getJobState(slug)?.consecutiveFailures ?? 0;
255
+ }
256
+ getNextRun(slug) {
257
+ const task = this.cronTasks.get(slug);
258
+ if (!task)
259
+ return undefined;
260
+ const next = task.nextRun();
261
+ return next ? next.toISOString() : undefined;
262
+ }
263
+ /**
264
+ * Called when a job's session completes. Captures output and notifies via messenger.
265
+ */
266
+ async notifyJobComplete(sessionName, tmuxSession) {
267
+ if (!this.messenger)
268
+ return;
269
+ // Find which job this session belongs to by looking up session state
270
+ const session = this.state.getSession(sessionName);
271
+ if (!session?.jobSlug)
272
+ return;
273
+ const job = this.jobs.find(j => j.slug === session.jobSlug);
274
+ if (!job)
275
+ return;
276
+ // Capture the last output from the tmux session
277
+ let output = '';
278
+ try {
279
+ output = this.sessionManager.captureOutput(tmuxSession) ?? '';
280
+ }
281
+ catch {
282
+ // Session may already be dead — that's fine
283
+ }
284
+ // Build a summary message
285
+ const duration = session.startedAt
286
+ ? Math.round((Date.now() - new Date(session.startedAt).getTime()) / 1000)
287
+ : 0;
288
+ const durationStr = duration > 60
289
+ ? `${Math.floor(duration / 60)}m ${duration % 60}s`
290
+ : `${duration}s`;
291
+ let summary = `*Job Complete: ${job.name}*\n`;
292
+ summary += `Status: ${session.status === 'failed' ? 'Failed' : 'Done'}\n`;
293
+ if (duration > 0)
294
+ summary += `Duration: ${durationStr}\n`;
295
+ if (output) {
296
+ // Trim to last ~500 chars to keep the message readable
297
+ const trimmed = output.length > 500
298
+ ? '...' + output.slice(-500)
299
+ : output;
300
+ summary += `\n\`\`\`\n${trimmed}\n\`\`\``;
301
+ }
302
+ else {
303
+ summary += '\n_No output captured (session already closed)_';
304
+ }
305
+ // Send to the job's dedicated topic if available, otherwise fall back to generic messenger
306
+ if (this.telegram && job.topicId) {
307
+ try {
308
+ await this.telegram.sendToTopic(job.topicId, summary);
309
+ }
310
+ catch (err) {
311
+ console.error(`[scheduler] Failed to send to job topic ${job.topicId}: ${err}`);
312
+ // Topic may have been deleted — try to recreate
313
+ try {
314
+ const newTopic = await this.telegram.createForumTopic(`Job: ${job.name}`, 7322096);
315
+ job.topicId = newTopic.topicId;
316
+ this.saveJobTopicMapping(job.slug, newTopic.topicId);
317
+ await this.telegram.sendToTopic(newTopic.topicId, summary);
318
+ }
319
+ catch (recreateErr) {
320
+ console.error(`[scheduler] Failed to recreate topic for ${job.slug}: ${recreateErr}`);
321
+ }
322
+ }
323
+ }
324
+ else if (this.messenger) {
325
+ try {
326
+ await this.messenger.send({
327
+ userId: 'system',
328
+ content: summary,
329
+ });
330
+ }
331
+ catch (err) {
332
+ console.error(`[scheduler] Failed to send job notification: ${err}`);
333
+ }
334
+ }
335
+ }
336
+ /**
337
+ * Ensure every enabled job has a Telegram topic.
338
+ * Creates topics for jobs that don't have one.
339
+ * This is the "job-topic coupling" — every job lives in a topic.
340
+ */
341
+ async ensureJobTopics(enabledJobs) {
342
+ if (!this.telegram)
343
+ return;
344
+ // Load existing topic mappings
345
+ const mappings = this.state.get('job-topic-mappings') ?? {};
346
+ for (const job of enabledJobs) {
347
+ // If job already has a topicId (from jobs.json or previous mapping), use it
348
+ if (job.topicId) {
349
+ mappings[job.slug] = job.topicId;
350
+ continue;
351
+ }
352
+ // Check if we have a saved mapping
353
+ if (mappings[job.slug]) {
354
+ job.topicId = mappings[job.slug];
355
+ continue;
356
+ }
357
+ // Create a new topic for this job
358
+ try {
359
+ const topic = await this.telegram.createForumTopic(`Job: ${job.name}`, 7322096);
360
+ job.topicId = topic.topicId;
361
+ mappings[job.slug] = topic.topicId;
362
+ await this.telegram.sendToTopic(topic.topicId, `*${job.name}*\n${job.description}\n\nSchedule: \`${job.schedule}\`\nPriority: ${job.priority}\n\nThis topic is the home for this job. Reports, status updates, and errors will appear here.`);
363
+ }
364
+ catch (err) {
365
+ console.error(`[scheduler] Failed to create topic for job ${job.slug}: ${err}`);
366
+ }
367
+ }
368
+ this.state.set('job-topic-mappings', mappings);
369
+ }
370
+ /**
371
+ * Save a job-topic mapping (used when recreating a deleted topic).
372
+ */
373
+ saveJobTopicMapping(slug, topicId) {
374
+ const mappings = this.state.get('job-topic-mappings') ?? {};
375
+ mappings[slug] = topicId;
376
+ this.state.set('job-topic-mappings', mappings);
377
+ }
378
+ checkMissedJobs(enabledJobs) {
379
+ const now = Date.now();
380
+ for (const job of enabledJobs) {
381
+ const jobState = this.state.getJobState(job.slug);
382
+ if (!jobState?.lastRun)
383
+ continue;
384
+ const lastRun = new Date(jobState.lastRun).getTime();
385
+ const task = this.cronTasks.get(job.slug);
386
+ if (!task)
387
+ continue;
388
+ // Get expected interval from next two runs
389
+ const nextRun = task.nextRun();
390
+ const nextNextRun = task.nextRuns(2)[1];
391
+ if (!nextRun || !nextNextRun)
392
+ continue;
393
+ const intervalMs = nextNextRun.getTime() - nextRun.getTime();
394
+ const timeSinceLastRun = now - lastRun;
395
+ // If overdue by more than 1.5x the interval, trigger immediately
396
+ if (timeSinceLastRun > intervalMs * 1.5) {
397
+ this.triggerJob(job.slug, 'missed');
398
+ }
399
+ }
400
+ }
401
+ }
402
+ //# sourceMappingURL=JobScheduler.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Agent Server — HTTP server wrapping Express.
3
+ *
4
+ * Provides health checks, session management, job triggering,
5
+ * and event querying over a simple REST API.
6
+ */
7
+ import { type Express } from 'express';
8
+ import type { SessionManager } from '../core/SessionManager.js';
9
+ import type { StateManager } from '../core/StateManager.js';
10
+ import type { JobScheduler } from '../scheduler/JobScheduler.js';
11
+ import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
12
+ import type { AgentKitConfig } from '../core/types.js';
13
+ import type { RelationshipManager } from '../core/RelationshipManager.js';
14
+ export declare class AgentServer {
15
+ private app;
16
+ private server;
17
+ private config;
18
+ private startTime;
19
+ constructor(options: {
20
+ config: AgentKitConfig;
21
+ sessionManager: SessionManager;
22
+ state: StateManager;
23
+ scheduler?: JobScheduler;
24
+ telegram?: TelegramAdapter;
25
+ relationships?: RelationshipManager;
26
+ });
27
+ /**
28
+ * Start the HTTP server.
29
+ */
30
+ start(): Promise<void>;
31
+ /**
32
+ * Stop the HTTP server gracefully.
33
+ */
34
+ stop(): Promise<void>;
35
+ /**
36
+ * Expose the Express app for testing with supertest.
37
+ */
38
+ getApp(): Express;
39
+ }
40
+ //# sourceMappingURL=AgentServer.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Agent Server — HTTP server wrapping Express.
3
+ *
4
+ * Provides health checks, session management, job triggering,
5
+ * and event querying over a simple REST API.
6
+ */
7
+ import express from 'express';
8
+ import { createRoutes } from './routes.js';
9
+ import { corsMiddleware, authMiddleware, errorHandler } from './middleware.js';
10
+ export class AgentServer {
11
+ app;
12
+ server = null;
13
+ config;
14
+ startTime;
15
+ constructor(options) {
16
+ this.config = options.config;
17
+ this.startTime = new Date();
18
+ this.app = express();
19
+ // Middleware
20
+ this.app.use(express.json());
21
+ this.app.use(corsMiddleware);
22
+ this.app.use(authMiddleware(options.config.authToken));
23
+ // Routes
24
+ const routes = createRoutes({
25
+ config: options.config,
26
+ sessionManager: options.sessionManager,
27
+ state: options.state,
28
+ scheduler: options.scheduler ?? null,
29
+ telegram: options.telegram ?? null,
30
+ relationships: options.relationships ?? null,
31
+ startTime: this.startTime,
32
+ });
33
+ this.app.use(routes);
34
+ // Error handler (must be last)
35
+ this.app.use(errorHandler);
36
+ }
37
+ /**
38
+ * Start the HTTP server.
39
+ */
40
+ async start() {
41
+ return new Promise((resolve) => {
42
+ this.server = this.app.listen(this.config.port, () => {
43
+ console.log(`[instar] Server listening on port ${this.config.port}`);
44
+ resolve();
45
+ });
46
+ });
47
+ }
48
+ /**
49
+ * Stop the HTTP server gracefully.
50
+ */
51
+ async stop() {
52
+ return new Promise((resolve, reject) => {
53
+ if (!this.server) {
54
+ resolve();
55
+ return;
56
+ }
57
+ this.server.close((err) => {
58
+ if (err)
59
+ reject(err);
60
+ else
61
+ resolve();
62
+ this.server = null;
63
+ });
64
+ });
65
+ }
66
+ /**
67
+ * Expose the Express app for testing with supertest.
68
+ */
69
+ getApp() {
70
+ return this.app;
71
+ }
72
+ }
73
+ //# sourceMappingURL=AgentServer.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Express middleware — JSON parsing, CORS, auth, error handling.
3
+ */
4
+ import type { Request, Response, NextFunction } from 'express';
5
+ export declare function corsMiddleware(req: Request, res: Response, next: NextFunction): void;
6
+ /**
7
+ * Auth middleware — enforces Bearer token on API endpoints.
8
+ * Health endpoint is exempt (used for external monitoring).
9
+ */
10
+ export declare function authMiddleware(authToken?: string): (req: Request, res: Response, next: NextFunction) => void;
11
+ export declare function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction): void;
12
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Express middleware — JSON parsing, CORS, auth, error handling.
3
+ */
4
+ export function corsMiddleware(req, res, next) {
5
+ res.header('Access-Control-Allow-Origin', 'http://localhost:*');
6
+ res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
7
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
8
+ if (req.method === 'OPTIONS') {
9
+ res.sendStatus(204);
10
+ return;
11
+ }
12
+ next();
13
+ }
14
+ /**
15
+ * Auth middleware — enforces Bearer token on API endpoints.
16
+ * Health endpoint is exempt (used for external monitoring).
17
+ */
18
+ export function authMiddleware(authToken) {
19
+ return (req, res, next) => {
20
+ // Skip auth if no token configured
21
+ if (!authToken) {
22
+ next();
23
+ return;
24
+ }
25
+ // Health endpoint is always public
26
+ if (req.path === '/health') {
27
+ next();
28
+ return;
29
+ }
30
+ const header = req.headers.authorization;
31
+ if (!header || !header.startsWith('Bearer ')) {
32
+ res.status(401).json({ error: 'Missing or invalid Authorization header' });
33
+ return;
34
+ }
35
+ const token = header.slice(7);
36
+ if (token !== authToken) {
37
+ res.status(403).json({ error: 'Invalid auth token' });
38
+ return;
39
+ }
40
+ next();
41
+ };
42
+ }
43
+ export function errorHandler(err, _req, res, _next) {
44
+ console.error(`[server] Error: ${err.message}`);
45
+ res.status(500).json({
46
+ error: err.message,
47
+ timestamp: new Date().toISOString(),
48
+ });
49
+ }
50
+ //# sourceMappingURL=middleware.js.map