@threadbase-sh/streamer 1.30.0 → 1.31.1
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/dist/cli.cjs +89 -92
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +74 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +74 -89
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -168,6 +168,10 @@ interface PermissionOption {
|
|
|
168
168
|
label: string;
|
|
169
169
|
answerKeys?: string;
|
|
170
170
|
}
|
|
171
|
+
interface UserMessage {
|
|
172
|
+
text: string;
|
|
173
|
+
ts: number;
|
|
174
|
+
}
|
|
171
175
|
type WSMessage = {
|
|
172
176
|
type: "terminal_output";
|
|
173
177
|
sessionId: string;
|
|
@@ -214,10 +218,16 @@ type WSMessage = {
|
|
|
214
218
|
} | {
|
|
215
219
|
type: "ping";
|
|
216
220
|
ts: number;
|
|
221
|
+
} | {
|
|
222
|
+
type: "user_message";
|
|
223
|
+
sessionId: string;
|
|
224
|
+
text: string;
|
|
225
|
+
ts: number;
|
|
217
226
|
} | {
|
|
218
227
|
type: "terminal_replay";
|
|
219
228
|
sessionId: string;
|
|
220
229
|
lines: string[];
|
|
230
|
+
userMessages?: UserMessage[];
|
|
221
231
|
} | {
|
|
222
232
|
type: "session_ready";
|
|
223
233
|
session: SessionResponse;
|
|
@@ -321,7 +331,6 @@ interface ServerConfig {
|
|
|
321
331
|
codexRoots?: string[];
|
|
322
332
|
scannerPersistent?: boolean;
|
|
323
333
|
ptyGracePeriodMs?: number;
|
|
324
|
-
wsAuthTimeoutMs?: number;
|
|
325
334
|
cacheDir?: string;
|
|
326
335
|
tailSize?: number;
|
|
327
336
|
directoryScanDebounceMs?: number;
|
|
@@ -340,6 +349,7 @@ interface PTYManagerOptions {
|
|
|
340
349
|
} | null) => void;
|
|
341
350
|
onLiveQuestion?: (sessionId: string, questions: AskQuestion[]) => void;
|
|
342
351
|
onLiveQuestionGone?: (sessionId: string) => void;
|
|
352
|
+
onUserMessage?: (sessionId: string, text: string, ts: number) => void;
|
|
343
353
|
logger?: Logger;
|
|
344
354
|
}
|
|
345
355
|
interface StartSessionOptions {
|
|
@@ -364,6 +374,7 @@ interface SessionRunner {
|
|
|
364
374
|
putOnHold(sessionId: string): void;
|
|
365
375
|
getOutput(sessionId: string): string;
|
|
366
376
|
getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
|
|
377
|
+
getInputHistory(sessionId: string): UserMessage[];
|
|
367
378
|
getSession(sessionId: string): ManagedSession | null;
|
|
368
379
|
hasSession(sessionId: string): boolean;
|
|
369
380
|
listSessions(): ManagedSession[];
|
|
@@ -794,6 +805,7 @@ declare class LiveSessionManager {
|
|
|
794
805
|
putOnHold(sessionId: string): void;
|
|
795
806
|
getOutput(sessionId: string): string;
|
|
796
807
|
getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
|
|
808
|
+
getInputHistory(sessionId: string): UserMessage[];
|
|
797
809
|
getSession(sessionId: string): ManagedSession | null;
|
|
798
810
|
hasSession(sessionId: string): boolean;
|
|
799
811
|
listSessions(): ManagedSession[];
|
|
@@ -860,7 +872,7 @@ type ApiDeps = {
|
|
|
860
872
|
handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
861
873
|
handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
|
|
862
874
|
handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
863
|
-
handleWsOpen: (ws: WebSocket
|
|
875
|
+
handleWsOpen: (ws: WebSocket) => void;
|
|
864
876
|
handleWsMessage: (ws: WebSocket, raw: unknown) => void;
|
|
865
877
|
handleWsClose: (ws: WebSocket) => void;
|
|
866
878
|
agentClient: AgentClient | null;
|
|
@@ -938,6 +950,7 @@ declare class PTYManager implements SessionRunner {
|
|
|
938
950
|
private onPermissionChange;
|
|
939
951
|
private onLiveQuestion;
|
|
940
952
|
private onLiveQuestionGone;
|
|
953
|
+
private onUserMessage;
|
|
941
954
|
private permissionOpen;
|
|
942
955
|
private lastScreenQuestionKey;
|
|
943
956
|
private shellPromptOpen;
|
|
@@ -962,6 +975,8 @@ declare class PTYManager implements SessionRunner {
|
|
|
962
975
|
putOnHold(sessionId: string): void;
|
|
963
976
|
getOutput(sessionId: string): string;
|
|
964
977
|
getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
|
|
978
|
+
getInputHistory(sessionId: string): UserMessage[];
|
|
979
|
+
private recordUserMessage;
|
|
965
980
|
getSession(sessionId: string): ManagedSession | null;
|
|
966
981
|
hasSession(sessionId: string): boolean;
|
|
967
982
|
listSessions(): ManagedSession[];
|
|
@@ -1016,9 +1031,6 @@ declare class StreamerServer {
|
|
|
1016
1031
|
private sessionSubscribers;
|
|
1017
1032
|
private clientIdToWs;
|
|
1018
1033
|
private wsToClientId;
|
|
1019
|
-
private wsAuthed;
|
|
1020
|
-
private wsAuthPending;
|
|
1021
|
-
private wsAuthTimeoutMs;
|
|
1022
1034
|
private cache;
|
|
1023
1035
|
private projectsRepo;
|
|
1024
1036
|
private conversationsRepo;
|
|
@@ -1047,7 +1059,6 @@ declare class StreamerServer {
|
|
|
1047
1059
|
* a full broadcast if no match exists (old clients, or no WS registered yet).
|
|
1048
1060
|
*/
|
|
1049
1061
|
private broadcastOrUnicastSessionList;
|
|
1050
|
-
private completeWsAuth;
|
|
1051
1062
|
private addSessionSubscriber;
|
|
1052
1063
|
private startGraceTimer;
|
|
1053
1064
|
get port(): number;
|
|
@@ -1197,4 +1208,4 @@ declare class ConversationWatcher {
|
|
|
1197
1208
|
private readNewLines;
|
|
1198
1209
|
}
|
|
1199
1210
|
|
|
1200
|
-
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
|
1211
|
+
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
package/dist/index.d.ts
CHANGED
|
@@ -168,6 +168,10 @@ interface PermissionOption {
|
|
|
168
168
|
label: string;
|
|
169
169
|
answerKeys?: string;
|
|
170
170
|
}
|
|
171
|
+
interface UserMessage {
|
|
172
|
+
text: string;
|
|
173
|
+
ts: number;
|
|
174
|
+
}
|
|
171
175
|
type WSMessage = {
|
|
172
176
|
type: "terminal_output";
|
|
173
177
|
sessionId: string;
|
|
@@ -214,10 +218,16 @@ type WSMessage = {
|
|
|
214
218
|
} | {
|
|
215
219
|
type: "ping";
|
|
216
220
|
ts: number;
|
|
221
|
+
} | {
|
|
222
|
+
type: "user_message";
|
|
223
|
+
sessionId: string;
|
|
224
|
+
text: string;
|
|
225
|
+
ts: number;
|
|
217
226
|
} | {
|
|
218
227
|
type: "terminal_replay";
|
|
219
228
|
sessionId: string;
|
|
220
229
|
lines: string[];
|
|
230
|
+
userMessages?: UserMessage[];
|
|
221
231
|
} | {
|
|
222
232
|
type: "session_ready";
|
|
223
233
|
session: SessionResponse;
|
|
@@ -321,7 +331,6 @@ interface ServerConfig {
|
|
|
321
331
|
codexRoots?: string[];
|
|
322
332
|
scannerPersistent?: boolean;
|
|
323
333
|
ptyGracePeriodMs?: number;
|
|
324
|
-
wsAuthTimeoutMs?: number;
|
|
325
334
|
cacheDir?: string;
|
|
326
335
|
tailSize?: number;
|
|
327
336
|
directoryScanDebounceMs?: number;
|
|
@@ -340,6 +349,7 @@ interface PTYManagerOptions {
|
|
|
340
349
|
} | null) => void;
|
|
341
350
|
onLiveQuestion?: (sessionId: string, questions: AskQuestion[]) => void;
|
|
342
351
|
onLiveQuestionGone?: (sessionId: string) => void;
|
|
352
|
+
onUserMessage?: (sessionId: string, text: string, ts: number) => void;
|
|
343
353
|
logger?: Logger;
|
|
344
354
|
}
|
|
345
355
|
interface StartSessionOptions {
|
|
@@ -364,6 +374,7 @@ interface SessionRunner {
|
|
|
364
374
|
putOnHold(sessionId: string): void;
|
|
365
375
|
getOutput(sessionId: string): string;
|
|
366
376
|
getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
|
|
377
|
+
getInputHistory(sessionId: string): UserMessage[];
|
|
367
378
|
getSession(sessionId: string): ManagedSession | null;
|
|
368
379
|
hasSession(sessionId: string): boolean;
|
|
369
380
|
listSessions(): ManagedSession[];
|
|
@@ -794,6 +805,7 @@ declare class LiveSessionManager {
|
|
|
794
805
|
putOnHold(sessionId: string): void;
|
|
795
806
|
getOutput(sessionId: string): string;
|
|
796
807
|
getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
|
|
808
|
+
getInputHistory(sessionId: string): UserMessage[];
|
|
797
809
|
getSession(sessionId: string): ManagedSession | null;
|
|
798
810
|
hasSession(sessionId: string): boolean;
|
|
799
811
|
listSessions(): ManagedSession[];
|
|
@@ -860,7 +872,7 @@ type ApiDeps = {
|
|
|
860
872
|
handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
861
873
|
handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
|
|
862
874
|
handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
863
|
-
handleWsOpen: (ws: WebSocket
|
|
875
|
+
handleWsOpen: (ws: WebSocket) => void;
|
|
864
876
|
handleWsMessage: (ws: WebSocket, raw: unknown) => void;
|
|
865
877
|
handleWsClose: (ws: WebSocket) => void;
|
|
866
878
|
agentClient: AgentClient | null;
|
|
@@ -938,6 +950,7 @@ declare class PTYManager implements SessionRunner {
|
|
|
938
950
|
private onPermissionChange;
|
|
939
951
|
private onLiveQuestion;
|
|
940
952
|
private onLiveQuestionGone;
|
|
953
|
+
private onUserMessage;
|
|
941
954
|
private permissionOpen;
|
|
942
955
|
private lastScreenQuestionKey;
|
|
943
956
|
private shellPromptOpen;
|
|
@@ -962,6 +975,8 @@ declare class PTYManager implements SessionRunner {
|
|
|
962
975
|
putOnHold(sessionId: string): void;
|
|
963
976
|
getOutput(sessionId: string): string;
|
|
964
977
|
getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
|
|
978
|
+
getInputHistory(sessionId: string): UserMessage[];
|
|
979
|
+
private recordUserMessage;
|
|
965
980
|
getSession(sessionId: string): ManagedSession | null;
|
|
966
981
|
hasSession(sessionId: string): boolean;
|
|
967
982
|
listSessions(): ManagedSession[];
|
|
@@ -1016,9 +1031,6 @@ declare class StreamerServer {
|
|
|
1016
1031
|
private sessionSubscribers;
|
|
1017
1032
|
private clientIdToWs;
|
|
1018
1033
|
private wsToClientId;
|
|
1019
|
-
private wsAuthed;
|
|
1020
|
-
private wsAuthPending;
|
|
1021
|
-
private wsAuthTimeoutMs;
|
|
1022
1034
|
private cache;
|
|
1023
1035
|
private projectsRepo;
|
|
1024
1036
|
private conversationsRepo;
|
|
@@ -1047,7 +1059,6 @@ declare class StreamerServer {
|
|
|
1047
1059
|
* a full broadcast if no match exists (old clients, or no WS registered yet).
|
|
1048
1060
|
*/
|
|
1049
1061
|
private broadcastOrUnicastSessionList;
|
|
1050
|
-
private completeWsAuth;
|
|
1051
1062
|
private addSessionSubscriber;
|
|
1052
1063
|
private startGraceTimer;
|
|
1053
1064
|
get port(): number;
|
|
@@ -1197,4 +1208,4 @@ declare class ConversationWatcher {
|
|
|
1197
1208
|
private readNewLines;
|
|
1198
1209
|
}
|
|
1199
1210
|
|
|
1200
|
-
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
|
1211
|
+
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
package/dist/index.js
CHANGED
|
@@ -164,7 +164,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
167
|
-
if (!timestampHeader) return
|
|
167
|
+
if (!timestampHeader) return true;
|
|
168
168
|
const t = Number(timestampHeader);
|
|
169
169
|
if (!Number.isFinite(t)) return false;
|
|
170
170
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -745,6 +745,7 @@ function debounce(fn, waitMs) {
|
|
|
745
745
|
|
|
746
746
|
// src/codex-pty-runner.ts
|
|
747
747
|
var OUTPUT_BUFFER_MAX = 65536;
|
|
748
|
+
var INPUT_HISTORY_MAX = 50;
|
|
748
749
|
var PTY_COLS = 120;
|
|
749
750
|
var PTY_ROWS = 40;
|
|
750
751
|
var SCREEN_SCROLLBACK = 1e3;
|
|
@@ -825,6 +826,7 @@ var CodexPtyRunner = class {
|
|
|
825
826
|
onPermissionChange;
|
|
826
827
|
onLiveQuestion;
|
|
827
828
|
onLiveQuestionGone;
|
|
829
|
+
onUserMessage;
|
|
828
830
|
log;
|
|
829
831
|
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
830
832
|
// "Ready" status bar — i.e. onReady hasn't fired.
|
|
@@ -855,6 +857,7 @@ var CodexPtyRunner = class {
|
|
|
855
857
|
this.onPermissionChange = options.onPermissionChange;
|
|
856
858
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
857
859
|
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
860
|
+
this.onUserMessage = options.onUserMessage;
|
|
858
861
|
this.log = options.logger ?? getLogger("codex-pty");
|
|
859
862
|
}
|
|
860
863
|
// Resume an existing Codex session. sessionId is the Codex-persisted
|
|
@@ -898,7 +901,8 @@ var CodexPtyRunner = class {
|
|
|
898
901
|
lastOutput: "",
|
|
899
902
|
process: proc,
|
|
900
903
|
outputBuffer: Buffer.alloc(0),
|
|
901
|
-
screen: createScreen()
|
|
904
|
+
screen: createScreen(),
|
|
905
|
+
inputHistory: []
|
|
902
906
|
};
|
|
903
907
|
this.sessions.set(sessionId, session);
|
|
904
908
|
this.pendingReady.add(sessionId);
|
|
@@ -944,7 +948,8 @@ var CodexPtyRunner = class {
|
|
|
944
948
|
lastOutput: "",
|
|
945
949
|
process: proc,
|
|
946
950
|
outputBuffer: Buffer.alloc(0),
|
|
947
|
-
screen: createScreen()
|
|
951
|
+
screen: createScreen(),
|
|
952
|
+
inputHistory: []
|
|
948
953
|
};
|
|
949
954
|
this.sessions.set(sessionId, session);
|
|
950
955
|
this.pendingReady.add(sessionId);
|
|
@@ -1062,6 +1067,7 @@ var CodexPtyRunner = class {
|
|
|
1062
1067
|
// confirmed Codex accepts plain keystrokes), then submit \r after a short
|
|
1063
1068
|
// delay so Codex's TUI gets an event-loop tick to process the input first.
|
|
1064
1069
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1070
|
+
this.recordUserMessage(session, input);
|
|
1065
1071
|
this.log.info(
|
|
1066
1072
|
`[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,
|
|
1067
1073
|
{
|
|
@@ -1191,6 +1197,19 @@ var CodexPtyRunner = class {
|
|
|
1191
1197
|
}
|
|
1192
1198
|
return lines.slice(-maxLines);
|
|
1193
1199
|
}
|
|
1200
|
+
getInputHistory(sessionId) {
|
|
1201
|
+
return this.sessions.get(sessionId)?.inputHistory ?? [];
|
|
1202
|
+
}
|
|
1203
|
+
// Record a submitted user message as ground truth and fire onUserMessage.
|
|
1204
|
+
// Called from writeSubmit (direct and flush paths) — never from sendKeys.
|
|
1205
|
+
recordUserMessage(session, text) {
|
|
1206
|
+
const ts = Date.now();
|
|
1207
|
+
session.inputHistory.push({ text, ts });
|
|
1208
|
+
if (session.inputHistory.length > INPUT_HISTORY_MAX) {
|
|
1209
|
+
session.inputHistory.shift();
|
|
1210
|
+
}
|
|
1211
|
+
this.onUserMessage?.(session.id, text, ts);
|
|
1212
|
+
}
|
|
1194
1213
|
getSession(sessionId) {
|
|
1195
1214
|
const session = this.sessions.get(sessionId);
|
|
1196
1215
|
return session ? toPublicSession(session) : null;
|
|
@@ -1568,6 +1587,7 @@ function detectShellPrompt(lines) {
|
|
|
1568
1587
|
|
|
1569
1588
|
// src/pty-manager.ts
|
|
1570
1589
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1590
|
+
var INPUT_HISTORY_MAX2 = 50;
|
|
1571
1591
|
var PTY_COLS2 = 120;
|
|
1572
1592
|
var PTY_ROWS2 = 40;
|
|
1573
1593
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
@@ -1626,6 +1646,7 @@ var PTYManager = class {
|
|
|
1626
1646
|
onPermissionChange;
|
|
1627
1647
|
onLiveQuestion;
|
|
1628
1648
|
onLiveQuestionGone;
|
|
1649
|
+
onUserMessage;
|
|
1629
1650
|
// Per-session permission-gate state. True between an OSC 777 (gate open) and
|
|
1630
1651
|
// the next prompt-ready without a fresh 777 (gate closed). Prevents
|
|
1631
1652
|
// re-broadcasting open/close on every chunk.
|
|
@@ -1671,6 +1692,7 @@ var PTYManager = class {
|
|
|
1671
1692
|
this.onPermissionChange = options.onPermissionChange;
|
|
1672
1693
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
1673
1694
|
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
1695
|
+
this.onUserMessage = options.onUserMessage;
|
|
1674
1696
|
this.log = options.logger ?? getLogger("pty");
|
|
1675
1697
|
}
|
|
1676
1698
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
@@ -1733,7 +1755,8 @@ var PTYManager = class {
|
|
|
1733
1755
|
lastOutput: "",
|
|
1734
1756
|
process: proc,
|
|
1735
1757
|
outputBuffer: Buffer.alloc(0),
|
|
1736
|
-
screen: createScreen2()
|
|
1758
|
+
screen: createScreen2(),
|
|
1759
|
+
inputHistory: []
|
|
1737
1760
|
};
|
|
1738
1761
|
this.sessions.set(sessionId, session);
|
|
1739
1762
|
this.pendingReady.add(sessionId);
|
|
@@ -1784,7 +1807,8 @@ var PTYManager = class {
|
|
|
1784
1807
|
lastOutput: "",
|
|
1785
1808
|
process: proc,
|
|
1786
1809
|
outputBuffer: Buffer.alloc(0),
|
|
1787
|
-
screen: createScreen2()
|
|
1810
|
+
screen: createScreen2(),
|
|
1811
|
+
inputHistory: []
|
|
1788
1812
|
};
|
|
1789
1813
|
this.sessions.set(sessionId, session);
|
|
1790
1814
|
this.pendingReady.add(sessionId);
|
|
@@ -1864,6 +1888,7 @@ var PTYManager = class {
|
|
|
1864
1888
|
// step gives the TUI as many extra ticks as it needs, capped at
|
|
1865
1889
|
// SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
|
|
1866
1890
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1891
|
+
this.recordUserMessage(session, input);
|
|
1867
1892
|
const pasteBytes = buildPasteBytes(input);
|
|
1868
1893
|
this.log.info(
|
|
1869
1894
|
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
|
|
@@ -1994,6 +2019,20 @@ var PTYManager = class {
|
|
|
1994
2019
|
}
|
|
1995
2020
|
return lines.slice(-maxLines);
|
|
1996
2021
|
}
|
|
2022
|
+
getInputHistory(sessionId) {
|
|
2023
|
+
return this.sessions.get(sessionId)?.inputHistory ?? [];
|
|
2024
|
+
}
|
|
2025
|
+
// Record a submitted user message as ground truth and fire onUserMessage.
|
|
2026
|
+
// Called from writeSubmit (both direct and flush paths) — never from
|
|
2027
|
+
// sendKeys, so raw keystrokes aren't logged as messages.
|
|
2028
|
+
recordUserMessage(session, text) {
|
|
2029
|
+
const ts = Date.now();
|
|
2030
|
+
session.inputHistory.push({ text, ts });
|
|
2031
|
+
if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
|
|
2032
|
+
session.inputHistory.shift();
|
|
2033
|
+
}
|
|
2034
|
+
this.onUserMessage?.(session.id, text, ts);
|
|
2035
|
+
}
|
|
1997
2036
|
getSession(sessionId) {
|
|
1998
2037
|
const session = this.sessions.get(sessionId);
|
|
1999
2038
|
return session ? toPublicSession2(session) : null;
|
|
@@ -2290,6 +2329,9 @@ var LiveSessionManager = class {
|
|
|
2290
2329
|
getOutputLines(sessionId, maxLines) {
|
|
2291
2330
|
return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
|
|
2292
2331
|
}
|
|
2332
|
+
getInputHistory(sessionId) {
|
|
2333
|
+
return this.runnerFor(sessionId).getInputHistory(sessionId);
|
|
2334
|
+
}
|
|
2293
2335
|
getSession(sessionId) {
|
|
2294
2336
|
for (const runner of this.runners.values()) {
|
|
2295
2337
|
const session = runner.getSession(sessionId);
|
|
@@ -2763,7 +2805,7 @@ function isLocalRequest(remoteAddr) {
|
|
|
2763
2805
|
const addr = remoteAddr ?? "";
|
|
2764
2806
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2765
2807
|
}
|
|
2766
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"
|
|
2808
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
2767
2809
|
var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
|
|
2768
2810
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
2769
2811
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
@@ -3360,16 +3402,14 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3360
3402
|
const app = new Hono11();
|
|
3361
3403
|
app.get(
|
|
3362
3404
|
"/ws",
|
|
3363
|
-
upgradeWebSocket((
|
|
3364
|
-
const key = c.req.query("key");
|
|
3365
|
-
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
3405
|
+
upgradeWebSocket(() => {
|
|
3366
3406
|
let openWs = null;
|
|
3367
3407
|
return {
|
|
3368
3408
|
onOpen(_evt, ws) {
|
|
3369
3409
|
const raw = ws.raw;
|
|
3370
3410
|
if (!raw) return;
|
|
3371
3411
|
openWs = raw;
|
|
3372
|
-
deps.handleWsOpen(raw
|
|
3412
|
+
deps.handleWsOpen(raw);
|
|
3373
3413
|
},
|
|
3374
3414
|
onMessage(evt, _ws) {
|
|
3375
3415
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -5277,13 +5317,6 @@ function deriveProjectChatTitle(input) {
|
|
|
5277
5317
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
5278
5318
|
}
|
|
5279
5319
|
|
|
5280
|
-
// src/services/questions/permissionAnswerKeys.ts
|
|
5281
|
-
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
5282
|
-
function sanitizeAnswerKeys(keys) {
|
|
5283
|
-
if (keys === void 0) return void 0;
|
|
5284
|
-
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
5285
|
-
}
|
|
5286
|
-
|
|
5287
5320
|
// src/services/questions/detectAskUserQuestion.ts
|
|
5288
5321
|
function normalizeContent2(raw) {
|
|
5289
5322
|
if (Array.isArray(raw)) return raw;
|
|
@@ -5888,8 +5921,6 @@ var WSHub = class {
|
|
|
5888
5921
|
var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
|
|
5889
5922
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
5890
5923
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5891
|
-
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5892
|
-
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5893
5924
|
var REFRESH_TTL_MS = 2e3;
|
|
5894
5925
|
var START_READY_TIMEOUT_MS = 1e4;
|
|
5895
5926
|
function parseIncludeAgentsEnv(raw) {
|
|
@@ -5980,14 +6011,6 @@ var StreamerServer = class {
|
|
|
5980
6011
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
5981
6012
|
// Reverse map for cleanup on close
|
|
5982
6013
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
5983
|
-
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
5984
|
-
// { type: "auth", token } first message). Only authed sockets are added to
|
|
5985
|
-
// the hub and receive broadcasts.
|
|
5986
|
-
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
5987
|
-
wsAuthed = /* @__PURE__ */ new Set();
|
|
5988
|
-
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
5989
|
-
wsAuthPending = /* @__PURE__ */ new Map();
|
|
5990
|
-
wsAuthTimeoutMs;
|
|
5991
6014
|
cache = null;
|
|
5992
6015
|
projectsRepo = null;
|
|
5993
6016
|
conversationsRepo = null;
|
|
@@ -6027,7 +6050,6 @@ var StreamerServer = class {
|
|
|
6027
6050
|
this.scanProfiles = config.scanProfiles;
|
|
6028
6051
|
this.codexRoots = config.codexRoots ?? [join15(homedir7(), ".codex", "sessions")];
|
|
6029
6052
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
6030
|
-
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
6031
6053
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
6032
6054
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
6033
6055
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join15(homedir7(), ".threadbase", "cache");
|
|
@@ -6156,6 +6178,9 @@ var StreamerServer = class {
|
|
|
6156
6178
|
onOutput: (sessionId, data) => {
|
|
6157
6179
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
6158
6180
|
},
|
|
6181
|
+
onUserMessage: (sessionId, text, ts) => {
|
|
6182
|
+
this.wsHub.broadcast({ type: "user_message", sessionId, text, ts });
|
|
6183
|
+
},
|
|
6159
6184
|
onPermissionChange: (sessionId, gate) => {
|
|
6160
6185
|
this.handlePermissionChange(sessionId, gate);
|
|
6161
6186
|
},
|
|
@@ -6285,39 +6310,17 @@ var StreamerServer = class {
|
|
|
6285
6310
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
6286
6311
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
6287
6312
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
6288
|
-
handleWsOpen: (ws
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6313
|
+
handleWsOpen: (ws) => {
|
|
6314
|
+
this.wsHub.addClient(ws);
|
|
6315
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6316
|
+
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6317
|
+
if (this.cacheReady) {
|
|
6318
|
+
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6292
6319
|
}
|
|
6293
|
-
const timer = setTimeout(() => {
|
|
6294
|
-
this.wsAuthPending.delete(ws);
|
|
6295
|
-
try {
|
|
6296
|
-
ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
6297
|
-
} catch {
|
|
6298
|
-
}
|
|
6299
|
-
}, this.wsAuthTimeoutMs);
|
|
6300
|
-
this.wsAuthPending.set(ws, timer);
|
|
6301
6320
|
},
|
|
6302
6321
|
handleWsMessage: async (ws, raw) => {
|
|
6303
6322
|
try {
|
|
6304
6323
|
const msg = JSON.parse(String(raw));
|
|
6305
|
-
if (!this.wsAuthed.has(ws)) {
|
|
6306
|
-
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
6307
|
-
const t = this.wsAuthPending.get(ws);
|
|
6308
|
-
if (t) clearTimeout(t);
|
|
6309
|
-
this.wsAuthPending.delete(ws);
|
|
6310
|
-
if (validateApiKey(msg.token, this.apiKey)) {
|
|
6311
|
-
this.completeWsAuth(ws);
|
|
6312
|
-
} else {
|
|
6313
|
-
try {
|
|
6314
|
-
ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
6315
|
-
} catch {
|
|
6316
|
-
}
|
|
6317
|
-
}
|
|
6318
|
-
}
|
|
6319
|
-
return;
|
|
6320
|
-
}
|
|
6321
6324
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
6322
6325
|
const oldClientId = this.wsToClientId.get(ws);
|
|
6323
6326
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -6328,7 +6331,15 @@ var StreamerServer = class {
|
|
|
6328
6331
|
this.addSessionSubscriber(msg.sessionId, ws);
|
|
6329
6332
|
if (this.ptyManager.hasSession(msg.sessionId)) {
|
|
6330
6333
|
const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
6331
|
-
|
|
6334
|
+
const userMessages = this.ptyManager.getInputHistory(msg.sessionId);
|
|
6335
|
+
ws.send(
|
|
6336
|
+
JSON.stringify({
|
|
6337
|
+
type: "terminal_replay",
|
|
6338
|
+
sessionId: msg.sessionId,
|
|
6339
|
+
lines,
|
|
6340
|
+
userMessages
|
|
6341
|
+
})
|
|
6342
|
+
);
|
|
6332
6343
|
}
|
|
6333
6344
|
const pendingGate = this.pendingPermission.get(msg.sessionId);
|
|
6334
6345
|
if (pendingGate) {
|
|
@@ -6364,20 +6375,12 @@ var StreamerServer = class {
|
|
|
6364
6375
|
}
|
|
6365
6376
|
}
|
|
6366
6377
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
6367
|
-
|
|
6368
|
-
this.startGraceTimer(msg.sessionId, 0);
|
|
6369
|
-
}
|
|
6378
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
6370
6379
|
}
|
|
6371
6380
|
} catch {
|
|
6372
6381
|
}
|
|
6373
6382
|
},
|
|
6374
6383
|
handleWsClose: (ws) => {
|
|
6375
|
-
const pendingTimer = this.wsAuthPending.get(ws);
|
|
6376
|
-
if (pendingTimer) {
|
|
6377
|
-
clearTimeout(pendingTimer);
|
|
6378
|
-
this.wsAuthPending.delete(ws);
|
|
6379
|
-
}
|
|
6380
|
-
this.wsAuthed.delete(ws);
|
|
6381
6384
|
const clientId = this.wsToClientId.get(ws);
|
|
6382
6385
|
if (clientId) {
|
|
6383
6386
|
this.clientIdToWs.delete(clientId);
|
|
@@ -6447,20 +6450,6 @@ var StreamerServer = class {
|
|
|
6447
6450
|
this.wsHub.broadcast(payload);
|
|
6448
6451
|
}
|
|
6449
6452
|
}
|
|
6450
|
-
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
6451
|
-
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
6452
|
-
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
6453
|
-
// broadcast.
|
|
6454
|
-
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6455
|
-
completeWsAuth(ws) {
|
|
6456
|
-
this.wsAuthed.add(ws);
|
|
6457
|
-
this.wsHub.addClient(ws);
|
|
6458
|
-
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6459
|
-
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6460
|
-
if (this.cacheReady) {
|
|
6461
|
-
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6462
|
-
}
|
|
6463
|
-
}
|
|
6464
6453
|
addSessionSubscriber(sessionId, ws) {
|
|
6465
6454
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
6466
6455
|
if (!subs) {
|
|
@@ -6747,9 +6736,6 @@ var StreamerServer = class {
|
|
|
6747
6736
|
this.ptyManager.dispose();
|
|
6748
6737
|
this.fileWatcher.dispose();
|
|
6749
6738
|
this.wsHub.dispose();
|
|
6750
|
-
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
6751
|
-
this.wsAuthPending.clear();
|
|
6752
|
-
this.wsAuthed.clear();
|
|
6753
6739
|
this.pairTokens.dispose();
|
|
6754
6740
|
if (this.dbPool) {
|
|
6755
6741
|
await this.dbPool.end();
|
|
@@ -7706,7 +7692,10 @@ var StreamerServer = class {
|
|
|
7706
7692
|
const projectPath = jsonlCwd ?? conv?.projectPath;
|
|
7707
7693
|
if (!projectPath) {
|
|
7708
7694
|
if (!conv && !jsonlPath) {
|
|
7709
|
-
json(res, 404, {
|
|
7695
|
+
json(res, 404, {
|
|
7696
|
+
error: "Conversation history file is missing; it can no longer be resumed",
|
|
7697
|
+
code: "history_file_missing"
|
|
7698
|
+
});
|
|
7710
7699
|
return;
|
|
7711
7700
|
}
|
|
7712
7701
|
json(res, 400, { error: "Could not determine project path" });
|
|
@@ -7876,10 +7865,6 @@ var StreamerServer = class {
|
|
|
7876
7865
|
return;
|
|
7877
7866
|
}
|
|
7878
7867
|
this.pendingPermission.set(sessionId, gate);
|
|
7879
|
-
const safeOptions = gate.options.map((o) => {
|
|
7880
|
-
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
7881
|
-
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
7882
|
-
});
|
|
7883
7868
|
const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
|
|
7884
7869
|
this.log.info(
|
|
7885
7870
|
`[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
|
|
@@ -7890,7 +7875,7 @@ var StreamerServer = class {
|
|
|
7890
7875
|
sessionId,
|
|
7891
7876
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
7892
7877
|
...gate.detail ? { detail: gate.detail } : {},
|
|
7893
|
-
options:
|
|
7878
|
+
options: gate.options,
|
|
7894
7879
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
7895
7880
|
});
|
|
7896
7881
|
}
|