@appkit/llamacpp-cli 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +294 -168
  2. package/dist/cli.js +35 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/launch/claude.d.ts +6 -0
  5. package/dist/commands/launch/claude.d.ts.map +1 -0
  6. package/dist/commands/launch/claude.js +277 -0
  7. package/dist/commands/launch/claude.js.map +1 -0
  8. package/dist/lib/integration-checker.d.ts +26 -0
  9. package/dist/lib/integration-checker.d.ts.map +1 -0
  10. package/dist/lib/integration-checker.js +77 -0
  11. package/dist/lib/integration-checker.js.map +1 -0
  12. package/dist/lib/router-manager.d.ts +4 -0
  13. package/dist/lib/router-manager.d.ts.map +1 -1
  14. package/dist/lib/router-manager.js +10 -0
  15. package/dist/lib/router-manager.js.map +1 -1
  16. package/dist/lib/router-server.d.ts +13 -0
  17. package/dist/lib/router-server.d.ts.map +1 -1
  18. package/dist/lib/router-server.js +267 -7
  19. package/dist/lib/router-server.js.map +1 -1
  20. package/dist/types/integration-config.d.ts +28 -0
  21. package/dist/types/integration-config.d.ts.map +1 -0
  22. package/dist/types/integration-config.js +3 -0
  23. package/dist/types/integration-config.js.map +1 -0
  24. package/package.json +10 -2
  25. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  26. package/web/dist/assets/index-CVmonw3T.js +17 -0
  27. package/web/{index.html → dist/index.html} +2 -1
  28. package/.versionrc.json +0 -16
  29. package/CHANGELOG.md +0 -213
  30. package/docs/images/.gitkeep +0 -1
  31. package/docs/images/web-ui-servers.png +0 -0
  32. package/src/cli.ts +0 -523
  33. package/src/commands/admin/config.ts +0 -121
  34. package/src/commands/admin/logs.ts +0 -91
  35. package/src/commands/admin/restart.ts +0 -26
  36. package/src/commands/admin/start.ts +0 -27
  37. package/src/commands/admin/status.ts +0 -84
  38. package/src/commands/admin/stop.ts +0 -16
  39. package/src/commands/config-global.ts +0 -38
  40. package/src/commands/config.ts +0 -323
  41. package/src/commands/create.ts +0 -183
  42. package/src/commands/delete.ts +0 -74
  43. package/src/commands/list.ts +0 -37
  44. package/src/commands/logs-all.ts +0 -251
  45. package/src/commands/logs.ts +0 -345
  46. package/src/commands/monitor.ts +0 -110
  47. package/src/commands/ps.ts +0 -84
  48. package/src/commands/pull.ts +0 -44
  49. package/src/commands/rm.ts +0 -107
  50. package/src/commands/router/config.ts +0 -116
  51. package/src/commands/router/logs.ts +0 -256
  52. package/src/commands/router/restart.ts +0 -36
  53. package/src/commands/router/start.ts +0 -60
  54. package/src/commands/router/status.ts +0 -119
  55. package/src/commands/router/stop.ts +0 -33
  56. package/src/commands/run.ts +0 -233
  57. package/src/commands/search.ts +0 -107
  58. package/src/commands/server-show.ts +0 -161
  59. package/src/commands/show.ts +0 -207
  60. package/src/commands/start.ts +0 -101
  61. package/src/commands/stop.ts +0 -39
  62. package/src/commands/tui.ts +0 -25
  63. package/src/lib/admin-manager.ts +0 -435
  64. package/src/lib/admin-server.ts +0 -1243
  65. package/src/lib/config-generator.ts +0 -130
  66. package/src/lib/download-job-manager.ts +0 -213
  67. package/src/lib/history-manager.ts +0 -172
  68. package/src/lib/launchctl-manager.ts +0 -225
  69. package/src/lib/metrics-aggregator.ts +0 -257
  70. package/src/lib/model-downloader.ts +0 -328
  71. package/src/lib/model-scanner.ts +0 -157
  72. package/src/lib/model-search.ts +0 -114
  73. package/src/lib/models-dir-setup.ts +0 -46
  74. package/src/lib/port-manager.ts +0 -80
  75. package/src/lib/router-logger.ts +0 -201
  76. package/src/lib/router-manager.ts +0 -414
  77. package/src/lib/router-server.ts +0 -538
  78. package/src/lib/state-manager.ts +0 -206
  79. package/src/lib/status-checker.ts +0 -113
  80. package/src/lib/system-collector.ts +0 -315
  81. package/src/tui/ConfigApp.ts +0 -1085
  82. package/src/tui/HistoricalMonitorApp.ts +0 -587
  83. package/src/tui/ModelsApp.ts +0 -368
  84. package/src/tui/MonitorApp.ts +0 -386
  85. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  86. package/src/tui/RootNavigator.ts +0 -74
  87. package/src/tui/SearchApp.ts +0 -511
  88. package/src/tui/SplashScreen.ts +0 -149
  89. package/src/types/admin-config.ts +0 -25
  90. package/src/types/global-config.ts +0 -26
  91. package/src/types/history-types.ts +0 -39
  92. package/src/types/model-info.ts +0 -8
  93. package/src/types/monitor-types.ts +0 -162
  94. package/src/types/router-config.ts +0 -25
  95. package/src/types/server-config.ts +0 -46
  96. package/src/utils/downsample-utils.ts +0 -128
  97. package/src/utils/file-utils.ts +0 -146
  98. package/src/utils/format-utils.ts +0 -98
  99. package/src/utils/log-parser.ts +0 -284
  100. package/src/utils/log-utils.ts +0 -178
  101. package/src/utils/process-utils.ts +0 -316
  102. package/src/utils/prompt-utils.ts +0 -47
  103. package/test-load.sh +0 -100
  104. package/tsconfig.json +0 -20
  105. package/web/eslint.config.js +0 -23
  106. package/web/llamacpp-web-dist.tar.gz +0 -0
  107. package/web/package-lock.json +0 -4017
  108. package/web/package.json +0 -38
  109. package/web/postcss.config.js +0 -6
  110. package/web/src/App.css +0 -42
  111. package/web/src/App.tsx +0 -86
  112. package/web/src/assets/react.svg +0 -1
  113. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  114. package/web/src/components/CreateServerModal.tsx +0 -372
  115. package/web/src/components/DownloadProgress.tsx +0 -123
  116. package/web/src/components/Nav.tsx +0 -89
  117. package/web/src/components/RouterConfigModal.tsx +0 -240
  118. package/web/src/components/SearchModal.tsx +0 -306
  119. package/web/src/components/ServerConfigModal.tsx +0 -291
  120. package/web/src/hooks/useApi.ts +0 -259
  121. package/web/src/index.css +0 -42
  122. package/web/src/lib/api.ts +0 -226
  123. package/web/src/main.tsx +0 -10
  124. package/web/src/pages/Dashboard.tsx +0 -103
  125. package/web/src/pages/Models.tsx +0 -258
  126. package/web/src/pages/Router.tsx +0 -270
  127. package/web/src/pages/RouterLogs.tsx +0 -201
  128. package/web/src/pages/ServerLogs.tsx +0 -553
  129. package/web/src/pages/Servers.tsx +0 -358
  130. package/web/src/types/api.ts +0 -140
  131. package/web/tailwind.config.js +0 -31
  132. package/web/tsconfig.app.json +0 -28
  133. package/web/tsconfig.json +0 -7
  134. package/web/tsconfig.node.json +0 -26
  135. package/web/vite.config.ts +0 -25
  136. /package/web/{public → dist}/vite.svg +0 -0
