agent-tank 0.9.6 → 0.9.8
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/README.md +77 -34
- package/bin/agent-tank.js +61 -16
- package/package.json +5 -3
- package/src/activity-monitor.js +18 -7
- package/src/agents/agy.js +301 -0
- package/src/agents/base.js +27 -4
- package/src/agents/claude.js +39 -10
- package/src/agents/codex.js +1 -1
- package/src/agents/pty-output-parser.js +10 -4
- package/src/cli-background.js +198 -0
- package/src/client-auto-refresh-helpers.js +7 -7
- package/src/client-auto-refresh.js +2 -2
- package/src/client-monitor.js +2 -2
- package/src/discovery.js +9 -24
- package/src/history-store.js +1 -1
- package/src/icons.js +2 -2
- package/src/index.js +7 -6
- package/src/logger.js +2 -2
- package/src/pace-attachment.js +7 -7
- package/src/process-utils.js +215 -0
- package/src/public-status.js +5 -5
- package/src/snapshot-metrics.js +6 -6
- package/src/status-page.css +6 -6
- package/src/status-page.js +56 -38
- package/src/usage-formatters.js +7 -8
- package/src/agents/gemini-direct.js +0 -234
- package/src/agents/gemini.js +0 -463
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
const {
|
|
6
|
+
filterBackgroundArgs,
|
|
7
|
+
findAgentTankProcessesAsync,
|
|
8
|
+
} = require('./process-utils.js');
|
|
9
|
+
const { ANSI } = require('./logger.js');
|
|
10
|
+
|
|
11
|
+
const BACKGROUND_STARTUP_GRACE_MS = 750;
|
|
12
|
+
|
|
13
|
+
function createBackgroundLogPath({
|
|
14
|
+
env = process.env,
|
|
15
|
+
tmpdir = os.tmpdir,
|
|
16
|
+
pid = process.pid,
|
|
17
|
+
now = Date.now,
|
|
18
|
+
} = {}) {
|
|
19
|
+
return env.AGENT_TANK_BACKGROUND_LOG ||
|
|
20
|
+
path.join(tmpdir(), `agent-tank-${pid}-${now()}.log`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeBackgroundStartMessage({ pid, logPath, stdout = process.stdout }) {
|
|
24
|
+
stdout.write(`Agent Tank started in the background with PID ${pid}\n`);
|
|
25
|
+
stdout.write(`Background log: ${logPath}\n`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeBackgroundStartFailure({ message, logPath, stderr = process.stderr }) {
|
|
29
|
+
stderr.write(`Failed to start Agent Tank in the background: ${message}\n`);
|
|
30
|
+
stderr.write(`Background log: ${logPath}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ignoreLateChildError() {}
|
|
34
|
+
|
|
35
|
+
function openBackgroundLog(logPath, openSync = fs.openSync) {
|
|
36
|
+
return openSync(logPath, 'a');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function closeBackgroundLog(logFd, closeSync = fs.closeSync) {
|
|
40
|
+
try {
|
|
41
|
+
closeSync(logFd);
|
|
42
|
+
} catch (_err) {
|
|
43
|
+
// The child owns the duplicated descriptor after spawn; parent close failures are non-fatal.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getBackgroundStartupGraceMs(env = process.env) {
|
|
48
|
+
const parsed = Number.parseInt(env.AGENT_TANK_BACKGROUND_GRACE_MS, 10);
|
|
49
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : BACKGROUND_STARTUP_GRACE_MS;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function waitForBackgroundStartup(child, graceMs, setTimer = setTimeout, clearTimer = clearTimeout) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
let settled = false;
|
|
55
|
+
|
|
56
|
+
const finish = (callback, value) => {
|
|
57
|
+
if (settled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
settled = true;
|
|
61
|
+
clearTimer(timer);
|
|
62
|
+
child.off('error', onError);
|
|
63
|
+
child.off('exit', onExit);
|
|
64
|
+
callback(value);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const onError = (err) => finish(reject, err);
|
|
68
|
+
const onExit = (code, signal) => {
|
|
69
|
+
const detail = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
70
|
+
finish(reject, new Error(`child exited during startup (${detail})`));
|
|
71
|
+
};
|
|
72
|
+
const timer = setTimer(() => finish(resolve), graceMs);
|
|
73
|
+
|
|
74
|
+
child.once('error', onError);
|
|
75
|
+
child.once('exit', onExit);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function spawnBackgroundProcess({
|
|
80
|
+
argv = process.argv,
|
|
81
|
+
env = process.env,
|
|
82
|
+
execPath = process.execPath,
|
|
83
|
+
spawnFn = spawn,
|
|
84
|
+
stdout = process.stdout,
|
|
85
|
+
stderr = process.stderr,
|
|
86
|
+
openSync = fs.openSync,
|
|
87
|
+
closeSync = fs.closeSync,
|
|
88
|
+
startupGraceMs,
|
|
89
|
+
} = {}) {
|
|
90
|
+
const resolvedStartupGraceMs = startupGraceMs ?? getBackgroundStartupGraceMs(env);
|
|
91
|
+
const logPath = createBackgroundLogPath({ env });
|
|
92
|
+
let logFd;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
logFd = openBackgroundLog(logPath, openSync);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
writeBackgroundStartFailure({ message: err.message, logPath, stderr });
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const childArgs = [
|
|
102
|
+
argv[1],
|
|
103
|
+
...filterBackgroundArgs(argv.slice(2)),
|
|
104
|
+
];
|
|
105
|
+
const childEnv = {
|
|
106
|
+
...env,
|
|
107
|
+
AGENT_TANK_BACKGROUND_CHILD: '1',
|
|
108
|
+
};
|
|
109
|
+
delete childEnv.AGENT_TANK_BACKGROUND;
|
|
110
|
+
delete childEnv.AGENT_TANK_BACKGROUND_LOG;
|
|
111
|
+
delete childEnv.AGENT_TANK_BACKGROUND_GRACE_MS;
|
|
112
|
+
|
|
113
|
+
let child;
|
|
114
|
+
try {
|
|
115
|
+
child = spawnFn(execPath, childArgs, {
|
|
116
|
+
detached: true,
|
|
117
|
+
stdio: ['ignore', logFd, logFd],
|
|
118
|
+
env: childEnv,
|
|
119
|
+
windowsHide: true,
|
|
120
|
+
});
|
|
121
|
+
} catch (err) {
|
|
122
|
+
closeBackgroundLog(logFd, closeSync);
|
|
123
|
+
writeBackgroundStartFailure({ message: err.message, logPath, stderr });
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
closeBackgroundLog(logFd, closeSync);
|
|
128
|
+
child.on('error', ignoreLateChildError);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await waitForBackgroundStartup(child, resolvedStartupGraceMs);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
writeBackgroundStartFailure({ message: err.message, logPath, stderr });
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!child.pid) {
|
|
138
|
+
// Extremely defensive: normal spawn failures are reported via the error event above.
|
|
139
|
+
writeBackgroundStartFailure({ message: 'child process PID was not assigned', logPath, stderr });
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
child.unref();
|
|
144
|
+
writeBackgroundStartMessage({ pid: child.pid, logPath, stdout });
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function redactProcessCommand(command) {
|
|
149
|
+
return command
|
|
150
|
+
.replace(/(--auth-pass(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/g, '$1[redacted]')
|
|
151
|
+
.replace(/(--auth-token(?:=|\s+))(?:"[^"]*"|'[^']*'|\S+)/g, '$1[redacted]');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function warnAboutRunningProcesses({
|
|
155
|
+
findProcesses = findAgentTankProcessesAsync,
|
|
156
|
+
stderr = process.stderr,
|
|
157
|
+
} = {}) {
|
|
158
|
+
let running;
|
|
159
|
+
try {
|
|
160
|
+
running = await findProcesses();
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (running.length === 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const lines = running
|
|
170
|
+
.map(entry => ` PID ${entry.pid}: ${redactProcessCommand(entry.command)}`)
|
|
171
|
+
.join('\n');
|
|
172
|
+
|
|
173
|
+
// Only emit color when writing to an interactive terminal, so redirected
|
|
174
|
+
// logs stay free of ANSI escape sequences.
|
|
175
|
+
const header = 'Warning: other Agent Tank process(es) already running:';
|
|
176
|
+
const coloredHeader = stderr.isTTY
|
|
177
|
+
? `${ANSI.bold}${ANSI.red}${header}${ANSI.reset}`
|
|
178
|
+
: header;
|
|
179
|
+
|
|
180
|
+
const pids = running.map(entry => entry.pid).join(' ');
|
|
181
|
+
|
|
182
|
+
stderr.write(`${coloredHeader}\n`);
|
|
183
|
+
stderr.write(`${lines}\n`);
|
|
184
|
+
stderr.write('If this instance uses the same port, startup will fail; choose a different --port or stop the other process.\n');
|
|
185
|
+
stderr.write(`To stop the process(es) above, run: kill ${pids}\n`);
|
|
186
|
+
stderr.write(`If a process does not exit, force it with: kill -9 ${pids}\n`);
|
|
187
|
+
stderr.write('Tip: start Agent Tank in the background with agent-tank --background\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
BACKGROUND_STARTUP_GRACE_MS,
|
|
192
|
+
createBackgroundLogPath,
|
|
193
|
+
getBackgroundStartupGraceMs,
|
|
194
|
+
redactProcessCommand,
|
|
195
|
+
spawnBackgroundProcess,
|
|
196
|
+
waitForBackgroundStartup,
|
|
197
|
+
warnAboutRunningProcesses,
|
|
198
|
+
};
|
|
@@ -41,7 +41,7 @@ const timeHelpers = `
|
|
|
41
41
|
// Cycle duration constants (mirrored from server-side)
|
|
42
42
|
const CYCLE_DURATIONS = {
|
|
43
43
|
session: 5 * 60 * 60,
|
|
44
|
-
|
|
44
|
+
sessionAgy: 24 * 60 * 60,
|
|
45
45
|
weekly: 7 * 24 * 60 * 60,
|
|
46
46
|
fiveHour: 5 * 60 * 60
|
|
47
47
|
};
|
|
@@ -74,21 +74,21 @@ const metricExtractors = `
|
|
|
74
74
|
return metrics;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Extract metrics from
|
|
78
|
-
function
|
|
77
|
+
// Extract metrics from Antigravity usage data
|
|
78
|
+
function extractAgyMetrics(usage) {
|
|
79
79
|
const metrics = [];
|
|
80
80
|
if (usage.models && usage.models.length > 0) {
|
|
81
81
|
for (const model of usage.models) {
|
|
82
|
-
const modelName = model.model
|
|
83
|
-
const metricId = \`
|
|
82
|
+
const modelName = model.model;
|
|
83
|
+
const metricId = \`agy-\${modelName.toLowerCase().replace(/[^a-z0-9]/g, '-')}\`;
|
|
84
84
|
metrics.push({
|
|
85
85
|
metricId,
|
|
86
|
-
agent: '
|
|
86
|
+
agent: 'agy',
|
|
87
87
|
label: modelName,
|
|
88
88
|
percent: model.percentUsed ?? 0,
|
|
89
89
|
resetsIn: model.resetsIn || '',
|
|
90
90
|
resetsAt: model.resetsAt || '',
|
|
91
|
-
cycle: '
|
|
91
|
+
cycle: 'sessionAgy'
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -142,8 +142,8 @@ ${metricExtractors}
|
|
|
142
142
|
let metrics = [];
|
|
143
143
|
if (agentName === 'claude') {
|
|
144
144
|
metrics = extractClaudeMetrics(agentData.usage);
|
|
145
|
-
} else if (agentName === '
|
|
146
|
-
metrics =
|
|
145
|
+
} else if (agentName === 'agy') {
|
|
146
|
+
metrics = extractAgyMetrics(agentData.usage);
|
|
147
147
|
} else if (agentName === 'codex') {
|
|
148
148
|
metrics = extractCodexMetrics(agentData.usage);
|
|
149
149
|
}
|
package/src/client-monitor.js
CHANGED
|
@@ -183,7 +183,7 @@ const monitorScript = `
|
|
|
183
183
|
const { percent, color, agent, label } = trackedMetric;
|
|
184
184
|
|
|
185
185
|
// Update page title: XX% Agent | Agent Tank (clean format for squeezed tabs)
|
|
186
|
-
const agentName = agent.charAt(0).toUpperCase() + agent.slice(1);
|
|
186
|
+
const agentName = agent === 'agy' ? 'Antigravity' : agent.charAt(0).toUpperCase() + agent.slice(1);
|
|
187
187
|
document.title = \`\${percent}% \${agentName} | Agent Tank\`;
|
|
188
188
|
|
|
189
189
|
// Generate favicon using Canvas API
|
|
@@ -204,7 +204,7 @@ const monitorScript = `
|
|
|
204
204
|
|
|
205
205
|
// Send browser notification
|
|
206
206
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
207
|
-
const agentName = agent.charAt(0).toUpperCase() + agent.slice(1);
|
|
207
|
+
const agentName = agent === 'agy' ? 'Antigravity' : agent.charAt(0).toUpperCase() + agent.slice(1);
|
|
208
208
|
new Notification('Agent Tank Warning', {
|
|
209
209
|
body: \`\${agentName} \${label} is at \${percent}% usage!\`,
|
|
210
210
|
icon: '/favicon.ico',
|
package/src/discovery.js
CHANGED
|
@@ -22,22 +22,15 @@ async function discoverAgents() {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Check for
|
|
26
|
-
if (commandExists('
|
|
27
|
-
const version =
|
|
25
|
+
// Check for Antigravity CLI
|
|
26
|
+
if (commandExists('agy')) {
|
|
27
|
+
const version = getAgyVersion();
|
|
28
28
|
if (version) {
|
|
29
|
-
|
|
30
|
-
logger.warn(`Gemini CLI version ${version.full} detected. Version 0.24.5+ required for /stats command.`);
|
|
31
|
-
logger.warn('Please update: npm update -g gemini');
|
|
32
|
-
} else {
|
|
33
|
-
logger.success(`✅ Gemini CLI version ${version.full} detected`);
|
|
34
|
-
found.push('gemini');
|
|
35
|
-
}
|
|
29
|
+
logger.success(`✅ Antigravity CLI version ${version.full} detected`);
|
|
36
30
|
} else {
|
|
37
|
-
|
|
38
|
-
logger.warn('Gemini found but version unknown. Version 0.24.5+ required for /stats command.');
|
|
39
|
-
found.push('gemini');
|
|
31
|
+
logger.success('✅ Antigravity CLI detected (version unknown)');
|
|
40
32
|
}
|
|
33
|
+
found.push('agy');
|
|
41
34
|
}
|
|
42
35
|
|
|
43
36
|
// Check for Codex with version info
|
|
@@ -81,9 +74,9 @@ function getClaudeVersion() {
|
|
|
81
74
|
return null;
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
function
|
|
77
|
+
function getAgyVersion() {
|
|
85
78
|
try {
|
|
86
|
-
const output = execSync('
|
|
79
|
+
const output = execSync('agy --version 2>&1', { encoding: 'utf-8' });
|
|
87
80
|
const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
88
81
|
if (match) {
|
|
89
82
|
return {
|
|
@@ -117,12 +110,4 @@ function getCodexVersion() {
|
|
|
117
110
|
return null;
|
|
118
111
|
}
|
|
119
112
|
|
|
120
|
-
|
|
121
|
-
if (version.major > minMajor) return true;
|
|
122
|
-
if (version.major < minMajor) return false;
|
|
123
|
-
if (version.minor > minMinor) return true;
|
|
124
|
-
if (version.minor < minMinor) return false;
|
|
125
|
-
return version.patch >= minPatch;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = { discoverAgents };
|
|
113
|
+
module.exports = { discoverAgents };
|
package/src/history-store.js
CHANGED
|
@@ -130,7 +130,7 @@ class HistoryStore {
|
|
|
130
130
|
/**
|
|
131
131
|
* Add a usage snapshot to history.
|
|
132
132
|
*
|
|
133
|
-
* @param {string} agentName - Name of the agent (e.g., 'claude', '
|
|
133
|
+
* @param {string} agentName - Name of the agent (e.g., 'claude', 'agy', 'codex')
|
|
134
134
|
* @param {Object} usageData - The usage data to store
|
|
135
135
|
* @param {string} [timestamp] - ISO timestamp (defaults to current time)
|
|
136
136
|
* @returns {Object} The stored record
|
package/src/icons.js
CHANGED
|
@@ -12,8 +12,8 @@ const agentIcons = {
|
|
|
12
12
|
claude: `<svg class="agent-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
13
13
|
<path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />
|
|
14
14
|
</svg>`,
|
|
15
|
-
|
|
16
|
-
<path d="
|
|
15
|
+
agy: `<svg class="agent-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Antigravity">
|
|
16
|
+
<path d="M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z" />
|
|
17
17
|
</svg>`,
|
|
18
18
|
codex: `<svg class="agent-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
19
19
|
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- Tracked in issue #90; keep this PR scoped to background startup behavior. */
|
|
2
|
+
|
|
1
3
|
const path = require('node:path');
|
|
2
4
|
const os = require('node:os');
|
|
3
5
|
const { execFileSync } = require('node:child_process');
|
|
4
6
|
const { ClaudeAgent } = require('./agents/claude.js');
|
|
5
|
-
const {
|
|
7
|
+
const { AgyAgent } = require('./agents/agy.js');
|
|
6
8
|
const { CodexAgent } = require('./agents/codex.js');
|
|
7
9
|
const { discoverAgents } = require('./discovery.js');
|
|
8
10
|
const { fetchPublicStatus } = require('./public-status.js');
|
|
@@ -121,7 +123,6 @@ class AgentTank {
|
|
|
121
123
|
this.requestedAgents = options.agents || null;
|
|
122
124
|
this.freshProcess = options.freshProcess || false;
|
|
123
125
|
this.claudeApi = options.claudeApi || false; // Use direct Anthropic API for Claude
|
|
124
|
-
this.geminiMode = options.geminiMode || 'fallback';
|
|
125
126
|
this.auth = options.auth || {};
|
|
126
127
|
this.agents = new Map();
|
|
127
128
|
this.server = null;
|
|
@@ -236,11 +237,11 @@ class AgentTank {
|
|
|
236
237
|
logger.warn('No compatible LLM agents found.');
|
|
237
238
|
logger.info('Requirements:');
|
|
238
239
|
logger.info(' - Claude Code 2.0+ (for /usage command support)');
|
|
239
|
-
logger.info(' -
|
|
240
|
+
logger.info(' - Antigravity CLI as agy (for /usage command support)');
|
|
240
241
|
logger.info(' - Codex CLI');
|
|
241
242
|
logger.info('\nInstall/update with:');
|
|
242
243
|
logger.info(' npm install -g @anthropic-ai/claude-code@latest');
|
|
243
|
-
logger.info('
|
|
244
|
+
logger.info(' Install Antigravity CLI from https://antigravity.google/docs/cli-getting-started');
|
|
244
245
|
throw new Error('No agents found');
|
|
245
246
|
}
|
|
246
247
|
logger.success(`✅ Found agents: ${agentNames.join(', ')}`);
|
|
@@ -327,8 +328,8 @@ class AgentTank {
|
|
|
327
328
|
switch (name) {
|
|
328
329
|
case 'claude':
|
|
329
330
|
return new ClaudeAgent({ useApi: this.claudeApi });
|
|
330
|
-
case '
|
|
331
|
-
return new
|
|
331
|
+
case 'agy':
|
|
332
|
+
return new AgyAgent();
|
|
332
333
|
case 'codex':
|
|
333
334
|
return new CodexAgent();
|
|
334
335
|
default:
|
package/src/logger.js
CHANGED
|
@@ -39,7 +39,7 @@ const ANSI = {
|
|
|
39
39
|
// Agent color mapping
|
|
40
40
|
const AGENT_COLORS = {
|
|
41
41
|
claude: ANSI.magenta,
|
|
42
|
-
|
|
42
|
+
agy: ANSI.cyan,
|
|
43
43
|
codex: ANSI.green,
|
|
44
44
|
};
|
|
45
45
|
|
|
@@ -139,7 +139,7 @@ const logger = {
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Log an agent-specific message with mapped color
|
|
142
|
-
* @param {string} agentName - Name of the agent (claude,
|
|
142
|
+
* @param {string} agentName - Name of the agent (claude, agy, codex)
|
|
143
143
|
* @param {...any} args - Message arguments
|
|
144
144
|
*/
|
|
145
145
|
agent(agentName, ...args) {
|
package/src/pace-attachment.js
CHANGED
|
@@ -47,14 +47,14 @@ function attachClaudePace(usage) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Attach pace evaluation to
|
|
51
|
-
* @param {Object} usage -
|
|
50
|
+
* Attach pace evaluation to Antigravity usage metrics.
|
|
51
|
+
* @param {Object} usage - Antigravity usage data
|
|
52
52
|
*/
|
|
53
|
-
function
|
|
53
|
+
function attachAgyPace(usage) {
|
|
54
54
|
if (!usage.models || !Array.isArray(usage.models)) return;
|
|
55
55
|
|
|
56
56
|
for (const model of usage.models) {
|
|
57
|
-
attachPaceToData(model, 'percentUsed', '
|
|
57
|
+
attachPaceToData(model, 'percentUsed', 'sessionAgy');
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -86,8 +86,8 @@ function attachPaceEvaluation(agentName, usage) {
|
|
|
86
86
|
case 'claude':
|
|
87
87
|
attachClaudePace(usage);
|
|
88
88
|
break;
|
|
89
|
-
case '
|
|
90
|
-
|
|
89
|
+
case 'agy':
|
|
90
|
+
attachAgyPace(usage);
|
|
91
91
|
break;
|
|
92
92
|
case 'codex':
|
|
93
93
|
attachCodexPace(usage);
|
|
@@ -98,7 +98,7 @@ function attachPaceEvaluation(agentName, usage) {
|
|
|
98
98
|
module.exports = {
|
|
99
99
|
attachPaceEvaluation,
|
|
100
100
|
attachClaudePace,
|
|
101
|
-
|
|
101
|
+
attachAgyPace,
|
|
102
102
|
attachCodexPace,
|
|
103
103
|
attachPaceToData
|
|
104
104
|
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const { execFile } = require('node:child_process');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const BACKGROUND_ARG_RE = /^--(?:no-)?background$/;
|
|
5
|
+
const AGENT_TANK_BIN_RE = /(^|[\\/])bin[\\/]agent-tank\.js$/;
|
|
6
|
+
const PROCESS_SCAN_TIMEOUT_MS = 2000;
|
|
7
|
+
const WINDOWS_PROCESS_SCAN_TIMEOUT_MS = 8000;
|
|
8
|
+
const PROCESS_SCAN_MAX_BUFFER = 8 * 1024 * 1024;
|
|
9
|
+
const WINDOWS_PROCESS_SCAN_MAX_BUFFER = 32 * 1024 * 1024;
|
|
10
|
+
// This covers common Node flags that take a separate value; unknown flags may
|
|
11
|
+
// still be heuristic misses, but process warnings are best-effort only.
|
|
12
|
+
const NODE_OPTIONS_WITH_VALUE = new Set([
|
|
13
|
+
'-C',
|
|
14
|
+
'--conditions',
|
|
15
|
+
'-e',
|
|
16
|
+
'--eval',
|
|
17
|
+
'--experimental-loader',
|
|
18
|
+
'--import',
|
|
19
|
+
'--loader',
|
|
20
|
+
'-r',
|
|
21
|
+
'--require',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function isTruthyEnv(value) {
|
|
25
|
+
return value === '1' || value === 'true';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function filterBackgroundArgs(args) {
|
|
29
|
+
return args.filter(arg => !BACKGROUND_ARG_RE.test(arg));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseProcessList(output) {
|
|
33
|
+
return output
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map(line => line.trim())
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map(line => {
|
|
38
|
+
const match = line.match(/^(\d+)\s+(.+)$/);
|
|
39
|
+
if (!match) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
pid: Number(match[1]),
|
|
44
|
+
command: match[2],
|
|
45
|
+
};
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseWindowsProcessList(output) {
|
|
51
|
+
const parsed = JSON.parse(output || '[]');
|
|
52
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
53
|
+
|
|
54
|
+
return entries
|
|
55
|
+
.map(entry => ({
|
|
56
|
+
pid: Number(entry.ProcessId),
|
|
57
|
+
command: entry.CommandLine || '',
|
|
58
|
+
}))
|
|
59
|
+
.filter(entry => Number.isFinite(entry.pid) && entry.command);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function commandTokens(command) {
|
|
63
|
+
const tokens = [];
|
|
64
|
+
const tokenRe = /"([^"]*)"|'([^']*)'|[^\s]+/g;
|
|
65
|
+
let match;
|
|
66
|
+
|
|
67
|
+
while ((match = tokenRe.exec(command)) !== null) {
|
|
68
|
+
tokens.push(match[1] ?? match[2] ?? match[0]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return tokens;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function commandBasename(token) {
|
|
75
|
+
return path.basename(token.replace(/^"|"$/g, '').replace(/\\/g, '/')).toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isAgentTankExecutable(token) {
|
|
79
|
+
const executable = commandBasename(token);
|
|
80
|
+
return executable === 'agent-tank' ||
|
|
81
|
+
executable === 'agent-tank.cmd' ||
|
|
82
|
+
executable === 'agent-tank.ps1';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isAgentTankNodeScript(token) {
|
|
86
|
+
const scriptPath = token.replace(/^"|"$/g, '');
|
|
87
|
+
return AGENT_TANK_BIN_RE.test(scriptPath) || isAgentTankExecutable(scriptPath);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isNodeInterpreter(token) {
|
|
91
|
+
return /^(?:node|nodejs|node\d*)(?:\.exe)?$/.test(commandBasename(token));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findNodeScriptToken(tokens) {
|
|
95
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
96
|
+
const token = tokens[index];
|
|
97
|
+
|
|
98
|
+
if (NODE_OPTIONS_WITH_VALUE.has(token)) {
|
|
99
|
+
index += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (token.startsWith('--') && NODE_OPTIONS_WITH_VALUE.has(token.split('=')[0])) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (token.startsWith('-')) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return token;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isAgentTankCommand(command) {
|
|
118
|
+
const tokens = commandTokens(command);
|
|
119
|
+
if (tokens.length === 0) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (isAgentTankExecutable(tokens[0])) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!isNodeInterpreter(tokens[0])) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const scriptToken = findNodeScriptToken(tokens);
|
|
132
|
+
return Boolean(scriptToken && isAgentTankNodeScript(scriptToken));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getProcessScanCommand(platform, scanTimeoutMs) {
|
|
136
|
+
const timeout = scanTimeoutMs ?? (
|
|
137
|
+
platform === 'win32' ? WINDOWS_PROCESS_SCAN_TIMEOUT_MS : PROCESS_SCAN_TIMEOUT_MS
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (platform === 'win32') {
|
|
141
|
+
return {
|
|
142
|
+
file: 'powershell.exe',
|
|
143
|
+
args: [
|
|
144
|
+
'-NoProfile',
|
|
145
|
+
'-Command',
|
|
146
|
+
'Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress',
|
|
147
|
+
],
|
|
148
|
+
options: {
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
maxBuffer: WINDOWS_PROCESS_SCAN_MAX_BUFFER,
|
|
151
|
+
timeout,
|
|
152
|
+
},
|
|
153
|
+
parse: parseWindowsProcessList,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
file: 'ps',
|
|
159
|
+
args: ['-eo', 'pid=,args='],
|
|
160
|
+
options: {
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
maxBuffer: PROCESS_SCAN_MAX_BUFFER,
|
|
163
|
+
timeout,
|
|
164
|
+
},
|
|
165
|
+
parse: parseProcessList,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function filterAgentTankProcesses(processes, currentPid) {
|
|
170
|
+
return processes
|
|
171
|
+
.filter(entry => entry.pid !== currentPid)
|
|
172
|
+
.filter(entry => isAgentTankCommand(entry.command));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function execFilePromise(execFileFn, file, args, options) {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
execFileFn(file, args, options, (err, stdout) => {
|
|
178
|
+
if (err) {
|
|
179
|
+
reject(err);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
resolve(stdout);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function findAgentTankProcessesAsync({
|
|
189
|
+
currentPid = process.pid,
|
|
190
|
+
execFileFn = execFile,
|
|
191
|
+
platform = process.platform,
|
|
192
|
+
scanTimeoutMs,
|
|
193
|
+
} = {}) {
|
|
194
|
+
const command = getProcessScanCommand(platform, scanTimeoutMs);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const output = await execFilePromise(execFileFn, command.file, command.args, command.options);
|
|
198
|
+
return filterAgentTankProcesses(command.parse(output), currentPid);
|
|
199
|
+
} catch (_err) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
filterBackgroundArgs,
|
|
206
|
+
findAgentTankProcessesAsync,
|
|
207
|
+
isAgentTankCommand,
|
|
208
|
+
isTruthyEnv,
|
|
209
|
+
parseProcessList,
|
|
210
|
+
parseWindowsProcessList,
|
|
211
|
+
PROCESS_SCAN_MAX_BUFFER,
|
|
212
|
+
PROCESS_SCAN_TIMEOUT_MS,
|
|
213
|
+
WINDOWS_PROCESS_SCAN_MAX_BUFFER,
|
|
214
|
+
WINDOWS_PROCESS_SCAN_TIMEOUT_MS,
|
|
215
|
+
};
|