nova-terminal-assistant 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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,460 @@
1
+ // ============================================================================
2
+ // SessionManager - Manages session lifecycle and conversation state
3
+ // ============================================================================
4
+
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import type {
10
+ SessionId,
11
+ MessageId,
12
+ Message,
13
+ Conversation,
14
+ SessionConfig,
15
+ SessionState,
16
+ SessionEvent,
17
+ ApprovalMode,
18
+ } from '../types/session.js';
19
+ import { createSessionId, createMessageId } from '../types/session.js';
20
+ import { SessionError, ContextOverflowError } from '../types/errors.js';
21
+
22
+ export type SessionListener<T = unknown> = (event: SessionEvent<T>) => void;
23
+
24
+ /** Serializable snapshot of a session for disk persistence */
25
+ export interface SessionSnapshot {
26
+ id: string;
27
+ config: SessionConfig;
28
+ messages: Message[];
29
+ createdAt: number;
30
+ updatedAt: number;
31
+ turnCount: number;
32
+ totalInputTokens: number;
33
+ totalOutputTokens: number;
34
+ title?: string; // first user message (for listing)
35
+ workingDirectory: string;
36
+ }
37
+
38
+ export class SessionManager {
39
+ private sessions = new Map<SessionId, Session>();
40
+ private listeners = new Map<string, Set<SessionListener>>();
41
+ private persistDir: string;
42
+
43
+ constructor(persistDir?: string) {
44
+ this.persistDir = persistDir || path.join(os.homedir(), '.nova', 'sessions');
45
+ }
46
+
47
+ /** Create a new session */
48
+ create(config: Partial<SessionConfig> & { workingDirectory: string }): Session {
49
+ const id = createSessionId(uuidv4());
50
+ const session: Session = {
51
+ id,
52
+ config: {
53
+ id,
54
+ model: config.model || 'claude-3-sonnet-20240229',
55
+ maxTokens: config.maxTokens || 4096,
56
+ temperature: config.temperature ?? 0.7,
57
+ workingDirectory: config.workingDirectory,
58
+ approvalMode: config.approvalMode || 'default',
59
+ streaming: config.streaming ?? true,
60
+ maxTurns: config.maxTurns || 100,
61
+ systemPrompt: config.systemPrompt,
62
+ name: config.name,
63
+ tools: config.tools,
64
+ mcpServers: config.mcpServers,
65
+ hooks: config.hooks,
66
+ metadata: config.metadata,
67
+ },
68
+ state: 'idle',
69
+ conversation: {
70
+ messages: [],
71
+ context: {
72
+ workingDirectory: config.workingDirectory,
73
+ environment: {},
74
+ sessionId: id,
75
+ toolResults: new Map(),
76
+ },
77
+ },
78
+ createdAt: Date.now(),
79
+ updatedAt: Date.now(),
80
+ turnCount: 0,
81
+ totalInputTokens: 0,
82
+ totalOutputTokens: 0,
83
+ };
84
+
85
+ this.sessions.set(id, session);
86
+ this.emit(id, 'state_change', { from: undefined, to: 'idle' });
87
+ return session;
88
+ }
89
+
90
+ /** Get a session by ID */
91
+ get(id: SessionId): Session | undefined {
92
+ return this.sessions.get(id);
93
+ }
94
+
95
+ /** Get all sessions */
96
+ getAll(): Session[] {
97
+ return Array.from(this.sessions.values());
98
+ }
99
+
100
+ /** Update session state */
101
+ setState(id: SessionId, newState: SessionState): void {
102
+ const session = this.sessions.get(id);
103
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
104
+
105
+ const oldState = session.state;
106
+ session.state = newState;
107
+ session.updatedAt = Date.now();
108
+
109
+ this.emit(id, 'state_change', { from: oldState, to: newState });
110
+ }
111
+
112
+ /** Add a message to the conversation */
113
+ addMessage(id: SessionId, role: Message['role'], content: Message['content']): Message {
114
+ const session = this.sessions.get(id);
115
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
116
+
117
+ const message: Message = {
118
+ id: createMessageId(uuidv4()),
119
+ role,
120
+ content,
121
+ timestamp: Date.now(),
122
+ createdAt: new Date(),
123
+ };
124
+
125
+ session.conversation.messages.push(message);
126
+ session.updatedAt = Date.now();
127
+
128
+ this.emit(id, 'message_added', { message });
129
+ return message;
130
+ }
131
+
132
+ /** Get conversation messages */
133
+ getMessages(id: SessionId): Message[] {
134
+ const session = this.sessions.get(id);
135
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
136
+ return session.conversation.messages;
137
+ }
138
+
139
+ /** Get the conversation */
140
+ getConversation(id: SessionId): Conversation {
141
+ const session = this.sessions.get(id);
142
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
143
+ return session.conversation;
144
+ }
145
+
146
+ /** Increment turn counter */
147
+ incrementTurn(id: SessionId): number {
148
+ const session = this.sessions.get(id);
149
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
150
+
151
+ session.turnCount++;
152
+ session.updatedAt = Date.now();
153
+
154
+ // Check max turns
155
+ if (session.config.maxTurns && session.turnCount >= session.config.maxTurns) {
156
+ this.setState(id, 'completed');
157
+ }
158
+
159
+ return session.turnCount;
160
+ }
161
+
162
+ /** Update token usage */
163
+ updateTokenUsage(id: SessionId, inputTokens: number, outputTokens: number): void {
164
+ const session = this.sessions.get(id);
165
+ if (!session) return;
166
+
167
+ session.totalInputTokens += inputTokens;
168
+ session.totalOutputTokens += outputTokens;
169
+ session.updatedAt = Date.now();
170
+ }
171
+
172
+ /** Get session config */
173
+ getConfig(id: SessionId): SessionConfig {
174
+ const session = this.sessions.get(id);
175
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
176
+ return session.config;
177
+ }
178
+
179
+ /** Get session state */
180
+ getState(id: SessionId): SessionState {
181
+ const session = this.sessions.get(id);
182
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
183
+ return session.state;
184
+ }
185
+
186
+ /** Store a tool result in session context */
187
+ storeToolResult(id: SessionId, key: string, value: unknown): void {
188
+ const session = this.sessions.get(id);
189
+ if (!session) return;
190
+ session.conversation.context?.toolResults.set(key, value);
191
+ }
192
+
193
+ /** Get a stored tool result */
194
+ getToolResult(id: SessionId, key: string): unknown {
195
+ const session = this.sessions.get(id);
196
+ return session?.conversation.context?.toolResults.get(key);
197
+ }
198
+
199
+ /** Truncate conversation to manage context window */
200
+ truncateConversation(id: SessionId, maxMessages: number, keepSystemMessages: boolean = true): Message[] {
201
+ const session = this.sessions.get(id);
202
+ if (!session) throw new SessionError(`Session not found: ${id}`, id);
203
+
204
+ const messages = session.conversation.messages;
205
+ if (messages.length <= maxMessages) return [];
206
+
207
+ // Keep system messages if requested
208
+ const systemMessages = keepSystemMessages
209
+ ? messages.filter((m) => m.role === 'system')
210
+ : [];
211
+ const nonSystemMessages = messages.filter((m) => m.role !== 'system');
212
+
213
+ const removed = nonSystemMessages.slice(0, nonSystemMessages.length - maxMessages);
214
+ const kept = nonSystemMessages.slice(nonSystemMessages.length - maxMessages);
215
+
216
+ session.conversation.messages = [...systemMessages, ...kept];
217
+ session.updatedAt = Date.now();
218
+
219
+ this.emit(id, 'context_update', { removedCount: removed.length, keptCount: kept.length });
220
+ return removed;
221
+ }
222
+
223
+ /** Update session environment variables */
224
+ setEnvironment(id: SessionId, env: Record<string, string>): void {
225
+ const session = this.sessions.get(id);
226
+ if (!session) return;
227
+ Object.assign(session.conversation.context?.environment || {}, env);
228
+ session.updatedAt = Date.now();
229
+ }
230
+
231
+ /** Register an event listener */
232
+ on<T = unknown>(id: SessionId, eventType: string, listener: SessionListener<T>): () => void {
233
+ const key = `${id}:${eventType}`;
234
+ if (!this.listeners.has(key)) {
235
+ this.listeners.set(key, new Set());
236
+ }
237
+ this.listeners.get(key)!.add(listener as SessionListener);
238
+
239
+ return () => {
240
+ this.listeners.get(key)?.delete(listener as SessionListener);
241
+ };
242
+ }
243
+
244
+ /** Emit an event */
245
+ private emit<T = unknown>(id: SessionId, eventType: string, data: T): void {
246
+ const event: SessionEvent<T> = {
247
+ type: eventType as Session['id'] extends string ? 'state_change' : never,
248
+ sessionId: id,
249
+ timestamp: Date.now(),
250
+ data,
251
+ };
252
+
253
+ const key = `${id}:${eventType}`;
254
+ const listeners = this.listeners.get(key);
255
+ if (listeners) {
256
+ for (const listener of listeners) {
257
+ try {
258
+ listener(event);
259
+ } catch {
260
+ // Swallow listener errors
261
+ }
262
+ }
263
+ }
264
+
265
+ // Also emit to wildcard listeners
266
+ const wildcardKey = `${id}:*`;
267
+ const wildcardListeners = this.listeners.get(wildcardKey);
268
+ if (wildcardListeners) {
269
+ for (const listener of wildcardListeners) {
270
+ try {
271
+ listener(event);
272
+ } catch {
273
+ // Swallow listener errors
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ /** Delete a session */
280
+ delete(id: SessionId): boolean {
281
+ return this.sessions.delete(id);
282
+ }
283
+
284
+ /** Get session statistics */
285
+ getStats(id: SessionId) {
286
+ const session = this.sessions.get(id);
287
+ if (!session) return null;
288
+ return {
289
+ id: session.id,
290
+ state: session.state,
291
+ turnCount: session.turnCount,
292
+ messageCount: session.conversation.messages.length,
293
+ totalInputTokens: session.totalInputTokens,
294
+ totalOutputTokens: session.totalOutputTokens,
295
+ createdAt: session.createdAt,
296
+ updatedAt: session.updatedAt,
297
+ };
298
+ }
299
+
300
+ // ========================================================================
301
+ // Persistence: save/load/list sessions to disk (~/.nova/sessions/)
302
+ // ========================================================================
303
+
304
+ private ensurePersistDir(): void {
305
+ if (!fs.existsSync(this.persistDir)) {
306
+ fs.mkdirSync(this.persistDir, { recursive: true });
307
+ }
308
+ }
309
+
310
+ private sessionFilePath(id: string): string {
311
+ return path.join(this.persistDir, `${id}.json`);
312
+ }
313
+
314
+ /** Save a session to disk. Called after every agent turn. */
315
+ persist(id: SessionId): void {
316
+ try {
317
+ this.ensurePersistDir();
318
+ const session = this.sessions.get(id);
319
+ if (!session) return;
320
+
321
+ // Determine title from first user message
322
+ const firstUser = session.conversation.messages.find((m) => m.role === 'user');
323
+ let title = 'New session';
324
+ if (firstUser) {
325
+ const c = firstUser.content;
326
+ const text = typeof c === 'string' ? c : Array.isArray(c) ? c.map((b: any) => (b.type === 'text' ? b.text : '')).join('') : '';
327
+ title = text.slice(0, 80).replace(/\n/g, ' ').trim() || 'New session';
328
+ }
329
+
330
+ const snapshot: SessionSnapshot = {
331
+ id: session.id,
332
+ config: session.config,
333
+ messages: session.conversation.messages,
334
+ createdAt: session.createdAt,
335
+ updatedAt: session.updatedAt,
336
+ turnCount: session.turnCount,
337
+ totalInputTokens: session.totalInputTokens,
338
+ totalOutputTokens: session.totalOutputTokens,
339
+ title,
340
+ workingDirectory: session.config.workingDirectory,
341
+ };
342
+
343
+ fs.writeFileSync(this.sessionFilePath(session.id), JSON.stringify(snapshot, null, 2), 'utf-8');
344
+ } catch {
345
+ // Persist is best-effort
346
+ }
347
+ }
348
+
349
+ /** Load a session from disk by its ID. Returns session or null. */
350
+ loadFromDisk(rawId: string): Session | null {
351
+ try {
352
+ const filePath = this.sessionFilePath(rawId);
353
+ if (!fs.existsSync(filePath)) return null;
354
+
355
+ const raw = fs.readFileSync(filePath, 'utf-8');
356
+ const snap: SessionSnapshot = JSON.parse(raw);
357
+
358
+ const id = createSessionId(snap.id);
359
+ const session: Session = {
360
+ id,
361
+ config: { ...snap.config, id },
362
+ state: 'idle',
363
+ conversation: {
364
+ messages: snap.messages,
365
+ context: {
366
+ workingDirectory: snap.workingDirectory,
367
+ environment: {},
368
+ sessionId: id,
369
+ toolResults: new Map(),
370
+ },
371
+ },
372
+ createdAt: snap.createdAt,
373
+ updatedAt: snap.updatedAt,
374
+ turnCount: snap.turnCount,
375
+ totalInputTokens: snap.totalInputTokens,
376
+ totalOutputTokens: snap.totalOutputTokens,
377
+ };
378
+
379
+ this.sessions.set(id, session);
380
+ return session;
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+
386
+ /** Load the most recent session from disk (for -c / --continue). */
387
+ loadLatestSession(): Session | null {
388
+ try {
389
+ this.ensurePersistDir();
390
+ const files = fs.readdirSync(this.persistDir)
391
+ .filter((f) => f.endsWith('.json'))
392
+ .map((f) => ({
393
+ name: f,
394
+ mtime: fs.statSync(path.join(this.persistDir, f)).mtime.getTime(),
395
+ }))
396
+ .sort((a, b) => b.mtime - a.mtime);
397
+
398
+ if (files.length === 0) return null;
399
+ const id = files[0].name.replace('.json', '');
400
+ return this.loadFromDisk(id);
401
+ } catch {
402
+ return null;
403
+ }
404
+ }
405
+
406
+ /** List all persisted sessions (most recent first). */
407
+ listPersistedSessions(limit = 20): SessionSnapshot[] {
408
+ try {
409
+ this.ensurePersistDir();
410
+ const files = fs.readdirSync(this.persistDir)
411
+ .filter((f) => f.endsWith('.json'))
412
+ .map((f) => {
413
+ const p = path.join(this.persistDir, f);
414
+ return { name: f, mtime: fs.statSync(p).mtime.getTime() };
415
+ })
416
+ .sort((a, b) => b.mtime - a.mtime)
417
+ .slice(0, limit);
418
+
419
+ const snapshots: SessionSnapshot[] = [];
420
+ for (const f of files) {
421
+ try {
422
+ const raw = fs.readFileSync(path.join(this.persistDir, f.name), 'utf-8');
423
+ snapshots.push(JSON.parse(raw));
424
+ } catch {
425
+ // skip corrupt files
426
+ }
427
+ }
428
+ return snapshots;
429
+ } catch {
430
+ return [];
431
+ }
432
+ }
433
+
434
+ /** Delete a persisted session file from disk */
435
+ deletePersisted(rawId: string): boolean {
436
+ try {
437
+ const filePath = this.sessionFilePath(rawId);
438
+ if (fs.existsSync(filePath)) {
439
+ fs.unlinkSync(filePath);
440
+ return true;
441
+ }
442
+ return false;
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
447
+ }
448
+
449
+ /** Internal session data structure */
450
+ interface Session {
451
+ id: SessionId;
452
+ config: SessionConfig;
453
+ state: SessionState;
454
+ conversation: Conversation;
455
+ createdAt: number;
456
+ updatedAt: number;
457
+ turnCount: number;
458
+ totalInputTokens: number;
459
+ totalOutputTokens: number;
460
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { SessionManager } from './SessionManager.js';
2
+ export { AgentLoop } from './AgentLoop.js';
3
+ export type { AgentLoopOptions, AgentLoopResult } from './AgentLoop.js';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Telemetry.d.ts","sourceRoot":"","sources":["Telemetry.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,eAAe;IAKnC,qBAAqB;IACrB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IAoBpE,0BAA0B;IACpB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5B,8BAA8B;IAC9B,cAAc,CAAC,UAAU,GAAE,MAAc,GAAG,IAAI;IAKhD,6BAA6B;IAC7B,aAAa,IAAI,IAAI;IAOrB,wBAAwB;IACxB,OAAO,IAAI,IAAI;IAMf,SAAS,IAAI,OAAO;CAGrB"}
@@ -0,0 +1,90 @@
1
+ // ============================================================================
2
+ // Telemetry - Anonymous usage tracking
3
+ // ============================================================================
4
+
5
+ import { randomBytes } from 'node:crypto';
6
+ import type { TelemetryConfig } from '../types/config.js';
7
+
8
+ export interface TelemetryEvent {
9
+ event: string;
10
+ properties: Record<string, unknown>;
11
+ timestamp: number;
12
+ }
13
+
14
+ export class Telemetry {
15
+ private config: TelemetryConfig;
16
+ private queue: TelemetryEvent[] = [];
17
+ private flushTimer: ReturnType<typeof setInterval> | null = null;
18
+ private clientId: string;
19
+
20
+ constructor(config: TelemetryConfig) {
21
+ this.config = config;
22
+ this.clientId = config.clientId || randomBytes(16).toString('hex');
23
+ }
24
+
25
+ /** Track an event */
26
+ track(event: string, properties: Record<string, unknown> = {}): void {
27
+ if (!this.config.enabled) return;
28
+
29
+ const shouldTrack = (
30
+ (event.startsWith('usage') && this.config.track?.usage) ||
31
+ (event.startsWith('error') && this.config.track?.errors) ||
32
+ (event.startsWith('performance') && this.config.track?.performance)
33
+ );
34
+
35
+ if (!shouldTrack) return;
36
+
37
+ this.queue.push({
38
+ event,
39
+ properties: { ...properties, clientId: this.clientId, version: '0.1.0' },
40
+ timestamp: Date.now(),
41
+ });
42
+
43
+ if (this.queue.length >= 10) this.flush();
44
+ }
45
+
46
+ /** Flush queued events */
47
+ async flush(): Promise<void> {
48
+ if (!this.config.enabled || this.queue.length === 0) return;
49
+
50
+ const events = [...this.queue];
51
+ this.queue = [];
52
+
53
+ if (this.config.endpoint) {
54
+ try {
55
+ await fetch(this.config.endpoint, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({ events }),
59
+ });
60
+ } catch {
61
+ // Silently fail
62
+ }
63
+ }
64
+ }
65
+
66
+ /** Start periodic flushing */
67
+ startAutoFlush(intervalMs: number = 60000): void {
68
+ if (this.flushTimer) return;
69
+ this.flushTimer = setInterval(() => this.flush(), intervalMs);
70
+ }
71
+
72
+ /** Stop periodic flushing */
73
+ stopAutoFlush(): void {
74
+ if (this.flushTimer) {
75
+ clearInterval(this.flushTimer);
76
+ this.flushTimer = null;
77
+ }
78
+ }
79
+
80
+ /** Disable telemetry */
81
+ disable(): void {
82
+ this.config.enabled = false;
83
+ this.stopAutoFlush();
84
+ this.queue = [];
85
+ }
86
+
87
+ isEnabled(): boolean {
88
+ return this.config.enabled;
89
+ }
90
+ }