suzi-cli 0.1.26 → 0.1.28
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/CLAUDE.md +1 -0
- package/dist/commands/agent.d.ts +12 -2
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +277 -27
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +1 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/portfolio.d.ts.map +1 -1
- package/dist/commands/portfolio.js +525 -102
- package/dist/commands/portfolio.js.map +1 -1
- package/dist/commands/prompt-suggestions.d.ts.map +1 -1
- package/dist/commands/prompt-suggestions.js +19 -0
- package/dist/commands/prompt-suggestions.js.map +1 -1
- package/dist/commands/subagents.d.ts.map +1 -1
- package/dist/commands/subagents.js +2 -3
- package/dist/commands/subagents.js.map +1 -1
- package/dist/types/portfolio.d.ts +61 -0
- package/dist/types/portfolio.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,8 @@ const config_1 = require("../lib/config");
|
|
|
11
11
|
const api_1 = require("../lib/api");
|
|
12
12
|
const ui_1 = require("../utils/ui");
|
|
13
13
|
const tty_1 = require("../utils/tty");
|
|
14
|
+
const agent_picker_1 = require("../utils/agent-picker");
|
|
15
|
+
const PORTFOLIO_ORDERS_INCLUDES = new Set(['fills']);
|
|
14
16
|
function formatPnlPercent(pct) {
|
|
15
17
|
if (pct == null)
|
|
16
18
|
return chalk_1.default.gray('—');
|
|
@@ -35,17 +37,230 @@ function exposureLabel(pos) {
|
|
|
35
37
|
return '—';
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
function formatPortfolioOrderStatus(status) {
|
|
41
|
+
if (status === 'filled')
|
|
42
|
+
return chalk_1.default.green('Filled');
|
|
43
|
+
if (status === 'partial')
|
|
44
|
+
return chalk_1.default.yellow('Partial');
|
|
45
|
+
if (status === 'cancelled')
|
|
46
|
+
return chalk_1.default.red('Cancelled');
|
|
47
|
+
if (status === 'expired')
|
|
48
|
+
return chalk_1.default.gray('Expired');
|
|
49
|
+
return chalk_1.default.cyan('Open');
|
|
50
|
+
}
|
|
51
|
+
function renderPortfolioPositionsSection(positions) {
|
|
52
|
+
console.log(chalk_1.default.bold(' Positions'));
|
|
53
|
+
const byProtocol = new Map();
|
|
54
|
+
for (const pos of positions) {
|
|
55
|
+
const existing = byProtocol.get(pos.protocol) ?? [];
|
|
56
|
+
existing.push(pos);
|
|
57
|
+
byProtocol.set(pos.protocol, existing);
|
|
58
|
+
}
|
|
59
|
+
for (const [protocol, protocolPositions] of byProtocol) {
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(ui_1.colors.secondary(` ${protocol}`));
|
|
62
|
+
const hasPerps = protocolPositions.some((p) => p.detail?.kind === 'perps');
|
|
63
|
+
const posHead = [
|
|
64
|
+
chalk_1.default.gray('Instrument'),
|
|
65
|
+
chalk_1.default.gray('Type'),
|
|
66
|
+
chalk_1.default.gray('Exposure'),
|
|
67
|
+
chalk_1.default.gray('Value'),
|
|
68
|
+
chalk_1.default.gray('PnL%'),
|
|
69
|
+
...(hasPerps ? [chalk_1.default.gray('Funding')] : []),
|
|
70
|
+
chalk_1.default.gray('Status'),
|
|
71
|
+
];
|
|
72
|
+
const posTable = new cli_table3_1.default({
|
|
73
|
+
head: posHead,
|
|
74
|
+
style: { head: [], border: chalk_1.default.level > 0 ? ['gray'] : [] },
|
|
75
|
+
});
|
|
76
|
+
for (const pos of protocolPositions) {
|
|
77
|
+
const fundingCell = pos.detail?.kind === 'perps' && pos.detail.fundingRate != null
|
|
78
|
+
? (pos.detail.fundingRate >= 0
|
|
79
|
+
? chalk_1.default.green(`${(pos.detail.fundingRate * 100).toFixed(4)}%`)
|
|
80
|
+
: chalk_1.default.red(`${(pos.detail.fundingRate * 100).toFixed(4)}%`))
|
|
81
|
+
: chalk_1.default.gray('—');
|
|
82
|
+
posTable.push([
|
|
83
|
+
chalk_1.default.white(pos.instrument),
|
|
84
|
+
pos.type,
|
|
85
|
+
exposureLabel(pos),
|
|
86
|
+
(0, ui_1.formatUsd)(pos.valueUsd),
|
|
87
|
+
formatPnlPercent(pos.pnlPercent),
|
|
88
|
+
...(hasPerps ? [fundingCell] : []),
|
|
89
|
+
pos.status,
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
console.log(posTable.toString());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function renderPortfolioOrdersSection(orders) {
|
|
96
|
+
console.log(chalk_1.default.bold(' Orders'));
|
|
97
|
+
const ordersTable = new cli_table3_1.default({
|
|
98
|
+
head: [
|
|
99
|
+
chalk_1.default.gray('Instrument'),
|
|
100
|
+
chalk_1.default.gray('Side'),
|
|
101
|
+
chalk_1.default.gray('Type'),
|
|
102
|
+
chalk_1.default.gray('Amount'),
|
|
103
|
+
chalk_1.default.gray('Price'),
|
|
104
|
+
chalk_1.default.gray('Status'),
|
|
105
|
+
chalk_1.default.gray('Protocol'),
|
|
106
|
+
],
|
|
107
|
+
style: { head: [], border: chalk_1.default.level > 0 ? ['gray'] : [] },
|
|
108
|
+
});
|
|
109
|
+
for (const o of orders) {
|
|
110
|
+
const sideColor = o.side === 'buy' || o.side === 'long' ? chalk_1.default.green : chalk_1.default.red;
|
|
111
|
+
const filledInfo = o.amountFilled != null && o.amountFilled > 0
|
|
112
|
+
? ` (${o.amountFilled.toFixed(2)} filled)`
|
|
113
|
+
: '';
|
|
114
|
+
ordersTable.push([
|
|
115
|
+
chalk_1.default.white(o.instrument),
|
|
116
|
+
sideColor(o.side.toUpperCase()),
|
|
117
|
+
o.type,
|
|
118
|
+
`${(o.amount ?? 0).toFixed(4)} ${o.amountSymbol}${filledInfo}`,
|
|
119
|
+
o.price,
|
|
120
|
+
formatPortfolioOrderStatus(o.status),
|
|
121
|
+
ui_1.colors.muted(o.protocol),
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
console.log(ordersTable.toString());
|
|
125
|
+
}
|
|
126
|
+
function attributedOrderStatus(status) {
|
|
127
|
+
if (status === 'filled')
|
|
128
|
+
return chalk_1.default.green(status);
|
|
129
|
+
if (status === 'open' || status === 'partially_filled')
|
|
130
|
+
return chalk_1.default.yellow(status);
|
|
131
|
+
if (status === 'cancelled')
|
|
132
|
+
return chalk_1.default.gray(status);
|
|
133
|
+
return status;
|
|
134
|
+
}
|
|
135
|
+
function renderAttributedOrdersSection(orders) {
|
|
136
|
+
console.log(chalk_1.default.bold(' Orders'));
|
|
137
|
+
const table = new cli_table3_1.default({
|
|
138
|
+
head: [
|
|
139
|
+
chalk_1.default.gray('Order ID'),
|
|
140
|
+
chalk_1.default.gray('Proto'),
|
|
141
|
+
chalk_1.default.gray('Status'),
|
|
142
|
+
chalk_1.default.gray('Side'),
|
|
143
|
+
chalk_1.default.gray('Asset'),
|
|
144
|
+
chalk_1.default.gray('Size'),
|
|
145
|
+
chalk_1.default.gray('Filled'),
|
|
146
|
+
chalk_1.default.gray('Price'),
|
|
147
|
+
chalk_1.default.gray('Time'),
|
|
148
|
+
],
|
|
149
|
+
style: { head: [], border: chalk_1.default.level > 0 ? ['gray'] : [] },
|
|
150
|
+
});
|
|
151
|
+
for (const order of orders) {
|
|
152
|
+
table.push([
|
|
153
|
+
order.orderId.length > 14 ? `${order.orderId.slice(0, 13)}…` : order.orderId,
|
|
154
|
+
order.protocol === 'hyperliquid' ? 'HL' : order.protocol === 'polymarket' ? 'PM' : order.protocol,
|
|
155
|
+
attributedOrderStatus(order.status),
|
|
156
|
+
order.side === 'buy' ? chalk_1.default.green(order.side) : chalk_1.default.red(order.side),
|
|
157
|
+
order.asset.length > 10 ? `${order.asset.slice(0, 6)}…` : order.asset,
|
|
158
|
+
order.size,
|
|
159
|
+
order.filledSize,
|
|
160
|
+
order.price ?? '-',
|
|
161
|
+
order.createdAt ? new Date(order.createdAt).toLocaleString() : '-',
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
console.log(table.toString());
|
|
165
|
+
}
|
|
166
|
+
function renderAttributedPositionsSection(positions) {
|
|
167
|
+
console.log(chalk_1.default.bold(' Positions'));
|
|
168
|
+
const table = new cli_table3_1.default({
|
|
169
|
+
head: [
|
|
170
|
+
chalk_1.default.gray('Proto'),
|
|
171
|
+
chalk_1.default.gray('Asset'),
|
|
172
|
+
chalk_1.default.gray('Net Size'),
|
|
173
|
+
chalk_1.default.gray('Bought'),
|
|
174
|
+
chalk_1.default.gray('Sold'),
|
|
175
|
+
chalk_1.default.gray('Status'),
|
|
176
|
+
],
|
|
177
|
+
style: { head: [], border: chalk_1.default.level > 0 ? ['gray'] : [] },
|
|
178
|
+
});
|
|
179
|
+
for (const position of positions) {
|
|
180
|
+
const keyTail = position.positionKey.split(':').pop() || position.positionKey;
|
|
181
|
+
table.push([
|
|
182
|
+
position.protocol === 'hyperliquid' ? 'HL' : position.protocol === 'polymarket' ? 'PM' : position.protocol,
|
|
183
|
+
keyTail.length > 10 ? `${keyTail.slice(0, 6)}…` : keyTail,
|
|
184
|
+
position.netSize,
|
|
185
|
+
position.totalBought,
|
|
186
|
+
position.totalSold,
|
|
187
|
+
position.status,
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
console.log(table.toString());
|
|
191
|
+
}
|
|
192
|
+
function renderPortfolioFillsSection(fills) {
|
|
193
|
+
console.log(chalk_1.default.bold(' Fills'));
|
|
194
|
+
const table = new cli_table3_1.default({
|
|
195
|
+
head: [
|
|
196
|
+
chalk_1.default.gray('Proto'),
|
|
197
|
+
chalk_1.default.gray('Side'),
|
|
198
|
+
chalk_1.default.gray('Asset'),
|
|
199
|
+
chalk_1.default.gray('Size'),
|
|
200
|
+
chalk_1.default.gray('Price'),
|
|
201
|
+
chalk_1.default.gray('Fee'),
|
|
202
|
+
chalk_1.default.gray('Role'),
|
|
203
|
+
chalk_1.default.gray('Time'),
|
|
204
|
+
],
|
|
205
|
+
style: { head: [], border: chalk_1.default.level > 0 ? ['gray'] : [] },
|
|
206
|
+
});
|
|
207
|
+
for (const fill of fills) {
|
|
208
|
+
const protocol = fill.protocol === 'hyperliquid' ? 'HL' : fill.protocol === 'polymarket' ? 'PM' : fill.protocol;
|
|
209
|
+
const side = fill.side === 'buy' ? chalk_1.default.green(fill.side) : chalk_1.default.red(fill.side);
|
|
210
|
+
const asset = fill.asset.length > 10 ? `${fill.asset.slice(0, 6)}…` : fill.asset;
|
|
211
|
+
table.push([
|
|
212
|
+
protocol,
|
|
213
|
+
side,
|
|
214
|
+
asset,
|
|
215
|
+
fill.fillSize,
|
|
216
|
+
fill.fillPrice,
|
|
217
|
+
fill.fee ?? '-',
|
|
218
|
+
fill.liquidityRole ?? '-',
|
|
219
|
+
new Date(fill.filledAt).toLocaleString(),
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
console.log(table.toString());
|
|
223
|
+
}
|
|
224
|
+
function renderPaginationHint(meta, shownCount, labelText) {
|
|
225
|
+
if (meta.total <= meta.offset + shownCount)
|
|
226
|
+
return;
|
|
227
|
+
(0, ui_1.info)(`Showing ${meta.offset + 1}-${meta.offset + shownCount} of ${meta.total} ${labelText}. Use --limit/--offset to page.`);
|
|
228
|
+
}
|
|
229
|
+
function renderAgentPortfolioDisplay(agentId, positions, orders, protocol) {
|
|
230
|
+
(0, ui_1.header)('Portfolio');
|
|
231
|
+
console.log();
|
|
232
|
+
(0, ui_1.label)('Agent', agentId);
|
|
233
|
+
if (protocol) {
|
|
234
|
+
(0, ui_1.label)('Protocol', protocol);
|
|
235
|
+
}
|
|
236
|
+
console.log();
|
|
237
|
+
if (positions.length > 0) {
|
|
238
|
+
(0, ui_1.divider)();
|
|
239
|
+
renderAttributedPositionsSection(positions);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
(0, ui_1.info)('No attributed positions found.');
|
|
243
|
+
}
|
|
244
|
+
console.log();
|
|
245
|
+
if (orders.length > 0) {
|
|
246
|
+
(0, ui_1.divider)();
|
|
247
|
+
renderAttributedOrdersSection(orders);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
(0, ui_1.info)('No attributed orders found.');
|
|
251
|
+
}
|
|
252
|
+
console.log();
|
|
253
|
+
(0, ui_1.divider)();
|
|
254
|
+
}
|
|
38
255
|
function renderPortfolioDisplay(snapshot) {
|
|
39
256
|
(0, ui_1.header)('Portfolio');
|
|
40
257
|
console.log();
|
|
41
|
-
// === Totals ===
|
|
42
258
|
const t = snapshot.totals;
|
|
43
259
|
(0, ui_1.label)('Total Value', (0, ui_1.formatUsd)(t.totalValueUsd));
|
|
44
260
|
(0, ui_1.label)('Exposure', (0, ui_1.formatUsd)(t.totalExposure));
|
|
45
261
|
(0, ui_1.label)('Spot', (0, ui_1.formatUsd)(t.totalSpot));
|
|
46
262
|
(0, ui_1.label)('Limit Orders', (0, ui_1.formatUsd)(t.totalLimitOrders));
|
|
47
263
|
console.log();
|
|
48
|
-
// === Holdings (dust already filtered server-side) ===
|
|
49
264
|
if (snapshot.holdings && snapshot.holdings.length > 0) {
|
|
50
265
|
(0, ui_1.divider)();
|
|
51
266
|
console.log(chalk_1.default.bold(' Holdings'));
|
|
@@ -67,102 +282,19 @@ function renderPortfolioDisplay(snapshot) {
|
|
|
67
282
|
else if (snapshot.holdings !== null) {
|
|
68
283
|
(0, ui_1.info)('No spot balances found.');
|
|
69
284
|
}
|
|
70
|
-
// === Positions ===
|
|
71
285
|
if (snapshot.positions && snapshot.positions.length > 0) {
|
|
72
286
|
console.log();
|
|
73
287
|
(0, ui_1.divider)();
|
|
74
|
-
|
|
75
|
-
// Group by protocol
|
|
76
|
-
const byProtocol = new Map();
|
|
77
|
-
for (const pos of snapshot.positions) {
|
|
78
|
-
const existing = byProtocol.get(pos.protocol) ?? [];
|
|
79
|
-
existing.push(pos);
|
|
80
|
-
byProtocol.set(pos.protocol, existing);
|
|
81
|
-
}
|
|
82
|
-
for (const [protocol, positions] of byProtocol) {
|
|
83
|
-
console.log();
|
|
84
|
-
console.log(ui_1.colors.secondary(` ${protocol}`));
|
|
85
|
-
const hasPerps = positions.some((p) => p.detail?.kind === 'perps');
|
|
86
|
-
const posHead = [
|
|
87
|
-
chalk_1.default.gray('Instrument'),
|
|
88
|
-
chalk_1.default.gray('Type'),
|
|
89
|
-
chalk_1.default.gray('Exposure'),
|
|
90
|
-
chalk_1.default.gray('Value'),
|
|
91
|
-
chalk_1.default.gray('PnL%'),
|
|
92
|
-
...(hasPerps ? [chalk_1.default.gray('Funding')] : []),
|
|
93
|
-
chalk_1.default.gray('Status'),
|
|
94
|
-
];
|
|
95
|
-
const posTable = new cli_table3_1.default({
|
|
96
|
-
head: posHead,
|
|
97
|
-
style: { head: [], border: chalk_1.default.level > 0 ? ['gray'] : [] },
|
|
98
|
-
});
|
|
99
|
-
for (const pos of positions) {
|
|
100
|
-
const fundingCell = pos.detail?.kind === 'perps' && pos.detail.fundingRate != null
|
|
101
|
-
? (pos.detail.fundingRate >= 0
|
|
102
|
-
? chalk_1.default.green(`${(pos.detail.fundingRate * 100).toFixed(4)}%`)
|
|
103
|
-
: chalk_1.default.red(`${(pos.detail.fundingRate * 100).toFixed(4)}%`))
|
|
104
|
-
: chalk_1.default.gray('—');
|
|
105
|
-
posTable.push([
|
|
106
|
-
chalk_1.default.white(pos.instrument),
|
|
107
|
-
pos.type,
|
|
108
|
-
exposureLabel(pos),
|
|
109
|
-
(0, ui_1.formatUsd)(pos.valueUsd),
|
|
110
|
-
formatPnlPercent(pos.pnlPercent),
|
|
111
|
-
...(hasPerps ? [fundingCell] : []),
|
|
112
|
-
pos.status,
|
|
113
|
-
]);
|
|
114
|
-
}
|
|
115
|
-
console.log(posTable.toString());
|
|
116
|
-
}
|
|
288
|
+
renderPortfolioPositionsSection(snapshot.positions);
|
|
117
289
|
}
|
|
118
290
|
else if (snapshot.positions !== null) {
|
|
119
291
|
console.log();
|
|
120
292
|
(0, ui_1.info)('No open positions.');
|
|
121
293
|
}
|
|
122
|
-
// === Orders ===
|
|
123
294
|
if (snapshot.orders && snapshot.orders.length > 0) {
|
|
124
295
|
console.log();
|
|
125
296
|
(0, ui_1.divider)();
|
|
126
|
-
|
|
127
|
-
const ordersTable = new cli_table3_1.default({
|
|
128
|
-
head: [
|
|
129
|
-
chalk_1.default.gray('Instrument'),
|
|
130
|
-
chalk_1.default.gray('Side'),
|
|
131
|
-
chalk_1.default.gray('Type'),
|
|
132
|
-
chalk_1.default.gray('Amount'),
|
|
133
|
-
chalk_1.default.gray('Price'),
|
|
134
|
-
chalk_1.default.gray('Status'),
|
|
135
|
-
chalk_1.default.gray('Protocol'),
|
|
136
|
-
],
|
|
137
|
-
style: { head: [], border: chalk_1.default.level > 0 ? ['gray'] : [] },
|
|
138
|
-
});
|
|
139
|
-
for (const o of snapshot.orders) {
|
|
140
|
-
const sideColor = o.side === 'buy' || o.side === 'long' ? chalk_1.default.green : chalk_1.default.red;
|
|
141
|
-
let statusLabel;
|
|
142
|
-
if (o.status === 'filled')
|
|
143
|
-
statusLabel = chalk_1.default.green('Filled');
|
|
144
|
-
else if (o.status === 'partial')
|
|
145
|
-
statusLabel = chalk_1.default.yellow('Partial');
|
|
146
|
-
else if (o.status === 'cancelled')
|
|
147
|
-
statusLabel = chalk_1.default.red('Cancelled');
|
|
148
|
-
else if (o.status === 'expired')
|
|
149
|
-
statusLabel = chalk_1.default.gray('Expired');
|
|
150
|
-
else
|
|
151
|
-
statusLabel = chalk_1.default.cyan('Open');
|
|
152
|
-
const filledInfo = o.amountFilled != null && o.amountFilled > 0
|
|
153
|
-
? ` (${o.amountFilled.toFixed(2)} filled)`
|
|
154
|
-
: '';
|
|
155
|
-
ordersTable.push([
|
|
156
|
-
chalk_1.default.white(o.instrument),
|
|
157
|
-
sideColor(o.side.toUpperCase()),
|
|
158
|
-
o.type,
|
|
159
|
-
`${(o.amount ?? 0).toFixed(4)} ${o.amountSymbol}${filledInfo}`,
|
|
160
|
-
o.price,
|
|
161
|
-
statusLabel,
|
|
162
|
-
ui_1.colors.muted(o.protocol),
|
|
163
|
-
]);
|
|
164
|
-
}
|
|
165
|
-
console.log(ordersTable.toString());
|
|
297
|
+
renderPortfolioOrdersSection(snapshot.orders);
|
|
166
298
|
}
|
|
167
299
|
else if (snapshot.orders !== null) {
|
|
168
300
|
console.log();
|
|
@@ -171,34 +303,183 @@ function renderPortfolioDisplay(snapshot) {
|
|
|
171
303
|
console.log();
|
|
172
304
|
(0, ui_1.divider)();
|
|
173
305
|
}
|
|
306
|
+
function parseSectionLimit(limit, fallback) {
|
|
307
|
+
if (!limit)
|
|
308
|
+
return fallback;
|
|
309
|
+
const parsed = parseInt(limit, 10);
|
|
310
|
+
return Number.isNaN(parsed) || parsed < 1 ? fallback : Math.min(parsed, 200);
|
|
311
|
+
}
|
|
312
|
+
function parseSectionOffset(offset) {
|
|
313
|
+
if (!offset)
|
|
314
|
+
return 0;
|
|
315
|
+
const parsed = parseInt(offset, 10);
|
|
316
|
+
return Number.isNaN(parsed) || parsed < 0 ? 0 : parsed;
|
|
317
|
+
}
|
|
318
|
+
function buildSectionQuery(opts, defaults) {
|
|
319
|
+
const params = new URLSearchParams();
|
|
320
|
+
if (opts.agent)
|
|
321
|
+
params.set('agentId', opts.agent);
|
|
322
|
+
if (opts.protocol)
|
|
323
|
+
params.set('protocol', opts.protocol);
|
|
324
|
+
if (opts.status)
|
|
325
|
+
params.set('status', opts.status);
|
|
326
|
+
if (opts.include)
|
|
327
|
+
params.set('include', opts.include);
|
|
328
|
+
params.set('limit', String(parseSectionLimit(opts.limit, defaults.limit)));
|
|
329
|
+
params.set('offset', String(parseSectionOffset(opts.offset)));
|
|
330
|
+
return params;
|
|
331
|
+
}
|
|
332
|
+
function parseOrdersIncludeOption(include) {
|
|
333
|
+
if (!include)
|
|
334
|
+
return { ok: true };
|
|
335
|
+
const includes = include
|
|
336
|
+
.split(',')
|
|
337
|
+
.map((token) => token.trim())
|
|
338
|
+
.filter((token) => token.length > 0);
|
|
339
|
+
if (includes.length === 0)
|
|
340
|
+
return { ok: true };
|
|
341
|
+
const invalid = includes.find((token) => !PORTFOLIO_ORDERS_INCLUDES.has(token));
|
|
342
|
+
if (invalid) {
|
|
343
|
+
return { ok: false, invalid };
|
|
344
|
+
}
|
|
345
|
+
return { ok: true, value: Array.from(new Set(includes)).join(',') };
|
|
346
|
+
}
|
|
347
|
+
function getApiErrorBody(resp) {
|
|
348
|
+
if (resp == null ||
|
|
349
|
+
typeof resp !== 'object' ||
|
|
350
|
+
!Object.prototype.hasOwnProperty.call(resp, 'data')) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
const tmp = resp;
|
|
354
|
+
return tmp.data;
|
|
355
|
+
}
|
|
356
|
+
function formatProtocolFlag(protocol) {
|
|
357
|
+
return protocol ? ` --protocol ${protocol}` : '';
|
|
358
|
+
}
|
|
359
|
+
async function fetchPortfolioSection(path, labelText, jsonMode) {
|
|
360
|
+
const spinner = (0, tty_1.createSpinner)(`Loading ${labelText}...`, jsonMode);
|
|
361
|
+
spinner?.start();
|
|
362
|
+
try {
|
|
363
|
+
const resp = await (0, api_1.get)(path);
|
|
364
|
+
spinner?.stop();
|
|
365
|
+
if (!resp.ok) {
|
|
366
|
+
const errorBody = getApiErrorBody(resp);
|
|
367
|
+
const msg = `Failed to load ${labelText}: ${errorBody?.message || errorBody?.error || 'Unknown error'}`;
|
|
368
|
+
if (jsonMode)
|
|
369
|
+
(0, tty_1.outputJson)({ success: false, error: msg });
|
|
370
|
+
else
|
|
371
|
+
(0, ui_1.error)(msg);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
return resp.data;
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
spinner?.stop();
|
|
378
|
+
const msg = `Failed to load ${labelText}: ${err instanceof Error ? err.message : 'Unknown error'}`;
|
|
379
|
+
if (jsonMode)
|
|
380
|
+
(0, tty_1.outputJson)({ success: false, error: msg });
|
|
381
|
+
else
|
|
382
|
+
(0, ui_1.error)(msg);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
174
386
|
function registerPortfolioCommand(program) {
|
|
175
387
|
const portfolioCmd = program
|
|
176
388
|
.command('portfolio')
|
|
177
389
|
.description('View your complete portfolio — balances, positions, P&L across all protocols')
|
|
178
390
|
.enablePositionalOptions()
|
|
179
391
|
.passThroughOptions();
|
|
392
|
+
const ensureActiveAccount = (jsonMode) => {
|
|
393
|
+
const account = (0, config_1.getActiveAccount)();
|
|
394
|
+
if (account)
|
|
395
|
+
return true;
|
|
396
|
+
const msg = 'No active account.';
|
|
397
|
+
if (jsonMode)
|
|
398
|
+
(0, tty_1.outputJson)({ success: false, error: msg });
|
|
399
|
+
else
|
|
400
|
+
(0, ui_1.error)(msg);
|
|
401
|
+
return false;
|
|
402
|
+
};
|
|
403
|
+
const resolveAgentOption = async (agentOpt, jsonMode) => {
|
|
404
|
+
if (agentOpt == null)
|
|
405
|
+
return null;
|
|
406
|
+
return (0, agent_picker_1.fetchAndPickAgent)(typeof agentOpt === 'string' ? agentOpt : undefined, jsonMode);
|
|
407
|
+
};
|
|
180
408
|
// Default action — runs when no subcommand is given (existing behavior)
|
|
181
409
|
portfolioCmd
|
|
182
410
|
.option('--json', 'Output as JSON')
|
|
183
411
|
.option('--all-balances', 'Show low/dust spot balances (default hides tiny balances)')
|
|
184
412
|
.option('--svm-wallet <address>', 'Override SVM wallet address')
|
|
185
413
|
.option('--evm-wallet <address>', 'Override EVM wallet address')
|
|
414
|
+
.option('--agent [agentIdOrName]', 'Filter to a specific agent; omit value to pick interactively')
|
|
415
|
+
.option('-p, --protocol <protocol>', 'Filter positions/orders by protocol in agent summary mode')
|
|
186
416
|
.action(async (opts) => {
|
|
187
417
|
const jsonMode = opts?.json || false;
|
|
188
418
|
if (!(0, ui_1.requireAuth)(jsonMode))
|
|
189
419
|
return;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
420
|
+
if (!ensureActiveAccount(jsonMode))
|
|
421
|
+
return;
|
|
422
|
+
const protocol = opts?.protocol;
|
|
423
|
+
const resolvedAgent = await resolveAgentOption(opts?.agent, jsonMode);
|
|
424
|
+
if (opts?.agent && !resolvedAgent)
|
|
425
|
+
return;
|
|
426
|
+
if (resolvedAgent) {
|
|
427
|
+
const spinner = (0, tty_1.createSpinner)('Loading attributed portfolio...', jsonMode);
|
|
428
|
+
spinner?.start();
|
|
429
|
+
try {
|
|
430
|
+
const params = new URLSearchParams();
|
|
431
|
+
params.set('agentId', resolvedAgent.id);
|
|
432
|
+
params.set('limit', '20');
|
|
433
|
+
params.set('offset', '0');
|
|
434
|
+
if (protocol)
|
|
435
|
+
params.set('protocol', protocol);
|
|
436
|
+
const qs = params.toString();
|
|
437
|
+
const [ordersResp, positionsResp] = await Promise.all([
|
|
438
|
+
(0, api_1.get)(`/api/portfolio/agent-orders?${qs}`),
|
|
439
|
+
(0, api_1.get)(`/api/portfolio/agent-positions?${qs}`),
|
|
440
|
+
]);
|
|
441
|
+
spinner?.stop();
|
|
442
|
+
if (!ordersResp.ok || !positionsResp.ok) {
|
|
443
|
+
const errorBody = getApiErrorBody(!ordersResp.ok ? ordersResp : positionsResp);
|
|
444
|
+
const msg = `Failed to load attributed portfolio: ${errorBody?.message || errorBody?.error || 'Unknown error'}`;
|
|
445
|
+
if (jsonMode)
|
|
446
|
+
(0, tty_1.outputJson)({ success: false, error: msg });
|
|
447
|
+
else
|
|
448
|
+
(0, ui_1.error)(msg);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (jsonMode) {
|
|
452
|
+
(0, tty_1.outputJson)({
|
|
453
|
+
success: true,
|
|
454
|
+
data: {
|
|
455
|
+
agentId: resolvedAgent.id,
|
|
456
|
+
protocol: protocol ?? null,
|
|
457
|
+
orders: ordersResp.data,
|
|
458
|
+
positions: positionsResp.data,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
renderAgentPortfolioDisplay(resolvedAgent.title || resolvedAgent.id, positionsResp.data.positions, ordersResp.data.orders, protocol);
|
|
464
|
+
if (positionsResp.data.pagination.total > positionsResp.data.positions.length) {
|
|
465
|
+
(0, ui_1.info)(`Use ${chalk_1.default.cyan(`suzi portfolio positions --agent ${resolvedAgent.id}${formatProtocolFlag(protocol)}`)} for full position pagination.`);
|
|
466
|
+
}
|
|
467
|
+
if (ordersResp.data.pagination.total > ordersResp.data.orders.length) {
|
|
468
|
+
(0, ui_1.info)(`Use ${chalk_1.default.cyan(`suzi portfolio orders --agent ${resolvedAgent.id}${formatProtocolFlag(protocol)}`)} for full order pagination.`);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
196
471
|
}
|
|
197
|
-
|
|
198
|
-
(
|
|
472
|
+
catch (err) {
|
|
473
|
+
spinner?.stop();
|
|
474
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
475
|
+
if (jsonMode)
|
|
476
|
+
(0, tty_1.outputJson)({ success: false, error: `Failed to load attributed portfolio: ${errorMessage}` });
|
|
477
|
+
else
|
|
478
|
+
(0, ui_1.error)(`Failed to load attributed portfolio: ${errorMessage}`);
|
|
479
|
+
return;
|
|
199
480
|
}
|
|
200
|
-
return;
|
|
201
481
|
}
|
|
482
|
+
const showAllBalances = opts?.allBalances || false;
|
|
202
483
|
const spinner = (0, tty_1.createSpinner)('Loading portfolio...', jsonMode);
|
|
203
484
|
spinner?.start();
|
|
204
485
|
try {
|
|
@@ -214,14 +495,12 @@ function registerPortfolioCommand(program) {
|
|
|
214
495
|
const resp = await (0, api_1.get)(endpoint);
|
|
215
496
|
if (!resp.ok) {
|
|
216
497
|
spinner?.stop();
|
|
217
|
-
const errorBody = resp
|
|
498
|
+
const errorBody = getApiErrorBody(resp);
|
|
218
499
|
const msg = `Failed to load portfolio: ${errorBody?.message || errorBody?.error || 'Unknown error'}`;
|
|
219
|
-
if (jsonMode)
|
|
500
|
+
if (jsonMode)
|
|
220
501
|
(0, tty_1.outputJson)({ success: false, error: msg });
|
|
221
|
-
|
|
222
|
-
else {
|
|
502
|
+
else
|
|
223
503
|
(0, ui_1.error)(msg);
|
|
224
|
-
}
|
|
225
504
|
return;
|
|
226
505
|
}
|
|
227
506
|
spinner?.stop();
|
|
@@ -238,6 +517,150 @@ function registerPortfolioCommand(program) {
|
|
|
238
517
|
(0, ui_1.error)(`Failed to load portfolio: ${errorMessage}`);
|
|
239
518
|
}
|
|
240
519
|
});
|
|
520
|
+
portfolioCmd
|
|
521
|
+
.command('orders')
|
|
522
|
+
.description('View account-level or agent-attributed orders')
|
|
523
|
+
.option('--json', 'Output as JSON')
|
|
524
|
+
.option('--agent [agentIdOrName]', 'Filter to a specific agent; omit value to pick interactively')
|
|
525
|
+
.option('-p, --protocol <protocol>', 'Filter by protocol')
|
|
526
|
+
.option('--include <sections>', 'Include extra sections (currently: fills)')
|
|
527
|
+
.option('-s, --status <status>', 'Status filter (supports aliases like active/inactive or exact statuses like filled)')
|
|
528
|
+
.option('-n, --limit <limit>', 'Max rows to return', '50')
|
|
529
|
+
.option('--offset <offset>', 'Pagination offset', '0')
|
|
530
|
+
.action(async (opts) => {
|
|
531
|
+
const jsonMode = opts.json || false;
|
|
532
|
+
if (!(0, ui_1.requireAuth)(jsonMode))
|
|
533
|
+
return;
|
|
534
|
+
if (!ensureActiveAccount(jsonMode))
|
|
535
|
+
return;
|
|
536
|
+
const resolvedAgent = await resolveAgentOption(opts.agent, jsonMode);
|
|
537
|
+
if (opts.agent && !resolvedAgent)
|
|
538
|
+
return;
|
|
539
|
+
const includeResult = parseOrdersIncludeOption(opts.include);
|
|
540
|
+
if (!includeResult.ok) {
|
|
541
|
+
const msg = `Invalid include section: "${includeResult.invalid}". Valid values: fills`;
|
|
542
|
+
if (jsonMode)
|
|
543
|
+
(0, tty_1.outputJson)({ success: false, error: msg });
|
|
544
|
+
else
|
|
545
|
+
(0, ui_1.error)(msg);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const params = buildSectionQuery({ ...opts, agent: resolvedAgent?.id, include: includeResult.value }, { limit: 50 });
|
|
549
|
+
const qs = params.toString();
|
|
550
|
+
const path = resolvedAgent
|
|
551
|
+
? `/api/portfolio/agent-orders?${qs}`
|
|
552
|
+
: `/api/portfolio/orders?${qs}`;
|
|
553
|
+
const result = await fetchPortfolioSection(path, 'portfolio orders', jsonMode);
|
|
554
|
+
if (!result)
|
|
555
|
+
return;
|
|
556
|
+
if (jsonMode) {
|
|
557
|
+
(0, tty_1.outputJson)({ success: true, data: result });
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
(0, ui_1.header)('Portfolio Orders');
|
|
561
|
+
console.log();
|
|
562
|
+
if (resolvedAgent) {
|
|
563
|
+
const data = result;
|
|
564
|
+
if (data.orders.length === 0 && (!data.fills || data.fills.length === 0)) {
|
|
565
|
+
(0, ui_1.info)('No attributed orders found.');
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (data.orders.length > 0) {
|
|
569
|
+
renderAttributedOrdersSection(data.orders);
|
|
570
|
+
renderPaginationHint(data.pagination, data.orders.length, 'orders');
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
(0, ui_1.info)('No attributed orders found.');
|
|
574
|
+
}
|
|
575
|
+
if (includeResult.value?.includes('fills')) {
|
|
576
|
+
console.log();
|
|
577
|
+
if (data.fills && data.fills.length > 0) {
|
|
578
|
+
renderPortfolioFillsSection(data.fills);
|
|
579
|
+
if (data.fillsPagination) {
|
|
580
|
+
renderPaginationHint(data.fillsPagination, data.fills.length, 'fills');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
(0, ui_1.info)('No attributed fills found.');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const data = result;
|
|
590
|
+
if (data.orders.length === 0 && (!data.fills || data.fills.length === 0)) {
|
|
591
|
+
(0, ui_1.info)('No orders found.');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (data.orders.length > 0) {
|
|
595
|
+
renderPortfolioOrdersSection(data.orders);
|
|
596
|
+
renderPaginationHint(data.pagination, data.orders.length, 'orders');
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
(0, ui_1.info)('No orders found.');
|
|
600
|
+
}
|
|
601
|
+
if (includeResult.value?.includes('fills')) {
|
|
602
|
+
console.log();
|
|
603
|
+
if (data.fills && data.fills.length > 0) {
|
|
604
|
+
renderPortfolioFillsSection(data.fills);
|
|
605
|
+
if (data.fillsPagination) {
|
|
606
|
+
renderPaginationHint(data.fillsPagination, data.fills.length, 'fills');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
(0, ui_1.info)('No fills found.');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
portfolioCmd
|
|
615
|
+
.command('positions')
|
|
616
|
+
.description('View account-level or agent-attributed positions')
|
|
617
|
+
.option('--json', 'Output as JSON')
|
|
618
|
+
.option('--agent [agentIdOrName]', 'Filter to a specific agent; omit value to pick interactively')
|
|
619
|
+
.option('-p, --protocol <protocol>', 'Filter by protocol')
|
|
620
|
+
.option('-s, --status <status>', 'Status filter (supports aliases like open/closed or exact statuses like live)')
|
|
621
|
+
.option('-n, --limit <limit>', 'Max rows to return', '50')
|
|
622
|
+
.option('--offset <offset>', 'Pagination offset', '0')
|
|
623
|
+
.action(async (opts) => {
|
|
624
|
+
const jsonMode = opts.json || false;
|
|
625
|
+
if (!(0, ui_1.requireAuth)(jsonMode))
|
|
626
|
+
return;
|
|
627
|
+
if (!ensureActiveAccount(jsonMode))
|
|
628
|
+
return;
|
|
629
|
+
const resolvedAgent = await resolveAgentOption(opts.agent, jsonMode);
|
|
630
|
+
if (opts.agent && !resolvedAgent)
|
|
631
|
+
return;
|
|
632
|
+
const params = buildSectionQuery({ ...opts, agent: resolvedAgent?.id }, { limit: 50 });
|
|
633
|
+
const qs = params.toString();
|
|
634
|
+
const path = resolvedAgent
|
|
635
|
+
? `/api/portfolio/agent-positions?${qs}`
|
|
636
|
+
: `/api/portfolio/positions?${qs}`;
|
|
637
|
+
const result = await fetchPortfolioSection(path, 'portfolio positions', jsonMode);
|
|
638
|
+
if (!result)
|
|
639
|
+
return;
|
|
640
|
+
if (jsonMode) {
|
|
641
|
+
(0, tty_1.outputJson)({ success: true, data: result });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
(0, ui_1.header)('Portfolio Positions');
|
|
645
|
+
console.log();
|
|
646
|
+
if (resolvedAgent) {
|
|
647
|
+
const data = result;
|
|
648
|
+
if (data.positions.length === 0) {
|
|
649
|
+
(0, ui_1.info)('No attributed positions found.');
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
renderAttributedPositionsSection(data.positions);
|
|
653
|
+
renderPaginationHint(data.pagination, data.positions.length, 'positions');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const data = result;
|
|
657
|
+
if (data.positions.length === 0) {
|
|
658
|
+
(0, ui_1.info)('No positions found.');
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
renderPortfolioPositionsSection(data.positions);
|
|
662
|
+
renderPaginationHint(data.pagination, data.positions.length, 'positions');
|
|
663
|
+
});
|
|
241
664
|
// Snapshots subcommand
|
|
242
665
|
portfolioCmd
|
|
243
666
|
.command('snapshots')
|
|
@@ -270,7 +693,7 @@ function registerPortfolioCommand(program) {
|
|
|
270
693
|
const resp = await (0, api_1.get)(endpoint);
|
|
271
694
|
if (!resp.ok) {
|
|
272
695
|
spinner?.stop();
|
|
273
|
-
const errorBody = resp
|
|
696
|
+
const errorBody = getApiErrorBody(resp);
|
|
274
697
|
const msg = `Failed to load snapshots: ${errorBody?.message || errorBody?.error || 'Unknown error'}`;
|
|
275
698
|
if (jsonMode) {
|
|
276
699
|
(0, tty_1.outputJson)({ success: false, error: msg });
|