agent-tank 0.9.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,163 @@
1
+ /**
2
+ * KeepaliveManager - Manages periodic session keepalive for registered agents
3
+ *
4
+ * Prevents session expiration by scheduling lightweight ping/keepalive operations
5
+ * on registered agents at a configurable interval.
6
+ */
7
+
8
+ class KeepaliveManager {
9
+ /**
10
+ * Create a KeepaliveManager instance.
11
+ *
12
+ * @param {Object} options - Configuration options
13
+ * @param {number} [options.interval=300] - Keepalive interval in seconds (default: 5 minutes)
14
+ * @param {boolean} [options.enabled=true] - Whether keepalive is enabled
15
+ */
16
+ constructor(options = {}) {
17
+ this.interval = options.interval ?? 300; // Default: 5 minutes
18
+ this.enabled = options.enabled !== false; // Default: true
19
+ this.agents = new Map();
20
+ this.timer = null;
21
+ this.lastKeepaliveAt = null;
22
+ this.isRunning = false;
23
+ }
24
+
25
+ /**
26
+ * Register an agent with the keepalive manager.
27
+ *
28
+ * @param {string} name - Agent name
29
+ * @param {Object} agent - Agent instance (must have a keepalive() method)
30
+ */
31
+ register(name, agent) {
32
+ if (!agent) {
33
+ console.warn(`[KeepaliveManager] Cannot register null agent: ${name}`);
34
+ return;
35
+ }
36
+
37
+ if (typeof agent.keepalive !== 'function') {
38
+ console.warn(`[KeepaliveManager] Agent ${name} does not implement keepalive() method`);
39
+ return;
40
+ }
41
+
42
+ this.agents.set(name, agent);
43
+ console.log(`[KeepaliveManager] Registered agent: ${name}`);
44
+ }
45
+
46
+ /**
47
+ * Unregister an agent from the keepalive manager.
48
+ *
49
+ * @param {string} name - Agent name to unregister
50
+ */
51
+ unregister(name) {
52
+ if (this.agents.has(name)) {
53
+ this.agents.delete(name);
54
+ console.log(`[KeepaliveManager] Unregistered agent: ${name}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Start the keepalive scheduler.
60
+ * Runs keepalive immediately on start, then at the configured interval.
61
+ */
62
+ start() {
63
+ if (this.isRunning) {
64
+ console.log('[KeepaliveManager] Already running');
65
+ return;
66
+ }
67
+
68
+ if (!this.enabled) {
69
+ console.log('[KeepaliveManager] Keepalive disabled');
70
+ return;
71
+ }
72
+
73
+ if (this.interval <= 0) {
74
+ console.log('[KeepaliveManager] Keepalive interval is 0, disabled');
75
+ return;
76
+ }
77
+
78
+ if (this.agents.size === 0) {
79
+ console.log('[KeepaliveManager] No agents registered, not starting');
80
+ return;
81
+ }
82
+
83
+ this.isRunning = true;
84
+ const intervalMs = this.interval * 1000;
85
+
86
+ console.log(`[KeepaliveManager] Starting with ${this.interval}s interval for ${this.agents.size} agent(s)`);
87
+
88
+ // Start the periodic timer
89
+ this.timer = setInterval(() => {
90
+ this._runKeepalive();
91
+ }, intervalMs);
92
+ }
93
+
94
+ /**
95
+ * Stop the keepalive scheduler.
96
+ */
97
+ stop() {
98
+ if (this.timer) {
99
+ clearInterval(this.timer);
100
+ this.timer = null;
101
+ }
102
+ this.isRunning = false;
103
+ console.log('[KeepaliveManager] Stopped');
104
+ }
105
+
106
+ /**
107
+ * Run keepalive on all registered agents.
108
+ * Errors in individual agents do not affect others.
109
+ *
110
+ * @returns {Promise<Object>} Results keyed by agent name
111
+ */
112
+ async _runKeepalive() {
113
+ if (this.agents.size === 0) {
114
+ return {};
115
+ }
116
+
117
+ console.log(`[KeepaliveManager] Running keepalive for ${this.agents.size} agent(s)...`);
118
+ const results = {};
119
+
120
+ const promises = Array.from(this.agents.entries()).map(async ([name, agent]) => {
121
+ try {
122
+ await agent.keepalive();
123
+ results[name] = { success: true };
124
+ console.log(`[KeepaliveManager] Keepalive successful: ${name}`);
125
+ } catch (err) {
126
+ // Log error but don't crash - keepalive errors should not affect the server
127
+ results[name] = { success: false, error: err.message };
128
+ console.error(`[KeepaliveManager] Keepalive failed for ${name}: ${err.message}`);
129
+ }
130
+ });
131
+
132
+ await Promise.all(promises);
133
+ this.lastKeepaliveAt = new Date().toISOString();
134
+
135
+ return results;
136
+ }
137
+
138
+ /**
139
+ * Manually trigger a keepalive run.
140
+ *
141
+ * @returns {Promise<Object>} Results keyed by agent name
142
+ */
143
+ async runNow() {
144
+ return this._runKeepalive();
145
+ }
146
+
147
+ /**
148
+ * Get the current status of the keepalive manager.
149
+ *
150
+ * @returns {Object} Status information
151
+ */
152
+ getStatus() {
153
+ return {
154
+ enabled: this.enabled,
155
+ interval: this.interval,
156
+ isRunning: this.isRunning,
157
+ registeredAgents: Array.from(this.agents.keys()),
158
+ lastKeepaliveAt: this.lastKeepaliveAt,
159
+ };
160
+ }
161
+ }
162
+
163
+ module.exports = { KeepaliveManager };
package/src/logger.js ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Custom ANSI Logger Utility
3
+ *
4
+ * A lightweight, dependency-free ANSI logger providing color-coded output,
5
+ * agent-specific styles, and highlighted URLs.
6
+ */
7
+
8
+ // ANSI escape codes for styling
9
+ const ANSI = {
10
+ // Reset
11
+ reset: '\x1b[0m',
12
+
13
+ // Text styles
14
+ bold: '\x1b[1m',
15
+ dim: '\x1b[2m',
16
+ underline: '\x1b[4m',
17
+
18
+ // Foreground colors
19
+ black: '\x1b[30m',
20
+ red: '\x1b[31m',
21
+ green: '\x1b[32m',
22
+ yellow: '\x1b[33m',
23
+ blue: '\x1b[34m',
24
+ magenta: '\x1b[35m',
25
+ cyan: '\x1b[36m',
26
+ white: '\x1b[37m',
27
+
28
+ // Bright foreground colors
29
+ brightBlack: '\x1b[90m',
30
+ brightRed: '\x1b[91m',
31
+ brightGreen: '\x1b[92m',
32
+ brightYellow: '\x1b[93m',
33
+ brightBlue: '\x1b[94m',
34
+ brightMagenta: '\x1b[95m',
35
+ brightCyan: '\x1b[96m',
36
+ brightWhite: '\x1b[97m',
37
+ };
38
+
39
+ // Agent color mapping
40
+ const AGENT_COLORS = {
41
+ claude: ANSI.magenta,
42
+ gemini: ANSI.cyan,
43
+ codex: ANSI.green,
44
+ };
45
+
46
+ // URL regex pattern for highlighting
47
+ const URL_PATTERN = /https?:\/\/[^\s<>"{}|\\^`[\]]+/g;
48
+
49
+ /**
50
+ * Highlights URLs in a string with underline and bright white color
51
+ * (bright white provides better readability on dark terminal backgrounds)
52
+ * @param {string} text - The text to process
53
+ * @returns {string} - Text with highlighted URLs
54
+ */
55
+ function highlightUrls(text) {
56
+ if (typeof text !== 'string') {
57
+ return text;
58
+ }
59
+ return text.replace(URL_PATTERN, (url) => {
60
+ return `${ANSI.brightWhite}${ANSI.underline}${url}${ANSI.reset}`;
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Formats arguments into a single string, handling objects and arrays
66
+ * @param {...any} args - Arguments to format
67
+ * @returns {string} - Formatted string
68
+ */
69
+ function formatArgs(...args) {
70
+ return args.map(arg => {
71
+ if (typeof arg === 'object' && arg !== null) {
72
+ try {
73
+ return JSON.stringify(arg);
74
+ } catch {
75
+ return String(arg);
76
+ }
77
+ }
78
+ return String(arg);
79
+ }).join(' ');
80
+ }
81
+
82
+ /**
83
+ * Colorizes text with the specified ANSI color
84
+ * @param {string} color - ANSI color code
85
+ * @param {string} text - Text to colorize
86
+ * @returns {string} - Colorized text
87
+ */
88
+ function colorize(color, text) {
89
+ return `${color}${text}${ANSI.reset}`;
90
+ }
91
+
92
+ /**
93
+ * Creates a prefixed log message
94
+ * @param {string} prefix - The prefix (e.g., "INFO", "ERROR")
95
+ * @param {string} color - ANSI color for the prefix
96
+ * @param {...any} args - Message arguments
97
+ * @returns {string} - Formatted message
98
+ */
99
+ function createPrefixedMessage(prefix, color, ...args) {
100
+ const formattedMessage = highlightUrls(formatArgs(...args));
101
+ return `${colorize(color, `[${prefix}]`)} ${formattedMessage}`;
102
+ }
103
+
104
+ /**
105
+ * Logger object with formatting methods
106
+ */
107
+ const logger = {
108
+ /**
109
+ * Log an informational message (blue prefix)
110
+ * @param {...any} args - Message arguments
111
+ */
112
+ info(...args) {
113
+ console.log(createPrefixedMessage('INFO', ANSI.blue, ...args));
114
+ },
115
+
116
+ /**
117
+ * Log a success message (green prefix)
118
+ * @param {...any} args - Message arguments
119
+ */
120
+ success(...args) {
121
+ console.log(createPrefixedMessage('SUCCESS', ANSI.green, ...args));
122
+ },
123
+
124
+ /**
125
+ * Log a warning message (yellow prefix)
126
+ * @param {...any} args - Message arguments
127
+ */
128
+ warn(...args) {
129
+ console.warn(createPrefixedMessage('WARN', ANSI.yellow, ...args));
130
+ },
131
+
132
+ /**
133
+ * Log an error message (red prefix)
134
+ * @param {...any} args - Message arguments
135
+ */
136
+ error(...args) {
137
+ console.error(createPrefixedMessage('ERROR', ANSI.red, ...args));
138
+ },
139
+
140
+ /**
141
+ * Log an agent-specific message with mapped color
142
+ * @param {string} agentName - Name of the agent (claude, gemini, codex)
143
+ * @param {...any} args - Message arguments
144
+ */
145
+ agent(agentName, ...args) {
146
+ const normalizedName = agentName.toLowerCase();
147
+ const color = AGENT_COLORS[normalizedName] || ANSI.white;
148
+ const formattedMessage = highlightUrls(formatArgs(...args));
149
+ console.log(`${colorize(color, `[${agentName}]`)} ${formattedMessage}`);
150
+ },
151
+
152
+ /**
153
+ * Log a server-related message (cyan prefix with "SERVER" label)
154
+ * @param {...any} args - Message arguments
155
+ */
156
+ server(...args) {
157
+ console.log(createPrefixedMessage('SERVER', ANSI.cyan, ...args));
158
+ },
159
+
160
+ /**
161
+ * Format text with dim styling (for verbose/secondary output)
162
+ * @param {string} text - Text to dim
163
+ * @returns {string} - Dimmed text
164
+ */
165
+ dim(text) {
166
+ return `${ANSI.dim}${text}${ANSI.reset}`;
167
+ },
168
+
169
+ /**
170
+ * Format and dim JSON data (for large data payloads)
171
+ * @param {any} data - Data to format as JSON
172
+ * @param {boolean} [pretty=false] - Whether to pretty-print the JSON
173
+ * @returns {string} - Dimmed JSON string
174
+ */
175
+ json(data, pretty = false) {
176
+ let jsonStr;
177
+ try {
178
+ jsonStr = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
179
+ } catch {
180
+ jsonStr = String(data);
181
+ }
182
+ return `${ANSI.dim}${jsonStr}${ANSI.reset}`;
183
+ },
184
+
185
+ /**
186
+ * Get the color code for a specific agent
187
+ * @param {string} agentName - Name of the agent
188
+ * @returns {string} - ANSI color code
189
+ */
190
+ getAgentColor(agentName) {
191
+ const normalizedName = agentName.toLowerCase();
192
+ return AGENT_COLORS[normalizedName] || ANSI.white;
193
+ },
194
+
195
+ /**
196
+ * Highlight URLs in text (exposed for external use)
197
+ * @param {string} text - Text to process
198
+ * @returns {string} - Text with highlighted URLs
199
+ */
200
+ highlightUrls,
201
+
202
+ /**
203
+ * Raw ANSI codes for custom formatting
204
+ */
205
+ ANSI,
206
+
207
+ /**
208
+ * Agent color mapping for custom formatting
209
+ */
210
+ AGENT_COLORS,
211
+ };
212
+
213
+ module.exports = logger;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Pace evaluation attachment utilities.
3
+ *
4
+ * Attaches pace evaluation data to agent usage metrics.
5
+ */
6
+
7
+ const { evaluatePace } = require('./pace-evaluator.js');
8
+ const { CYCLE_DURATIONS } = require('./usage-formatters.js');
9
+
10
+ /**
11
+ * Attach pace evaluation to a usage data object if it has required fields.
12
+ * @param {Object} data - Usage data object with percentUsed and resetsInSeconds
13
+ * @param {string} percentField - Field name for percent value
14
+ * @param {string} cycle - Cycle type for duration lookup
15
+ */
16
+ function attachPaceToData(data, percentField, cycle) {
17
+ if (!data) return;
18
+ const percent = data[percentField];
19
+ if (typeof percent !== 'number' || typeof data.resetsInSeconds !== 'number') return;
20
+
21
+ const paceEval = evaluatePace({
22
+ usagePercent: percent,
23
+ resetsInSeconds: data.resetsInSeconds,
24
+ cycleDurationSeconds: CYCLE_DURATIONS[cycle]
25
+ });
26
+ if (paceEval) {
27
+ data.paceEval = paceEval;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Attach pace evaluation to Claude usage metrics.
33
+ * @param {Object} usage - Claude usage data
34
+ */
35
+ function attachClaudePace(usage) {
36
+ const sections = [
37
+ { data: usage.session, cycle: 'session' },
38
+ { data: usage.weeklyAll, cycle: 'weekly' },
39
+ { data: usage.weeklySonnet, cycle: 'weekly' },
40
+ { data: usage.weekly, cycle: 'weekly' },
41
+ { data: usage.extraUsage, cycle: 'weekly' }
42
+ ];
43
+
44
+ for (const { data, cycle } of sections) {
45
+ attachPaceToData(data, 'percent', cycle);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Attach pace evaluation to Gemini usage metrics.
51
+ * @param {Object} usage - Gemini usage data
52
+ */
53
+ function attachGeminiPace(usage) {
54
+ if (!usage.models || !Array.isArray(usage.models)) return;
55
+
56
+ for (const model of usage.models) {
57
+ attachPaceToData(model, 'percentUsed', 'sessionGemini');
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Attach pace evaluation to Codex usage metrics.
63
+ * @param {Object} usage - Codex usage data
64
+ */
65
+ function attachCodexPace(usage) {
66
+ // Main limits
67
+ attachPaceToData(usage.fiveHour, 'percentUsed', 'fiveHour');
68
+ attachPaceToData(usage.weekly, 'percentUsed', 'weekly');
69
+
70
+ // Per-model limits
71
+ if (usage.modelLimits && Array.isArray(usage.modelLimits)) {
72
+ for (const ml of usage.modelLimits) {
73
+ attachPaceToData(ml.fiveHour, 'percentUsed', 'fiveHour');
74
+ attachPaceToData(ml.weekly, 'percentUsed', 'weekly');
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Attach pace evaluation data to usage metrics based on agent type.
81
+ * @param {string} agentName - Name of the agent
82
+ * @param {Object} usage - Usage data object
83
+ */
84
+ function attachPaceEvaluation(agentName, usage) {
85
+ switch (agentName) {
86
+ case 'claude':
87
+ attachClaudePace(usage);
88
+ break;
89
+ case 'gemini':
90
+ attachGeminiPace(usage);
91
+ break;
92
+ case 'codex':
93
+ attachCodexPace(usage);
94
+ break;
95
+ }
96
+ }
97
+
98
+ module.exports = {
99
+ attachPaceEvaluation,
100
+ attachClaudePace,
101
+ attachGeminiPace,
102
+ attachCodexPace,
103
+ attachPaceToData
104
+ };