@kikimoradev/kcloud 1.7.0 → 1.8.0
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/main.js +365 -158
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -2856,6 +2856,8 @@ z.object({
|
|
|
2856
2856
|
"agent.state",
|
|
2857
2857
|
"notification",
|
|
2858
2858
|
"notification.count",
|
|
2859
|
+
"dashboard.agent",
|
|
2860
|
+
"dashboard.removed",
|
|
2859
2861
|
"stream.closed",
|
|
2860
2862
|
"error"
|
|
2861
2863
|
]),
|
|
@@ -2918,6 +2920,78 @@ z.object({
|
|
|
2918
2920
|
contentDigest: agentBackupContentDigestSchema
|
|
2919
2921
|
});
|
|
2920
2922
|
z.object({ backupId: idSchema.nullable() });
|
|
2923
|
+
//#endregion
|
|
2924
|
+
//#region ../../packages/contracts/src/agent-phase.ts
|
|
2925
|
+
const agentPhaseKinds = [
|
|
2926
|
+
"offHours",
|
|
2927
|
+
"limitWait",
|
|
2928
|
+
"authBlocked",
|
|
2929
|
+
"queued",
|
|
2930
|
+
"waiting",
|
|
2931
|
+
"patrolling",
|
|
2932
|
+
"sleeping",
|
|
2933
|
+
"working",
|
|
2934
|
+
"summarising"
|
|
2935
|
+
];
|
|
2936
|
+
const dashboardStatusSchema = z.enum([
|
|
2937
|
+
"working",
|
|
2938
|
+
"waiting",
|
|
2939
|
+
"paused",
|
|
2940
|
+
"stopped",
|
|
2941
|
+
"error",
|
|
2942
|
+
"transitioning"
|
|
2943
|
+
]);
|
|
2944
|
+
const dashboardFrameNames = [
|
|
2945
|
+
"dashboard.agent",
|
|
2946
|
+
"dashboard.removed",
|
|
2947
|
+
"stream.closed",
|
|
2948
|
+
"error"
|
|
2949
|
+
];
|
|
2950
|
+
const dashboardActivitySchema = z.object({
|
|
2951
|
+
role: agentRoleSchema,
|
|
2952
|
+
kind: z.enum(agentPhaseKinds),
|
|
2953
|
+
taskId: z.string().nullable(),
|
|
2954
|
+
title: z.string().nullable(),
|
|
2955
|
+
cycle: z.int().nullable(),
|
|
2956
|
+
sessionId: z.string().nullable(),
|
|
2957
|
+
startedAt: z.iso.datetime()
|
|
2958
|
+
}).meta({ id: "DashboardActivity" });
|
|
2959
|
+
const dashboardAgentSchema = z.object({
|
|
2960
|
+
id: idSchema,
|
|
2961
|
+
projectId: idSchema,
|
|
2962
|
+
name: z.string(),
|
|
2963
|
+
slug: slugSchema,
|
|
2964
|
+
state: agentStateSchema,
|
|
2965
|
+
status: dashboardStatusSchema,
|
|
2966
|
+
since: z.iso.datetime(),
|
|
2967
|
+
until: z.iso.datetime().nullable(),
|
|
2968
|
+
failureReason: z.string().nullable(),
|
|
2969
|
+
retryAt: z.iso.datetime().nullable(),
|
|
2970
|
+
activity: dashboardActivitySchema.nullable(),
|
|
2971
|
+
costTodayUsd: z.number().nonnegative()
|
|
2972
|
+
}).meta({ id: "DashboardAgent" });
|
|
2973
|
+
const dashboardProjectSchema = z.object({
|
|
2974
|
+
id: idSchema,
|
|
2975
|
+
name: z.string(),
|
|
2976
|
+
slug: slugSchema,
|
|
2977
|
+
color: colorSchema,
|
|
2978
|
+
logoHash: logoHashSchema.nullable(),
|
|
2979
|
+
agents: z.array(dashboardAgentSchema)
|
|
2980
|
+
}).meta({ id: "DashboardProject" });
|
|
2981
|
+
z.object({
|
|
2982
|
+
day: z.iso.date(),
|
|
2983
|
+
projects: z.array(dashboardProjectSchema)
|
|
2984
|
+
}).meta({ id: "OrganizationDashboard" });
|
|
2985
|
+
const dashboardRemovalSchema = z.object({ agentId: idSchema }).meta({ id: "DashboardRemoval" });
|
|
2986
|
+
z.object({
|
|
2987
|
+
event: z.enum(dashboardFrameNames),
|
|
2988
|
+
data: z.union([
|
|
2989
|
+
dashboardAgentSchema,
|
|
2990
|
+
dashboardRemovalSchema,
|
|
2991
|
+
z.object({ reason: z.string() }),
|
|
2992
|
+
z.object({ code: errorCodeSchema })
|
|
2993
|
+
])
|
|
2994
|
+
}).meta({ id: "DashboardFrame" });
|
|
2921
2995
|
[
|
|
2922
2996
|
"application/vnd.oci.image.index.v1+json",
|
|
2923
2997
|
"application/vnd.oci.image.manifest.v1+json",
|
|
@@ -4207,10 +4281,10 @@ const splitHints = {
|
|
|
4207
4281
|
"node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host."
|
|
4208
4282
|
};
|
|
4209
4283
|
const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
|
|
4210
|
-
const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
|
|
4284
|
+
const namesOf$1 = (nodes) => nodes.map(({ name }) => name).join(", ");
|
|
4211
4285
|
const nodeIn = (nodes, reference, organization) => {
|
|
4212
4286
|
const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
|
|
4213
|
-
if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf(nodes)}.`);
|
|
4287
|
+
if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf$1(nodes)}.`);
|
|
4214
4288
|
return found;
|
|
4215
4289
|
};
|
|
4216
4290
|
const resolveNode = async (scope, reference) => {
|
|
@@ -5233,6 +5307,284 @@ const createCommand = (adaptersOf) => defineCommand({
|
|
|
5233
5307
|
}
|
|
5234
5308
|
});
|
|
5235
5309
|
//#endregion
|
|
5310
|
+
//#region src/client/stream.ts
|
|
5311
|
+
const firstBackoffMs = 1e3;
|
|
5312
|
+
const lastBackoffMs = 3e4;
|
|
5313
|
+
const defaultIdleMs = 1e3;
|
|
5314
|
+
const fatalStatuses = /* @__PURE__ */ new Set([
|
|
5315
|
+
400,
|
|
5316
|
+
401,
|
|
5317
|
+
403,
|
|
5318
|
+
404
|
|
5319
|
+
]);
|
|
5320
|
+
const fatalReasons = /* @__PURE__ */ new Set(["session.expired", "member.removed"]);
|
|
5321
|
+
const jittered = (milliseconds) => Math.round(milliseconds * (.8 + Math.random() * .2));
|
|
5322
|
+
const nextBackoff = (previous) => Math.min(previous === 0 ? firstBackoffMs : previous * 2, lastBackoffMs);
|
|
5323
|
+
const framesOf = (block) => {
|
|
5324
|
+
let event = "message";
|
|
5325
|
+
let id = null;
|
|
5326
|
+
const data = [];
|
|
5327
|
+
for (const line of block.split("\n")) {
|
|
5328
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
5329
|
+
const separator = line.indexOf(":");
|
|
5330
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
5331
|
+
const value = separator === -1 ? "" : line.slice(separator + 1).trimStart();
|
|
5332
|
+
if (field === "event") event = value;
|
|
5333
|
+
else if (field === "id") id = value;
|
|
5334
|
+
else if (field === "data") data.push(value);
|
|
5335
|
+
}
|
|
5336
|
+
if (data.length === 0) return null;
|
|
5337
|
+
const text = data.join("\n");
|
|
5338
|
+
try {
|
|
5339
|
+
return {
|
|
5340
|
+
event,
|
|
5341
|
+
id,
|
|
5342
|
+
data: JSON.parse(text)
|
|
5343
|
+
};
|
|
5344
|
+
} catch {
|
|
5345
|
+
return {
|
|
5346
|
+
event,
|
|
5347
|
+
id,
|
|
5348
|
+
data: text
|
|
5349
|
+
};
|
|
5350
|
+
}
|
|
5351
|
+
};
|
|
5352
|
+
const readBody = async (response, request, onId) => {
|
|
5353
|
+
const body = response.body;
|
|
5354
|
+
if (body === null) return {
|
|
5355
|
+
closed: null,
|
|
5356
|
+
frames: 0
|
|
5357
|
+
};
|
|
5358
|
+
const reader = body.getReader();
|
|
5359
|
+
const decoder = new TextDecoder();
|
|
5360
|
+
const timer = { current: null };
|
|
5361
|
+
let buffer = "";
|
|
5362
|
+
let frames = 0;
|
|
5363
|
+
let closed = null;
|
|
5364
|
+
const armIdle = () => {
|
|
5365
|
+
if (request.follow) return;
|
|
5366
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
5367
|
+
timer.current = setTimeout(() => {
|
|
5368
|
+
reader.cancel();
|
|
5369
|
+
}, request.idleMs ?? 1e3);
|
|
5370
|
+
};
|
|
5371
|
+
const stop = () => {
|
|
5372
|
+
reader.cancel().catch(() => void 0);
|
|
5373
|
+
};
|
|
5374
|
+
request.signal?.addEventListener("abort", stop, { once: true });
|
|
5375
|
+
if (request.signal?.aborted === true) stop();
|
|
5376
|
+
armIdle();
|
|
5377
|
+
try {
|
|
5378
|
+
for (;;) {
|
|
5379
|
+
const chunk = await reader.read().catch((error) => {
|
|
5380
|
+
if (request.signal?.aborted === true) return { done: true };
|
|
5381
|
+
throw error;
|
|
5382
|
+
});
|
|
5383
|
+
if (chunk.done) break;
|
|
5384
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
5385
|
+
let boundary = buffer.indexOf("\n\n");
|
|
5386
|
+
while (boundary !== -1) {
|
|
5387
|
+
const block = buffer.slice(0, boundary);
|
|
5388
|
+
buffer = buffer.slice(boundary + 2);
|
|
5389
|
+
const frame = framesOf(block);
|
|
5390
|
+
if (frame !== null) {
|
|
5391
|
+
frames += 1;
|
|
5392
|
+
if (frame.id !== null) onId(frame.id);
|
|
5393
|
+
if (frame.event === "stream.closed") closed = reasonOf(frame.data);
|
|
5394
|
+
else if (frame.event === "error") throw new CliError(`${request.action} failed on the instance: ${codeOf(frame.data) ?? "internal.error"}.`);
|
|
5395
|
+
else {
|
|
5396
|
+
request.onFrame(frame);
|
|
5397
|
+
armIdle();
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
boundary = buffer.indexOf("\n\n");
|
|
5401
|
+
}
|
|
5402
|
+
if (closed !== null) break;
|
|
5403
|
+
}
|
|
5404
|
+
} finally {
|
|
5405
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
5406
|
+
request.signal?.removeEventListener("abort", stop);
|
|
5407
|
+
}
|
|
5408
|
+
return {
|
|
5409
|
+
closed,
|
|
5410
|
+
frames
|
|
5411
|
+
};
|
|
5412
|
+
};
|
|
5413
|
+
const reasonOf = (data) => {
|
|
5414
|
+
if (typeof data === "object" && data !== null && "reason" in data) {
|
|
5415
|
+
const { reason } = data;
|
|
5416
|
+
if (typeof reason === "string") return reason;
|
|
5417
|
+
}
|
|
5418
|
+
return "unknown";
|
|
5419
|
+
};
|
|
5420
|
+
const streamFrames = async (request) => {
|
|
5421
|
+
const fetching = request.fetch ?? globalThis.fetch;
|
|
5422
|
+
const query = new URLSearchParams(request.query ?? {});
|
|
5423
|
+
const url = `${request.origin}/api/v1${request.path}${query.size === 0 ? "" : `?${query.toString()}`}`;
|
|
5424
|
+
const resume = { lastEventId: null };
|
|
5425
|
+
let backoff = 0;
|
|
5426
|
+
const aborted = () => request.signal?.aborted === true;
|
|
5427
|
+
for (;;) {
|
|
5428
|
+
if (aborted()) return;
|
|
5429
|
+
let response;
|
|
5430
|
+
try {
|
|
5431
|
+
response = await fetching(url, {
|
|
5432
|
+
headers: {
|
|
5433
|
+
accept: "text/event-stream",
|
|
5434
|
+
authorization: `Bearer ${request.token}`,
|
|
5435
|
+
...request.resumable === true && resume.lastEventId !== null ? { "last-event-id": resume.lastEventId } : {}
|
|
5436
|
+
},
|
|
5437
|
+
redirect: "error",
|
|
5438
|
+
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
5439
|
+
});
|
|
5440
|
+
} catch (error) {
|
|
5441
|
+
if (aborted()) return;
|
|
5442
|
+
throw unreachable(request.origin, error);
|
|
5443
|
+
}
|
|
5444
|
+
if (!response.ok) {
|
|
5445
|
+
const body = await response.json().catch(() => void 0);
|
|
5446
|
+
const failure = new ApiError(response.status, codeOf(body), request.action, detailsOf$1(body));
|
|
5447
|
+
if (fatalStatuses.has(response.status) || !request.follow) throw failure;
|
|
5448
|
+
request.onNotice?.(`${failure.message} Waiting to try again.`);
|
|
5449
|
+
backoff = nextBackoff(backoff);
|
|
5450
|
+
await request.wait(jittered(backoff));
|
|
5451
|
+
continue;
|
|
5452
|
+
}
|
|
5453
|
+
backoff = 0;
|
|
5454
|
+
const { closed } = await readBody(response, request, (id) => {
|
|
5455
|
+
resume.lastEventId = id;
|
|
5456
|
+
});
|
|
5457
|
+
if (closed !== null && fatalReasons.has(closed)) throw new CliError(`The instance closed the stream: ${closed}.`, closed === "session.expired" ? "The token is no longer valid; run kcloud login again." : "You are no longer a member of that project.");
|
|
5458
|
+
if (!request.follow || aborted()) return;
|
|
5459
|
+
request.onNotice?.(closed === null ? "The stream ended; opening it again." : `The instance closed the stream: ${closed}. Opening it again.`);
|
|
5460
|
+
backoff = nextBackoff(backoff);
|
|
5461
|
+
await request.wait(jittered(backoff));
|
|
5462
|
+
}
|
|
5463
|
+
};
|
|
5464
|
+
//#endregion
|
|
5465
|
+
//#region src/commands/agents/dashboard.ts
|
|
5466
|
+
const noTask = "-";
|
|
5467
|
+
const workKinds = /* @__PURE__ */ new Set([
|
|
5468
|
+
"working",
|
|
5469
|
+
"patrolling",
|
|
5470
|
+
"summarising"
|
|
5471
|
+
]);
|
|
5472
|
+
const organizationDashboardOf = async (api, organizationId) => dataOf("show the dashboard of the organization", await api.GET("/organizations/{organizationId}/dashboard", { params: { path: { organizationId } } }), organizationHints);
|
|
5473
|
+
const statusOf = (agent) => agent.status === "transitioning" ? `${agent.status} (${agent.state})` : agent.status;
|
|
5474
|
+
const taskOf = (agent) => {
|
|
5475
|
+
const { activity } = agent;
|
|
5476
|
+
if (activity === null) return noTask;
|
|
5477
|
+
if (!workKinds.has(activity.kind)) return agent.until === null ? activity.kind : `${activity.kind} until ${agent.until}`;
|
|
5478
|
+
if (activity.kind === "patrolling") return activity.cycle === null ? "cycle" : `cycle ${activity.cycle}`;
|
|
5479
|
+
if (activity.kind === "summarising") return "summary";
|
|
5480
|
+
return activity.title ?? activity.taskId ?? noTask;
|
|
5481
|
+
};
|
|
5482
|
+
const dashboardText = (dashboard) => {
|
|
5483
|
+
const rows = dashboard.projects.flatMap((project) => project.agents.map((agent) => [
|
|
5484
|
+
project.slug,
|
|
5485
|
+
agent.slug,
|
|
5486
|
+
statusOf(agent),
|
|
5487
|
+
agent.since,
|
|
5488
|
+
taskOf(agent),
|
|
5489
|
+
agent.costTodayUsd.toFixed(2)
|
|
5490
|
+
]));
|
|
5491
|
+
return rows.length === 0 ? `No agent in the projects you can see on ${dashboard.day}.` : renderTable([
|
|
5492
|
+
{ header: "PROJECT" },
|
|
5493
|
+
{ header: "AGENT" },
|
|
5494
|
+
{ header: "STATUS" },
|
|
5495
|
+
{ header: "SINCE" },
|
|
5496
|
+
{ header: "TASK" },
|
|
5497
|
+
{
|
|
5498
|
+
header: "COST TODAY",
|
|
5499
|
+
align: "right"
|
|
5500
|
+
}
|
|
5501
|
+
], rows);
|
|
5502
|
+
};
|
|
5503
|
+
const namesOf = (dashboard) => ({
|
|
5504
|
+
projects: new Map(dashboard.projects.map(({ id, slug }) => [id, slug])),
|
|
5505
|
+
agents: new Map(dashboard.projects.flatMap((project) => project.agents.map(({ id, slug }) => [id, {
|
|
5506
|
+
projectId: project.id,
|
|
5507
|
+
slug
|
|
5508
|
+
}])))
|
|
5509
|
+
});
|
|
5510
|
+
const frameLineOf = (names, frame, at) => {
|
|
5511
|
+
const changed = dashboardAgentSchema.safeParse(frame.data);
|
|
5512
|
+
if (frame.event === "dashboard.agent" && changed.success) {
|
|
5513
|
+
const agent = changed.data;
|
|
5514
|
+
names.agents.set(agent.id, {
|
|
5515
|
+
projectId: agent.projectId,
|
|
5516
|
+
slug: agent.slug
|
|
5517
|
+
});
|
|
5518
|
+
const project = names.projects.get(agent.projectId) ?? agent.projectId;
|
|
5519
|
+
return `${at.toISOString()} ${project}/${agent.slug} ${statusOf(agent)} ${taskOf(agent)}`;
|
|
5520
|
+
}
|
|
5521
|
+
const removed = dashboardRemovalSchema.safeParse(frame.data);
|
|
5522
|
+
if (frame.event === "dashboard.removed" && removed.success) {
|
|
5523
|
+
const known = names.agents.get(removed.data.agentId);
|
|
5524
|
+
names.agents.delete(removed.data.agentId);
|
|
5525
|
+
const project = known === void 0 ? "-" : names.projects.get(known.projectId) ?? known.projectId;
|
|
5526
|
+
return `${at.toISOString()} ${project}/${known?.slug ?? removed.data.agentId} removed ${noTask}`;
|
|
5527
|
+
}
|
|
5528
|
+
return null;
|
|
5529
|
+
};
|
|
5530
|
+
const dashboardCommand = (adaptersOf) => defineCommand({
|
|
5531
|
+
name: "dashboard",
|
|
5532
|
+
summary: "Show what each agent of the organization is doing now, by project.",
|
|
5533
|
+
usage: "agents dashboard [--follow]",
|
|
5534
|
+
optionsUsage: [["-f, --follow", "keep printing a line for each change of an agent after the table"]],
|
|
5535
|
+
options: { follow: {
|
|
5536
|
+
type: "boolean",
|
|
5537
|
+
short: "f",
|
|
5538
|
+
default: false
|
|
5539
|
+
} },
|
|
5540
|
+
positionals: {
|
|
5541
|
+
min: 0,
|
|
5542
|
+
max: 0
|
|
5543
|
+
},
|
|
5544
|
+
run: async ({ values, settings, context }) => {
|
|
5545
|
+
const adapters = adaptersOf(context);
|
|
5546
|
+
const session = await openSession({
|
|
5547
|
+
settings,
|
|
5548
|
+
context,
|
|
5549
|
+
api: adapters.api
|
|
5550
|
+
});
|
|
5551
|
+
const { id } = await session.organization();
|
|
5552
|
+
const dashboard = await organizationDashboardOf(session.api, id);
|
|
5553
|
+
if (!values.follow) {
|
|
5554
|
+
if (settings.json) context.terminal.json(dashboard);
|
|
5555
|
+
else context.terminal.line(dashboardText(dashboard));
|
|
5556
|
+
return exitCodes.ok;
|
|
5557
|
+
}
|
|
5558
|
+
context.terminal.line(settings.json ? JSON.stringify(dashboard) : dashboardText(dashboard));
|
|
5559
|
+
const names = namesOf(dashboard);
|
|
5560
|
+
await streamFrames({
|
|
5561
|
+
origin: session.origin,
|
|
5562
|
+
token: session.token,
|
|
5563
|
+
path: `/organizations/${encodeURIComponent(id)}/dashboard/stream`,
|
|
5564
|
+
follow: true,
|
|
5565
|
+
resumable: false,
|
|
5566
|
+
idleMs: defaultIdleMs,
|
|
5567
|
+
action: "follow the dashboard of the organization",
|
|
5568
|
+
wait: adapters.wait,
|
|
5569
|
+
onNotice: (text) => {
|
|
5570
|
+
context.terminal.error(context.terminal.errorStyles.dim(text));
|
|
5571
|
+
},
|
|
5572
|
+
onFrame: (frame) => {
|
|
5573
|
+
if (settings.json) {
|
|
5574
|
+
context.terminal.line(JSON.stringify({
|
|
5575
|
+
event: frame.event,
|
|
5576
|
+
data: frame.data
|
|
5577
|
+
}));
|
|
5578
|
+
return;
|
|
5579
|
+
}
|
|
5580
|
+
const line = frameLineOf(names, frame, adapters.now());
|
|
5581
|
+
if (line !== null) context.terminal.line(line);
|
|
5582
|
+
}
|
|
5583
|
+
});
|
|
5584
|
+
return exitCodes.ok;
|
|
5585
|
+
}
|
|
5586
|
+
});
|
|
5587
|
+
//#endregion
|
|
5236
5588
|
//#region src/commands/agents/lifecycle.ts
|
|
5237
5589
|
const summaries = {
|
|
5238
5590
|
start: "Start the agent and let it work.",
|
|
@@ -5645,161 +5997,6 @@ const observabilityCommands = (adaptersOf) => [
|
|
|
5645
5997
|
})
|
|
5646
5998
|
];
|
|
5647
5999
|
//#endregion
|
|
5648
|
-
//#region src/client/stream.ts
|
|
5649
|
-
const firstBackoffMs = 1e3;
|
|
5650
|
-
const lastBackoffMs = 3e4;
|
|
5651
|
-
const defaultIdleMs = 1e3;
|
|
5652
|
-
const fatalStatuses = /* @__PURE__ */ new Set([
|
|
5653
|
-
400,
|
|
5654
|
-
401,
|
|
5655
|
-
403,
|
|
5656
|
-
404
|
|
5657
|
-
]);
|
|
5658
|
-
const fatalReasons = /* @__PURE__ */ new Set(["session.expired", "member.removed"]);
|
|
5659
|
-
const jittered = (milliseconds) => Math.round(milliseconds * (.8 + Math.random() * .2));
|
|
5660
|
-
const nextBackoff = (previous) => Math.min(previous === 0 ? firstBackoffMs : previous * 2, lastBackoffMs);
|
|
5661
|
-
const framesOf = (block) => {
|
|
5662
|
-
let event = "message";
|
|
5663
|
-
let id = null;
|
|
5664
|
-
const data = [];
|
|
5665
|
-
for (const line of block.split("\n")) {
|
|
5666
|
-
if (line === "" || line.startsWith(":")) continue;
|
|
5667
|
-
const separator = line.indexOf(":");
|
|
5668
|
-
const field = separator === -1 ? line : line.slice(0, separator);
|
|
5669
|
-
const value = separator === -1 ? "" : line.slice(separator + 1).trimStart();
|
|
5670
|
-
if (field === "event") event = value;
|
|
5671
|
-
else if (field === "id") id = value;
|
|
5672
|
-
else if (field === "data") data.push(value);
|
|
5673
|
-
}
|
|
5674
|
-
if (data.length === 0) return null;
|
|
5675
|
-
const text = data.join("\n");
|
|
5676
|
-
try {
|
|
5677
|
-
return {
|
|
5678
|
-
event,
|
|
5679
|
-
id,
|
|
5680
|
-
data: JSON.parse(text)
|
|
5681
|
-
};
|
|
5682
|
-
} catch {
|
|
5683
|
-
return {
|
|
5684
|
-
event,
|
|
5685
|
-
id,
|
|
5686
|
-
data: text
|
|
5687
|
-
};
|
|
5688
|
-
}
|
|
5689
|
-
};
|
|
5690
|
-
const readBody = async (response, request, onId) => {
|
|
5691
|
-
const body = response.body;
|
|
5692
|
-
if (body === null) return {
|
|
5693
|
-
closed: null,
|
|
5694
|
-
frames: 0
|
|
5695
|
-
};
|
|
5696
|
-
const reader = body.getReader();
|
|
5697
|
-
const decoder = new TextDecoder();
|
|
5698
|
-
const timer = { current: null };
|
|
5699
|
-
let buffer = "";
|
|
5700
|
-
let frames = 0;
|
|
5701
|
-
let closed = null;
|
|
5702
|
-
const armIdle = () => {
|
|
5703
|
-
if (request.follow) return;
|
|
5704
|
-
if (timer.current !== null) clearTimeout(timer.current);
|
|
5705
|
-
timer.current = setTimeout(() => {
|
|
5706
|
-
reader.cancel();
|
|
5707
|
-
}, request.idleMs ?? 1e3);
|
|
5708
|
-
};
|
|
5709
|
-
const stop = () => {
|
|
5710
|
-
reader.cancel().catch(() => void 0);
|
|
5711
|
-
};
|
|
5712
|
-
request.signal?.addEventListener("abort", stop, { once: true });
|
|
5713
|
-
if (request.signal?.aborted === true) stop();
|
|
5714
|
-
armIdle();
|
|
5715
|
-
try {
|
|
5716
|
-
for (;;) {
|
|
5717
|
-
const chunk = await reader.read().catch((error) => {
|
|
5718
|
-
if (request.signal?.aborted === true) return { done: true };
|
|
5719
|
-
throw error;
|
|
5720
|
-
});
|
|
5721
|
-
if (chunk.done) break;
|
|
5722
|
-
buffer += decoder.decode(chunk.value, { stream: true });
|
|
5723
|
-
let boundary = buffer.indexOf("\n\n");
|
|
5724
|
-
while (boundary !== -1) {
|
|
5725
|
-
const block = buffer.slice(0, boundary);
|
|
5726
|
-
buffer = buffer.slice(boundary + 2);
|
|
5727
|
-
const frame = framesOf(block);
|
|
5728
|
-
if (frame !== null) {
|
|
5729
|
-
frames += 1;
|
|
5730
|
-
if (frame.id !== null) onId(frame.id);
|
|
5731
|
-
if (frame.event === "stream.closed") closed = reasonOf(frame.data);
|
|
5732
|
-
else if (frame.event === "error") throw new CliError(`${request.action} failed on the instance: ${codeOf(frame.data) ?? "internal.error"}.`);
|
|
5733
|
-
else {
|
|
5734
|
-
request.onFrame(frame);
|
|
5735
|
-
armIdle();
|
|
5736
|
-
}
|
|
5737
|
-
}
|
|
5738
|
-
boundary = buffer.indexOf("\n\n");
|
|
5739
|
-
}
|
|
5740
|
-
if (closed !== null) break;
|
|
5741
|
-
}
|
|
5742
|
-
} finally {
|
|
5743
|
-
if (timer.current !== null) clearTimeout(timer.current);
|
|
5744
|
-
request.signal?.removeEventListener("abort", stop);
|
|
5745
|
-
}
|
|
5746
|
-
return {
|
|
5747
|
-
closed,
|
|
5748
|
-
frames
|
|
5749
|
-
};
|
|
5750
|
-
};
|
|
5751
|
-
const reasonOf = (data) => {
|
|
5752
|
-
if (typeof data === "object" && data !== null && "reason" in data) {
|
|
5753
|
-
const { reason } = data;
|
|
5754
|
-
if (typeof reason === "string") return reason;
|
|
5755
|
-
}
|
|
5756
|
-
return "unknown";
|
|
5757
|
-
};
|
|
5758
|
-
const streamFrames = async (request) => {
|
|
5759
|
-
const fetching = request.fetch ?? globalThis.fetch;
|
|
5760
|
-
const query = new URLSearchParams(request.query ?? {});
|
|
5761
|
-
const url = `${request.origin}/api/v1${request.path}${query.size === 0 ? "" : `?${query.toString()}`}`;
|
|
5762
|
-
const resume = { lastEventId: null };
|
|
5763
|
-
let backoff = 0;
|
|
5764
|
-
const aborted = () => request.signal?.aborted === true;
|
|
5765
|
-
for (;;) {
|
|
5766
|
-
if (aborted()) return;
|
|
5767
|
-
let response;
|
|
5768
|
-
try {
|
|
5769
|
-
response = await fetching(url, {
|
|
5770
|
-
headers: {
|
|
5771
|
-
accept: "text/event-stream",
|
|
5772
|
-
authorization: `Bearer ${request.token}`,
|
|
5773
|
-
...request.resumable === true && resume.lastEventId !== null ? { "last-event-id": resume.lastEventId } : {}
|
|
5774
|
-
},
|
|
5775
|
-
redirect: "error",
|
|
5776
|
-
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
5777
|
-
});
|
|
5778
|
-
} catch (error) {
|
|
5779
|
-
if (aborted()) return;
|
|
5780
|
-
throw unreachable(request.origin, error);
|
|
5781
|
-
}
|
|
5782
|
-
if (!response.ok) {
|
|
5783
|
-
const body = await response.json().catch(() => void 0);
|
|
5784
|
-
const failure = new ApiError(response.status, codeOf(body), request.action, detailsOf$1(body));
|
|
5785
|
-
if (fatalStatuses.has(response.status) || !request.follow) throw failure;
|
|
5786
|
-
request.onNotice?.(`${failure.message} Waiting to try again.`);
|
|
5787
|
-
backoff = nextBackoff(backoff);
|
|
5788
|
-
await request.wait(jittered(backoff));
|
|
5789
|
-
continue;
|
|
5790
|
-
}
|
|
5791
|
-
backoff = 0;
|
|
5792
|
-
const { closed } = await readBody(response, request, (id) => {
|
|
5793
|
-
resume.lastEventId = id;
|
|
5794
|
-
});
|
|
5795
|
-
if (closed !== null && fatalReasons.has(closed)) throw new CliError(`The instance closed the stream: ${closed}.`, closed === "session.expired" ? "The token is no longer valid; run kcloud login again." : "You are no longer a member of that project.");
|
|
5796
|
-
if (!request.follow || aborted()) return;
|
|
5797
|
-
request.onNotice?.(closed === null ? "The stream ended; opening it again." : `The instance closed the stream: ${closed}. Opening it again.`);
|
|
5798
|
-
backoff = nextBackoff(backoff);
|
|
5799
|
-
await request.wait(jittered(backoff));
|
|
5800
|
-
}
|
|
5801
|
-
};
|
|
5802
|
-
//#endregion
|
|
5803
6000
|
//#region src/commands/agents/streams.ts
|
|
5804
6001
|
const streamOptions = {
|
|
5805
6002
|
...agentOption,
|
|
@@ -6165,7 +6362,8 @@ const createAgentsCommand = (adaptersOf) => defineGroup({
|
|
|
6165
6362
|
backupsCommand(adaptersOf),
|
|
6166
6363
|
restoreCommand(adaptersOf),
|
|
6167
6364
|
...observabilityCommands(adaptersOf),
|
|
6168
|
-
...streamCommands(adaptersOf)
|
|
6365
|
+
...streamCommands(adaptersOf),
|
|
6366
|
+
dashboardCommand(adaptersOf)
|
|
6169
6367
|
]
|
|
6170
6368
|
});
|
|
6171
6369
|
//#endregion
|
|
@@ -8567,6 +8765,15 @@ const organizationTools = [
|
|
|
8567
8765
|
return organizationCostsOf(run.api, id, costWindowFrom(from, to, run.now()));
|
|
8568
8766
|
}
|
|
8569
8767
|
}),
|
|
8768
|
+
defineTool({
|
|
8769
|
+
name: "organization_dashboard",
|
|
8770
|
+
title: "Show what each agent of the organization is doing",
|
|
8771
|
+
description: "Show the agents of the organization this server acts in, by project, each with its status and since when, the task or the cycle it works on, and what it spent today, a UTC day. A member sees the projects they belong to.",
|
|
8772
|
+
operations: ["getOrganizationDashboard"],
|
|
8773
|
+
safety: "read",
|
|
8774
|
+
input: z.object({}),
|
|
8775
|
+
run: async (_, run) => organizationDashboardOf(run.api, (await run.organization()).id)
|
|
8776
|
+
}),
|
|
8570
8777
|
defineTool({
|
|
8571
8778
|
name: "organization_members_list",
|
|
8572
8779
|
title: "List the members of the organization",
|