@threadbase-sh/streamer 1.15.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.
@@ -0,0 +1,834 @@
1
+ import { Client } from '@temporalio/client';
2
+ import { UserInputSignal, Stage } from '@threadbase-sh/agent-types';
3
+ import * as hono_types from 'hono/types';
4
+ import { Hono } from 'hono';
5
+ import { Logger as Logger$1 } from 'pino';
6
+ import { HttpBindings } from '@hono/node-server';
7
+ import { ServerResponse, IncomingMessage } from 'http';
8
+ import { WebSocket } from 'ws';
9
+ import Database from 'better-sqlite3';
10
+ import { z } from 'zod';
11
+ import { Pool } from 'pg';
12
+
13
+ interface AgentClient {
14
+ startSession(sessionId: string): Promise<string>;
15
+ sendUserInput(sessionId: string, payload: UserInputSignal): Promise<void>;
16
+ endSession(sessionId: string): Promise<void>;
17
+ getSessionStage(sessionId: string): Promise<string>;
18
+ }
19
+ interface AgentClientOpts {
20
+ temporalClient: Client;
21
+ taskQueue: string;
22
+ }
23
+ declare function createAgentClient({ temporalClient, taskQueue }: AgentClientOpts): AgentClient;
24
+
25
+ interface AgentConfig {
26
+ enabled: boolean;
27
+ temporal: {
28
+ address: string;
29
+ namespace: string;
30
+ taskQueue: string;
31
+ };
32
+ webhook: {
33
+ hmacSecret: string;
34
+ timestampSkewSeconds: number;
35
+ };
36
+ dedupe: {
37
+ perSessionCapacity: number;
38
+ };
39
+ payload: {
40
+ limitBytes: number;
41
+ trajectoryLogBytes: number;
42
+ trajectoryLogTurns: number;
43
+ };
44
+ sessionBusyRetryMs: number;
45
+ conversationsDir: string;
46
+ }
47
+ declare function readAgentConfig(env?: NodeJS.ProcessEnv): AgentConfig;
48
+
49
+ interface AppendArgs {
50
+ sessionId: string;
51
+ turnId: string;
52
+ content: string;
53
+ reviewerOverruled?: boolean;
54
+ }
55
+ interface ConversationWriter {
56
+ appendAssistantTurn(args: AppendArgs): Promise<void>;
57
+ }
58
+ declare function createConversationWriter(opts: {
59
+ baseDir: string;
60
+ }): ConversationWriter;
61
+
62
+ interface ProgressDedupeLRU {
63
+ hasSeen(eventId: string): boolean;
64
+ readonly size: number;
65
+ }
66
+ declare function createProgressDedupeLRU(capacity: number): ProgressDedupeLRU;
67
+
68
+ type LogDest = "console" | "pino" | "both";
69
+ type LogLevel = "debug" | "info" | "warn" | "error";
70
+ interface Logger {
71
+ debug(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
72
+ info(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
73
+ warn(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
74
+ error(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
75
+ log(level: LogLevel, msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;
76
+ pino: Logger$1;
77
+ }
78
+
79
+ type SessionStatus = "running" | "waiting_input" | "idle";
80
+ interface ManagedSession {
81
+ id: string;
82
+ projectId?: string;
83
+ projectPath: string;
84
+ projectName: string;
85
+ branch: string;
86
+ status: SessionStatus;
87
+ startedAt: Date;
88
+ completedAt: Date | null;
89
+ promptCount: number;
90
+ lastOutput: string;
91
+ failureReason?: string;
92
+ sessionName?: string;
93
+ model?: string;
94
+ account?: string;
95
+ messageCount?: number;
96
+ preview?: string;
97
+ firstMessageText?: string;
98
+ firstMessageAt?: Date;
99
+ lastMessageText?: string;
100
+ lastMessageAt?: Date;
101
+ lastActivityAt?: Date;
102
+ filePath?: string;
103
+ resumedFromConversationId?: string;
104
+ /**
105
+ * Multi-agent mode only. Per-session in-memory LRU of progress event ids
106
+ * seen by the webhook receiver. Used to drop Temporal-replay duplicates
107
+ * before they reach the WebSocket. See spec §7.1.
108
+ */
109
+ progressDedupeIds?: ProgressDedupeLRU;
110
+ /** Multi-agent: current stage of the active turn (advisory; advisory wire field). */
111
+ stage?: Stage | string;
112
+ /** Multi-agent: ms since the session last emitted a stage transition. */
113
+ stalledSinceMs?: number;
114
+ /** Multi-agent: 1 or 2 when stage === "rework". */
115
+ reworkAttempt?: number;
116
+ /**
117
+ * Multi-agent: id of the in-flight turn, or null when idle. Set when
118
+ * `POST /api/sessions/:id/input` accepts a request; cleared by the webhook
119
+ * receiver on stage=done or terminal_failure. Undefined in PTY mode.
120
+ */
121
+ currentTurnId?: string | null;
122
+ /**
123
+ * Multi-agent resume: stable identity of the underlying conversation
124
+ * (the JSONL filename), distinct from `id` which is per-orchestrator-instance.
125
+ * Undefined in PTY mode (PTY uses `resumedFromConversationId` instead).
126
+ */
127
+ conversationId?: string;
128
+ }
129
+ interface DiscoveredProcess {
130
+ pid: number;
131
+ projectPath: string;
132
+ projectName: string;
133
+ branch: string;
134
+ conversationId: string | null;
135
+ startedAt: Date;
136
+ }
137
+ type WSMessage = {
138
+ type: "terminal_output";
139
+ sessionId: string;
140
+ data: string;
141
+ } | {
142
+ type: "session_update";
143
+ session?: SessionResponse;
144
+ sessionId?: string;
145
+ turnId?: string;
146
+ stage?: Stage | string;
147
+ stalledSinceMs?: number;
148
+ reworkAttempt?: number;
149
+ } | {
150
+ type: "session_list";
151
+ sessions: SessionResponse[];
152
+ } | {
153
+ type: "conversation_event";
154
+ sessionId: string;
155
+ line: string;
156
+ } | {
157
+ type: "conversation_events";
158
+ sessionId: string;
159
+ lines: string[];
160
+ } | {
161
+ type: "ping";
162
+ ts: number;
163
+ } | {
164
+ type: "terminal_replay";
165
+ sessionId: string;
166
+ lines: string[];
167
+ } | {
168
+ type: "session_ready";
169
+ session: SessionResponse;
170
+ } | {
171
+ type: "agent_output";
172
+ sessionId: string;
173
+ turnId: string;
174
+ role: "worker" | "reviewer" | "signoff";
175
+ content: string;
176
+ partial?: boolean;
177
+ reviewerOverruled?: boolean;
178
+ stage?: Stage | string;
179
+ reworkAttempt?: number;
180
+ } | {
181
+ type: "turn_failure";
182
+ sessionId: string;
183
+ turnId: string;
184
+ reason: string;
185
+ } | {
186
+ type: "cache_ready";
187
+ } | {
188
+ type: "scan_progress";
189
+ scanned: number;
190
+ total: number;
191
+ };
192
+ interface SessionResponse {
193
+ id: string;
194
+ conversationId: string;
195
+ projectId?: string;
196
+ status: SessionStatus;
197
+ projectPath: string;
198
+ projectName: string;
199
+ branch: string;
200
+ lastOutput: string;
201
+ elapsedMs: number;
202
+ promptCount: number;
203
+ startedAt: string;
204
+ completedAt: string | null;
205
+ ptyAttached: boolean;
206
+ failureReason?: string;
207
+ pid?: number;
208
+ sessionName?: string;
209
+ model?: string;
210
+ account?: string;
211
+ messageCount?: number;
212
+ preview?: string;
213
+ firstMessageText?: string;
214
+ firstMessageAt?: string;
215
+ lastMessageText?: string;
216
+ lastMessageAt?: string;
217
+ lastActivityAt?: string;
218
+ filePath?: string;
219
+ resumedFromConversationId?: string;
220
+ }
221
+ interface ConversationListResponse {
222
+ conversations: unknown[];
223
+ hasMore: boolean;
224
+ offset: number;
225
+ total: number;
226
+ }
227
+ type SessionSortKey = "startedAt" | "lastActivityAt" | "projectName" | "status";
228
+ type SortOrder = "asc" | "desc";
229
+ interface SessionListPage {
230
+ sessions: SessionResponse[];
231
+ nextCursor: string | null;
232
+ total: number;
233
+ }
234
+ interface SessionListQuery {
235
+ limit: number;
236
+ cursor?: string;
237
+ sortBy: SessionSortKey;
238
+ order: SortOrder;
239
+ status?: SessionStatus[];
240
+ }
241
+ interface SessionCursor {
242
+ k: string | number;
243
+ id: string;
244
+ }
245
+ interface ServerConfig {
246
+ port: number;
247
+ apiKey?: string;
248
+ localNoAuth?: boolean;
249
+ verbose?: boolean;
250
+ browseRoot?: string;
251
+ publicUrl?: string;
252
+ disableDb?: boolean;
253
+ scanProfiles?: Array<{
254
+ id: string;
255
+ label: string;
256
+ configDir: string;
257
+ enabled: boolean;
258
+ emoji: string;
259
+ }>;
260
+ ptyGracePeriodMs?: number;
261
+ cacheDir?: string;
262
+ tailSize?: number;
263
+ directoryScanDebounceMs?: number;
264
+ }
265
+ interface PTYManagerOptions {
266
+ onOutput?: (sessionId: string, data: string) => void;
267
+ onStatusChange?: (session: ManagedSession) => void;
268
+ onReady?: (session: ManagedSession) => void;
269
+ logger?: Logger;
270
+ }
271
+ interface StartSessionOptions {
272
+ projectPath: string;
273
+ projectName?: string;
274
+ branch?: string;
275
+ }
276
+ interface StartFreshSessionOptions {
277
+ projectPath: string;
278
+ projectName?: string;
279
+ systemPrompt?: string;
280
+ }
281
+
282
+ interface ConversationCacheOptions {
283
+ filterAgentConversations?: boolean;
284
+ agentEntrypoints?: ReadonlySet<string>;
285
+ onAgentFileDetected?: (filePath: string) => void;
286
+ }
287
+ interface ConversationListItem {
288
+ id: string;
289
+ filePath: string;
290
+ projectId: string | null;
291
+ projectPath: string | null;
292
+ projectName: string | null;
293
+ title: string | null;
294
+ model: string | null;
295
+ account: string | null;
296
+ branch: string | null;
297
+ messageCount: number;
298
+ lastActivity: string;
299
+ firstMessage: string | null;
300
+ lastMessage: string | null;
301
+ preview: string | null;
302
+ source: string | null;
303
+ }
304
+ interface CachedTailMessage {
305
+ role: string;
306
+ timestamp: string;
307
+ text: string;
308
+ content?: unknown[];
309
+ }
310
+ interface CachedTail {
311
+ conversationId: string;
312
+ messages: CachedTailMessage[];
313
+ tailSize: number;
314
+ }
315
+ interface ScannerMeta {
316
+ id: string;
317
+ sessionId?: string;
318
+ filePath: string;
319
+ projectPath?: string;
320
+ projectName?: string;
321
+ title?: string;
322
+ model?: string;
323
+ account?: string;
324
+ gitBranch?: string;
325
+ messageCount?: number;
326
+ timestamp?: string;
327
+ firstMessage?: unknown;
328
+ lastMessage?: unknown;
329
+ preview?: string;
330
+ }
331
+ declare class ConversationCache {
332
+ private db;
333
+ private tailSize;
334
+ private fileIndex;
335
+ private fileIndexLoaded;
336
+ private tailSeq;
337
+ private nameSeq;
338
+ private stmts;
339
+ private migrationsDir?;
340
+ private filterAgentConversations;
341
+ private agentEntrypoints;
342
+ private onAgentFileDetected?;
343
+ private constructor();
344
+ /**
345
+ * Expose the underlying handle so projects/cache_metadata repositories can
346
+ * share the same connection. Internal API; not part of the public surface.
347
+ */
348
+ getDatabase(): Database.Database;
349
+ getAgentEntrypoints(): ReadonlySet<string>;
350
+ static open(dbPath: string, tailSize?: number, migrationsDir?: string, options?: ConversationCacheOptions): ConversationCache;
351
+ close(): void;
352
+ getPopularProjects(limit: number): Array<{
353
+ path: string;
354
+ name: string;
355
+ sessionCount: number;
356
+ }>;
357
+ private ensureFileIndex;
358
+ updateFromLine(filePath: string, rawLine: string): void;
359
+ /**
360
+ * Batched form of updateFromLine: applies a burst of newly-appended lines
361
+ * (one chokidar read) in a single transaction with one message_count bump,
362
+ * one meta write, and one tail read/write — instead of 2-4 synchronous
363
+ * writes per line. Semantics are identical to replaying each line through
364
+ * updateFromLine in order: the agent filter short-circuits the whole batch,
365
+ * project context is backfilled last-wins, message_count increases by the
366
+ * number of surviving message lines, and last_activity/last_message reflect
367
+ * the final message line.
368
+ */
369
+ updateFromLines(filePath: string, rawLines: string[]): void;
370
+ upsertFromScannerMeta(metas: ScannerMeta[]): string[];
371
+ deleteByFilePath(filePath: string): boolean;
372
+ populateTailFromFile(convId: string, filePath: string): boolean;
373
+ listConversations(opts: {
374
+ project?: string;
375
+ limit: number;
376
+ offset: number;
377
+ }): {
378
+ conversations: ConversationListItem[];
379
+ total: number;
380
+ };
381
+ /** Returns a map of filePath → { mtimeMs, size } for all rows that have
382
+ * stat data stored. Used by the server to build the statCache passed to
383
+ * ConversationScanner.scan() so unchanged files are skipped. */
384
+ getFileStats(): Map<string, {
385
+ mtimeMs: number;
386
+ size: number;
387
+ }>;
388
+ getMetaById(id: string): ConversationListItem | null;
389
+ setConversationProjectId(conversationId: string, projectId: string): void;
390
+ markAsStreamer(id: string): void;
391
+ getLatestConversation(): {
392
+ id: string;
393
+ lastActivity: string | null;
394
+ } | null;
395
+ hasOrphanProjectId(): boolean;
396
+ listConversationsForProjectBackfill(): Array<{
397
+ id: string;
398
+ projectPath: string | null;
399
+ projectId: string | null;
400
+ lastActivity: string | null;
401
+ }>;
402
+ getConversationTail(id: string): CachedTail | null;
403
+ hasConversation(id: string): boolean;
404
+ upsertSessionName(sessionId: string, name: string): void;
405
+ getSessionName(sessionId: string): string | null;
406
+ listSessionNames(): Record<string, string>;
407
+ invalidate(id?: string): void;
408
+ invalidateByFilePath(filePath: string): string | null;
409
+ /**
410
+ * Drop rows whose `file_path` no longer exists on disk AND which have no
411
+ * cached tail to fall back to. Rows with a tail are left alone so
412
+ * `handleGetConversation` can still serve the cached tail even when the
413
+ * JSONL has been deleted.
414
+ */
415
+ pruneGhostFiles(exists?: (filePath: string) => boolean): string[];
416
+ }
417
+
418
+ type CacheMetadataKey = "last_conversation_id" | "last_conversation_created_at" | "projects_last_indexed_at" | "conversations_last_indexed_at" | "conversations_dirty";
419
+ declare class CacheMetadataRepository {
420
+ private get;
421
+ private upsert;
422
+ private del;
423
+ constructor(db: Database.Database);
424
+ getCacheMetadata(key: CacheMetadataKey): string | null;
425
+ setCacheMetadata(key: CacheMetadataKey, value: string): void;
426
+ deleteCacheMetadata(key: CacheMetadataKey): void;
427
+ }
428
+
429
+ /**
430
+ * Thin repository wrapper around ConversationCache for the project-id flow.
431
+ * The cache is the source of truth for conversation rows; this just exposes
432
+ * a stable, repo-style API for services that don't want to know about the
433
+ * cache class directly.
434
+ */
435
+ declare class ConversationsRepository {
436
+ private cache;
437
+ constructor(cache: ConversationCache);
438
+ updateConversationProjectId(args: {
439
+ conversationId: string;
440
+ projectId: string;
441
+ }): void;
442
+ listConversationsForProjectBackfill(): {
443
+ id: string;
444
+ projectPath: string | null;
445
+ projectId: string | null;
446
+ lastActivity: string | null;
447
+ }[];
448
+ getLatestConversation(): {
449
+ id: string;
450
+ lastActivity: string | null;
451
+ } | null;
452
+ hasOrphanRows(): boolean;
453
+ }
454
+
455
+ /**
456
+ * Persisted Project row. Mirrors the projects SQLite table.
457
+ */
458
+ declare const ProjectSchema: z.ZodObject<{
459
+ id: z.ZodString;
460
+ path: z.ZodString;
461
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
462
+ lastConversationId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
463
+ lastConversationCreatedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
464
+ lastIndexedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
465
+ latestMessageAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
466
+ latestMessageId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
467
+ messageCount: z.ZodOptional<z.ZodNumber>;
468
+ createdAt: z.ZodString;
469
+ updatedAt: z.ZodString;
470
+ }, z.core.$strip>;
471
+ type Project = z.infer<typeof ProjectSchema>;
472
+
473
+ interface UpsertProjectInput {
474
+ lastConversationId?: string | null;
475
+ lastConversationCreatedAt?: string | null;
476
+ latestMessageAt?: string | null;
477
+ latestMessageId?: string | null;
478
+ name?: string | null;
479
+ }
480
+ declare class ProjectsRepository {
481
+ private getByPath;
482
+ private getById;
483
+ private listAll;
484
+ private insert;
485
+ private update;
486
+ constructor(db: Database.Database);
487
+ getProjectByPath(rawPath: string): Project | null;
488
+ getProjectById(id: string): Project | null;
489
+ listProjects(): Project[];
490
+ /**
491
+ * Insert a project at the given canonical path, or update its metadata
492
+ * if one already exists. Returns the persisted Project row.
493
+ *
494
+ * Idempotent: passing the same path twice returns the same project id.
495
+ */
496
+ upsertProjectByPath(rawPath: string, input?: UpsertProjectInput): Project;
497
+ }
498
+
499
+ declare class SessionStore {
500
+ private managed;
501
+ private discovered;
502
+ addManaged(session: ManagedSession): void;
503
+ /**
504
+ * Multi-agent mode only. Attach a dedupe LRU to a session record. Idempotent —
505
+ * calling twice keeps the existing LRU (and its contents).
506
+ */
507
+ initAgentSession(sessionId: string, dedupeCapacity: number): void;
508
+ updateManaged(sessionId: string, updates: Partial<ManagedSession>): ManagedSession | null;
509
+ removeManaged(sessionId: string): boolean;
510
+ getManaged(sessionId: string): ManagedSession | null;
511
+ setDiscovered(processes: DiscoveredProcess[]): void;
512
+ listManaged(): ManagedSession[];
513
+ list(ptyAttachedIds: Set<string>): SessionResponse[];
514
+ get(sessionId: string, ptyAttachedIds: Set<string>): SessionResponse | null;
515
+ paginate(ptyAttachedIds: Set<string>, query: SessionListQuery): SessionListPage;
516
+ }
517
+
518
+ /**
519
+ * Sessions live in-memory in SessionStore (Postgres-backed persistence
520
+ * was dropped per the SQLite-only direction). This repository wraps the
521
+ * store with a stable, project-id-aware API for the new services.
522
+ */
523
+ declare class SessionsRepository {
524
+ private store;
525
+ constructor(store: SessionStore);
526
+ updateSessionProjectId(args: {
527
+ sessionId: string;
528
+ projectId: string;
529
+ }): void;
530
+ listManagedSessions(): ManagedSession[];
531
+ }
532
+
533
+ declare class PTYManager {
534
+ private sessions;
535
+ private onOutput;
536
+ private onStatusChange;
537
+ private onReady;
538
+ private pendingReady;
539
+ private queuedInputs;
540
+ private log;
541
+ private firstChunkAt;
542
+ private chunkIndex;
543
+ private lastChunkAt;
544
+ constructor(options?: PTYManagerOptions);
545
+ start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession>;
546
+ startFresh(options: StartFreshSessionOptions): Promise<ManagedSession>;
547
+ sendKeys(sessionId: string, keys: string): void;
548
+ sendInput(sessionId: string, input: string): number;
549
+ private writeSubmit;
550
+ private flushQueuedInputs;
551
+ cancel(sessionId: string): void;
552
+ killPid(pid: number): void;
553
+ putOnHold(sessionId: string): void;
554
+ getOutput(sessionId: string): string;
555
+ getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
556
+ getSession(sessionId: string): ManagedSession | null;
557
+ hasSession(sessionId: string): boolean;
558
+ listSessions(): ManagedSession[];
559
+ dispose(): void;
560
+ private handleOutput;
561
+ private markReady;
562
+ private handleExit;
563
+ }
564
+
565
+ declare class WSHub {
566
+ private clients;
567
+ private pingTimer;
568
+ private pongTimers;
569
+ addClient(ws: WebSocket): void;
570
+ broadcast(message: WSMessage): void;
571
+ unicast(ws: WebSocket, message: WSMessage): void;
572
+ get connectionCount(): number;
573
+ dispose(): void;
574
+ private startPing;
575
+ }
576
+
577
+ type ApiDeps = {
578
+ apiKey: string;
579
+ localNoAuth: boolean;
580
+ publicUrl: string | null;
581
+ browseRoot: string | null;
582
+ ptyManager: PTYManager;
583
+ sessionStore: SessionStore;
584
+ wsHub: WSHub;
585
+ cache: () => ConversationCache | null;
586
+ projectsRepo: () => ProjectsRepository | null;
587
+ conversationsRepo: () => ConversationsRepository | null;
588
+ sessionsRepo: () => SessionsRepository | null;
589
+ cacheMetadataRepo: () => CacheMetadataRepository | null;
590
+ ptyAttachedIds: () => Set<string>;
591
+ handleListSessions: (url: URL, res: ServerResponse) => Promise<void>;
592
+ handleSessionsCount: (res: ServerResponse) => void;
593
+ handleGetRecentSessions: (url: URL, res: ServerResponse) => void;
594
+ handleGetSessionNames: (res: ServerResponse) => void;
595
+ handleGetSession: (sessionId: string, res: ServerResponse) => void;
596
+ handleGetOutput: (sessionId: string, res: ServerResponse) => void;
597
+ handleSendInput: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
598
+ handleCancel: (sessionId: string, res: ServerResponse) => void;
599
+ handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
600
+ handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
601
+ handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
602
+ handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
603
+ handleStartSession: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
604
+ handleListConversations: (url: URL, res: ServerResponse) => Promise<void>;
605
+ handleConversationsCount: (url: URL, res: ServerResponse) => Promise<void>;
606
+ handleGetConversation: (id: string, url: URL, res: ServerResponse, ifNoneMatch?: string) => Promise<void>;
607
+ handleSearch: (url: URL, res: ServerResponse) => Promise<void>;
608
+ handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
609
+ handleListProjectChats: (url: URL, res: ServerResponse) => Promise<void>;
610
+ handlePairStart: (res: ServerResponse) => void;
611
+ handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
612
+ handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
613
+ handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
614
+ handleWsOpen: (ws: WebSocket) => void;
615
+ handleWsMessage: (ws: WebSocket, raw: unknown) => void;
616
+ handleWsClose: (ws: WebSocket) => void;
617
+ agentClient: AgentClient | null;
618
+ conversationWriter: ConversationWriter | null;
619
+ agentConfig: AgentConfig;
620
+ };
621
+
622
+ type AppEnv = {
623
+ Bindings: HttpBindings;
624
+ Variables: {
625
+ requestId?: string;
626
+ validatedBody?: unknown;
627
+ validatedQuery?: unknown;
628
+ };
629
+ };
630
+
631
+ interface AgentDeps {
632
+ sessionStore: {
633
+ getManaged: (sessionId: string) => {
634
+ id: string;
635
+ progressDedupeIds?: {
636
+ hasSeen: (id: string) => boolean;
637
+ };
638
+ currentTurnId?: string | null;
639
+ } | null;
640
+ };
641
+ wsHub: {
642
+ broadcast: (m: WSMessage) => void;
643
+ };
644
+ conversationWriter: {
645
+ appendAssistantTurn: (a: {
646
+ sessionId: string;
647
+ turnId: string;
648
+ content: string;
649
+ reviewerOverruled?: boolean;
650
+ }) => Promise<void>;
651
+ } | null;
652
+ agentConfig: {
653
+ enabled: boolean;
654
+ webhook: {
655
+ hmacSecret: string;
656
+ timestampSkewSeconds: number;
657
+ };
658
+ dedupe: {
659
+ perSessionCapacity: number;
660
+ };
661
+ };
662
+ }
663
+ declare const createProgressRoutes: (deps: ApiDeps & AgentDeps) => Hono<AppEnv, hono_types.BlankSchema, "/">;
664
+
665
+ declare function generateApiKey(): string;
666
+ declare function validateApiKey(provided: string, expected: string): boolean;
667
+ declare function loadOrCreateApiKey(): string;
668
+
669
+ interface DbConfig {
670
+ connectionString: string;
671
+ max: number;
672
+ ssl?: string;
673
+ statementTimeout?: number;
674
+ instanceId: string;
675
+ }
676
+ declare function isDbEnabled(): boolean;
677
+ declare function getDbConfig(): DbConfig | null;
678
+
679
+ declare function maskConnectionString(url: string): string;
680
+ declare function createPool(config: DbConfig): Promise<Pool>;
681
+
682
+ declare function discoverClaudeProcesses(): Promise<DiscoveredProcess[]>;
683
+
684
+ declare class StreamerServer {
685
+ private httpServer;
686
+ private ptyManager;
687
+ private sessionStore;
688
+ private wsHub;
689
+ private fileWatcher;
690
+ private sessionFileMap;
691
+ private scanner;
692
+ private scannerReady;
693
+ private scannerStale;
694
+ private binding;
695
+ private cacheReady;
696
+ private apiKey;
697
+ private localNoAuth;
698
+ private verbose;
699
+ private scanProfiles;
700
+ private dbPool;
701
+ private dbInstanceId;
702
+ private disableDb;
703
+ private browseRoot;
704
+ private publicUrl;
705
+ private pairTokens;
706
+ private exchangeAttempts;
707
+ private ptyGracePeriodMs;
708
+ private ptyGraceTimers;
709
+ private sessionSubscribers;
710
+ private clientIdToWs;
711
+ private wsToClientId;
712
+ private cache;
713
+ private projectsRepo;
714
+ private conversationsRepo;
715
+ private sessionsRepo;
716
+ private cacheMetadataRepo;
717
+ private discoveryCache;
718
+ private cacheDir;
719
+ private tailSize;
720
+ private directoryDebounceMs;
721
+ private markScannerStaleDebounced;
722
+ private includeAgents;
723
+ private agentEntrypoints;
724
+ private honoApp;
725
+ private log;
726
+ private agentConfig;
727
+ private agentClient;
728
+ constructor(config: ServerConfig & {
729
+ apiKey: string;
730
+ });
731
+ private ptyAttachedIds;
732
+ /**
733
+ * Send a session_list to only the client that triggered this HTTP request
734
+ * (identified by X-Client-Id header → registered WS socket). Falls back to
735
+ * a full broadcast if no match exists (old clients, or no WS registered yet).
736
+ */
737
+ private broadcastOrUnicastSessionList;
738
+ private addSessionSubscriber;
739
+ private startGraceTimer;
740
+ get port(): number;
741
+ listen(port: number, opts?: {
742
+ awaitReady?: boolean;
743
+ }): Promise<void>;
744
+ private bindWithRetry;
745
+ private bindWithRetryLoop;
746
+ close(): Promise<void>;
747
+ private handleRequest;
748
+ private handlePairStart;
749
+ private handlePairExchange;
750
+ private checkExchangeRateLimit;
751
+ private handleListConversations;
752
+ private handleConversationsCount;
753
+ private handleSessionsCount;
754
+ private handleGetRecentSessions;
755
+ private handleGetPopularProjects;
756
+ private handleListProjectChats;
757
+ private buildStatCache;
758
+ private getScanner;
759
+ private getFreshScanner;
760
+ private findJsonlPath;
761
+ private readCwdFromJsonl;
762
+ private findConversationByUuid;
763
+ private isConversationSnapshotStale;
764
+ private handleGetConversation;
765
+ private handleSearch;
766
+ private handleListSessions;
767
+ private handleGetSession;
768
+ private handleResume;
769
+ private handleSendInput;
770
+ private handleUploadFile;
771
+ private handleGetOutput;
772
+ private handleCancel;
773
+ private handleAdopt;
774
+ private handleStartSession;
775
+ private linkSessionToProject;
776
+ private watchConversationFile;
777
+ private watchForJsonl;
778
+ private handleBrowse;
779
+ private handleMkdir;
780
+ private handleSetSessionName;
781
+ private handleGetSessionNames;
782
+ }
783
+
784
+ interface ConversationWatcherEvents {
785
+ /** Fires once per new newline-terminated line appended to a watched file. */
786
+ onNewLine?: (filePath: string, line: string) => void;
787
+ /**
788
+ * Fires once per chokidar read with ALL new lines from that read, batched.
789
+ * When set, it REPLACES the per-line onNewLine dispatch for that file —
790
+ * callers pick one. Lets a burst of appended lines collapse into a single
791
+ * downstream cache write + WebSocket broadcast.
792
+ */
793
+ onNewLines?: (filePath: string, lines: string[]) => void;
794
+ /** Fires when chokidar reports an add/change/unlink at the directory level. */
795
+ onConversationChanged?: (filePath: string) => void | Promise<void>;
796
+ /** Fires when a tailed file is deleted (per-file watcher unlink event). */
797
+ onFileDeleted?: (filePath: string) => void;
798
+ /** Reported errors per file. */
799
+ onError?: (filePath: string, error: Error) => void;
800
+ }
801
+ /**
802
+ * Chokidar-backed replacement for src/file-watcher.ts.
803
+ *
804
+ * - watch(filePath) → tail a single JSONL file, emitting onNewLine
805
+ * for each appended line.
806
+ * - watchDirectory(dir) → mark cache dirty on add/change/unlink events
807
+ * for any file inside the directory.
808
+ *
809
+ * Per the refactor plan, file watching is an OPTIMIZATION; correctness
810
+ * still relies on refresh=1 / the latest HDD conversation id check.
811
+ */
812
+ declare class ConversationWatcher {
813
+ private files;
814
+ private directories;
815
+ private onNewLine;
816
+ private onNewLines;
817
+ private onConversationChanged;
818
+ private onFileDeleted;
819
+ private onError;
820
+ constructor(events?: ConversationWatcherEvents);
821
+ watch(filePath: string): void;
822
+ unwatch(filePath: string): void;
823
+ /**
824
+ * Watch a directory of conversation JSONL files. Fires
825
+ * onConversationChanged for any add/change/unlink event so the caller
826
+ * can mark the cache dirty without scanning everything immediately.
827
+ */
828
+ watchDirectory(directory: string): void;
829
+ unwatchDirectory(directory: string): void;
830
+ dispose(): void;
831
+ private readNewLines;
832
+ }
833
+
834
+ export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, type ManagedSession, PTYManager, type PTYManagerOptions, type ProgressDedupeLRU, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };