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.
- package/LICENSE +21 -0
- package/README.md +675 -0
- package/bin/agent-tank.js +301 -0
- package/media/www-preview.png +0 -0
- package/package.json +65 -0
- package/src/activity-monitor.js +312 -0
- package/src/agents/api-response-parser.js +255 -0
- package/src/agents/base.js +396 -0
- package/src/agents/claude.js +388 -0
- package/src/agents/codex-pty-helpers.js +226 -0
- package/src/agents/codex-rpc-helpers.js +167 -0
- package/src/agents/codex.js +383 -0
- package/src/agents/gemini.js +302 -0
- package/src/agents/keepalive-helper.js +115 -0
- package/src/agents/oauth-helper.js +210 -0
- package/src/agents/pty-output-parser.js +301 -0
- package/src/auto-refresh-manager.js +246 -0
- package/src/client-auto-refresh-helpers.js +143 -0
- package/src/client-auto-refresh.js +321 -0
- package/src/client-favicon.js +198 -0
- package/src/client-monitor.js +242 -0
- package/src/client-script.js +147 -0
- package/src/discovery.js +128 -0
- package/src/history-store.js +273 -0
- package/src/http-handler.js +182 -0
- package/src/icons.js +67 -0
- package/src/index.js +376 -0
- package/src/json-rpc-client.js +285 -0
- package/src/keepalive-manager.js +163 -0
- package/src/logger.js +213 -0
- package/src/pace-attachment.js +104 -0
- package/src/pace-evaluator.js +269 -0
- package/src/public-status.js +276 -0
- package/src/server.js +144 -0
- package/src/snapshot-metrics.js +79 -0
- package/src/status-page.css +1149 -0
- package/src/status-page.js +128 -0
- package/src/usage-formatters.js +259 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const { calculatePace } = require('../pace-evaluator.js');
|
|
2
|
+
const { CYCLE_DURATIONS } = require('../usage-formatters.js');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format RPC response as if it were PTY output for parseOutput compatibility
|
|
6
|
+
* Or directly parse and store the usage data
|
|
7
|
+
* @param {Object} rateLimits - The rate limits from RPC
|
|
8
|
+
* @returns {string} - Formatted string for parseOutput
|
|
9
|
+
* @throws {Error} - If the response structure is unexpected
|
|
10
|
+
*/
|
|
11
|
+
function formatRpcResponseAsOutput(rateLimits) {
|
|
12
|
+
if (!rateLimits) {
|
|
13
|
+
throw new Error('Empty rate limits response');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if we got a structured response we can use directly
|
|
17
|
+
if (rateLimits.fiveHour || rateLimits.weekly || rateLimits.limits) {
|
|
18
|
+
return { marker: '__RPC_RESPONSE__', data: rateLimits };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Handle different response formats
|
|
22
|
+
if (rateLimits.rateLimits) {
|
|
23
|
+
return { marker: '__RPC_RESPONSE__', data: rateLimits.rateLimits };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If structure is unknown, throw to fall back to PTY
|
|
27
|
+
throw new Error('Unexpected rate limits response structure');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse usage data from JSON-RPC response
|
|
32
|
+
* @param {Object} rateLimits - Rate limits from RPC
|
|
33
|
+
* @param {Object} context - Context object with metadata and version info
|
|
34
|
+
* @returns {Object} - Parsed usage object
|
|
35
|
+
*/
|
|
36
|
+
function parseRpcRateLimits(rateLimits, context = {}) {
|
|
37
|
+
const usage = {
|
|
38
|
+
fiveHour: null,
|
|
39
|
+
weekly: null,
|
|
40
|
+
version: context.versionInfo || null,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Parse five hour limit
|
|
44
|
+
if (rateLimits.fiveHour) {
|
|
45
|
+
usage.fiveHour = parseRpcLimitEntry(rateLimits.fiveHour, '5h limit', 'fiveHour', context);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse weekly limit
|
|
49
|
+
if (rateLimits.weekly) {
|
|
50
|
+
usage.weekly = parseRpcLimitEntry(rateLimits.weekly, 'Weekly limit', 'weekly', context);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse model-specific limits if present
|
|
54
|
+
if (rateLimits.modelLimits && Array.isArray(rateLimits.modelLimits)) {
|
|
55
|
+
usage.modelLimits = rateLimits.modelLimits.map(ml => ({
|
|
56
|
+
name: ml.name || ml.model,
|
|
57
|
+
fiveHour: ml.fiveHour ? parseRpcLimitEntry(ml.fiveHour, '5h limit', 'fiveHour', context) : null,
|
|
58
|
+
weekly: ml.weekly ? parseRpcLimitEntry(ml.weekly, 'Weekly limit', 'weekly', context) : null,
|
|
59
|
+
})).filter(ml => ml.fiveHour || ml.weekly);
|
|
60
|
+
|
|
61
|
+
if (usage.modelLimits.length === 0) {
|
|
62
|
+
delete usage.modelLimits;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Extract model and account
|
|
67
|
+
if (rateLimits.model) {
|
|
68
|
+
usage.model = rateLimits.model;
|
|
69
|
+
}
|
|
70
|
+
if (rateLimits.account || rateLimits.email) {
|
|
71
|
+
usage.account = rateLimits.account || rateLimits.email;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build metadata updates
|
|
75
|
+
const metadataUpdates = {};
|
|
76
|
+
if (rateLimits.model) {
|
|
77
|
+
metadataUpdates.model = rateLimits.model;
|
|
78
|
+
}
|
|
79
|
+
if (rateLimits.account || rateLimits.email) {
|
|
80
|
+
metadataUpdates.email = rateLimits.account || rateLimits.email;
|
|
81
|
+
}
|
|
82
|
+
if (rateLimits.sessionId) {
|
|
83
|
+
metadataUpdates.sessionId = rateLimits.sessionId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { usage, metadataUpdates };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse a single limit entry from RPC response
|
|
91
|
+
* @param {Object} limit - Limit data from RPC
|
|
92
|
+
* @param {string} label - Label for the limit
|
|
93
|
+
* @param {string} cycleType - Cycle type for pace calculation
|
|
94
|
+
* @param {Object} context - Context with parseResetTime function
|
|
95
|
+
* @returns {Object} - Parsed limit entry
|
|
96
|
+
*/
|
|
97
|
+
function parseRpcLimitEntry(limit, label, cycleType, context = {}) {
|
|
98
|
+
// Handle different possible structures
|
|
99
|
+
const percentLeft = limit.percentLeft ?? limit.remaining ?? limit.percent ?? 100;
|
|
100
|
+
const percentUsed = 100 - percentLeft;
|
|
101
|
+
|
|
102
|
+
// Handle reset time
|
|
103
|
+
let resetsAt = limit.resetsAt || limit.resetAt || limit.reset || null;
|
|
104
|
+
let resetsInSeconds = limit.resetsInSeconds || limit.secondsUntilReset || null;
|
|
105
|
+
let resetsIn = null;
|
|
106
|
+
|
|
107
|
+
// If we have seconds, calculate text
|
|
108
|
+
if (resetsInSeconds !== null) {
|
|
109
|
+
resetsIn = formatDuration(resetsInSeconds);
|
|
110
|
+
} else if (resetsAt && context.parseResetTime) {
|
|
111
|
+
// Try to parse the reset time
|
|
112
|
+
const resetData = context.parseResetTime(resetsAt);
|
|
113
|
+
if (resetData) {
|
|
114
|
+
resetsIn = resetData.text;
|
|
115
|
+
resetsInSeconds = resetData.seconds;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const entry = {
|
|
120
|
+
percentLeft,
|
|
121
|
+
resetsAt,
|
|
122
|
+
label,
|
|
123
|
+
percentUsed,
|
|
124
|
+
resetsIn,
|
|
125
|
+
resetsInSeconds,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Calculate pace if we have the necessary data
|
|
129
|
+
const cycleDuration = CYCLE_DURATIONS[cycleType];
|
|
130
|
+
if (cycleDuration && resetsInSeconds != null) {
|
|
131
|
+
const paceData = calculatePace({
|
|
132
|
+
usagePercent: percentUsed,
|
|
133
|
+
resetsInSeconds,
|
|
134
|
+
cycleDurationSeconds: cycleDuration,
|
|
135
|
+
});
|
|
136
|
+
if (paceData) entry.pace = paceData;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return entry;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Format seconds as duration string
|
|
144
|
+
* @param {number} seconds - Seconds to format
|
|
145
|
+
* @returns {string} - Formatted duration
|
|
146
|
+
*/
|
|
147
|
+
function formatDuration(seconds) {
|
|
148
|
+
const hours = Math.floor(seconds / 3600);
|
|
149
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
150
|
+
|
|
151
|
+
if (hours > 24) {
|
|
152
|
+
const days = Math.floor(hours / 24);
|
|
153
|
+
const remainingHours = hours % 24;
|
|
154
|
+
return `${days}d ${remainingHours}h`;
|
|
155
|
+
} else if (hours > 0) {
|
|
156
|
+
return `${hours}h ${mins}m`;
|
|
157
|
+
} else {
|
|
158
|
+
return `${mins}m`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
formatRpcResponseAsOutput,
|
|
164
|
+
parseRpcRateLimits,
|
|
165
|
+
parseRpcLimitEntry,
|
|
166
|
+
formatDuration,
|
|
167
|
+
};
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
const { BaseAgent } = require('./base.js');
|
|
2
|
+
const logger = require('../logger.js');
|
|
3
|
+
const { JsonRpcClient } = require('../json-rpc-client.js');
|
|
4
|
+
const {
|
|
5
|
+
formatRpcResponseAsOutput,
|
|
6
|
+
parseRpcRateLimits,
|
|
7
|
+
} = require('./codex-rpc-helpers.js');
|
|
8
|
+
const {
|
|
9
|
+
parseResetTime,
|
|
10
|
+
parseLimitEntry,
|
|
11
|
+
parseVersionInfo,
|
|
12
|
+
parseModelLimits,
|
|
13
|
+
extractMetadataFromOutput,
|
|
14
|
+
} = require('./codex-pty-helpers.js');
|
|
15
|
+
const { pingKeepalive } = require('./keepalive-helper.js');
|
|
16
|
+
|
|
17
|
+
class CodexAgent extends BaseAgent {
|
|
18
|
+
constructor() {
|
|
19
|
+
super('codex', 'codex');
|
|
20
|
+
this._rpcClient = null;
|
|
21
|
+
this._rpcSupported = null; // null = unknown, true/false = tested
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getTimeout() { return 25000; }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Override runCommand to attempt JSON-RPC first, then fall back to PTY
|
|
28
|
+
*/
|
|
29
|
+
async runCommand() {
|
|
30
|
+
if (this._rpcSupported === false) {
|
|
31
|
+
return this._runWithPty();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await this._runWithJsonRpc();
|
|
36
|
+
this._rpcSupported = true;
|
|
37
|
+
return result;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (this._rpcSupported === null) {
|
|
40
|
+
console.log(`[${this.name}] JSON-RPC not available, falling back to PTY: ${err.message}`);
|
|
41
|
+
this._rpcSupported = false;
|
|
42
|
+
}
|
|
43
|
+
return this._runWithPty();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run using JSON-RPC app-server mode
|
|
49
|
+
*/
|
|
50
|
+
async _runWithJsonRpc() {
|
|
51
|
+
console.log(`[${this.name}] Attempting JSON-RPC mode...`);
|
|
52
|
+
|
|
53
|
+
if (this._rpcClient) {
|
|
54
|
+
this._rpcClient.stop();
|
|
55
|
+
this._rpcClient = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this._rpcClient = new JsonRpcClient('codex', ['-s', 'read-only', '-a', 'untrusted', 'app-server'], {
|
|
59
|
+
cwd: '/tmp',
|
|
60
|
+
timeout: this.getTimeout(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await this._rpcClient.start();
|
|
65
|
+
console.log(`[${this.name}] JSON-RPC server started`);
|
|
66
|
+
|
|
67
|
+
const rateLimits = await this._rpcClient.call('account/rateLimits/read', {});
|
|
68
|
+
console.log(`[${this.name}] Received rate limits via JSON-RPC:`, JSON.stringify(rateLimits));
|
|
69
|
+
|
|
70
|
+
const { marker, data } = formatRpcResponseAsOutput(rateLimits);
|
|
71
|
+
this._rpcRateLimits = data;
|
|
72
|
+
return marker;
|
|
73
|
+
} finally {
|
|
74
|
+
if (this._rpcClient) {
|
|
75
|
+
this._rpcClient.stop();
|
|
76
|
+
this._rpcClient = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async _runWithPty() {
|
|
82
|
+
if (this.freshProcess) return this._runCommandFresh();
|
|
83
|
+
if (!this.shell || !this.processReady) await this.spawnProcess();
|
|
84
|
+
return this.sendCommandAndWait();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
handleTrustPrompt(shell, output) {
|
|
88
|
+
if (!shell) return false;
|
|
89
|
+
const patterns = ['Do you trust', 'trust the files', 'trust this folder', 'Trust this workspace', 'allow access'];
|
|
90
|
+
if (!patterns.some(p => output.toLowerCase().includes(p.toLowerCase()))) return false;
|
|
91
|
+
logger.agent(this.name, 'Detected trust prompt, auto-accepting...');
|
|
92
|
+
shell.write('y\r');
|
|
93
|
+
setTimeout(() => { if (shell) { logger.agent(this.name, 'Sending Enter to proceed...'); shell.write('\r'); } }, 500);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
handleUpdateScreen(shell, output) {
|
|
97
|
+
if (!shell) return false;
|
|
98
|
+
const clean = this.stripAnsi(output);
|
|
99
|
+
if (!(/u?pdate available/i.test(clean) && /[\d.]+\s*->\s*[\d.]+/.test(clean))) return false;
|
|
100
|
+
logger.agent(this.name, 'Detected update screen, skipping...');
|
|
101
|
+
// v0.115+: may show numbered menu (1. Update, 2. Skip) or just info box
|
|
102
|
+
// Send "2" to select Skip, Enter to confirm, then Escape + Enter to clear any leftover
|
|
103
|
+
shell.write('2');
|
|
104
|
+
setTimeout(() => { if (shell) shell.write('\r'); }, 300);
|
|
105
|
+
setTimeout(() => { if (shell) shell.write('\x1b'); }, 1000);
|
|
106
|
+
setTimeout(() => { if (shell) shell.write('\r'); }, 1500);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
parseVersionInfo(output) { return parseVersionInfo(output, this.stripAnsi.bind(this)); }
|
|
110
|
+
isReadyForCommands(output) { return this.isReadyForStatus(output); }
|
|
111
|
+
isReadyForStatus(output) {
|
|
112
|
+
return output.includes('? for shortcuts') || output.includes('To get started') ||
|
|
113
|
+
output.includes('% left') || /›\s*\S/.test(this.stripAnsi(output));
|
|
114
|
+
}
|
|
115
|
+
sendCommands(shell, _output) { logger.agent(this.name, 'Sending /status command...'); setTimeout(() => { if (shell) shell.write('/status\r'); }, 100); }
|
|
116
|
+
|
|
117
|
+
_handleAdditionalPrompts(shell, _data, output) {
|
|
118
|
+
if (!shell) return;
|
|
119
|
+
if (!this._updateHandled && this.handleUpdateScreen(shell, output)) { this._updateHandled = true; return; }
|
|
120
|
+
const cleanOutput = this.stripAnsi(output);
|
|
121
|
+
const isUpdateScreen = /u?pdate available/i.test(cleanOutput) && /[\d.]+\s*->\s*[\d.]+/.test(cleanOutput);
|
|
122
|
+
if (!this._continuationHandled && !isUpdateScreen && output.includes('Press enter to continue')) {
|
|
123
|
+
logger.agent(this.name, 'Detected continuation prompt');
|
|
124
|
+
this._continuationHandled = true;
|
|
125
|
+
shell.write('\r');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
handleInteractivePrompts(shell, data, output, state) {
|
|
129
|
+
if (!shell) return false;
|
|
130
|
+
if (!state.trustHandled && this.handleTrustPrompt(shell, output)) { state.trustHandled = true; return true; }
|
|
131
|
+
if (!state.updateHandled && this.handleUpdateScreen(shell, output)) { state.updateHandled = true; return true; }
|
|
132
|
+
this._respondToTerminalQueries(data, shell);
|
|
133
|
+
const cleanOutput = this.stripAnsi(output);
|
|
134
|
+
const isUpdateScreen = /u?pdate available/i.test(cleanOutput) && /[\d.]+\s*->\s*[\d.]+/.test(cleanOutput);
|
|
135
|
+
if (!state.continuationHandled && !isUpdateScreen && output.includes('Press enter to continue')) {
|
|
136
|
+
logger.agent(this.name, 'Detected continuation prompt');
|
|
137
|
+
state.continuationHandled = true;
|
|
138
|
+
shell.write('\r');
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async spawnProcess() {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const pty = require('node-pty');
|
|
146
|
+
let spawnOutput = '';
|
|
147
|
+
const state = { trustHandled: false, continuationHandled: false, updateHandled: false };
|
|
148
|
+
|
|
149
|
+
logger.agent(this.name, 'Spawning persistent process:', logger.dim(`${this.command} ${this.args.join(' ')}`));
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
this.shell = pty.spawn(this.command, this.args, {
|
|
153
|
+
name: 'xterm-256color',
|
|
154
|
+
cols: 120,
|
|
155
|
+
rows: 40,
|
|
156
|
+
cwd: '/tmp',
|
|
157
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
158
|
+
});
|
|
159
|
+
} catch (spawnErr) {
|
|
160
|
+
logger.error(`[${this.name}] Failed to spawn:`, spawnErr.message);
|
|
161
|
+
reject(new Error(`Failed to spawn ${this.command}: ${spawnErr.message}`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
logger.error(`[${this.name}] Spawn timeout after ${this.getTimeout()}ms`);
|
|
167
|
+
this.killProcess();
|
|
168
|
+
reject(new Error('Timeout waiting for process to become ready'));
|
|
169
|
+
}, this.getTimeout());
|
|
170
|
+
|
|
171
|
+
let postPromptOutput = '';
|
|
172
|
+
const spawnDataHandler = this.shell.onData((data) => {
|
|
173
|
+
spawnOutput += data;
|
|
174
|
+
if (this.handleInteractivePrompts(this.shell, data, spawnOutput, state)) {
|
|
175
|
+
postPromptOutput = '';
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
postPromptOutput += data;
|
|
179
|
+
const checkOutput = (state.updateHandled || state.trustHandled) ? postPromptOutput : spawnOutput;
|
|
180
|
+
if (this.isReadyForStatus(checkOutput)) {
|
|
181
|
+
logger.agent(this.name, 'Process ready for commands');
|
|
182
|
+
const versionFromSpawn = this.parseVersionInfo(spawnOutput);
|
|
183
|
+
if (versionFromSpawn) {
|
|
184
|
+
this._spawnVersionInfo = versionFromSpawn;
|
|
185
|
+
logger.agent(this.name, 'Version info from spawn:', logger.json(versionFromSpawn));
|
|
186
|
+
}
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
spawnDataHandler.dispose();
|
|
189
|
+
this.processReady = true;
|
|
190
|
+
this._setupPersistentDataHandler();
|
|
191
|
+
this._setupPersistentExitHandler();
|
|
192
|
+
resolve();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const spawnExitHandler = this.shell.onExit(({ exitCode }) => {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
spawnDataHandler.dispose();
|
|
199
|
+
spawnExitHandler.dispose();
|
|
200
|
+
this.shell = null;
|
|
201
|
+
this.processReady = false;
|
|
202
|
+
reject(new Error(`Process exited during spawn with code ${exitCode}`));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async sendCommandAndWait() {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
this.output = '';
|
|
210
|
+
this._commandInFlight = true;
|
|
211
|
+
let retryTimer = null;
|
|
212
|
+
let settleTimer = null;
|
|
213
|
+
|
|
214
|
+
const finish = (result) => {
|
|
215
|
+
this._commandInFlight = false;
|
|
216
|
+
this._onDataCallback = null;
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
if (retryTimer) clearInterval(retryTimer);
|
|
219
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
220
|
+
resolve(result);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
if (retryTimer) clearInterval(retryTimer);
|
|
225
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
226
|
+
logger.agent(this.name, 'Command timeout after', logger.dim(`${this.getTimeout()}ms`), ', output length:', logger.dim(`${this.output.length}`));
|
|
227
|
+
if (this.output.length > 0) {
|
|
228
|
+
logger.agent(this.name, 'Partial output:', logger.dim(this.stripAnsi(this.output).substring(0, 500)));
|
|
229
|
+
require('fs').writeFileSync(`/tmp/${this.name}-output.txt`, this.output);
|
|
230
|
+
}
|
|
231
|
+
if (this.output.length > 100) {
|
|
232
|
+
finish(this.output);
|
|
233
|
+
} else {
|
|
234
|
+
this._commandInFlight = false;
|
|
235
|
+
this._onDataCallback = null;
|
|
236
|
+
reject(new Error('Timeout waiting for usage data'));
|
|
237
|
+
}
|
|
238
|
+
}, this.getTimeout());
|
|
239
|
+
|
|
240
|
+
retryTimer = setInterval(() => {
|
|
241
|
+
if (!this.hasCompleteOutput(this.output) && this.shell) {
|
|
242
|
+
logger.agent(this.name, 'Retrying /status...');
|
|
243
|
+
this.output = '';
|
|
244
|
+
this.shell.write('/status');
|
|
245
|
+
setTimeout(() => { if (this.shell) this.shell.write('\r'); }, 200);
|
|
246
|
+
}
|
|
247
|
+
}, 5000);
|
|
248
|
+
|
|
249
|
+
this._onDataCallback = () => {
|
|
250
|
+
if (this.hasCompleteOutput(this.output)) {
|
|
251
|
+
if (!settleTimer) {
|
|
252
|
+
logger.agent(this.name, 'Complete output detected, waiting to settle...');
|
|
253
|
+
settleTimer = setTimeout(() => finish(this.output), 200);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
logger.agent(this.name, 'Sending /status to persistent process...');
|
|
259
|
+
setTimeout(() => { if (this.shell) this.shell.write('/status\r'); }, 100);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
hasCompleteOutput(output) {
|
|
264
|
+
if (output.includes('5h limit') && output.includes('Weekly limit')) return true;
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
parseResetTime(resetStr) {
|
|
269
|
+
return parseResetTime(resetStr);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parseLimitEntry(match, label, cycleType) {
|
|
273
|
+
return parseLimitEntry(match, label, cycleType);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
parseOutput(output) {
|
|
277
|
+
// Check if this is an RPC response marker
|
|
278
|
+
if (output === '__RPC_RESPONSE__' && this._rpcRateLimits) {
|
|
279
|
+
const context = {
|
|
280
|
+
versionInfo: this._spawnVersionInfo || null,
|
|
281
|
+
parseResetTime: this.parseResetTime.bind(this),
|
|
282
|
+
};
|
|
283
|
+
const { usage, metadataUpdates } = parseRpcRateLimits(this._rpcRateLimits, context);
|
|
284
|
+
this._rpcRateLimits = null;
|
|
285
|
+
|
|
286
|
+
// Apply metadata updates
|
|
287
|
+
if (!this.metadata) this.metadata = {};
|
|
288
|
+
Object.assign(this.metadata, metadataUpdates);
|
|
289
|
+
|
|
290
|
+
return usage;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const clean = this.stripAnsi(output);
|
|
294
|
+
const usage = {
|
|
295
|
+
fiveHour: null,
|
|
296
|
+
weekly: null,
|
|
297
|
+
version: (() => {
|
|
298
|
+
const v = { ...this._spawnVersionInfo, ...this.parseVersionInfo(output) };
|
|
299
|
+
return Object.keys(v).length > 0 ? v : null;
|
|
300
|
+
})(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const fiveHourMatch = clean.match(/5h limit:\s*\[.*?\]\s*(\d+)%\s*left\s*\(resets\s*([^)]+)\)/i);
|
|
304
|
+
if (fiveHourMatch) {
|
|
305
|
+
usage.fiveHour = this.parseLimitEntry(fiveHourMatch, '5h limit', 'fiveHour');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const weeklyMatch = clean.match(/Weekly limit:\s*\[.*?\]\s*(\d+)%\s*left\s*\(resets\s*([^)]+)\)/i);
|
|
309
|
+
if (weeklyMatch) {
|
|
310
|
+
usage.weekly = this.parseLimitEntry(weeklyMatch, 'Weekly limit', 'weekly');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const modelLimits = parseModelLimits(clean);
|
|
314
|
+
if (modelLimits.length > 0) {
|
|
315
|
+
usage.modelLimits = modelLimits;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const modelMatch = clean.match(/Model:\s*([\w.-]+)/i);
|
|
319
|
+
if (modelMatch) {
|
|
320
|
+
usage.model = this.stripBoxChars(modelMatch[1]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const accountMatch = clean.match(/Account:\s*(\S+@\S+)/i);
|
|
324
|
+
if (accountMatch) {
|
|
325
|
+
usage.account = this.stripBoxChars(accountMatch[1]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this._updateMetadataFromOutput(clean);
|
|
329
|
+
|
|
330
|
+
return usage;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_updateMetadataFromOutput(clean) {
|
|
334
|
+
if (!this.metadata) this.metadata = {};
|
|
335
|
+
const extracted = extractMetadataFromOutput(clean, this.stripBoxChars.bind(this));
|
|
336
|
+
Object.assign(this.metadata, extracted);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get usingJsonRpc() {
|
|
340
|
+
return this._rpcSupported;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
setRpcMode(useRpc) {
|
|
344
|
+
this._rpcSupported = useRpc;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
killProcess() {
|
|
348
|
+
if (this._rpcClient) {
|
|
349
|
+
this._rpcClient.stop();
|
|
350
|
+
this._rpcClient = null;
|
|
351
|
+
}
|
|
352
|
+
super.killProcess();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Lightweight keepalive to prevent session expiration. @returns {Promise<boolean>} True if keepalive succeeded */
|
|
356
|
+
async keepalive() {
|
|
357
|
+
if (this._rpcSupported === true) { console.log(`[${this.name}] Keepalive skipped (JSON-RPC mode)`); return true; }
|
|
358
|
+
if (this.freshProcess) { console.log(`[${this.name}] Keepalive skipped (fresh process mode)`); return true; }
|
|
359
|
+
if (!this.shell || !this.processReady) { console.log(`[${this.name}] Keepalive: spawning process...`); await this.spawnProcess(); }
|
|
360
|
+
if (this.shell) { console.log(`[${this.name}] Keepalive: sending ping...`); this.shell.write('\x1b'); return true; }
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Spawns fresh CLI, sends /status to refresh session, then tears down cleanly. @returns {Promise<boolean>} */
|
|
365
|
+
async pingKeepalive() {
|
|
366
|
+
if (this._rpcSupported === true) { console.log(`[${this.name}] pingKeepalive: skipped (JSON-RPC mode)`); return true; }
|
|
367
|
+
const state = { trustHandled: false, continuationHandled: false, updateHandled: false };
|
|
368
|
+
return pingKeepalive({
|
|
369
|
+
name: this.name,
|
|
370
|
+
command: this.command,
|
|
371
|
+
args: this.args,
|
|
372
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
373
|
+
termName: 'xterm-256color',
|
|
374
|
+
isReady: (output) => this.isReadyForStatus(output),
|
|
375
|
+
sendCommand: (shell) => setTimeout(() => shell.write('/status\r'), 100),
|
|
376
|
+
isComplete: (output) => this.hasCompleteOutput(output),
|
|
377
|
+
handlePrompts: (shell, data, output) => this.handleInteractivePrompts(shell, data, output, state),
|
|
378
|
+
respondToTerminalQueries: (data, shell) => this._respondToTerminalQueries(data, shell),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = { CodexAgent };
|