@@ -1,130 +0,0 @@
1
- import * as os from 'os';
2
- import * as path from 'path';
3
- import { ServerConfig, sanitizeModelName } from '../types/server-config';
4
- import { getLogsDir, getLaunchAgentsDir } from '../utils/file-utils';
5
- import { stateManager } from './state-manager';
6
-
7
- export interface ServerOptions {
8
- port?: number;
9
- host?: string;
10
- threads?: number;
11
- ctxSize?: number;
12
- gpuLayers?: number;
13
- embeddings?: boolean;
14
- jinja?: boolean;
15
- verbose?: boolean;
16
- customFlags?: string[];
17
- }
18
-
19
- export interface SmartDefaults {
20
- threads: number;
21
- ctxSize: number;
22
- gpuLayers: number;
23
- }
24
-
25
- export class ConfigGenerator {
26
- /**
27
- * Calculate smart defaults based on model size
28
- */
29
- calculateSmartDefaults(modelSizeBytes: number): SmartDefaults {
30
- const sizeGB = modelSizeBytes / (1024 ** 3);
31
-
32
- // Context size based on model size
33
- let ctxSize: number;
34
- if (sizeGB < 1) {
35
- ctxSize = 2048; // < 1GB: small context
36
- } else if (sizeGB < 3) {
37
- ctxSize = 4096; // 1-3GB: medium
38
- } else if (sizeGB < 6) {
39
- ctxSize = 8192; // 3-6GB: large
40
- } else {
41
- ctxSize = 16384; // 6GB+: very large
42
- }
43
-
44
- // GPU layers - always max for Metal (macOS)
45
- const gpuLayers = 60; // llama.cpp auto-detects optimal value
46
-
47
- // Threads - use half of available cores (better performance)
48
- const cpuCount = os.cpus().length;
49
- const threads = Math.max(4, Math.floor(cpuCount / 2));
50
-
51
- return { threads, ctxSize, gpuLayers };
52
- }
53
-
54
- /**
55
- * Generate server configuration
56
- */
57
- async generateConfig(
58
- modelPath: string,
59
- modelName: string,
60
- modelSize: number,
61
- port: number,
62
- options?: ServerOptions
63
- ): Promise<ServerConfig> {
64
- // Calculate smart defaults
65
- const smartDefaults = this.calculateSmartDefaults(modelSize);
66
-
67
- // Apply user overrides
68
- const host = options?.host ?? '127.0.0.1'; // Default to localhost (secure)
69
- const threads = options?.threads ?? smartDefaults.threads;
70
- const ctxSize = options?.ctxSize ?? smartDefaults.ctxSize;
71
- const gpuLayers = options?.gpuLayers ?? smartDefaults.gpuLayers;
72
- const embeddings = options?.embeddings ?? true;
73
- const jinja = options?.jinja ?? true;
74
- const verbose = options?.verbose ?? true; // Default to true (HTTP request logging)
75
- const customFlags = options?.customFlags; // Optional custom flags
76
-
77
- // Generate server ID
78
- const id = sanitizeModelName(modelName);
79
-
80
- // Generate paths
81
- const label = `com.llama.${id}`;
82
- const plistPath = path.join(getLaunchAgentsDir(), `${label}.plist`);
83
- const logsDir = getLogsDir();
84
- const stdoutPath = path.join(logsDir, `${id}.stdout`);
85
- const stderrPath = path.join(logsDir, `${id}.stderr`);
86
-
87
- const config: ServerConfig = {
88
- id,
89
- modelPath,
90
- modelName,
91
- port,
92
- host,
93
- threads,
94
- ctxSize,
95
- gpuLayers,
96
- embeddings,
97
- jinja,
98
- verbose,
99
- customFlags,
100
- status: 'stopped',
101
- createdAt: new Date().toISOString(),
102
- plistPath,
103
- label,
104
- stdoutPath,
105
- stderrPath,
106
- };
107
-
108
- return config;
109
- }
110
-
111
- /**
112
- * Merge global defaults with user options
113
- */
114
- async mergeWithGlobalDefaults(options?: ServerOptions): Promise<Partial<ServerOptions>> {
115
- const globalConfig = await stateManager.loadGlobalConfig();
116
-
117
- return {
118
- host: options?.host ?? '127.0.0.1',
119
- threads: options?.threads ?? globalConfig.defaults.threads,
120
- ctxSize: options?.ctxSize ?? globalConfig.defaults.ctxSize,
121
- gpuLayers: options?.gpuLayers ?? globalConfig.defaults.gpuLayers,
122
- embeddings: options?.embeddings ?? true,
123
- jinja: options?.jinja ?? true,
124
- verbose: options?.verbose ?? true,
125
- };
126
- }
127
- }
128
-
129
- // Export singleton instance
130
- export const configGenerator = new ConfigGenerator();
@@ -1,213 +0,0 @@
1
- import * as path from 'path';
2
- import { modelDownloader, DownloadProgress } from './model-downloader';
3
- import { stateManager } from './state-manager';
4
-
5
- export type DownloadJobStatus = 'pending' | 'downloading' | 'completed' | 'failed' | 'cancelled';
6
-
7
- export interface DownloadJob {
8
- id: string;
9
- repo: string;
10
- filename: string;
11
- status: DownloadJobStatus;
12
- progress: {
13
- downloaded: number;
14
- total: number;
15
- percentage: number;
16
- speed: string;
17
- } | null;
18
- error?: string;
19
- createdAt: string;
20
- completedAt?: string;
21
- }
22
-
23
- interface InternalJob extends DownloadJob {
24
- abortController: AbortController;
25
- }
26
-
27
- /**
28
- * Manages download jobs with progress tracking and cancellation support
29
- */
30
- class DownloadJobManager {
31
- private jobs: Map<string, InternalJob> = new Map();
32
- private jobCounter = 0;
33
- private cleanupInterval: NodeJS.Timeout | null = null;
34
-
35
- constructor() {
36
- // Auto-cleanup completed/failed jobs after 5 minutes
37
- this.cleanupInterval = setInterval(() => this.cleanupOldJobs(), 60000);
38
- }
39
-
40
- /**
41
- * Create a new download job
42
- */
43
- createJob(repo: string, filename: string): string {
44
- const id = `download-${Date.now()}-${++this.jobCounter}`;
45
- const abortController = new AbortController();
46
-
47
- const job: InternalJob = {
48
- id,
49
- repo,
50
- filename,
51
- status: 'pending',
52
- progress: null,
53
- createdAt: new Date().toISOString(),
54
- abortController,
55
- };
56
-
57
- this.jobs.set(id, job);
58
-
59
- // Start download asynchronously
60
- this.startDownload(job);
61
-
62
- return id;
63
- }
64
-
65
- /**
66
- * Get a job by ID
67
- */
68
- getJob(id: string): DownloadJob | null {
69
- const job = this.jobs.get(id);
70
- if (!job) return null;
71
-
72
- // Return public job info (without abortController)
73
- return this.toPublicJob(job);
74
- }
75
-
76
- /**
77
- * List all jobs
78
- */
79
- listJobs(): DownloadJob[] {
80
- return Array.from(this.jobs.values()).map(job => this.toPublicJob(job));
81
- }
82
-
83
- /**
84
- * Cancel a download job
85
- */
86
- cancelJob(id: string): boolean {
87
- const job = this.jobs.get(id);
88
- if (!job) return false;
89
-
90
- if (job.status === 'pending' || job.status === 'downloading') {
91
- job.abortController.abort();
92
- job.status = 'cancelled';
93
- job.completedAt = new Date().toISOString();
94
- return true;
95
- }
96
-
97
- return false;
98
- }
99
-
100
- /**
101
- * Delete a job from the list
102
- */
103
- deleteJob(id: string): boolean {
104
- const job = this.jobs.get(id);
105
- if (!job) return false;
106
-
107
- // Cancel if still running
108
- if (job.status === 'pending' || job.status === 'downloading') {
109
- job.abortController.abort();
110
- }
111
-
112
- this.jobs.delete(id);
113
- return true;
114
- }
115
-
116
- /**
117
- * Start the download process for a job
118
- */
119
- private async startDownload(job: InternalJob): Promise<void> {
120
- job.status = 'downloading';
121
-
122
- try {
123
- const modelsDir = await stateManager.getModelsDirectory();
124
-
125
- await modelDownloader.downloadModel(
126
- job.repo,
127
- job.filename,
128
- (progress: DownloadProgress) => {
129
- job.progress = {
130
- downloaded: progress.downloaded,
131
- total: progress.total,
132
- percentage: progress.percentage,
133
- speed: progress.speed,
134
- };
135
- },
136
- modelsDir,
137
- {
138
- silent: true,
139
- signal: job.abortController.signal,
140
- }
141
- );
142
-
143
- // Only mark as completed if not cancelled
144
- if (job.status === 'downloading') {
145
- job.status = 'completed';
146
- job.completedAt = new Date().toISOString();
147
- // Ensure progress shows 100%
148
- if (job.progress) {
149
- job.progress.percentage = 100;
150
- }
151
- }
152
- } catch (error) {
153
- // Check if this was a cancellation (status may have been set by cancelJob)
154
- const currentStatus = job.status as DownloadJobStatus;
155
- if (currentStatus === 'cancelled') {
156
- return;
157
- }
158
-
159
- const message = (error as Error).message;
160
- if (message.includes('cancelled') || message.includes('interrupted')) {
161
- job.status = 'cancelled';
162
- } else {
163
- job.status = 'failed';
164
- job.error = message;
165
- }
166
- job.completedAt = new Date().toISOString();
167
- }
168
- }
169
-
170
- /**
171
- * Convert internal job to public job (strips internal fields)
172
- */
173
- private toPublicJob(job: InternalJob): DownloadJob {
174
- const { abortController, ...publicJob } = job;
175
- return publicJob;
176
- }
177
-
178
- /**
179
- * Clean up old completed/failed jobs
180
- */
181
- private cleanupOldJobs(): void {
182
- const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
183
-
184
- for (const [id, job] of this.jobs.entries()) {
185
- if (
186
- job.completedAt &&
187
- new Date(job.completedAt).getTime() < fiveMinutesAgo
188
- ) {
189
- this.jobs.delete(id);
190
- }
191
- }
192
- }
193
-
194
- /**
195
- * Cleanup on shutdown
196
- */
197
- shutdown(): void {
198
- if (this.cleanupInterval) {
199
- clearInterval(this.cleanupInterval);
200
- this.cleanupInterval = null;
201
- }
202
-
203
- // Cancel all active downloads
204
- for (const job of this.jobs.values()) {
205
- if (job.status === 'pending' || job.status === 'downloading') {
206
- job.abortController.abort();
207
- }
208
- }
209
- }
210
- }
211
-
212
- // Export singleton instance
213
- export const downloadJobManager = new DownloadJobManager();
@@ -1,172 +0,0 @@
1
- import { mkdir, readFile, writeFile, access, rename } from 'fs/promises';
2
- import { join } from 'path';
3
- import { homedir } from 'os';
4
- import { ServerMetrics, SystemMetrics } from '../types/monitor-types.js';
5
- import { HistoryData, HistorySnapshot, TIME_WINDOW_HOURS, TimeWindow } from '../types/history-types.js';
6
-
7
- export class HistoryManager {
8
- private serverId: string;
9
- private historyDir: string;
10
- private historyPath: string;
11
- private readonly MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
12
-
13
- constructor(serverId: string) {
14
- this.serverId = serverId;
15
- this.historyDir = join(homedir(), '.llamacpp', 'history');
16
- this.historyPath = join(this.historyDir, `${serverId}.json`);
17
- }
18
-
19
- /**
20
- * Append a new snapshot to history (with auto-pruning)
21
- */
22
- async appendSnapshot(serverMetrics: ServerMetrics, systemMetrics?: SystemMetrics): Promise<void> {
23
- try {
24
- // Ensure history directory exists
25
- await mkdir(this.historyDir, { recursive: true });
26
-
27
- // Load existing history
28
- const historyData = await this.loadHistoryData();
29
-
30
- // Create new snapshot
31
- const snapshot: HistorySnapshot = {
32
- timestamp: Date.now(),
33
- server: {
34
- healthy: serverMetrics.healthy,
35
- uptime: serverMetrics.uptime,
36
- activeSlots: serverMetrics.activeSlots,
37
- idleSlots: serverMetrics.idleSlots,
38
- totalSlots: serverMetrics.totalSlots,
39
- avgPromptSpeed: serverMetrics.avgPromptSpeed,
40
- avgGenerateSpeed: serverMetrics.avgGenerateSpeed,
41
- processMemory: serverMetrics.processMemory,
42
- processCpuUsage: serverMetrics.processCpuUsage,
43
- },
44
- system: systemMetrics ? {
45
- gpuUsage: systemMetrics.gpuUsage,
46
- cpuUsage: systemMetrics.cpuUsage,
47
- aneUsage: systemMetrics.aneUsage,
48
- temperature: systemMetrics.temperature,
49
- memoryUsed: systemMetrics.memoryUsed,
50
- memoryTotal: systemMetrics.memoryTotal,
51
- } : undefined,
52
- };
53
-
54
- // Append new snapshot
55
- historyData.snapshots.push(snapshot);
56
-
57
- // Prune old snapshots (keep only last 24h)
58
- historyData.snapshots = this.pruneOldSnapshots(historyData.snapshots, this.MAX_AGE_MS);
59
-
60
- // Atomic write: write to temp file in same directory, then rename
61
- // This prevents read collisions during concurrent access
62
- // IMPORTANT: temp file MUST be in same directory as destination for rename to work across filesystems
63
- const tempPath = join(this.historyDir, `.${this.serverId}-${Date.now()}.tmp`);
64
- await writeFile(tempPath, JSON.stringify(historyData, null, 2), 'utf-8');
65
- await rename(tempPath, this.historyPath);
66
- } catch (error) {
67
- // Silent failure - don't interrupt monitoring
68
- // Don't throw - just return silently to avoid polluting console
69
- return;
70
- }
71
- }
72
-
73
- /**
74
- * Load all snapshots within specified time window
75
- */
76
- async loadHistory(windowHours: number): Promise<HistorySnapshot[]> {
77
- // Retry logic for file I/O collisions during concurrent read/write
78
- const maxRetries = 3;
79
- let lastError: Error | null = null;
80
-
81
- for (let attempt = 0; attempt < maxRetries; attempt++) {
82
- try {
83
- const historyData = await this.loadHistoryData();
84
- return this.filterByTimeWindow(historyData.snapshots, windowHours);
85
- } catch (error) {
86
- lastError = error as Error;
87
- // Wait briefly before retry (exponential backoff)
88
- if (attempt < maxRetries - 1) {
89
- await new Promise(resolve => setTimeout(resolve, 50 * Math.pow(2, attempt)));
90
- }
91
- }
92
- }
93
-
94
- // All retries failed - throw error so it can be handled upstream
95
- throw new Error(`Failed to load history after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
96
- }
97
-
98
- /**
99
- * Load history for specific time window type
100
- */
101
- async loadHistoryByWindow(window: TimeWindow): Promise<HistorySnapshot[]> {
102
- return this.loadHistory(TIME_WINDOW_HOURS[window]);
103
- }
104
-
105
- /**
106
- * Get file path for server history
107
- */
108
- getHistoryPath(): string {
109
- return this.historyPath;
110
- }
111
-
112
- /**
113
- * Check if history file exists
114
- */
115
- async hasHistory(): Promise<boolean> {
116
- try {
117
- await access(this.historyPath);
118
- return true;
119
- } catch {
120
- return false;
121
- }
122
- }
123
-
124
- /**
125
- * Clear all history for server
126
- */
127
- async clearHistory(): Promise<void> {
128
- const emptyHistory: HistoryData = {
129
- serverId: this.serverId,
130
- snapshots: [],
131
- };
132
-
133
- await mkdir(this.historyDir, { recursive: true });
134
-
135
- // Atomic write - temp file in same directory as destination
136
- const tempPath = join(this.historyDir, `.${this.serverId}-${Date.now()}.tmp`);
137
- await writeFile(tempPath, JSON.stringify(emptyHistory, null, 2), 'utf-8');
138
- await rename(tempPath, this.historyPath);
139
- }
140
-
141
- /**
142
- * Load full history data from file
143
- */
144
- private async loadHistoryData(): Promise<HistoryData> {
145
- try {
146
- const content = await readFile(this.historyPath, 'utf-8');
147
- return JSON.parse(content) as HistoryData;
148
- } catch (error) {
149
- // File doesn't exist or is corrupted, return empty history
150
- return {
151
- serverId: this.serverId,
152
- snapshots: [],
153
- };
154
- }
155
- }
156
-
157
- /**
158
- * Prune snapshots older than maxAge
159
- */
160
- private pruneOldSnapshots(snapshots: HistorySnapshot[], maxAgeMs: number): HistorySnapshot[] {
161
- const cutoff = Date.now() - maxAgeMs;
162
- return snapshots.filter(s => s.timestamp >= cutoff);
163
- }
164
-
165
- /**
166
- * Filter snapshots by time window
167
- */
168
- private filterByTimeWindow(snapshots: HistorySnapshot[], windowHours: number): HistorySnapshot[] {
169
- const cutoff = Date.now() - (windowHours * 60 * 60 * 1000);
170
- return snapshots.filter(s => s.timestamp >= cutoff);
171
- }
172
- }