@runtypelabs/cli 2.59.1 → 2.59.4
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/chunk-0x7UKC1U.js +17 -0
- package/dist/chunk-2ySxXtcg.js +418 -0
- package/dist/chunk-3An_BRHv.js +501 -0
- package/dist/chunk-6uTxgdiY.js +16218 -0
- package/dist/chunk-87nRnNRG.js +1363 -0
- package/dist/chunk-B3wo8uRx.js +322 -0
- package/dist/chunk-BGp3ZdBy.js +560 -0
- package/dist/chunk-BJQv9Elp.js +107 -0
- package/dist/chunk-BOpO6xE-.js +6053 -0
- package/dist/chunk-BQXXCL8B.js +102 -0
- package/dist/chunk-BV-8isOd.js +34 -0
- package/dist/chunk-BVMhjGVe.js +826 -0
- package/dist/chunk-BW2fg46T.js +141 -0
- package/dist/chunk-BWJdY5Ag.js +36 -0
- package/dist/chunk-BYJOrLhh.js +302 -0
- package/dist/chunk-BYab8EVL.js +127 -0
- package/dist/chunk-BfEKvp7h.js +82 -0
- package/dist/chunk-BhgLSfH6.js +1123 -0
- package/dist/chunk-BirUxTtH.js +296 -0
- package/dist/chunk-BmB6UVZ9.js +1559 -0
- package/dist/chunk-BnWhIqW1.js +109 -0
- package/dist/chunk-Bpfnfk0t.js +123 -0
- package/dist/chunk-BrhgdnM3.js +108 -0
- package/dist/chunk-Bt0XAgX5.js +327 -0
- package/dist/chunk-Bu0FJr6S.js +84 -0
- package/dist/chunk-BvEoKpN3.js +255 -0
- package/dist/chunk-BxNJgYbE.js +28 -0
- package/dist/chunk-C1OFhd7F.js +6131 -0
- package/dist/chunk-C7e7WwbU.js +331 -0
- package/dist/chunk-CDTYP6bN.js +558 -0
- package/dist/chunk-CGDQoqDC.js +24 -0
- package/dist/chunk-CJraEBVd.js +127 -0
- package/dist/chunk-CQrNhOtv.js +40 -0
- package/dist/chunk-CW6AzBH6.js +128 -0
- package/dist/chunk-CXgL4w-9.js +317 -0
- package/dist/chunk-CZrqoPyf.js +233 -0
- package/dist/chunk-CcFCyqbB.js +83 -0
- package/dist/chunk-CdaIvO-y.js +485 -0
- package/dist/chunk-CehSXtgK.js +624 -0
- package/dist/chunk-Ciqkop65.js +218 -0
- package/dist/chunk-CoAWdSiE.js +43 -0
- package/dist/chunk-Crb4y7t4.js +830 -0
- package/dist/chunk-CrlqIfmb.js +337 -0
- package/dist/chunk-Cs4pwCQ-.js +324 -0
- package/dist/chunk-CwM_9y01.js +519 -0
- package/dist/chunk-CyRbIJg8.js +440 -0
- package/dist/chunk-D0OHsyMy.js +212 -0
- package/dist/chunk-D3y2bURs.js +1347 -0
- package/dist/chunk-D4_mJk03.js +2338 -0
- package/dist/chunk-D6C8rned.js +578 -0
- package/dist/chunk-D9ZbUnks.js +11 -0
- package/dist/chunk-DEPmLshX.js +834 -0
- package/dist/chunk-DFj1UPB2.js +330 -0
- package/dist/chunk-DGdFdZVf.js +197 -0
- package/dist/chunk-DGv7X9S_.js +63 -0
- package/dist/chunk-DJQ47WYZ.js +664 -0
- package/dist/chunk-DJw3O61j.js +1974 -0
- package/dist/chunk-DK4FbWVK.js +100 -0
- package/dist/chunk-DPuofXMp.js +3907 -0
- package/dist/chunk-DSRYhUus.js +993 -0
- package/dist/chunk-DajZ9Eg4.js +299 -0
- package/dist/chunk-Df4dunvU.js +195 -0
- package/dist/chunk-DjNTPhfO.js +481 -0
- package/dist/chunk-Dn8eaJuu.js +1042 -0
- package/dist/chunk-DwDWqSxM.js +637 -0
- package/dist/chunk-Dz7cEtxl.js +84 -0
- package/dist/chunk-E_0bgjrS.js +359 -0
- package/dist/chunk-KPYN2vUp.js +776 -0
- package/dist/chunk-KcqPQJBi.js +86 -0
- package/dist/chunk-N9H0JnoG.js +349 -0
- package/dist/chunk-NONFcXkn.js +728 -0
- package/dist/chunk-PrFjcpmH.js +256 -0
- package/dist/chunk-QSfNYiw9.js +11 -0
- package/dist/chunk-R265bzj9.js +226 -0
- package/dist/chunk-X9V_r6jM.js +166 -0
- package/dist/chunk-ZBtPUtyA.js +55 -0
- package/dist/chunk-ZCTfmOs7.js +4939 -0
- package/dist/chunk-h6oavj0t.js +97 -0
- package/dist/chunk-i7DwkvvD.js +42 -0
- package/dist/chunk-mG-4eZ6S.js +203 -0
- package/dist/chunk-o0ygUbCf.js +313 -0
- package/dist/chunk-ucu5lrDZ.js +5715 -0
- package/dist/index.js +11367 -73789
- package/package.json +5 -5
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as isTTY } from "./index.js";
|
|
3
|
+
import { DataList, EntityCard, MutationResult, React, render, useEffect, useState } from "./chunk-CoAWdSiE.js";
|
|
4
|
+
import { r as createCliClient } from "./chunk-BW2fg46T.js";
|
|
5
|
+
import { n as ensureAuth } from "./chunk-CJraEBVd.js";
|
|
6
|
+
import { a as printJson, i as printDetail, s as printNextCursorHint } from "./chunk-CcFCyqbB.js";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
// src/commands/logs.ts
|
|
10
|
+
/** The API's own sentence for a store refusal; both log commands answer on it. */
|
|
11
|
+
const STORE_UNAVAILABLE_MESSAGE = "Log store temporarily unavailable";
|
|
12
|
+
/**
|
|
13
|
+
* Report the store's refusal in the caller's own terms, or answer false so the
|
|
14
|
+
* caller prints its generic failure. The store is the only tier, so a refusal
|
|
15
|
+
* is an error rather than a short answer.
|
|
16
|
+
*/
|
|
17
|
+
function reportStoreUnavailable(message, subject) {
|
|
18
|
+
if (!message.includes(STORE_UNAVAILABLE_MESSAGE)) return false;
|
|
19
|
+
console.error(chalk.red(`The log store is temporarily unavailable. No ${subject} could be read; retry shortly.`));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
/** The rendered date counts begin on, or null when the window starts inside coverage. */
|
|
23
|
+
function countsStartAfter(coveredFrom, from) {
|
|
24
|
+
if (!coveredFrom) return null;
|
|
25
|
+
const covered = Date.parse(coveredFrom);
|
|
26
|
+
if (!Number.isFinite(covered)) return null;
|
|
27
|
+
if (from !== void 0 && covered <= Date.parse(from)) return null;
|
|
28
|
+
return new Date(covered).toISOString();
|
|
29
|
+
}
|
|
30
|
+
function filterParams(options) {
|
|
31
|
+
return {
|
|
32
|
+
from: options.from,
|
|
33
|
+
to: options.to,
|
|
34
|
+
level: options.level,
|
|
35
|
+
category: options.category,
|
|
36
|
+
flowId: options.flow,
|
|
37
|
+
agentId: options.agent,
|
|
38
|
+
executionId: options.execution,
|
|
39
|
+
surfaceId: options.surface,
|
|
40
|
+
conversationId: options.conversation,
|
|
41
|
+
status: options.status,
|
|
42
|
+
type: options.type
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function levelColor(level) {
|
|
46
|
+
switch (level) {
|
|
47
|
+
case "error": return chalk.red;
|
|
48
|
+
case "warn": return chalk.yellow;
|
|
49
|
+
case "debug": return chalk.gray;
|
|
50
|
+
default: return chalk.cyan;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const logsCommand = new Command("logs").description("Query historical logs and aggregate stats (use `tail` for live streaming)");
|
|
54
|
+
logsCommand.command("list").description("Query persisted logs with filtering, search, and pagination").option("--from <iso>", "Start timestamp (ISO 8601, default: 24 hours ago)").option("--to <iso>", "End timestamp (ISO 8601, default: now)").option("--level <levels>", "Comma-separated log levels (debug,info,warn,error)").option("--category <categories>", "Comma-separated categories (execution,agent,tool,...)").option("--flow <id>", "Filter by flow ID").option("--agent <id>", "Filter by agent ID").option("--execution <id>", "Filter by execution ID").option("--surface <id>", "Filter by surface ID").option("--conversation <id>", "Filter by conversation ID").option("--status <statuses>", "Comma-separated statuses (completed,failed,unknown,...)").option("--type <types>", "Comma-separated entry types (flow,step,tool,agent,model,...)").option("--search <text>", "Substring search over a span's message, name, operation, tool name or error class").option("--limit <n>", "Max results per page (default 500, max 1000)").option("--cursor <cursor>", "Pagination cursor from a previous page").option("--order <order>", "Sort order: asc or desc (default desc)").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
|
|
55
|
+
const apiKey = await ensureAuth();
|
|
56
|
+
if (!apiKey) return;
|
|
57
|
+
const client = createCliClient(apiKey);
|
|
58
|
+
const params = {
|
|
59
|
+
...filterParams(options),
|
|
60
|
+
search: options.search,
|
|
61
|
+
limit: options.limit,
|
|
62
|
+
cursor: options.cursor,
|
|
63
|
+
order: options.order
|
|
64
|
+
};
|
|
65
|
+
if (!isTTY(options) || options.json) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await client.get("/logs", params);
|
|
68
|
+
if (options.json) {
|
|
69
|
+
printJson(res);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (res.data?.live?.served === false && res.data.live.reason === "hot_tier_failed") console.error(chalk.yellow("Warning: entries from the last few minutes could not be loaded"));
|
|
73
|
+
if (res.data?.pagination?.cursorReset) console.error(chalk.yellow("Warning: the cursor was rejected and this is page one again. Do not re-follow it."));
|
|
74
|
+
const entries = res.data?.entries ?? [];
|
|
75
|
+
if (entries.length === 0) {
|
|
76
|
+
console.log(chalk.gray("No logs found"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.cyan("Logs:"));
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const color = levelColor(entry.level);
|
|
82
|
+
const level = (entry.level ?? "info").toUpperCase().padEnd(5);
|
|
83
|
+
const ts = entry.timestamp ?? "";
|
|
84
|
+
const cat = entry.category ? chalk.dim(`[${entry.category}]`) : "";
|
|
85
|
+
console.log(` ${chalk.dim(ts)} ${color(level)} ${cat} ${entry.message ?? ""}`);
|
|
86
|
+
const ids = [
|
|
87
|
+
entry.executionId ? `exec=${entry.executionId}` : null,
|
|
88
|
+
entry.flowId ? `flow=${entry.flowId}` : null,
|
|
89
|
+
entry.agentId ? `agent=${entry.agentId}` : null,
|
|
90
|
+
entry.surfaceId ? `surface=${entry.surfaceId}` : null,
|
|
91
|
+
entry.conversationId ? `conv=${entry.conversationId}` : null
|
|
92
|
+
].filter(Boolean);
|
|
93
|
+
if (ids.length > 0) console.log(chalk.dim(` ${ids.join(" ")}`));
|
|
94
|
+
}
|
|
95
|
+
console.log(chalk.dim(`\n Total: ${entries.length} entries`));
|
|
96
|
+
if (!res.data?.pagination?.cursorReset) printNextCursorHint("runtype logs list", res.data?.pagination);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
99
|
+
if (reportStoreUnavailable(message, "entries")) {
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.error(chalk.red(`Failed to query logs: ${message}`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const App = () => {
|
|
109
|
+
const [items, setItems] = useState(null);
|
|
110
|
+
const [error, setError] = useState(null);
|
|
111
|
+
const [total, setTotal] = useState();
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
client.get("/logs", params).then((res) => {
|
|
114
|
+
const entries = res.data?.entries ?? [];
|
|
115
|
+
setItems(entries);
|
|
116
|
+
setTotal(entries.length);
|
|
117
|
+
}).catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
|
|
118
|
+
}, []);
|
|
119
|
+
return React.createElement(DataList, {
|
|
120
|
+
title: "Logs",
|
|
121
|
+
items,
|
|
122
|
+
error,
|
|
123
|
+
loading: items === null && error === null,
|
|
124
|
+
total,
|
|
125
|
+
emptyMessage: "No logs found",
|
|
126
|
+
renderCard: (item) => {
|
|
127
|
+
const entry = item;
|
|
128
|
+
return React.createElement(EntityCard, { fields: [
|
|
129
|
+
{
|
|
130
|
+
label: "Time",
|
|
131
|
+
value: entry.timestamp,
|
|
132
|
+
color: "gray"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: "Level",
|
|
136
|
+
value: entry.level
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: "Category",
|
|
140
|
+
value: entry.category,
|
|
141
|
+
color: "gray"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: "Message",
|
|
145
|
+
value: entry.message
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
label: "Execution",
|
|
149
|
+
value: entry.executionId,
|
|
150
|
+
color: "gray"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: "Flow",
|
|
154
|
+
value: entry.flowId,
|
|
155
|
+
color: "gray"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
label: "Agent",
|
|
159
|
+
value: entry.agentId,
|
|
160
|
+
color: "gray"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
label: "Surface",
|
|
164
|
+
value: entry.surfaceId,
|
|
165
|
+
color: "gray"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
label: "Conversation",
|
|
169
|
+
value: entry.conversationId,
|
|
170
|
+
color: "gray"
|
|
171
|
+
}
|
|
172
|
+
] });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
const { waitUntilExit } = render(React.createElement(App));
|
|
177
|
+
await waitUntilExit();
|
|
178
|
+
});
|
|
179
|
+
logsCommand.command("stats").description("Aggregate log stats: totals, counts by level/category, and a histogram").option("--from <iso>", "Start timestamp (ISO 8601, default: 24 hours ago)").option("--to <iso>", "End timestamp (ISO 8601, default: now)").option("--level <levels>", "Comma-separated log levels (debug,info,warn,error)").option("--category <categories>", "Comma-separated categories (execution,agent,tool,...)").option("--flow <id>", "Filter by flow ID").option("--agent <id>", "Filter by agent ID").option("--execution <id>", "Filter by execution ID").option("--surface <id>", "Filter by surface ID").option("--conversation <id>", "Filter by conversation ID").option("--status <statuses>", "Comma-separated statuses (completed,failed,unknown,...)").option("--type <types>", "Comma-separated entry types (flow,step,tool,agent,model,...)").option("--search <text>", "Substring search over a span's message, name, operation, tool name or error class").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
|
|
180
|
+
const apiKey = await ensureAuth();
|
|
181
|
+
if (!apiKey) return;
|
|
182
|
+
const client = createCliClient(apiKey);
|
|
183
|
+
const params = {
|
|
184
|
+
...filterParams(options),
|
|
185
|
+
search: options.search
|
|
186
|
+
};
|
|
187
|
+
const formatCounts = (counts) => {
|
|
188
|
+
const entries = Object.entries(counts);
|
|
189
|
+
if (entries.length === 0) return "(none)";
|
|
190
|
+
return entries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
191
|
+
};
|
|
192
|
+
if (!isTTY(options) || options.json) {
|
|
193
|
+
try {
|
|
194
|
+
const res = await client.get("/logs/stats", params);
|
|
195
|
+
if (options.json) {
|
|
196
|
+
printJson(res);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const stats = res.data;
|
|
200
|
+
if (stats?.degraded) console.error(chalk.yellow("Warning: part of this window could not be read; counts are partial"));
|
|
201
|
+
const startsLate = countsStartAfter(stats.coveredFrom, params.from);
|
|
202
|
+
if (startsLate) console.error(chalk.yellow(`Counts start ${startsLate}`));
|
|
203
|
+
printDetail("Log Stats", [
|
|
204
|
+
{
|
|
205
|
+
label: "Total",
|
|
206
|
+
value: stats.total
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
label: "By Level",
|
|
210
|
+
value: formatCounts(stats.byLevel ?? {})
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
label: "By Category",
|
|
214
|
+
value: formatCounts(stats.byCategory ?? {})
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
label: "By Type",
|
|
218
|
+
value: formatCounts(stats.byType ?? {})
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
label: "By Status",
|
|
222
|
+
value: formatCounts(stats.byStatus ?? {})
|
|
223
|
+
}
|
|
224
|
+
]);
|
|
225
|
+
const histogram = stats.histogram ?? [];
|
|
226
|
+
console.log(chalk.cyan(`\nHistogram (${histogram.length} buckets):`));
|
|
227
|
+
if (histogram.length === 0) console.log(chalk.gray(" No data"));
|
|
228
|
+
else for (const b of histogram) {
|
|
229
|
+
const errPart = b.errors > 0 ? chalk.red(` (${b.errors} errors)`) : "";
|
|
230
|
+
console.log(` ${chalk.dim(b.bucket)} ${b.count}${errPart}`);
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
234
|
+
if (!reportStoreUnavailable(message, "counts")) console.error(chalk.red(`Failed to query log stats: ${message}`));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const App = () => {
|
|
240
|
+
const [loading, setLoading] = useState(true);
|
|
241
|
+
const [success, setSuccess] = useState(null);
|
|
242
|
+
const [error, setError] = useState(null);
|
|
243
|
+
const [resultNode, setResultNode] = useState(void 0);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
const run = async () => {
|
|
246
|
+
try {
|
|
247
|
+
const stats = (await client.get("/logs/stats", params)).data;
|
|
248
|
+
const histogram = stats.histogram ?? [];
|
|
249
|
+
const histogramSummary = histogram.length === 0 ? "No data" : histogram.map((b) => `${b.bucket}: ${b.count}${b.errors > 0 ? ` (${b.errors} err)` : ""}`).join("\n");
|
|
250
|
+
setResultNode(React.createElement(EntityCard, { fields: [
|
|
251
|
+
{
|
|
252
|
+
label: "Total",
|
|
253
|
+
value: String(stats.total)
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
label: "By Level",
|
|
257
|
+
value: formatCounts(stats.byLevel ?? {})
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
label: "By Category",
|
|
261
|
+
value: formatCounts(stats.byCategory ?? {})
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
label: "By Type",
|
|
265
|
+
value: formatCounts(stats.byType ?? {})
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
label: "By Status",
|
|
269
|
+
value: formatCounts(stats.byStatus ?? {})
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
label: `Histogram (${histogram.length})`,
|
|
273
|
+
value: histogramSummary
|
|
274
|
+
}
|
|
275
|
+
] }));
|
|
276
|
+
setSuccess(true);
|
|
277
|
+
setLoading(false);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
280
|
+
setSuccess(false);
|
|
281
|
+
setLoading(false);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
run();
|
|
285
|
+
}, []);
|
|
286
|
+
return React.createElement(MutationResult, {
|
|
287
|
+
loading,
|
|
288
|
+
loadingLabel: "Fetching log stats...",
|
|
289
|
+
success,
|
|
290
|
+
successMessage: "Log Stats",
|
|
291
|
+
error,
|
|
292
|
+
result: resultNode
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
const { waitUntilExit } = render(React.createElement(App));
|
|
296
|
+
await waitUntilExit();
|
|
297
|
+
});
|
|
298
|
+
export { logsCommand };
|
|
299
|
+
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as getApiUrl, r as createCliClient } from "./chunk-BW2fg46T.js";
|
|
3
|
+
import { n as ensureAuth } from "./chunk-CJraEBVd.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
// src/commands/tail.ts
|
|
7
|
+
/** Show TypeID prefix + first 4 chars of suffix for compact display. */
|
|
8
|
+
function abbreviateId(id) {
|
|
9
|
+
const underscoreIdx = id.lastIndexOf("_");
|
|
10
|
+
if (underscoreIdx === -1) return id.length > 8 ? `${id.slice(0, 8)}...` : id;
|
|
11
|
+
return `${id.slice(0, underscoreIdx)}_${id.slice(underscoreIdx + 1).slice(0, 4)}...`;
|
|
12
|
+
}
|
|
13
|
+
/** Format a timestamp string to HH:MM:SS.mmm */
|
|
14
|
+
function formatTime(ts) {
|
|
15
|
+
const d = new Date(ts);
|
|
16
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}.${String(d.getMilliseconds()).padStart(3, "0")}`;
|
|
17
|
+
}
|
|
18
|
+
const LEVEL_COLORS = {
|
|
19
|
+
debug: chalk.gray,
|
|
20
|
+
info: chalk.blue,
|
|
21
|
+
warn: chalk.yellow,
|
|
22
|
+
error: chalk.red
|
|
23
|
+
};
|
|
24
|
+
function levelBadge(level, useColor) {
|
|
25
|
+
const tag = level.toUpperCase().padEnd(5);
|
|
26
|
+
if (!useColor) return `[${tag}]`;
|
|
27
|
+
return (LEVEL_COLORS[level] || chalk.white)(`[${tag}]`);
|
|
28
|
+
}
|
|
29
|
+
function categoryBadge(category, useColor) {
|
|
30
|
+
if (!useColor) return `[${category}]`;
|
|
31
|
+
return chalk.magenta(`[${category}]`);
|
|
32
|
+
}
|
|
33
|
+
function formatTailData(data) {
|
|
34
|
+
return Object.entries(data).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
|
|
35
|
+
}
|
|
36
|
+
function formatContextIds(event, useColor) {
|
|
37
|
+
const ids = [];
|
|
38
|
+
if (event.executionId) ids.push(`exec=${abbreviateId(event.executionId)}`);
|
|
39
|
+
if (event.flowId) ids.push(`flow=${abbreviateId(event.flowId)}`);
|
|
40
|
+
if (event.agentId) ids.push(`agent=${abbreviateId(event.agentId)}`);
|
|
41
|
+
if (event.surfaceId) ids.push(`surface=${abbreviateId(event.surfaceId)}`);
|
|
42
|
+
if (event.requestId) ids.push(`req=${abbreviateId(event.requestId)}`);
|
|
43
|
+
if (event.conversationId) ids.push(`conv=${abbreviateId(event.conversationId)}`);
|
|
44
|
+
if (ids.length === 0) return "";
|
|
45
|
+
const text = ids.join(" ");
|
|
46
|
+
return useColor ? chalk.gray(` ${text}`) : ` ${text}`;
|
|
47
|
+
}
|
|
48
|
+
function formatEvent(event, useColor) {
|
|
49
|
+
const time = useColor ? chalk.gray(formatTime(event.timestamp)) : formatTime(event.timestamp);
|
|
50
|
+
const level = levelBadge(event.level, useColor);
|
|
51
|
+
const cat = categoryBadge(event.category, useColor);
|
|
52
|
+
const ctx = formatContextIds(event, useColor);
|
|
53
|
+
const data = event.tailData && Object.keys(event.tailData).length > 0 ? useColor ? chalk.gray(` | ${formatTailData(event.tailData)}`) : ` | ${formatTailData(event.tailData)}` : "";
|
|
54
|
+
return `${time} ${level} ${cat} ${event.message}${data}${ctx}`;
|
|
55
|
+
}
|
|
56
|
+
async function consumeSSEStream(reader, options, signal) {
|
|
57
|
+
const decoder = new TextDecoder();
|
|
58
|
+
let buffer = "";
|
|
59
|
+
const useColor = options.color;
|
|
60
|
+
while (!signal.aborted) {
|
|
61
|
+
const { done, value } = await reader.read();
|
|
62
|
+
if (done) return "eof";
|
|
63
|
+
buffer += decoder.decode(value, { stream: true });
|
|
64
|
+
let boundary;
|
|
65
|
+
while ((boundary = buffer.indexOf("\n\n")) !== -1) {
|
|
66
|
+
const chunk = buffer.slice(0, boundary);
|
|
67
|
+
buffer = buffer.slice(boundary + 2);
|
|
68
|
+
const dataLines = [];
|
|
69
|
+
for (const line of chunk.split("\n")) if (line.startsWith("data: ")) dataLines.push(line.slice(6));
|
|
70
|
+
if (dataLines.length === 0) continue;
|
|
71
|
+
const raw = dataLines.join("\n");
|
|
72
|
+
let msg;
|
|
73
|
+
try {
|
|
74
|
+
msg = JSON.parse(raw);
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
switch (msg.type) {
|
|
79
|
+
case "tail_event":
|
|
80
|
+
if (options.json) process.stdout.write(JSON.stringify(msg.event) + "\n");
|
|
81
|
+
else process.stdout.write(formatEvent(msg.event, useColor) + "\n");
|
|
82
|
+
break;
|
|
83
|
+
case "tail_gap":
|
|
84
|
+
process.stderr.write((useColor ? chalk.yellow(`[warn] Dropped ${msg.droppedCount} event(s) between ${msg.fromTimestamp} and ${msg.toTimestamp}`) : `[warn] Dropped ${msg.droppedCount} event(s) between ${msg.fromTimestamp} and ${msg.toTimestamp}`) + "\n");
|
|
85
|
+
break;
|
|
86
|
+
case "tail_connected":
|
|
87
|
+
process.stderr.write((useColor ? chalk.green(`Connected to tail session ${msg.sessionId}`) : `Connected to tail session ${msg.sessionId}`) + "\n");
|
|
88
|
+
break;
|
|
89
|
+
case "tail_error": process.stderr.write((useColor ? chalk.red(`[error] ${msg.error}`) : `[error] ${msg.error}`) + "\n");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return "aborted";
|
|
94
|
+
}
|
|
95
|
+
async function createSession(apiUrl, apiKey, filters) {
|
|
96
|
+
const filter = {};
|
|
97
|
+
const arrayFields = {
|
|
98
|
+
flowId: "flowIds",
|
|
99
|
+
agentId: "agentIds",
|
|
100
|
+
surfaceId: "surfaceIds",
|
|
101
|
+
executionId: "executionIds",
|
|
102
|
+
level: "levels",
|
|
103
|
+
category: "categories"
|
|
104
|
+
};
|
|
105
|
+
for (const [k, v] of Object.entries(filters)) if (v !== void 0 && arrayFields[k]) filter[arrayFields[k]] = [v];
|
|
106
|
+
const body = Object.keys(filter).length > 0 ? { filter } : {};
|
|
107
|
+
const client = createCliClient(apiKey, apiUrl);
|
|
108
|
+
try {
|
|
109
|
+
return await client.post("/tail/sessions", body);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const apiError = error;
|
|
112
|
+
if (typeof apiError.statusCode === "number") {
|
|
113
|
+
const statusText = typeof apiError.statusText === "string" ? ` ${apiError.statusText}` : "";
|
|
114
|
+
const message = typeof apiError.message === "string" && apiError.message ? ` — ${apiError.message}` : "";
|
|
115
|
+
throw new Error(`Failed to create tail session: ${apiError.statusCode}${statusText}${message}`, { cause: error });
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function connectAndStream(sseUrl, apiKey, options, signal) {
|
|
121
|
+
const res = await createCliClient(apiKey).requestStream(sseUrl, {
|
|
122
|
+
method: "GET",
|
|
123
|
+
headers: { Accept: "text/event-stream" },
|
|
124
|
+
signal
|
|
125
|
+
});
|
|
126
|
+
if (!res.body) throw new Error("SSE response has no body");
|
|
127
|
+
const reader = res.body.getReader();
|
|
128
|
+
try {
|
|
129
|
+
return await consumeSSEStream(reader, options, signal);
|
|
130
|
+
} finally {
|
|
131
|
+
reader.releaseLock();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const BASE_DELAY_MS = 1e3;
|
|
135
|
+
const MAX_DELAY_MS = 3e4;
|
|
136
|
+
const MAX_ATTEMPTS = 10;
|
|
137
|
+
async function runTail(options) {
|
|
138
|
+
const apiKey = await ensureAuth();
|
|
139
|
+
if (!apiKey) return;
|
|
140
|
+
const apiUrl = getApiUrl();
|
|
141
|
+
const filters = {
|
|
142
|
+
flowId: options.flow,
|
|
143
|
+
agentId: options.agent,
|
|
144
|
+
surfaceId: options.surface,
|
|
145
|
+
executionId: options.execution,
|
|
146
|
+
level: options.level,
|
|
147
|
+
category: options.category
|
|
148
|
+
};
|
|
149
|
+
const useColor = options.color;
|
|
150
|
+
const activeFilters = Object.entries(filters).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${v}`);
|
|
151
|
+
process.stderr.write((useColor ? chalk.gray(`Connecting to ${apiUrl}...${activeFilters.length ? ` filters: ${activeFilters.join(", ")}` : ""}`) : `Connecting to ${apiUrl}...${activeFilters.length ? ` filters: ${activeFilters.join(", ")}` : ""}`) + "\n");
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
let shuttingDown = false;
|
|
154
|
+
const shutdown = () => {
|
|
155
|
+
if (shuttingDown) return;
|
|
156
|
+
shuttingDown = true;
|
|
157
|
+
process.stderr.write((useColor ? chalk.gray("\nDisconnecting...") : "\nDisconnecting...") + "\n");
|
|
158
|
+
controller.abort();
|
|
159
|
+
};
|
|
160
|
+
process.on("SIGINT", shutdown);
|
|
161
|
+
process.on("SIGTERM", shutdown);
|
|
162
|
+
let attempt = 0;
|
|
163
|
+
while (!shuttingDown && attempt < MAX_ATTEMPTS) try {
|
|
164
|
+
const session = await createSession(apiUrl, apiKey, filters);
|
|
165
|
+
attempt = 0;
|
|
166
|
+
if (await connectAndStream(session.sseUrl, apiKey, options, controller.signal) === "aborted" || shuttingDown) break;
|
|
167
|
+
process.stderr.write((useColor ? chalk.yellow("Stream ended, reconnecting...") : "Stream ended, reconnecting...") + "\n");
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (shuttingDown || controller.signal.aborted) break;
|
|
170
|
+
attempt++;
|
|
171
|
+
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, attempt - 1), MAX_DELAY_MS);
|
|
172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
173
|
+
process.stderr.write((useColor ? chalk.red(`Connection error: ${message}`) : `Connection error: ${message}`) + "\n");
|
|
174
|
+
if (attempt >= MAX_ATTEMPTS) {
|
|
175
|
+
process.stderr.write((useColor ? chalk.red(`Max reconnect attempts (${MAX_ATTEMPTS}) reached. Exiting.`) : `Max reconnect attempts (${MAX_ATTEMPTS}) reached. Exiting.`) + "\n");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
process.stderr.write((useColor ? chalk.gray(`Retrying in ${delay / 1e3}s (attempt ${attempt}/${MAX_ATTEMPTS})...`) : `Retrying in ${delay / 1e3}s (attempt ${attempt}/${MAX_ATTEMPTS})...`) + "\n");
|
|
179
|
+
await new Promise((resolve) => {
|
|
180
|
+
const timer = setTimeout(resolve, delay);
|
|
181
|
+
const onAbort = () => {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
resolve();
|
|
184
|
+
};
|
|
185
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
process.removeListener("SIGINT", shutdown);
|
|
189
|
+
process.removeListener("SIGTERM", shutdown);
|
|
190
|
+
}
|
|
191
|
+
const tailCommand = new Command("tail").description("Stream live execution logs from the Runtype API").option("--flow <id>", "Filter by flow ID").option("--agent <id>", "Filter by agent ID").option("--surface <id>", "Filter by surface ID").option("--execution <id>", "Filter by execution ID").option("--level <level>", "Filter by level (debug/info/warn/error)").option("--category <category>", "Filter by category (execution/agent/tool/model/system/error)").option("--json", "Output raw JSON (one object per line)").option("--no-color", "Disable color output").action(async (options) => {
|
|
192
|
+
await runTail(options);
|
|
193
|
+
});
|
|
194
|
+
export { tailCommand };
|
|
195
|
+
|