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.
@@ -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
- sessionGemini: 24 * 60 * 60,
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 Gemini usage data
78
- function extractGeminiMetrics(usage) {
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.toLowerCase();
83
- const metricId = \`gemini-\${modelName.replace(/[^a-z0-9]/g, '-')}\`;
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: 'gemini',
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: 'sessionGemini'
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 === 'gemini') {
146
- metrics = extractGeminiMetrics(agentData.usage);
145
+ } else if (agentName === 'agy') {
146
+ metrics = extractAgyMetrics(agentData.usage);
147
147
  } else if (agentName === 'codex') {
148
148
  metrics = extractCodexMetrics(agentData.usage);
149
149
  }
@@ -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 Gemini with version requirement
26
- if (commandExists('gemini')) {
27
- const version = getGeminiVersion();
25
+ // Check for Antigravity CLI
26
+ if (commandExists('agy')) {
27
+ const version = getAgyVersion();
28
28
  if (version) {
29
- if (!compareVersion(version, 0, 24, 5)) {
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
- // If we can't determine version, add with warning
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 getGeminiVersion() {
77
+ function getAgyVersion() {
85
78
  try {
86
- const output = execSync('gemini --version 2>&1', { encoding: 'utf-8' });
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
- function compareVersion(version, minMajor, minMinor, minPatch) {
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 };
@@ -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', 'gemini', 'codex')
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
- gemini: `<svg class="agent-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
16
- <path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" />
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 { GeminiAgent } = require('./agents/gemini.js');
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(' - Gemini CLI 0.24.5+ (for /stats command support)');
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(' npm install -g gemini@latest');
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 'gemini':
331
- return new GeminiAgent({ mode: this.geminiMode });
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
- gemini: ANSI.cyan,
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, gemini, codex)
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) {
@@ -47,14 +47,14 @@ function attachClaudePace(usage) {
47
47
  }
48
48
 
49
49
  /**
50
- * Attach pace evaluation to Gemini usage metrics.
51
- * @param {Object} usage - Gemini usage data
50
+ * Attach pace evaluation to Antigravity usage metrics.
51
+ * @param {Object} usage - Antigravity usage data
52
52
  */
53
- function attachGeminiPace(usage) {
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', 'sessionGemini');
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 'gemini':
90
- attachGeminiPace(usage);
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
- attachGeminiPace,
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
+ };