@gopherhole/cli 0.1.13 ā 0.1.15
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/dist/index.js +264 -2
- package/package.json +1 -1
- package/src/index.ts +307 -2
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const brand = {
|
|
|
19
19
|
greenDark: chalk_1.default.hex('#16a34a'), // gopher-600 - emphasis
|
|
20
20
|
};
|
|
21
21
|
// Version
|
|
22
|
-
const VERSION = '0.1.
|
|
22
|
+
const VERSION = '0.1.14';
|
|
23
23
|
// ASCII art banner
|
|
24
24
|
function showBanner(context) {
|
|
25
25
|
const gopher = [
|
|
@@ -1087,11 +1087,16 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1087
1087
|
$ gopherhole discover search "email assistant"
|
|
1088
1088
|
$ gopherhole discover search --category productivity
|
|
1089
1089
|
$ gopherhole discover search --tag ai --sort popular
|
|
1090
|
+
$ gopherhole discover search --skill-tag memory --scope tenant
|
|
1090
1091
|
`)
|
|
1091
1092
|
.option('-c, --category <category>', 'Filter by category')
|
|
1092
1093
|
.option('-t, --tag <tag>', 'Filter by tag')
|
|
1094
|
+
.option('--skill-tag <skillTag>', 'Filter by skill tag')
|
|
1095
|
+
.option('--content-mode <contentMode>', 'Filter by content mode (MIME type)')
|
|
1093
1096
|
.option('-s, --sort <sort>', 'Sort by: rating, popular, recent', 'rating')
|
|
1094
|
-
.option('-l, --limit <limit>', 'Number of results', '
|
|
1097
|
+
.option('-l, --limit <limit>', 'Number of results (max 50)', '10')
|
|
1098
|
+
.option('-o, --offset <offset>', 'Pagination offset')
|
|
1099
|
+
.option('--scope <scope>', 'Scope: tenant (same-tenant agents only)')
|
|
1095
1100
|
.action(async (query, options) => {
|
|
1096
1101
|
const spinner = (0, ora_1.default)('Searching agents...').start();
|
|
1097
1102
|
try {
|
|
@@ -1102,10 +1107,18 @@ ${chalk_1.default.bold('Examples:')}
|
|
|
1102
1107
|
params.set('category', options.category);
|
|
1103
1108
|
if (options.tag)
|
|
1104
1109
|
params.set('tag', options.tag);
|
|
1110
|
+
if (options.skillTag)
|
|
1111
|
+
params.set('skillTag', options.skillTag);
|
|
1112
|
+
if (options.contentMode)
|
|
1113
|
+
params.set('contentMode', options.contentMode);
|
|
1105
1114
|
if (options.sort)
|
|
1106
1115
|
params.set('sort', options.sort);
|
|
1107
1116
|
if (options.limit)
|
|
1108
1117
|
params.set('limit', options.limit);
|
|
1118
|
+
if (options.offset)
|
|
1119
|
+
params.set('offset', options.offset);
|
|
1120
|
+
if (options.scope)
|
|
1121
|
+
params.set('scope', options.scope);
|
|
1109
1122
|
log('GET /discover/agents?' + params.toString());
|
|
1110
1123
|
const sessionId = config.get('sessionId');
|
|
1111
1124
|
const headers = {};
|
|
@@ -1723,6 +1736,255 @@ access
|
|
|
1723
1736
|
process.exit(1);
|
|
1724
1737
|
}
|
|
1725
1738
|
});
|
|
1739
|
+
// ========== BUDGET COMMAND ==========
|
|
1740
|
+
const budget = program
|
|
1741
|
+
.command('budget')
|
|
1742
|
+
.description(`View and manage agent spending limits
|
|
1743
|
+
|
|
1744
|
+
${chalk_1.default.bold('Examples:')}
|
|
1745
|
+
$ gopherhole budget my-agent # View budget
|
|
1746
|
+
$ gopherhole budget my-agent -d 10 -w 50 # Set daily=$10, weekly=$50
|
|
1747
|
+
$ gopherhole budget my-agent --clear # Remove all limits
|
|
1748
|
+
$ gopherhole budget --all # List all agents' budgets
|
|
1749
|
+
`);
|
|
1750
|
+
budget
|
|
1751
|
+
.argument('[agentId]', 'Agent ID (optional with --all)')
|
|
1752
|
+
.option('-a, --all', 'List all agents\' budgets')
|
|
1753
|
+
.option('-d, --daily <amount>', 'Set daily limit (dollars)')
|
|
1754
|
+
.option('-w, --weekly <amount>', 'Set weekly limit (dollars)')
|
|
1755
|
+
.option('-r, --per-request <amount>', 'Set per-request max (dollars)')
|
|
1756
|
+
.option('--clear', 'Remove all spending limits')
|
|
1757
|
+
.option('--json', 'Output as JSON')
|
|
1758
|
+
.action(async (agentId, options) => {
|
|
1759
|
+
const sessionId = config.get('sessionId');
|
|
1760
|
+
if (!sessionId) {
|
|
1761
|
+
console.log(chalk_1.default.yellow('Not logged in.'));
|
|
1762
|
+
console.log(chalk_1.default.gray('Run: gopherhole login'));
|
|
1763
|
+
process.exit(1);
|
|
1764
|
+
}
|
|
1765
|
+
// Handle --all flag
|
|
1766
|
+
if (options.all) {
|
|
1767
|
+
const spinner = (0, ora_1.default)('Fetching spending overview...').start();
|
|
1768
|
+
try {
|
|
1769
|
+
const res = await fetch(`${API_URL}/spending`, {
|
|
1770
|
+
headers: { 'X-Session-ID': sessionId },
|
|
1771
|
+
});
|
|
1772
|
+
if (!res.ok) {
|
|
1773
|
+
throw new Error('Failed to fetch spending overview');
|
|
1774
|
+
}
|
|
1775
|
+
const data = await res.json();
|
|
1776
|
+
spinner.stop();
|
|
1777
|
+
if (options.json) {
|
|
1778
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
console.log(chalk_1.default.bold('\nš° Spending Overview\n'));
|
|
1782
|
+
console.log(` Today: $${data.totalSpentToday.toFixed(2)} This week: $${data.totalSpentThisWeek.toFixed(2)}\n`);
|
|
1783
|
+
if (data.agents.length === 0) {
|
|
1784
|
+
console.log(chalk_1.default.gray(' No agents found.\n'));
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
// Header
|
|
1788
|
+
console.log(chalk_1.default.gray(' Agent Daily Weekly Status'));
|
|
1789
|
+
console.log(chalk_1.default.gray(' ā'.padEnd(70, 'ā')));
|
|
1790
|
+
for (const agent of data.agents) {
|
|
1791
|
+
const name = agent.name.padEnd(18).slice(0, 18);
|
|
1792
|
+
let daily = 'ā'.padEnd(12);
|
|
1793
|
+
if (agent.daily?.limit !== null && agent.daily?.limit !== undefined) {
|
|
1794
|
+
const bar = getProgressBar(agent.daily.percent ?? 0, 5);
|
|
1795
|
+
daily = `$${agent.daily.spent.toFixed(0)}/$${agent.daily.limit.toFixed(0)} ${bar}`.padEnd(12);
|
|
1796
|
+
}
|
|
1797
|
+
else if (agent.daily?.spent) {
|
|
1798
|
+
daily = `$${agent.daily.spent.toFixed(2)}`.padEnd(12);
|
|
1799
|
+
}
|
|
1800
|
+
let weekly = 'ā'.padEnd(12);
|
|
1801
|
+
if (agent.weekly?.limit !== null && agent.weekly?.limit !== undefined) {
|
|
1802
|
+
const bar = getProgressBar(agent.weekly.percent ?? 0, 5);
|
|
1803
|
+
weekly = `$${agent.weekly.spent.toFixed(0)}/$${agent.weekly.limit.toFixed(0)} ${bar}`.padEnd(12);
|
|
1804
|
+
}
|
|
1805
|
+
else if (agent.weekly?.spent) {
|
|
1806
|
+
weekly = `$${agent.weekly.spent.toFixed(2)}`.padEnd(12);
|
|
1807
|
+
}
|
|
1808
|
+
const statusEmoji = {
|
|
1809
|
+
ok: brand.green('ā
OK'),
|
|
1810
|
+
warning: chalk_1.default.yellow('ā ļø Warning'),
|
|
1811
|
+
critical: chalk_1.default.red('š“ Critical'),
|
|
1812
|
+
blocked: chalk_1.default.red('š« Blocked'),
|
|
1813
|
+
unlimited: chalk_1.default.gray('ā'),
|
|
1814
|
+
}[agent.status] ?? agent.status;
|
|
1815
|
+
console.log(` ${name} ${daily} ${weekly} ${statusEmoji}`);
|
|
1816
|
+
}
|
|
1817
|
+
console.log('');
|
|
1818
|
+
}
|
|
1819
|
+
catch (err) {
|
|
1820
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
}
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
// Need agentId for single-agent operations
|
|
1826
|
+
if (!agentId) {
|
|
1827
|
+
console.log(chalk_1.default.yellow('Please provide an agent ID or use --all'));
|
|
1828
|
+
console.log(chalk_1.default.gray('Usage: gopherhole budget <agentId> [options]'));
|
|
1829
|
+
console.log(chalk_1.default.gray(' gopherhole budget --all'));
|
|
1830
|
+
process.exit(1);
|
|
1831
|
+
}
|
|
1832
|
+
// Handle --clear flag
|
|
1833
|
+
if (options.clear) {
|
|
1834
|
+
const spinner = (0, ora_1.default)('Removing spending limits...').start();
|
|
1835
|
+
try {
|
|
1836
|
+
const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
|
|
1837
|
+
method: 'DELETE',
|
|
1838
|
+
headers: { 'X-Session-ID': sessionId },
|
|
1839
|
+
});
|
|
1840
|
+
if (!res.ok) {
|
|
1841
|
+
throw new Error('Failed to remove spending limits');
|
|
1842
|
+
}
|
|
1843
|
+
spinner.succeed('Spending limits removed');
|
|
1844
|
+
console.log(chalk_1.default.gray(` Agent ${agentId} now has no spending limits.\n`));
|
|
1845
|
+
}
|
|
1846
|
+
catch (err) {
|
|
1847
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
1848
|
+
process.exit(1);
|
|
1849
|
+
}
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
// Handle setting limits
|
|
1853
|
+
if (options.daily !== undefined || options.weekly !== undefined || options.perRequest !== undefined) {
|
|
1854
|
+
const spinner = (0, ora_1.default)('Updating spending limits...').start();
|
|
1855
|
+
try {
|
|
1856
|
+
const body = {};
|
|
1857
|
+
if (options.daily !== undefined) {
|
|
1858
|
+
body.dailyLimit = options.daily === 'unlimited' ? null : parseFloat(options.daily);
|
|
1859
|
+
}
|
|
1860
|
+
if (options.weekly !== undefined) {
|
|
1861
|
+
body.weeklyLimit = options.weekly === 'unlimited' ? null : parseFloat(options.weekly);
|
|
1862
|
+
}
|
|
1863
|
+
if (options.perRequest !== undefined) {
|
|
1864
|
+
body.maxPerRequest = options.perRequest === 'unlimited' ? null : parseFloat(options.perRequest);
|
|
1865
|
+
}
|
|
1866
|
+
const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
|
|
1867
|
+
method: 'PUT',
|
|
1868
|
+
headers: {
|
|
1869
|
+
'X-Session-ID': sessionId,
|
|
1870
|
+
'Content-Type': 'application/json',
|
|
1871
|
+
},
|
|
1872
|
+
body: JSON.stringify(body),
|
|
1873
|
+
});
|
|
1874
|
+
if (!res.ok) {
|
|
1875
|
+
const err = await res.json();
|
|
1876
|
+
throw new Error(err.error || 'Failed to update limits');
|
|
1877
|
+
}
|
|
1878
|
+
spinner.succeed('Spending limits updated');
|
|
1879
|
+
// Show the updated limits
|
|
1880
|
+
const data = await res.json();
|
|
1881
|
+
console.log(chalk_1.default.gray('\n New limits:'));
|
|
1882
|
+
if (data.limits.daily) {
|
|
1883
|
+
console.log(` Daily: $${data.limits.daily.limit.toFixed(2)}`);
|
|
1884
|
+
}
|
|
1885
|
+
if (data.limits.weekly) {
|
|
1886
|
+
console.log(` Weekly: $${data.limits.weekly.limit.toFixed(2)}`);
|
|
1887
|
+
}
|
|
1888
|
+
if (data.limits.maxPerRequest) {
|
|
1889
|
+
console.log(` Per Request: $${data.limits.maxPerRequest.toFixed(2)}`);
|
|
1890
|
+
}
|
|
1891
|
+
console.log('');
|
|
1892
|
+
}
|
|
1893
|
+
catch (err) {
|
|
1894
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
1895
|
+
process.exit(1);
|
|
1896
|
+
}
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
// Default: show budget
|
|
1900
|
+
const spinner = (0, ora_1.default)('Fetching budget...').start();
|
|
1901
|
+
try {
|
|
1902
|
+
const res = await fetch(`${API_URL}/agents/${agentId}/budget`, {
|
|
1903
|
+
headers: { 'X-Session-ID': sessionId },
|
|
1904
|
+
});
|
|
1905
|
+
if (!res.ok) {
|
|
1906
|
+
if (res.status === 404) {
|
|
1907
|
+
throw new Error('Agent not found');
|
|
1908
|
+
}
|
|
1909
|
+
throw new Error('Failed to fetch budget');
|
|
1910
|
+
}
|
|
1911
|
+
const data = await res.json();
|
|
1912
|
+
spinner.stop();
|
|
1913
|
+
if (options.json) {
|
|
1914
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
console.log(chalk_1.default.bold(`\nš° Budget for ${agentId}\n`));
|
|
1918
|
+
if (!data.daily && !data.weekly && !data.maxPerRequest) {
|
|
1919
|
+
console.log(chalk_1.default.gray(' No spending limits configured.\n'));
|
|
1920
|
+
console.log(chalk_1.default.gray(' Set limits: gopherhole budget <agentId> -d 10 -w 50\n'));
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (data.daily) {
|
|
1924
|
+
const percent = Math.round((data.daily.spent / data.daily.limit) * 100);
|
|
1925
|
+
const bar = getProgressBar(percent, 10);
|
|
1926
|
+
const timeLeft = getTimeUntil(data.daily.resetsAt);
|
|
1927
|
+
const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
|
|
1928
|
+
console.log(` Daily: ${color(`$${data.daily.spent.toFixed(2)} / $${data.daily.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
|
|
1929
|
+
console.log(chalk_1.default.gray(` Resets in ${timeLeft}`));
|
|
1930
|
+
}
|
|
1931
|
+
if (data.weekly) {
|
|
1932
|
+
const percent = Math.round((data.weekly.spent / data.weekly.limit) * 100);
|
|
1933
|
+
const bar = getProgressBar(percent, 10);
|
|
1934
|
+
const timeLeft = getTimeUntil(data.weekly.resetsAt);
|
|
1935
|
+
const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
|
|
1936
|
+
console.log(` Weekly: ${color(`$${data.weekly.spent.toFixed(2)} / $${data.weekly.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
|
|
1937
|
+
console.log(chalk_1.default.gray(` Resets ${timeLeft}`));
|
|
1938
|
+
}
|
|
1939
|
+
if (data.maxPerRequest) {
|
|
1940
|
+
console.log(` Per Request: $${data.maxPerRequest.toFixed(2)} max`);
|
|
1941
|
+
}
|
|
1942
|
+
// Status
|
|
1943
|
+
let status = brand.green('ā
OK');
|
|
1944
|
+
const dailyPercent = data.daily ? (data.daily.spent / data.daily.limit) * 100 : 0;
|
|
1945
|
+
const weeklyPercent = data.weekly ? (data.weekly.spent / data.weekly.limit) * 100 : 0;
|
|
1946
|
+
const maxPercent = Math.max(dailyPercent, weeklyPercent);
|
|
1947
|
+
if (maxPercent >= 100) {
|
|
1948
|
+
status = chalk_1.default.red('š« Blocked');
|
|
1949
|
+
}
|
|
1950
|
+
else if (maxPercent >= 80) {
|
|
1951
|
+
status = chalk_1.default.red('š“ Critical');
|
|
1952
|
+
}
|
|
1953
|
+
else if (maxPercent >= 50) {
|
|
1954
|
+
status = chalk_1.default.yellow('ā ļø Warning');
|
|
1955
|
+
}
|
|
1956
|
+
console.log(`\n Status: ${status}\n`);
|
|
1957
|
+
}
|
|
1958
|
+
catch (err) {
|
|
1959
|
+
spinner.fail(chalk_1.default.red(err.message));
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
// Helper: progress bar
|
|
1964
|
+
function getProgressBar(percent, width) {
|
|
1965
|
+
const filled = Math.round((percent / 100) * width);
|
|
1966
|
+
const empty = width - filled;
|
|
1967
|
+
const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
|
|
1968
|
+
return color('ā'.repeat(filled)) + chalk_1.default.gray('ā'.repeat(empty));
|
|
1969
|
+
}
|
|
1970
|
+
// Helper: time until
|
|
1971
|
+
function getTimeUntil(isoDate) {
|
|
1972
|
+
const target = new Date(isoDate);
|
|
1973
|
+
const now = new Date();
|
|
1974
|
+
const diffMs = target.getTime() - now.getTime();
|
|
1975
|
+
if (diffMs <= 0)
|
|
1976
|
+
return 'now';
|
|
1977
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
1978
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
1979
|
+
if (hours > 24) {
|
|
1980
|
+
const days = Math.floor(hours / 24);
|
|
1981
|
+
return `${days}d ${hours % 24}h`;
|
|
1982
|
+
}
|
|
1983
|
+
if (hours > 0) {
|
|
1984
|
+
return `${hours}h ${minutes}m`;
|
|
1985
|
+
}
|
|
1986
|
+
return `${minutes}m`;
|
|
1987
|
+
}
|
|
1726
1988
|
// ========== STATUS COMMAND ==========
|
|
1727
1989
|
program
|
|
1728
1990
|
.command('status')
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ const brand = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// Version
|
|
21
|
-
const VERSION = '0.1.
|
|
21
|
+
const VERSION = '0.1.14';
|
|
22
22
|
|
|
23
23
|
// ASCII art banner
|
|
24
24
|
function showBanner(context?: string) {
|
|
@@ -1208,11 +1208,16 @@ ${chalk.bold('Examples:')}
|
|
|
1208
1208
|
$ gopherhole discover search "email assistant"
|
|
1209
1209
|
$ gopherhole discover search --category productivity
|
|
1210
1210
|
$ gopherhole discover search --tag ai --sort popular
|
|
1211
|
+
$ gopherhole discover search --skill-tag memory --scope tenant
|
|
1211
1212
|
`)
|
|
1212
1213
|
.option('-c, --category <category>', 'Filter by category')
|
|
1213
1214
|
.option('-t, --tag <tag>', 'Filter by tag')
|
|
1215
|
+
.option('--skill-tag <skillTag>', 'Filter by skill tag')
|
|
1216
|
+
.option('--content-mode <contentMode>', 'Filter by content mode (MIME type)')
|
|
1214
1217
|
.option('-s, --sort <sort>', 'Sort by: rating, popular, recent', 'rating')
|
|
1215
|
-
.option('-l, --limit <limit>', 'Number of results', '
|
|
1218
|
+
.option('-l, --limit <limit>', 'Number of results (max 50)', '10')
|
|
1219
|
+
.option('-o, --offset <offset>', 'Pagination offset')
|
|
1220
|
+
.option('--scope <scope>', 'Scope: tenant (same-tenant agents only)')
|
|
1216
1221
|
.action(async (query, options) => {
|
|
1217
1222
|
const spinner = ora('Searching agents...').start();
|
|
1218
1223
|
|
|
@@ -1221,8 +1226,12 @@ ${chalk.bold('Examples:')}
|
|
|
1221
1226
|
if (query) params.set('q', query);
|
|
1222
1227
|
if (options.category) params.set('category', options.category);
|
|
1223
1228
|
if (options.tag) params.set('tag', options.tag);
|
|
1229
|
+
if (options.skillTag) params.set('skillTag', options.skillTag);
|
|
1230
|
+
if (options.contentMode) params.set('contentMode', options.contentMode);
|
|
1224
1231
|
if (options.sort) params.set('sort', options.sort);
|
|
1225
1232
|
if (options.limit) params.set('limit', options.limit);
|
|
1233
|
+
if (options.offset) params.set('offset', options.offset);
|
|
1234
|
+
if (options.scope) params.set('scope', options.scope);
|
|
1226
1235
|
|
|
1227
1236
|
log('GET /discover/agents?' + params.toString());
|
|
1228
1237
|
const sessionId = config.get('sessionId') as string | undefined;
|
|
@@ -1891,6 +1900,302 @@ access
|
|
|
1891
1900
|
}
|
|
1892
1901
|
});
|
|
1893
1902
|
|
|
1903
|
+
// ========== BUDGET COMMAND ==========
|
|
1904
|
+
|
|
1905
|
+
const budget = program
|
|
1906
|
+
.command('budget')
|
|
1907
|
+
.description(`View and manage agent spending limits
|
|
1908
|
+
|
|
1909
|
+
${chalk.bold('Examples:')}
|
|
1910
|
+
$ gopherhole budget my-agent # View budget
|
|
1911
|
+
$ gopherhole budget my-agent -d 10 -w 50 # Set daily=$10, weekly=$50
|
|
1912
|
+
$ gopherhole budget my-agent --clear # Remove all limits
|
|
1913
|
+
$ gopherhole budget --all # List all agents' budgets
|
|
1914
|
+
`);
|
|
1915
|
+
|
|
1916
|
+
budget
|
|
1917
|
+
.argument('[agentId]', 'Agent ID (optional with --all)')
|
|
1918
|
+
.option('-a, --all', 'List all agents\' budgets')
|
|
1919
|
+
.option('-d, --daily <amount>', 'Set daily limit (dollars)')
|
|
1920
|
+
.option('-w, --weekly <amount>', 'Set weekly limit (dollars)')
|
|
1921
|
+
.option('-r, --per-request <amount>', 'Set per-request max (dollars)')
|
|
1922
|
+
.option('--clear', 'Remove all spending limits')
|
|
1923
|
+
.option('--json', 'Output as JSON')
|
|
1924
|
+
.action(async (agentId: string | undefined, options) => {
|
|
1925
|
+
const sessionId = config.get('sessionId') as string;
|
|
1926
|
+
if (!sessionId) {
|
|
1927
|
+
console.log(chalk.yellow('Not logged in.'));
|
|
1928
|
+
console.log(chalk.gray('Run: gopherhole login'));
|
|
1929
|
+
process.exit(1);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Handle --all flag
|
|
1933
|
+
if (options.all) {
|
|
1934
|
+
const spinner = ora('Fetching spending overview...').start();
|
|
1935
|
+
try {
|
|
1936
|
+
const res = await fetch(`${API_URL}/spending`, {
|
|
1937
|
+
headers: { 'X-Session-ID': sessionId },
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
if (!res.ok) {
|
|
1941
|
+
throw new Error('Failed to fetch spending overview');
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const data = await res.json() as {
|
|
1945
|
+
agents: Array<{
|
|
1946
|
+
agentId: string;
|
|
1947
|
+
name: string;
|
|
1948
|
+
daily: { spent: number; limit: number | null; percent: number | null } | null;
|
|
1949
|
+
weekly: { spent: number; limit: number | null; percent: number | null } | null;
|
|
1950
|
+
status: string;
|
|
1951
|
+
}>;
|
|
1952
|
+
totalSpentToday: number;
|
|
1953
|
+
totalSpentThisWeek: number;
|
|
1954
|
+
};
|
|
1955
|
+
spinner.stop();
|
|
1956
|
+
|
|
1957
|
+
if (options.json) {
|
|
1958
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
console.log(chalk.bold('\nš° Spending Overview\n'));
|
|
1963
|
+
console.log(` Today: $${data.totalSpentToday.toFixed(2)} This week: $${data.totalSpentThisWeek.toFixed(2)}\n`);
|
|
1964
|
+
|
|
1965
|
+
if (data.agents.length === 0) {
|
|
1966
|
+
console.log(chalk.gray(' No agents found.\n'));
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Header
|
|
1971
|
+
console.log(chalk.gray(' Agent Daily Weekly Status'));
|
|
1972
|
+
console.log(chalk.gray(' ā'.padEnd(70, 'ā')));
|
|
1973
|
+
|
|
1974
|
+
for (const agent of data.agents) {
|
|
1975
|
+
const name = agent.name.padEnd(18).slice(0, 18);
|
|
1976
|
+
|
|
1977
|
+
let daily = 'ā'.padEnd(12);
|
|
1978
|
+
if (agent.daily?.limit !== null && agent.daily?.limit !== undefined) {
|
|
1979
|
+
const bar = getProgressBar(agent.daily.percent ?? 0, 5);
|
|
1980
|
+
daily = `$${agent.daily.spent.toFixed(0)}/$${agent.daily.limit.toFixed(0)} ${bar}`.padEnd(12);
|
|
1981
|
+
} else if (agent.daily?.spent) {
|
|
1982
|
+
daily = `$${agent.daily.spent.toFixed(2)}`.padEnd(12);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
let weekly = 'ā'.padEnd(12);
|
|
1986
|
+
if (agent.weekly?.limit !== null && agent.weekly?.limit !== undefined) {
|
|
1987
|
+
const bar = getProgressBar(agent.weekly.percent ?? 0, 5);
|
|
1988
|
+
weekly = `$${agent.weekly.spent.toFixed(0)}/$${agent.weekly.limit.toFixed(0)} ${bar}`.padEnd(12);
|
|
1989
|
+
} else if (agent.weekly?.spent) {
|
|
1990
|
+
weekly = `$${agent.weekly.spent.toFixed(2)}`.padEnd(12);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const statusEmoji = {
|
|
1994
|
+
ok: brand.green('ā
OK'),
|
|
1995
|
+
warning: chalk.yellow('ā ļø Warning'),
|
|
1996
|
+
critical: chalk.red('š“ Critical'),
|
|
1997
|
+
blocked: chalk.red('š« Blocked'),
|
|
1998
|
+
unlimited: chalk.gray('ā'),
|
|
1999
|
+
}[agent.status] ?? agent.status;
|
|
2000
|
+
|
|
2001
|
+
console.log(` ${name} ${daily} ${weekly} ${statusEmoji}`);
|
|
2002
|
+
}
|
|
2003
|
+
console.log('');
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
spinner.fail(chalk.red((err as Error).message));
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Need agentId for single-agent operations
|
|
2012
|
+
if (!agentId) {
|
|
2013
|
+
console.log(chalk.yellow('Please provide an agent ID or use --all'));
|
|
2014
|
+
console.log(chalk.gray('Usage: gopherhole budget <agentId> [options]'));
|
|
2015
|
+
console.log(chalk.gray(' gopherhole budget --all'));
|
|
2016
|
+
process.exit(1);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Handle --clear flag
|
|
2020
|
+
if (options.clear) {
|
|
2021
|
+
const spinner = ora('Removing spending limits...').start();
|
|
2022
|
+
try {
|
|
2023
|
+
const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
|
|
2024
|
+
method: 'DELETE',
|
|
2025
|
+
headers: { 'X-Session-ID': sessionId },
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
if (!res.ok) {
|
|
2029
|
+
throw new Error('Failed to remove spending limits');
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
spinner.succeed('Spending limits removed');
|
|
2033
|
+
console.log(chalk.gray(` Agent ${agentId} now has no spending limits.\n`));
|
|
2034
|
+
} catch (err) {
|
|
2035
|
+
spinner.fail(chalk.red((err as Error).message));
|
|
2036
|
+
process.exit(1);
|
|
2037
|
+
}
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Handle setting limits
|
|
2042
|
+
if (options.daily !== undefined || options.weekly !== undefined || options.perRequest !== undefined) {
|
|
2043
|
+
const spinner = ora('Updating spending limits...').start();
|
|
2044
|
+
try {
|
|
2045
|
+
const body: Record<string, number | null> = {};
|
|
2046
|
+
|
|
2047
|
+
if (options.daily !== undefined) {
|
|
2048
|
+
body.dailyLimit = options.daily === 'unlimited' ? null : parseFloat(options.daily);
|
|
2049
|
+
}
|
|
2050
|
+
if (options.weekly !== undefined) {
|
|
2051
|
+
body.weeklyLimit = options.weekly === 'unlimited' ? null : parseFloat(options.weekly);
|
|
2052
|
+
}
|
|
2053
|
+
if (options.perRequest !== undefined) {
|
|
2054
|
+
body.maxPerRequest = options.perRequest === 'unlimited' ? null : parseFloat(options.perRequest);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
|
|
2058
|
+
method: 'PUT',
|
|
2059
|
+
headers: {
|
|
2060
|
+
'X-Session-ID': sessionId,
|
|
2061
|
+
'Content-Type': 'application/json',
|
|
2062
|
+
},
|
|
2063
|
+
body: JSON.stringify(body),
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
if (!res.ok) {
|
|
2067
|
+
const err = await res.json();
|
|
2068
|
+
throw new Error((err as { error?: string }).error || 'Failed to update limits');
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
spinner.succeed('Spending limits updated');
|
|
2072
|
+
|
|
2073
|
+
// Show the updated limits
|
|
2074
|
+
const data = await res.json() as { limits: Record<string, unknown> };
|
|
2075
|
+
console.log(chalk.gray('\n New limits:'));
|
|
2076
|
+
if ((data.limits as any).daily) {
|
|
2077
|
+
console.log(` Daily: $${((data.limits as any).daily.limit as number).toFixed(2)}`);
|
|
2078
|
+
}
|
|
2079
|
+
if ((data.limits as any).weekly) {
|
|
2080
|
+
console.log(` Weekly: $${((data.limits as any).weekly.limit as number).toFixed(2)}`);
|
|
2081
|
+
}
|
|
2082
|
+
if ((data.limits as any).maxPerRequest) {
|
|
2083
|
+
console.log(` Per Request: $${((data.limits as any).maxPerRequest as number).toFixed(2)}`);
|
|
2084
|
+
}
|
|
2085
|
+
console.log('');
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
spinner.fail(chalk.red((err as Error).message));
|
|
2088
|
+
process.exit(1);
|
|
2089
|
+
}
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// Default: show budget
|
|
2094
|
+
const spinner = ora('Fetching budget...').start();
|
|
2095
|
+
try {
|
|
2096
|
+
const res = await fetch(`${API_URL}/agents/${agentId}/budget`, {
|
|
2097
|
+
headers: { 'X-Session-ID': sessionId },
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
if (!res.ok) {
|
|
2101
|
+
if (res.status === 404) {
|
|
2102
|
+
throw new Error('Agent not found');
|
|
2103
|
+
}
|
|
2104
|
+
throw new Error('Failed to fetch budget');
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
const data = await res.json() as {
|
|
2108
|
+
daily: { limit: number; spent: number; remaining: number; resetsAt: string } | null;
|
|
2109
|
+
weekly: { limit: number; spent: number; remaining: number; resetsAt: string } | null;
|
|
2110
|
+
maxPerRequest: number | null;
|
|
2111
|
+
};
|
|
2112
|
+
spinner.stop();
|
|
2113
|
+
|
|
2114
|
+
if (options.json) {
|
|
2115
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
console.log(chalk.bold(`\nš° Budget for ${agentId}\n`));
|
|
2120
|
+
|
|
2121
|
+
if (!data.daily && !data.weekly && !data.maxPerRequest) {
|
|
2122
|
+
console.log(chalk.gray(' No spending limits configured.\n'));
|
|
2123
|
+
console.log(chalk.gray(' Set limits: gopherhole budget <agentId> -d 10 -w 50\n'));
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
if (data.daily) {
|
|
2128
|
+
const percent = Math.round((data.daily.spent / data.daily.limit) * 100);
|
|
2129
|
+
const bar = getProgressBar(percent, 10);
|
|
2130
|
+
const timeLeft = getTimeUntil(data.daily.resetsAt);
|
|
2131
|
+
const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
|
|
2132
|
+
console.log(` Daily: ${color(`$${data.daily.spent.toFixed(2)} / $${data.daily.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
|
|
2133
|
+
console.log(chalk.gray(` Resets in ${timeLeft}`));
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
if (data.weekly) {
|
|
2137
|
+
const percent = Math.round((data.weekly.spent / data.weekly.limit) * 100);
|
|
2138
|
+
const bar = getProgressBar(percent, 10);
|
|
2139
|
+
const timeLeft = getTimeUntil(data.weekly.resetsAt);
|
|
2140
|
+
const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
|
|
2141
|
+
console.log(` Weekly: ${color(`$${data.weekly.spent.toFixed(2)} / $${data.weekly.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
|
|
2142
|
+
console.log(chalk.gray(` Resets ${timeLeft}`));
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
if (data.maxPerRequest) {
|
|
2146
|
+
console.log(` Per Request: $${data.maxPerRequest.toFixed(2)} max`);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// Status
|
|
2150
|
+
let status = brand.green('ā
OK');
|
|
2151
|
+
const dailyPercent = data.daily ? (data.daily.spent / data.daily.limit) * 100 : 0;
|
|
2152
|
+
const weeklyPercent = data.weekly ? (data.weekly.spent / data.weekly.limit) * 100 : 0;
|
|
2153
|
+
const maxPercent = Math.max(dailyPercent, weeklyPercent);
|
|
2154
|
+
|
|
2155
|
+
if (maxPercent >= 100) {
|
|
2156
|
+
status = chalk.red('š« Blocked');
|
|
2157
|
+
} else if (maxPercent >= 80) {
|
|
2158
|
+
status = chalk.red('š“ Critical');
|
|
2159
|
+
} else if (maxPercent >= 50) {
|
|
2160
|
+
status = chalk.yellow('ā ļø Warning');
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
console.log(`\n Status: ${status}\n`);
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
spinner.fail(chalk.red((err as Error).message));
|
|
2166
|
+
process.exit(1);
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
|
|
2170
|
+
// Helper: progress bar
|
|
2171
|
+
function getProgressBar(percent: number, width: number): string {
|
|
2172
|
+
const filled = Math.round((percent / 100) * width);
|
|
2173
|
+
const empty = width - filled;
|
|
2174
|
+
const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
|
|
2175
|
+
return color('ā'.repeat(filled)) + chalk.gray('ā'.repeat(empty));
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Helper: time until
|
|
2179
|
+
function getTimeUntil(isoDate: string): string {
|
|
2180
|
+
const target = new Date(isoDate);
|
|
2181
|
+
const now = new Date();
|
|
2182
|
+
const diffMs = target.getTime() - now.getTime();
|
|
2183
|
+
|
|
2184
|
+
if (diffMs <= 0) return 'now';
|
|
2185
|
+
|
|
2186
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
2187
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
2188
|
+
|
|
2189
|
+
if (hours > 24) {
|
|
2190
|
+
const days = Math.floor(hours / 24);
|
|
2191
|
+
return `${days}d ${hours % 24}h`;
|
|
2192
|
+
}
|
|
2193
|
+
if (hours > 0) {
|
|
2194
|
+
return `${hours}h ${minutes}m`;
|
|
2195
|
+
}
|
|
2196
|
+
return `${minutes}m`;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1894
2199
|
// ========== STATUS COMMAND ==========
|
|
1895
2200
|
|
|
1896
2201
|
program
|