@2kw/ai 7.1.0-dev.7 → 7.1.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/commands/agent-apply.js +22 -20
- package/dist/commands/agents.js +26 -5
- package/dist/commands/billing.js +93 -0
- package/dist/commands/installations.js +29 -0
- package/dist/lib/agent-publish.d.ts +15 -0
- package/dist/lib/agent-publish.js +18 -0
- package/dist/lib/errors.js +6 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import { getClient, runAction } from "../lib/client.js";
|
|
4
4
|
import { isJsonOutput } from "../lib/config.js";
|
|
5
5
|
import { BackboneApiError, CliUsageError } from "../lib/errors.js";
|
|
6
|
+
import { ensurePublished } from "../lib/agent-publish.js";
|
|
6
7
|
import { findAgentByName } from "../lib/agent-lookup.js";
|
|
7
8
|
import { loadAgentConfig, maskSecrets } from "../agent-config/file.js";
|
|
8
9
|
import { CONFIG_FIELDS, planApply } from "../agent-config/plan.js";
|
|
@@ -110,30 +111,31 @@ export function makeAgentApplyCommand() {
|
|
|
110
111
|
let versionId = latest?.id;
|
|
111
112
|
version = latest?.versionNumber ?? null;
|
|
112
113
|
if (plan.action === "created") {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}));
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
if (err instanceof BackboneApiError && err.status === 500) {
|
|
122
|
-
throw new BackboneApiError({
|
|
123
|
-
status: err.status,
|
|
124
|
-
error: err.errorType,
|
|
125
|
-
message: `${err.message} — an agent named "${config.name}" may already exist`,
|
|
126
|
-
timestamp: err.timestamp,
|
|
127
|
-
code: err.code,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
throw err;
|
|
131
|
-
}
|
|
114
|
+
// A duplicate name answers 409 with the backend's own message (#668).
|
|
115
|
+
const { data: created } = await client.POST("/v1/agents", {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
body: agentBody(config),
|
|
118
|
+
});
|
|
132
119
|
if (!created?.id)
|
|
133
120
|
throw new Error("Agent was created but the response has no id");
|
|
134
121
|
agentId = String(created.id);
|
|
122
|
+
// Create publishes version 1 (#668); an older backend does not, so publish as a fallback.
|
|
123
|
+
const head = await ensurePublished(client, created, versionBody(config, opts.message));
|
|
124
|
+
versionId = head.versionId;
|
|
125
|
+
version = head.version;
|
|
126
|
+
// A message given on the command line is kept in the history as a second version (D4);
|
|
127
|
+
// the default message is not worth a version of its own.
|
|
128
|
+
if (!head.fallback && command.getOptionValueSource("message") === "cli") {
|
|
129
|
+
const { data: v } = await client.POST("/v1/agents/{agentId}/versions", {
|
|
130
|
+
params: { path: { agentId } },
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
body: versionBody(config, opts.message),
|
|
133
|
+
});
|
|
134
|
+
versionId = v?.id;
|
|
135
|
+
version = v?.versionNumber ?? null;
|
|
136
|
+
}
|
|
135
137
|
}
|
|
136
|
-
if (plan.action === "
|
|
138
|
+
if (plan.action === "versioned") {
|
|
137
139
|
const { data: v } = await client.POST("/v1/agents/{agentId}/versions", {
|
|
138
140
|
params: { path: { agentId: agentId } },
|
|
139
141
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
package/dist/commands/agents.js
CHANGED
|
@@ -10,6 +10,7 @@ import { makeAgentExportCommand } from "./agent-export.js";
|
|
|
10
10
|
import { makeAgentPolicyCommand } from "./agent-policy.js";
|
|
11
11
|
import { makeAgentInitCommand, makeAgentSchemaCommand } from "./agent-init.js";
|
|
12
12
|
import { maskSecrets } from "../agent-config/file.js";
|
|
13
|
+
import { ensurePublished } from "../lib/agent-publish.js";
|
|
13
14
|
import { collectModel, MODEL_HELP, MODELS_HELP, modelFieldsFromOptions } from "../lib/agent-models.js";
|
|
14
15
|
import { assertNameNotBlank } from "../lib/overlay.js";
|
|
15
16
|
export function makeAgentsCommand() {
|
|
@@ -58,7 +59,7 @@ export function makeAgentsCommand() {
|
|
|
58
59
|
});
|
|
59
60
|
cmd
|
|
60
61
|
.command("create")
|
|
61
|
-
.description("Create a new agent")
|
|
62
|
+
.description("Create a new agent; it is published as version 1 and runnable at once")
|
|
62
63
|
.requiredOption("-n, --name <name>", "Agent name")
|
|
63
64
|
.option("-m, --model <model>", MODEL_HELP, collectModel)
|
|
64
65
|
.option("--models <list>", MODELS_HELP)
|
|
@@ -72,18 +73,38 @@ export function makeAgentsCommand() {
|
|
|
72
73
|
await runAction(command, async () => {
|
|
73
74
|
const models = modelFieldsFromOptions(opts, true);
|
|
74
75
|
const client = getClient(command);
|
|
76
|
+
const tools = opts.tools ? JSON.parse(opts.tools) : undefined;
|
|
77
|
+
const skills = opts.skills ? JSON.parse(opts.skills) : undefined;
|
|
78
|
+
const options = opts.options ? JSON.parse(opts.options) : undefined;
|
|
79
|
+
const hitlPolicy = opts.hitlPolicy ? JSON.parse(opts.hitlPolicy) : undefined;
|
|
75
80
|
const { data } = await client.POST("/v1/agents", {
|
|
76
81
|
body: {
|
|
77
82
|
name: opts.name,
|
|
78
83
|
...models,
|
|
79
84
|
description: opts.description,
|
|
80
85
|
instructions: opts.instructions,
|
|
81
|
-
options
|
|
82
|
-
tools
|
|
83
|
-
skills
|
|
84
|
-
hitlPolicy
|
|
86
|
+
options,
|
|
87
|
+
tools,
|
|
88
|
+
skills,
|
|
89
|
+
hitlPolicy,
|
|
85
90
|
},
|
|
86
91
|
});
|
|
92
|
+
if (data?.id) {
|
|
93
|
+
// Create publishes version 1; an older backend leaves it unpublished, so publish then.
|
|
94
|
+
const head = await ensurePublished(client, data, {
|
|
95
|
+
...models,
|
|
96
|
+
instructions: opts.instructions,
|
|
97
|
+
options,
|
|
98
|
+
tools,
|
|
99
|
+
skills,
|
|
100
|
+
hitlPolicy,
|
|
101
|
+
changeDescription: "Initial version",
|
|
102
|
+
});
|
|
103
|
+
// Report the head the fallback just published, not the pre-publish create response.
|
|
104
|
+
if (head.fallback && head.versionId) {
|
|
105
|
+
Object.assign(data, { latestVersionId: head.versionId, latestVersionNumber: head.version ?? undefined });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
87
108
|
formatDetail(maskSecrets(data, []), command);
|
|
88
109
|
});
|
|
89
110
|
});
|
package/dist/commands/billing.js
CHANGED
|
@@ -140,6 +140,99 @@ export function makeBillingCommand() {
|
|
|
140
140
|
});
|
|
141
141
|
cmd.addCommand(makeOverageCommand());
|
|
142
142
|
cmd.addCommand(makeTopUpsCommand());
|
|
143
|
+
cmd.addCommand(makeUsageCommand());
|
|
144
|
+
return cmd;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `bb billing usage periods|period|external` — the console usage page's reads (#1076): platform usage
|
|
148
|
+
* as a share of the period's included usage, and usage on the organisation's own provider keys. All
|
|
149
|
+
* three need a member login.
|
|
150
|
+
*/
|
|
151
|
+
function makeUsageCommand() {
|
|
152
|
+
const cmd = new Command("usage").description("Show platform usage and usage on your own provider keys");
|
|
153
|
+
cmd
|
|
154
|
+
.command("periods")
|
|
155
|
+
.description("List billing periods, newest first (needs `bb auth login`)")
|
|
156
|
+
.action(async (_opts, command) => {
|
|
157
|
+
await runAction(command, async () => {
|
|
158
|
+
const client = getClient(command);
|
|
159
|
+
const { data } = await client.GET("/v1/usage/periods");
|
|
160
|
+
formatList((data ?? []), command, ["periodStart", "periodEnd", "kind"]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
cmd
|
|
164
|
+
.command("period")
|
|
165
|
+
.description("Show platform usage of a period, the current one by default (needs `bb auth login`)")
|
|
166
|
+
.option("--start <instant>", "A period start from `bb billing usage periods`")
|
|
167
|
+
.action(async (opts, command) => {
|
|
168
|
+
await runAction(command, async () => {
|
|
169
|
+
const client = getClient(command);
|
|
170
|
+
const { data } = await client.GET("/v1/usage/period", {
|
|
171
|
+
params: { query: opts.start ? { periodStart: String(opts.start) } : {} },
|
|
172
|
+
});
|
|
173
|
+
if (isJsonOutput(command)) {
|
|
174
|
+
console.log(JSON.stringify(data, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!data || data.accounting === "UNAVAILABLE") {
|
|
178
|
+
console.log(chalk.dim("Platform usage is not available for this organisation yet."));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (data.accounting === "SHADOW") {
|
|
182
|
+
console.log(chalk.yellow("Preview: usage is counted but not yet enforced."));
|
|
183
|
+
}
|
|
184
|
+
const plan = data.plan ? `${data.plan} ${data.usageLevel ?? ""}`.trim() : data.kind;
|
|
185
|
+
console.log(`Period: ${data.periodStart?.slice(0, 10)} → ${data.periodEnd?.slice(0, 10)} (${plan})`);
|
|
186
|
+
console.log(`Used: ${data.usedPercent == null ? "no included usage" : `${data.usedPercent} %`}`);
|
|
187
|
+
for (const meter of data.meters ?? []) {
|
|
188
|
+
console.log(` ${(meter.meter ?? "").padEnd(18)} ${meter.percent ?? "-"} % ${meter.quantity} ${meter.unit}`);
|
|
189
|
+
}
|
|
190
|
+
if (data.extraUsage) {
|
|
191
|
+
const state = data.extraUsage.enabled ? "" : chalk.dim(" (off)");
|
|
192
|
+
console.log(`Extra usage: €${data.extraUsage.spentEur} of €${data.extraUsage.capEur}${state}`);
|
|
193
|
+
}
|
|
194
|
+
if (data.prepaid) {
|
|
195
|
+
const expiry = data.prepaid.nextExpiry ? chalk.dim(` (next expiry ${data.prepaid.nextExpiry})`) : "";
|
|
196
|
+
console.log(`Prepaid: €${data.prepaid.remainingEur}${expiry}`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
cmd
|
|
201
|
+
.command("external")
|
|
202
|
+
.description("Show usage on your own provider keys, estimated at list price (needs `bb auth login`)")
|
|
203
|
+
.option("--from <date>", "First day, YYYY-MM-DD (UTC); default 30 days ago")
|
|
204
|
+
.option("--to <date>", "Last day, YYYY-MM-DD (UTC), included; at most 90 days")
|
|
205
|
+
.action(async (opts, command) => {
|
|
206
|
+
await runAction(command, async () => {
|
|
207
|
+
const client = getClient(command);
|
|
208
|
+
const query = {};
|
|
209
|
+
if (opts.from)
|
|
210
|
+
query.startDate = String(opts.from);
|
|
211
|
+
if (opts.to)
|
|
212
|
+
query.endDate = String(opts.to);
|
|
213
|
+
const { data } = await client.GET("/v1/usage/external", { params: { query } });
|
|
214
|
+
if (isJsonOutput(command)) {
|
|
215
|
+
console.log(JSON.stringify(data, null, 2));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!data?.available) {
|
|
219
|
+
console.log(chalk.dim("Usage capture is off."));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const estimate = data.totals?.estimatedCostEur;
|
|
223
|
+
const total = estimate == null ? "not priced" : `~€${estimate}`;
|
|
224
|
+
console.log(`Range: ${data.startDate} → ${data.endDate}`);
|
|
225
|
+
console.log(`Requests: ${data.totals?.requests ?? 0}`);
|
|
226
|
+
console.log(`Estimated: ${total} ${chalk.dim("(provider list price, billed by the provider)")}`);
|
|
227
|
+
const rows = (data.rows ?? []).map((row) => ({
|
|
228
|
+
provider: row.provider ?? "-",
|
|
229
|
+
model: row.model,
|
|
230
|
+
requests: row.requests,
|
|
231
|
+
estimate: row.estimatedCostEur == null ? "-" : `~€${row.estimatedCostEur}`,
|
|
232
|
+
}));
|
|
233
|
+
formatList(rows, command, ["provider", "model", "requests", "estimate"]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
143
236
|
return cmd;
|
|
144
237
|
}
|
|
145
238
|
/** Whole euros from cents, e.g. 1234 → "€12.34". Amounts are net of VAT (#973 D-O). */
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { getClient, runAction } from "../lib/client.js";
|
|
3
|
+
import { resolveConfig } from "../lib/config.js";
|
|
3
4
|
import { CliUsageError } from "../lib/errors.js";
|
|
4
5
|
import { formatPage, formatDetail, formatList, formatSuccess } from "../lib/output.js";
|
|
5
6
|
import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
|
|
7
|
+
/**
|
|
8
|
+
* Switching member sign-in on or off is a decision a signed-in person makes: the backend refuses an
|
|
9
|
+
* API key, however admin it is, so the command says so before it sends anything (see `SESSION_ONLY`
|
|
10
|
+
* in connectors.ts for the same wording).
|
|
11
|
+
*/
|
|
12
|
+
const SESSION_ONLY = "Signed-in admins only: run 2kw auth login and sign in through the browser; an API key is refused";
|
|
6
13
|
const MIN_MONTHLY_PERCENT = 0;
|
|
7
14
|
const MAX_MONTHLY_PERCENT = 1000;
|
|
8
15
|
/**
|
|
@@ -217,6 +224,28 @@ export function makeInstallationsCommand() {
|
|
|
217
224
|
});
|
|
218
225
|
});
|
|
219
226
|
cmd.addCommand(budget);
|
|
227
|
+
cmd
|
|
228
|
+
.command("member-login")
|
|
229
|
+
.description(`Allow or stop members signing in to this installation's app as themselves. Stopping signs everyone out of it. ${SESSION_ONLY}`)
|
|
230
|
+
.argument("<id>", "Installation ID")
|
|
231
|
+
.option("--enable", "Allow members to sign in")
|
|
232
|
+
.option("--disable", "Stop member sign-in and sign everyone out of the app")
|
|
233
|
+
.action(async (id, opts, command) => {
|
|
234
|
+
await runAction(command, async () => {
|
|
235
|
+
if (Boolean(opts.enable) === Boolean(opts.disable)) {
|
|
236
|
+
throw new CliUsageError("pass exactly one of --enable and --disable");
|
|
237
|
+
}
|
|
238
|
+
if (resolveConfig(command).kind === "apiKey") {
|
|
239
|
+
throw new CliUsageError(SESSION_ONLY);
|
|
240
|
+
}
|
|
241
|
+
const client = getClient(command);
|
|
242
|
+
const { data } = await client.PUT("/v1/installations/{id}/member-login", {
|
|
243
|
+
params: { path: { id } },
|
|
244
|
+
body: { enabled: Boolean(opts.enable) },
|
|
245
|
+
});
|
|
246
|
+
formatDetail(data, command);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
220
249
|
return cmd;
|
|
221
250
|
}
|
|
222
251
|
//# sourceMappingURL=installations.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentDTO, ApiClient } from "./agent-lookup.js";
|
|
2
|
+
export interface PublishedHead {
|
|
3
|
+
versionId: string | undefined;
|
|
4
|
+
version: number | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The head of a just-created agent. A backend that publishes version 1 on create (#668) answers with
|
|
8
|
+
* `latestVersionId`: use it. An older backend leaves the agent unpublished, so publish with
|
|
9
|
+
* `POST /v1/agents/{agentId}/versions` as before. One path, correct against either backend, so neither
|
|
10
|
+
* deploy order nor a backend rollback leaves a new agent unrunnable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensurePublished(client: ApiClient, created: AgentDTO, versionBody: Record<string, unknown>): Promise<PublishedHead & {
|
|
13
|
+
fallback: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
//# sourceMappingURL=agent-publish.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head of a just-created agent. A backend that publishes version 1 on create (#668) answers with
|
|
3
|
+
* `latestVersionId`: use it. An older backend leaves the agent unpublished, so publish with
|
|
4
|
+
* `POST /v1/agents/{agentId}/versions` as before. One path, correct against either backend, so neither
|
|
5
|
+
* deploy order nor a backend rollback leaves a new agent unrunnable.
|
|
6
|
+
*/
|
|
7
|
+
export async function ensurePublished(client, created, versionBody) {
|
|
8
|
+
if (created.latestVersionId) {
|
|
9
|
+
return { versionId: created.latestVersionId, version: created.latestVersionNumber ?? 1, fallback: false };
|
|
10
|
+
}
|
|
11
|
+
const { data: v } = await client.POST("/v1/agents/{agentId}/versions", {
|
|
12
|
+
params: { path: { agentId: String(created.id) } },
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
body: versionBody,
|
|
15
|
+
});
|
|
16
|
+
return { versionId: v?.id, version: v?.versionNumber ?? null, fallback: true };
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=agent-publish.js.map
|
package/dist/lib/errors.js
CHANGED
|
@@ -94,6 +94,7 @@ const HINTS = {
|
|
|
94
94
|
429: "Rate limit exceeded. Wait a moment and try again.",
|
|
95
95
|
};
|
|
96
96
|
const PENDING_APPROVALS_HINT = "The approval was already decided or its response was superseded. See what is still pending: 2kw agents approvals --agent <agent-id> --status pending";
|
|
97
|
+
const NOT_PUBLISHED_HINT = "The agent has no published version. Run: 2kw agents apply -f agent.yaml (or 2kw agents versions create).";
|
|
97
98
|
const CODE_HINTS = {
|
|
98
99
|
approval_hmac_mismatch: PENDING_APPROVALS_HINT,
|
|
99
100
|
unknown_approval_id: PENDING_APPROVALS_HINT,
|
|
@@ -115,6 +116,10 @@ export function hintFor(err) {
|
|
|
115
116
|
}
|
|
116
117
|
if (err.code && CODE_HINTS[err.code])
|
|
117
118
|
return CODE_HINTS[err.code];
|
|
119
|
+
// The gateway answers `agent_not_published` (code), the REST layer AGENT_NOT_PUBLISHED (errorCode).
|
|
120
|
+
if (err.code === "agent_not_published" || err.errorCode === "AGENT_NOT_PUBLISHED") {
|
|
121
|
+
return NOT_PUBLISHED_HINT;
|
|
122
|
+
}
|
|
118
123
|
// The 403 whose cause is the credential, not the role (#805). Without this the
|
|
119
124
|
// generic 403 hint tells an organization admin to check a role that is already
|
|
120
125
|
// right, and no role would ever have fixed it. Matched on the backend's error code,
|
|
@@ -125,7 +130,7 @@ export function hintFor(err) {
|
|
|
125
130
|
// Anchored on the agent service's wording: prompts and schemas send the same
|
|
126
131
|
// "Label '<x>' not found" prefix, and must keep the generic 404 hint.
|
|
127
132
|
if (err.status === 404 && /^Label 'latest' not found (on agent|for this agent)/.test(err.message)) {
|
|
128
|
-
return
|
|
133
|
+
return NOT_PUBLISHED_HINT;
|
|
129
134
|
}
|
|
130
135
|
if (err.status === 404 && /^Label '.+' not found (on agent|for this agent)/.test(err.message)) {
|
|
131
136
|
return "That label does not exist on the agent. List labels: 2kw agents labels list --agent <agent-id>";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2kw/ai",
|
|
3
|
-
"version": "7.1.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|