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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for instar.
|
|
3
|
+
*
|
|
4
|
+
* These types define the contracts between all modules.
|
|
5
|
+
* Everything flows from these — sessions, jobs, users, messaging.
|
|
6
|
+
*/
|
|
7
|
+
export interface Session {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
status: SessionStatus;
|
|
11
|
+
/** The job that spawned this session, if any */
|
|
12
|
+
jobSlug?: string;
|
|
13
|
+
/** tmux session name */
|
|
14
|
+
tmuxSession: string;
|
|
15
|
+
/** When the session was created */
|
|
16
|
+
startedAt: string;
|
|
17
|
+
/** When the session ended (if completed) */
|
|
18
|
+
endedAt?: string;
|
|
19
|
+
/** User who triggered the session, if any */
|
|
20
|
+
triggeredBy?: string;
|
|
21
|
+
/** Model to use for this session */
|
|
22
|
+
model?: ModelTier;
|
|
23
|
+
/** The initial prompt/instruction sent to Claude */
|
|
24
|
+
prompt?: string;
|
|
25
|
+
}
|
|
26
|
+
export type SessionStatus = 'starting' | 'running' | 'completed' | 'failed' | 'killed';
|
|
27
|
+
export type ModelTier = 'opus' | 'sonnet' | 'haiku';
|
|
28
|
+
export interface SessionManagerConfig {
|
|
29
|
+
/** Path to tmux binary */
|
|
30
|
+
tmuxPath: string;
|
|
31
|
+
/** Path to claude CLI binary */
|
|
32
|
+
claudePath: string;
|
|
33
|
+
/** Project directory (where CLAUDE.md lives) */
|
|
34
|
+
projectDir: string;
|
|
35
|
+
/** Maximum concurrent sessions */
|
|
36
|
+
maxSessions: number;
|
|
37
|
+
/** Protected session names that should never be reaped */
|
|
38
|
+
protectedSessions: string[];
|
|
39
|
+
/** Patterns in tmux output that indicate session completion */
|
|
40
|
+
completionPatterns: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface JobDefinition {
|
|
43
|
+
slug: string;
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
/** Cron expression (e.g., "0 0/4 * * *" for every 4 hours) */
|
|
47
|
+
schedule: string;
|
|
48
|
+
/** Priority level — higher priority jobs run first and survive quota pressure */
|
|
49
|
+
priority: JobPriority;
|
|
50
|
+
/** Expected duration in minutes (for scheduling decisions) */
|
|
51
|
+
expectedDurationMinutes: number;
|
|
52
|
+
/** Model tier to use */
|
|
53
|
+
model: ModelTier;
|
|
54
|
+
/** Whether this job is currently enabled */
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
/** The skill or prompt to execute */
|
|
57
|
+
execute: JobExecution;
|
|
58
|
+
/** Tags for filtering/grouping */
|
|
59
|
+
tags?: string[];
|
|
60
|
+
/** Telegram topic ID this job reports to (auto-created if not set) */
|
|
61
|
+
topicId?: number;
|
|
62
|
+
}
|
|
63
|
+
export type JobPriority = 'critical' | 'high' | 'medium' | 'low';
|
|
64
|
+
export interface JobExecution {
|
|
65
|
+
/** Type of execution */
|
|
66
|
+
type: 'skill' | 'prompt' | 'script';
|
|
67
|
+
/** The skill name, prompt text, or script path */
|
|
68
|
+
value: string;
|
|
69
|
+
/** Additional arguments */
|
|
70
|
+
args?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface JobState {
|
|
73
|
+
slug: string;
|
|
74
|
+
lastRun?: string;
|
|
75
|
+
lastResult?: 'success' | 'failure' | 'timeout';
|
|
76
|
+
nextScheduled?: string;
|
|
77
|
+
consecutiveFailures: number;
|
|
78
|
+
}
|
|
79
|
+
export interface JobSchedulerConfig {
|
|
80
|
+
/** Path to jobs definition file */
|
|
81
|
+
jobsFile: string;
|
|
82
|
+
/** Whether the scheduler is active */
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
/** Maximum parallel job sessions */
|
|
85
|
+
maxParallelJobs: number;
|
|
86
|
+
/** Quota thresholds for load shedding */
|
|
87
|
+
quotaThresholds: {
|
|
88
|
+
/** Below this: all jobs run */
|
|
89
|
+
normal: number;
|
|
90
|
+
/** Above this: only high+ priority */
|
|
91
|
+
elevated: number;
|
|
92
|
+
/** Above this: only critical */
|
|
93
|
+
critical: number;
|
|
94
|
+
/** Above this: no jobs */
|
|
95
|
+
shutdown: number;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export interface UserProfile {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
/** Communication channels this user is reachable on */
|
|
102
|
+
channels: UserChannel[];
|
|
103
|
+
/** What this user is allowed to do */
|
|
104
|
+
permissions: string[];
|
|
105
|
+
/** How the agent should interact with this user */
|
|
106
|
+
preferences: UserPreferences;
|
|
107
|
+
/** Interaction history summary */
|
|
108
|
+
context?: string;
|
|
109
|
+
}
|
|
110
|
+
export interface UserChannel {
|
|
111
|
+
/** Channel type (telegram, slack, discord, email, etc.) */
|
|
112
|
+
type: string;
|
|
113
|
+
/** Channel-specific identifier (topic ID, Slack user ID, email address, etc.) */
|
|
114
|
+
identifier: string;
|
|
115
|
+
}
|
|
116
|
+
export interface UserPreferences {
|
|
117
|
+
/** Communication style (e.g., "technical and direct", "prefers explanations") */
|
|
118
|
+
style?: string;
|
|
119
|
+
/** Whether to auto-execute or confirm with this user */
|
|
120
|
+
autonomyLevel?: 'full' | 'confirm-destructive' | 'confirm-all';
|
|
121
|
+
/** Timezone for scheduling */
|
|
122
|
+
timezone?: string;
|
|
123
|
+
}
|
|
124
|
+
export interface Message {
|
|
125
|
+
/** Unique message ID */
|
|
126
|
+
id: string;
|
|
127
|
+
/** User who sent the message */
|
|
128
|
+
userId: string;
|
|
129
|
+
/** The message content */
|
|
130
|
+
content: string;
|
|
131
|
+
/** Channel the message came from */
|
|
132
|
+
channel: UserChannel;
|
|
133
|
+
/** When the message was received */
|
|
134
|
+
receivedAt: string;
|
|
135
|
+
/** Message metadata (platform-specific) */
|
|
136
|
+
metadata?: Record<string, unknown>;
|
|
137
|
+
}
|
|
138
|
+
export interface OutgoingMessage {
|
|
139
|
+
/** User to send to */
|
|
140
|
+
userId: string;
|
|
141
|
+
/** Message content */
|
|
142
|
+
content: string;
|
|
143
|
+
/** Specific channel to use (optional — uses default if omitted) */
|
|
144
|
+
channel?: UserChannel;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Messaging adapter interface.
|
|
148
|
+
* Implement this for each platform (Telegram, Slack, Discord, etc.)
|
|
149
|
+
*/
|
|
150
|
+
export interface MessagingAdapter {
|
|
151
|
+
/** Platform name (e.g., "telegram", "slack") */
|
|
152
|
+
platform: string;
|
|
153
|
+
/** Start listening for messages */
|
|
154
|
+
start(): Promise<void>;
|
|
155
|
+
/** Stop listening */
|
|
156
|
+
stop(): Promise<void>;
|
|
157
|
+
/** Send a message to a user */
|
|
158
|
+
send(message: OutgoingMessage): Promise<void>;
|
|
159
|
+
/** Register a handler for incoming messages */
|
|
160
|
+
onMessage(handler: (message: Message) => Promise<void>): void;
|
|
161
|
+
/** Resolve a platform-specific identifier to a user ID */
|
|
162
|
+
resolveUser(channelIdentifier: string): Promise<string | null>;
|
|
163
|
+
}
|
|
164
|
+
export interface QuotaState {
|
|
165
|
+
/** Current usage percentage (0-100) */
|
|
166
|
+
usagePercent: number;
|
|
167
|
+
/** When usage data was last updated */
|
|
168
|
+
lastUpdated: string;
|
|
169
|
+
/** Per-account breakdown if multi-account */
|
|
170
|
+
accounts?: AccountQuota[];
|
|
171
|
+
/** Recommended action based on usage */
|
|
172
|
+
recommendation?: 'normal' | 'reduce' | 'critical' | 'stop';
|
|
173
|
+
}
|
|
174
|
+
export interface AccountQuota {
|
|
175
|
+
email: string;
|
|
176
|
+
usagePercent: number;
|
|
177
|
+
isActive: boolean;
|
|
178
|
+
lastUpdated: string;
|
|
179
|
+
}
|
|
180
|
+
export interface HealthStatus {
|
|
181
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
182
|
+
components: Record<string, ComponentHealth>;
|
|
183
|
+
timestamp: string;
|
|
184
|
+
}
|
|
185
|
+
export interface ComponentHealth {
|
|
186
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
187
|
+
message?: string;
|
|
188
|
+
lastCheck: string;
|
|
189
|
+
}
|
|
190
|
+
export interface RelationshipRecord {
|
|
191
|
+
/** Unique identifier for this person */
|
|
192
|
+
id: string;
|
|
193
|
+
/** Display name */
|
|
194
|
+
name: string;
|
|
195
|
+
/** Known identifiers across platforms */
|
|
196
|
+
channels: UserChannel[];
|
|
197
|
+
/** When the agent first interacted with this person */
|
|
198
|
+
firstInteraction: string;
|
|
199
|
+
/** When the agent last interacted with this person */
|
|
200
|
+
lastInteraction: string;
|
|
201
|
+
/** Total number of interactions */
|
|
202
|
+
interactionCount: number;
|
|
203
|
+
/** Key topics discussed across conversations */
|
|
204
|
+
themes: string[];
|
|
205
|
+
/** Agent's notes about this person — observations, preferences, context */
|
|
206
|
+
notes: string;
|
|
207
|
+
/** Communication style preferences the agent has observed */
|
|
208
|
+
communicationStyle?: string;
|
|
209
|
+
/** How significant this relationship is (0-10, auto-derived from frequency and depth) */
|
|
210
|
+
significance: number;
|
|
211
|
+
/** Brief summary of the relationship arc */
|
|
212
|
+
arcSummary?: string;
|
|
213
|
+
/** Per-interaction log (last N interactions, kept compact) */
|
|
214
|
+
recentInteractions: InteractionSummary[];
|
|
215
|
+
}
|
|
216
|
+
export interface InteractionSummary {
|
|
217
|
+
/** When this interaction happened */
|
|
218
|
+
timestamp: string;
|
|
219
|
+
/** Which platform/channel */
|
|
220
|
+
channel: string;
|
|
221
|
+
/** Brief summary of what was discussed */
|
|
222
|
+
summary: string;
|
|
223
|
+
/** Topics touched on */
|
|
224
|
+
topics?: string[];
|
|
225
|
+
}
|
|
226
|
+
export interface RelationshipManagerConfig {
|
|
227
|
+
/** Directory to store relationship files */
|
|
228
|
+
relationshipsDir: string;
|
|
229
|
+
/** Maximum recent interactions to keep per relationship */
|
|
230
|
+
maxRecentInteractions: number;
|
|
231
|
+
}
|
|
232
|
+
export interface ActivityEvent {
|
|
233
|
+
type: string;
|
|
234
|
+
summary: string;
|
|
235
|
+
/** Which session generated this event */
|
|
236
|
+
sessionId?: string;
|
|
237
|
+
/** Which user triggered this, if any */
|
|
238
|
+
userId?: string;
|
|
239
|
+
timestamp: string;
|
|
240
|
+
metadata?: Record<string, unknown>;
|
|
241
|
+
}
|
|
242
|
+
export interface AgentKitConfig {
|
|
243
|
+
/** Project name (used in logging, tmux session names, etc.) */
|
|
244
|
+
projectName: string;
|
|
245
|
+
/** Project root directory */
|
|
246
|
+
projectDir: string;
|
|
247
|
+
/** Where instar stores its runtime state */
|
|
248
|
+
stateDir: string;
|
|
249
|
+
/** HTTP server port */
|
|
250
|
+
port: number;
|
|
251
|
+
/** Session manager config */
|
|
252
|
+
sessions: SessionManagerConfig;
|
|
253
|
+
/** Job scheduler config */
|
|
254
|
+
scheduler: JobSchedulerConfig;
|
|
255
|
+
/** Registered users */
|
|
256
|
+
users: UserProfile[];
|
|
257
|
+
/** Messaging adapters to enable */
|
|
258
|
+
messaging: MessagingAdapterConfig[];
|
|
259
|
+
/** Monitoring config */
|
|
260
|
+
monitoring: MonitoringConfig;
|
|
261
|
+
/** Auth token for API access (generated during setup) */
|
|
262
|
+
authToken?: string;
|
|
263
|
+
/** Relationship tracking config */
|
|
264
|
+
relationships: RelationshipManagerConfig;
|
|
265
|
+
}
|
|
266
|
+
export interface MessagingAdapterConfig {
|
|
267
|
+
type: string;
|
|
268
|
+
enabled: boolean;
|
|
269
|
+
config: Record<string, unknown>;
|
|
270
|
+
}
|
|
271
|
+
export interface MonitoringConfig {
|
|
272
|
+
/** Enable quota tracking */
|
|
273
|
+
quotaTracking: boolean;
|
|
274
|
+
/** Enable memory pressure monitoring */
|
|
275
|
+
memoryMonitoring: boolean;
|
|
276
|
+
/** Health check interval in ms */
|
|
277
|
+
healthCheckIntervalMs: number;
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* instar — Persistent autonomy infrastructure for AI agents.
|
|
3
|
+
*
|
|
4
|
+
* Public API for programmatic usage.
|
|
5
|
+
*/
|
|
6
|
+
export { SessionManager } from './core/SessionManager.js';
|
|
7
|
+
export { StateManager } from './core/StateManager.js';
|
|
8
|
+
export { RelationshipManager } from './core/RelationshipManager.js';
|
|
9
|
+
export { loadConfig, detectTmuxPath, detectClaudePath, ensureStateDir } from './core/Config.js';
|
|
10
|
+
export { UserManager } from './users/UserManager.js';
|
|
11
|
+
export { JobScheduler } from './scheduler/JobScheduler.js';
|
|
12
|
+
export { loadJobs, validateJob } from './scheduler/JobLoader.js';
|
|
13
|
+
export { AgentServer } from './server/AgentServer.js';
|
|
14
|
+
export { createRoutes } from './server/routes.js';
|
|
15
|
+
export { HealthChecker } from './monitoring/HealthChecker.js';
|
|
16
|
+
export { TelegramAdapter } from './messaging/TelegramAdapter.js';
|
|
17
|
+
export type { Session, SessionStatus, SessionManagerConfig, ModelTier, JobDefinition, JobPriority, JobExecution, JobState, JobSchedulerConfig, UserProfile, UserChannel, UserPreferences, Message, OutgoingMessage, MessagingAdapter, MessagingAdapterConfig, QuotaState, AccountQuota, HealthStatus, ComponentHealth, ActivityEvent, AgentKitConfig, MonitoringConfig, RelationshipRecord, RelationshipManagerConfig, InteractionSummary, } from './core/types.js';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* instar — Persistent autonomy infrastructure for AI agents.
|
|
3
|
+
*
|
|
4
|
+
* Public API for programmatic usage.
|
|
5
|
+
*/
|
|
6
|
+
// Core
|
|
7
|
+
export { SessionManager } from './core/SessionManager.js';
|
|
8
|
+
export { StateManager } from './core/StateManager.js';
|
|
9
|
+
export { RelationshipManager } from './core/RelationshipManager.js';
|
|
10
|
+
export { loadConfig, detectTmuxPath, detectClaudePath, ensureStateDir } from './core/Config.js';
|
|
11
|
+
// Users
|
|
12
|
+
export { UserManager } from './users/UserManager.js';
|
|
13
|
+
// Scheduler
|
|
14
|
+
export { JobScheduler } from './scheduler/JobScheduler.js';
|
|
15
|
+
export { loadJobs, validateJob } from './scheduler/JobLoader.js';
|
|
16
|
+
// Server
|
|
17
|
+
export { AgentServer } from './server/AgentServer.js';
|
|
18
|
+
export { createRoutes } from './server/routes.js';
|
|
19
|
+
// Monitoring
|
|
20
|
+
export { HealthChecker } from './monitoring/HealthChecker.js';
|
|
21
|
+
// Messaging
|
|
22
|
+
export { TelegramAdapter } from './messaging/TelegramAdapter.js';
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Messaging Adapter — send/receive messages via Telegram Bot API.
|
|
3
|
+
*
|
|
4
|
+
* Uses long polling to receive messages. Supports forum topics
|
|
5
|
+
* (each user gets a topic thread). Includes topic-session registry
|
|
6
|
+
* and message logging for session respawn with thread history.
|
|
7
|
+
*
|
|
8
|
+
* No external dependencies — uses native fetch for Telegram API calls.
|
|
9
|
+
*/
|
|
10
|
+
import type { MessagingAdapter, Message, OutgoingMessage } from '../core/types.js';
|
|
11
|
+
interface TelegramConfig {
|
|
12
|
+
/** Bot token from @BotFather */
|
|
13
|
+
token: string;
|
|
14
|
+
/** Forum chat ID (the supergroup where topics live) */
|
|
15
|
+
chatId: string;
|
|
16
|
+
/** Polling interval in ms */
|
|
17
|
+
pollIntervalMs?: number;
|
|
18
|
+
}
|
|
19
|
+
interface LogEntry {
|
|
20
|
+
messageId: number;
|
|
21
|
+
topicId: number | null;
|
|
22
|
+
text: string;
|
|
23
|
+
fromUser: boolean;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
sessionName: string | null;
|
|
26
|
+
}
|
|
27
|
+
export declare class TelegramAdapter implements MessagingAdapter {
|
|
28
|
+
readonly platform = "telegram";
|
|
29
|
+
private config;
|
|
30
|
+
private handler;
|
|
31
|
+
private polling;
|
|
32
|
+
private pollTimeout;
|
|
33
|
+
private lastUpdateId;
|
|
34
|
+
private topicToSession;
|
|
35
|
+
private sessionToTopic;
|
|
36
|
+
private topicToName;
|
|
37
|
+
private registryPath;
|
|
38
|
+
private messageLogPath;
|
|
39
|
+
onTopicMessage: ((message: Message) => void) | null;
|
|
40
|
+
constructor(config: TelegramConfig, stateDir: string);
|
|
41
|
+
start(): Promise<void>;
|
|
42
|
+
stop(): Promise<void>;
|
|
43
|
+
send(message: OutgoingMessage): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Send a message to a specific forum topic.
|
|
46
|
+
*/
|
|
47
|
+
sendToTopic(topicId: number, text: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Create a forum topic in the supergroup.
|
|
50
|
+
*/
|
|
51
|
+
createForumTopic(name: string, iconColor?: number): Promise<{
|
|
52
|
+
topicId: number;
|
|
53
|
+
name: string;
|
|
54
|
+
}>;
|
|
55
|
+
onMessage(handler: (message: Message) => Promise<void>): void;
|
|
56
|
+
resolveUser(channelIdentifier: string): Promise<string | null>;
|
|
57
|
+
registerTopicSession(topicId: number, sessionName: string): void;
|
|
58
|
+
getSessionForTopic(topicId: number): string | null;
|
|
59
|
+
getTopicForSession(sessionName: string): number | null;
|
|
60
|
+
getTopicName(topicId: number): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Get recent messages for a topic (for thread history on respawn).
|
|
63
|
+
*/
|
|
64
|
+
getTopicHistory(topicId: number, limit?: number): LogEntry[];
|
|
65
|
+
private appendToLog;
|
|
66
|
+
private loadRegistry;
|
|
67
|
+
private saveRegistry;
|
|
68
|
+
private poll;
|
|
69
|
+
private getUpdates;
|
|
70
|
+
private apiCall;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=TelegramAdapter.d.ts.map
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Messaging Adapter — send/receive messages via Telegram Bot API.
|
|
3
|
+
*
|
|
4
|
+
* Uses long polling to receive messages. Supports forum topics
|
|
5
|
+
* (each user gets a topic thread). Includes topic-session registry
|
|
6
|
+
* and message logging for session respawn with thread history.
|
|
7
|
+
*
|
|
8
|
+
* No external dependencies — uses native fetch for Telegram API calls.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
export class TelegramAdapter {
|
|
13
|
+
platform = 'telegram';
|
|
14
|
+
config;
|
|
15
|
+
handler = null;
|
|
16
|
+
polling = false;
|
|
17
|
+
pollTimeout = null;
|
|
18
|
+
lastUpdateId = 0;
|
|
19
|
+
// Topic-session registry (persisted to disk)
|
|
20
|
+
topicToSession = new Map();
|
|
21
|
+
sessionToTopic = new Map();
|
|
22
|
+
topicToName = new Map();
|
|
23
|
+
registryPath;
|
|
24
|
+
messageLogPath;
|
|
25
|
+
// Topic message callback — fires on every incoming topic message
|
|
26
|
+
onTopicMessage = null;
|
|
27
|
+
constructor(config, stateDir) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.registryPath = path.join(stateDir, 'topic-session-registry.json');
|
|
30
|
+
this.messageLogPath = path.join(stateDir, 'telegram-messages.jsonl');
|
|
31
|
+
this.loadRegistry();
|
|
32
|
+
}
|
|
33
|
+
async start() {
|
|
34
|
+
if (this.polling)
|
|
35
|
+
return;
|
|
36
|
+
this.polling = true;
|
|
37
|
+
console.log(`[telegram] Starting long-polling...`);
|
|
38
|
+
this.poll();
|
|
39
|
+
}
|
|
40
|
+
async stop() {
|
|
41
|
+
this.polling = false;
|
|
42
|
+
if (this.pollTimeout) {
|
|
43
|
+
clearTimeout(this.pollTimeout);
|
|
44
|
+
this.pollTimeout = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async send(message) {
|
|
48
|
+
const topicId = message.channel?.identifier;
|
|
49
|
+
const params = {
|
|
50
|
+
chat_id: this.config.chatId,
|
|
51
|
+
text: message.content,
|
|
52
|
+
parse_mode: 'Markdown',
|
|
53
|
+
};
|
|
54
|
+
if (topicId && parseInt(topicId) > 1) {
|
|
55
|
+
params.message_thread_id = parseInt(topicId);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await this.apiCall('sendMessage', params);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Fallback to plain text on parse errors
|
|
62
|
+
delete params.parse_mode;
|
|
63
|
+
await this.apiCall('sendMessage', params);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Send a message to a specific forum topic.
|
|
68
|
+
*/
|
|
69
|
+
async sendToTopic(topicId, text) {
|
|
70
|
+
const params = {
|
|
71
|
+
chat_id: this.config.chatId,
|
|
72
|
+
text,
|
|
73
|
+
};
|
|
74
|
+
// Topic ID 1 = General topic (our fallback) — omit message_thread_id for General
|
|
75
|
+
if (topicId > 1) {
|
|
76
|
+
params.message_thread_id = topicId;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await this.apiCall('sendMessage', { ...params, parse_mode: 'Markdown' });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
await this.apiCall('sendMessage', params);
|
|
83
|
+
}
|
|
84
|
+
// Log outbound messages too
|
|
85
|
+
this.appendToLog({
|
|
86
|
+
messageId: 0,
|
|
87
|
+
topicId,
|
|
88
|
+
text,
|
|
89
|
+
fromUser: false,
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
sessionName: this.topicToSession.get(topicId) ?? null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a forum topic in the supergroup.
|
|
96
|
+
*/
|
|
97
|
+
async createForumTopic(name, iconColor) {
|
|
98
|
+
const params = {
|
|
99
|
+
chat_id: this.config.chatId,
|
|
100
|
+
name,
|
|
101
|
+
};
|
|
102
|
+
if (iconColor !== undefined) {
|
|
103
|
+
params.icon_color = iconColor;
|
|
104
|
+
}
|
|
105
|
+
const result = await this.apiCall('createForumTopic', params);
|
|
106
|
+
this.topicToName.set(result.message_thread_id, name);
|
|
107
|
+
this.saveRegistry();
|
|
108
|
+
console.log(`[telegram] Created forum topic: "${name}" (ID: ${result.message_thread_id})`);
|
|
109
|
+
return { topicId: result.message_thread_id, name: result.name };
|
|
110
|
+
}
|
|
111
|
+
onMessage(handler) {
|
|
112
|
+
this.handler = handler;
|
|
113
|
+
}
|
|
114
|
+
async resolveUser(channelIdentifier) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
// ── Topic-Session Registry ─────────────────────────────────
|
|
118
|
+
registerTopicSession(topicId, sessionName) {
|
|
119
|
+
this.topicToSession.set(topicId, sessionName);
|
|
120
|
+
this.sessionToTopic.set(sessionName, topicId);
|
|
121
|
+
this.saveRegistry();
|
|
122
|
+
console.log(`[telegram] Registered topic ${topicId} <-> session "${sessionName}"`);
|
|
123
|
+
}
|
|
124
|
+
getSessionForTopic(topicId) {
|
|
125
|
+
return this.topicToSession.get(topicId) ?? null;
|
|
126
|
+
}
|
|
127
|
+
getTopicForSession(sessionName) {
|
|
128
|
+
return this.sessionToTopic.get(sessionName) ?? null;
|
|
129
|
+
}
|
|
130
|
+
getTopicName(topicId) {
|
|
131
|
+
return this.topicToName.get(topicId) ?? null;
|
|
132
|
+
}
|
|
133
|
+
// ── Message Log ────────────────────────────────────────────
|
|
134
|
+
/**
|
|
135
|
+
* Get recent messages for a topic (for thread history on respawn).
|
|
136
|
+
*/
|
|
137
|
+
getTopicHistory(topicId, limit = 20) {
|
|
138
|
+
if (!fs.existsSync(this.messageLogPath))
|
|
139
|
+
return [];
|
|
140
|
+
const lines = fs.readFileSync(this.messageLogPath, 'utf-8')
|
|
141
|
+
.split('\n')
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
const matching = [];
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
try {
|
|
146
|
+
const entry = JSON.parse(line);
|
|
147
|
+
if (entry.topicId === topicId) {
|
|
148
|
+
matching.push(entry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch { /* skip malformed */ }
|
|
152
|
+
}
|
|
153
|
+
return matching.slice(-limit);
|
|
154
|
+
}
|
|
155
|
+
appendToLog(entry) {
|
|
156
|
+
try {
|
|
157
|
+
fs.appendFileSync(this.messageLogPath, JSON.stringify(entry) + '\n');
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error(`[telegram] Failed to append to message log: ${err}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── Registry Persistence ───────────────────────────────────
|
|
164
|
+
loadRegistry() {
|
|
165
|
+
try {
|
|
166
|
+
const data = JSON.parse(fs.readFileSync(this.registryPath, 'utf-8'));
|
|
167
|
+
if (data.topicToSession) {
|
|
168
|
+
for (const [k, v] of Object.entries(data.topicToSession)) {
|
|
169
|
+
this.topicToSession.set(Number(k), v);
|
|
170
|
+
this.sessionToTopic.set(v, Number(k));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (data.topicToName) {
|
|
174
|
+
for (const [k, v] of Object.entries(data.topicToName)) {
|
|
175
|
+
this.topicToName.set(Number(k), v);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
console.log(`[telegram] Loaded ${this.topicToSession.size} topic-session mappings from disk`);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// File doesn't exist yet — start fresh
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
saveRegistry() {
|
|
185
|
+
try {
|
|
186
|
+
const data = {
|
|
187
|
+
topicToSession: Object.fromEntries(this.topicToSession),
|
|
188
|
+
topicToName: Object.fromEntries(this.topicToName),
|
|
189
|
+
};
|
|
190
|
+
fs.writeFileSync(this.registryPath, JSON.stringify(data, null, 2));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error(`[telegram] Failed to save registry: ${err}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── Polling ────────────────────────────────────────────────
|
|
197
|
+
async poll() {
|
|
198
|
+
if (!this.polling)
|
|
199
|
+
return;
|
|
200
|
+
try {
|
|
201
|
+
const updates = await this.getUpdates();
|
|
202
|
+
for (const update of updates) {
|
|
203
|
+
if (update.message?.text) {
|
|
204
|
+
const msg = update.message;
|
|
205
|
+
const text = msg.text;
|
|
206
|
+
// Use message_thread_id if present; fall back to 1 (General topic) for forum groups
|
|
207
|
+
const numericTopicId = msg.message_thread_id ?? 1;
|
|
208
|
+
const topicId = numericTopicId.toString();
|
|
209
|
+
// Auto-capture topic name from reply_to_message
|
|
210
|
+
if (msg.reply_to_message?.forum_topic_created?.name) {
|
|
211
|
+
if (!this.topicToName.has(numericTopicId)) {
|
|
212
|
+
this.topicToName.set(numericTopicId, msg.reply_to_message.forum_topic_created.name);
|
|
213
|
+
this.saveRegistry();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const message = {
|
|
217
|
+
id: `tg-${msg.message_id}`,
|
|
218
|
+
userId: msg.from.id.toString(),
|
|
219
|
+
content: text,
|
|
220
|
+
channel: { type: 'telegram', identifier: topicId },
|
|
221
|
+
receivedAt: new Date(msg.date * 1000).toISOString(),
|
|
222
|
+
metadata: {
|
|
223
|
+
telegramUserId: msg.from.id,
|
|
224
|
+
username: msg.from.username,
|
|
225
|
+
firstName: msg.from.first_name,
|
|
226
|
+
messageThreadId: numericTopicId,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
// Log the message
|
|
230
|
+
this.appendToLog({
|
|
231
|
+
messageId: msg.message_id,
|
|
232
|
+
topicId: numericTopicId,
|
|
233
|
+
text,
|
|
234
|
+
fromUser: true,
|
|
235
|
+
timestamp: new Date(msg.date * 1000).toISOString(),
|
|
236
|
+
sessionName: this.topicToSession.get(numericTopicId) ?? null,
|
|
237
|
+
});
|
|
238
|
+
// Fire topic message callback (always fires — General topic falls back to ID 1)
|
|
239
|
+
if (this.onTopicMessage) {
|
|
240
|
+
this.onTopicMessage(message);
|
|
241
|
+
}
|
|
242
|
+
// Fire general handler
|
|
243
|
+
if (this.handler) {
|
|
244
|
+
try {
|
|
245
|
+
await this.handler(message);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
console.error(`[telegram] Handler error: ${err}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.lastUpdateId = Math.max(this.lastUpdateId, update.update_id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
console.error(`[telegram] Poll error: ${err}`);
|
|
257
|
+
}
|
|
258
|
+
// Schedule next poll
|
|
259
|
+
const interval = this.config.pollIntervalMs ?? 2000;
|
|
260
|
+
this.pollTimeout = setTimeout(() => this.poll(), interval);
|
|
261
|
+
}
|
|
262
|
+
async getUpdates() {
|
|
263
|
+
const result = await this.apiCall('getUpdates', {
|
|
264
|
+
offset: this.lastUpdateId + 1,
|
|
265
|
+
timeout: 30,
|
|
266
|
+
allowed_updates: ['message'],
|
|
267
|
+
});
|
|
268
|
+
return result ?? [];
|
|
269
|
+
}
|
|
270
|
+
async apiCall(method, params) {
|
|
271
|
+
const url = `https://api.telegram.org/bot${this.config.token}/${method}`;
|
|
272
|
+
const response = await fetch(url, {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify(params),
|
|
276
|
+
});
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
const text = await response.text();
|
|
279
|
+
throw new Error(`Telegram API error (${response.status}): ${text}`);
|
|
280
|
+
}
|
|
281
|
+
const data = await response.json();
|
|
282
|
+
if (!data.ok) {
|
|
283
|
+
throw new Error(`Telegram API returned not ok: ${JSON.stringify(data)}`);
|
|
284
|
+
}
|
|
285
|
+
return data.result;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=TelegramAdapter.js.map
|