@2kw/ai 6.3.0-dev.98 → 6.4.0-dev.10
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/README.md +1 -1
- package/dist/agent-config/schema.d.ts +31 -0
- package/dist/agent-config/schema.js +21 -0
- package/dist/agent-config/template.js +2 -0
- package/dist/commands/agent-run.js +11 -5
- package/dist/commands/ai.js +2 -2
- package/dist/commands/billing.js +102 -0
- package/dist/commands/convert.js +13 -29
- package/dist/commands/experiments.js +20 -3
- package/dist/commands/installations.js +45 -0
- package/dist/commands/knowledge.js +8 -3
- package/dist/commands/skills.js +6 -8
- package/dist/commands/tracing.js +7 -5
- package/dist/commands/transcribe.js +9 -27
- package/dist/lib/agent-decide.d.ts +27 -1
- package/dist/lib/agent-decide.js +98 -4
- package/dist/lib/agent-run.d.ts +5 -1
- package/dist/lib/agent-run.js +145 -67
- package/dist/lib/approval-prompt.js +11 -4
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.js +5 -19
- package/dist/lib/connect-pause.d.ts +71 -0
- package/dist/lib/connect-pause.js +147 -0
- package/dist/lib/errors.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ kubectl-style contexts switch between organizations and environments:
|
|
|
112
112
|
| `providers` | Manage BYOK AI providers |
|
|
113
113
|
| `analytics` | Usage analytics: spend, quality, providers, errors |
|
|
114
114
|
| `billing` | Check subscription tier and usage limits |
|
|
115
|
-
| `agents` | Run agents and
|
|
115
|
+
| `agents` | Run agents and answer paused runs (approvals and relayed tool calls); apply, export and scaffold agent.yaml; manage versions, labels, approvals and tool catalogs |
|
|
116
116
|
| `skills` | Import and export SKILL.md bundles; manage skill versions and labels |
|
|
117
117
|
| `plugins` | Install plugin repositories, sync them, and inspect their reports |
|
|
118
118
|
| `conversations` | Create and manage conversations and their items |
|
|
@@ -221,6 +221,25 @@ export declare const agentSchema: {
|
|
|
221
221
|
};
|
|
222
222
|
};
|
|
223
223
|
};
|
|
224
|
+
readonly bashTool: {
|
|
225
|
+
readonly type: "object";
|
|
226
|
+
readonly additionalProperties: false;
|
|
227
|
+
readonly required: readonly ["type"];
|
|
228
|
+
readonly properties: {
|
|
229
|
+
readonly type: {
|
|
230
|
+
readonly const: "backbone.bash";
|
|
231
|
+
};
|
|
232
|
+
readonly approval: {
|
|
233
|
+
readonly enum: readonly ["sandbox", "pause"];
|
|
234
|
+
};
|
|
235
|
+
readonly outputs: {
|
|
236
|
+
readonly enum: readonly ["immediate", "held"];
|
|
237
|
+
};
|
|
238
|
+
readonly annotations: {
|
|
239
|
+
readonly $ref: "#/$defs/annotations";
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
};
|
|
224
243
|
readonly functionTool: {
|
|
225
244
|
readonly type: "object";
|
|
226
245
|
readonly additionalProperties: false;
|
|
@@ -335,6 +354,18 @@ export declare const agentSchema: {
|
|
|
335
354
|
readonly then: {
|
|
336
355
|
readonly $ref: "#/$defs/skillTool";
|
|
337
356
|
};
|
|
357
|
+
}, {
|
|
358
|
+
readonly if: {
|
|
359
|
+
required: string[];
|
|
360
|
+
properties: {
|
|
361
|
+
type: {
|
|
362
|
+
const: string;
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
readonly then: {
|
|
367
|
+
readonly $ref: "#/$defs/bashTool";
|
|
368
|
+
};
|
|
338
369
|
}, {
|
|
339
370
|
readonly if: {
|
|
340
371
|
required: string[];
|
|
@@ -21,6 +21,12 @@ const oneOfLowercase = (...values) => ({ type: "string", enum: values });
|
|
|
21
21
|
export const NON_AUTHORABLE_TOOL_TYPES = {
|
|
22
22
|
"backbone.tool_search": "the planner adds it automatically",
|
|
23
23
|
"backbone.skill_read": "it is added at run time when skills are pinned",
|
|
24
|
+
// #944: SkillsetPinner injects it for eligible callers and strips any stored copy (#779).
|
|
25
|
+
"backbone.skills_apply": "it is added at run time for callers allowed to author skills",
|
|
26
|
+
// #1241: injected while the conversation's outputs are held; an authored copy is dropped.
|
|
27
|
+
"backbone.deliver": "it is added at run time while sandbox outputs are held",
|
|
28
|
+
// #1244: injected wherever backbone.bash is advertised; an authored copy is dropped.
|
|
29
|
+
"backbone.view_image": "it is added at run time wherever the sandboxed shell is",
|
|
24
30
|
web_search: "it has no executor and is skipped",
|
|
25
31
|
};
|
|
26
32
|
const BUILTIN_TYPES = [
|
|
@@ -29,6 +35,7 @@ const BUILTIN_TYPES = [
|
|
|
29
35
|
"backbone.document_convert",
|
|
30
36
|
"backbone.todo",
|
|
31
37
|
"backbone.skill",
|
|
38
|
+
"backbone.bash",
|
|
32
39
|
"function",
|
|
33
40
|
];
|
|
34
41
|
const typeIs = (t) => ({ required: ["type"], properties: { type: { const: t } } });
|
|
@@ -155,6 +162,19 @@ export const agentSchema = {
|
|
|
155
162
|
skills: { type: "array", minItems: 1, maxItems: 20, uniqueItems: true, items: { type: "string", pattern: SKILL_REF } },
|
|
156
163
|
},
|
|
157
164
|
},
|
|
165
|
+
// The envelope BashToolEntryValidator accepts: no name, no requiresConfirmation, and none of
|
|
166
|
+
// the egress of the withdrawn per-agent network design (#925).
|
|
167
|
+
bashTool: {
|
|
168
|
+
type: "object",
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
required: ["type"],
|
|
171
|
+
properties: {
|
|
172
|
+
type: { const: "backbone.bash" },
|
|
173
|
+
approval: { enum: ["sandbox", "pause"] },
|
|
174
|
+
outputs: { enum: ["immediate", "held"] },
|
|
175
|
+
annotations: { $ref: "#/$defs/annotations" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
158
178
|
functionTool: {
|
|
159
179
|
type: "object",
|
|
160
180
|
additionalProperties: false,
|
|
@@ -188,6 +208,7 @@ export const agentSchema = {
|
|
|
188
208
|
{ if: typeIs("backbone.document_convert"), then: { $ref: "#/$defs/documentConvertTool" } },
|
|
189
209
|
{ if: typeIs("backbone.todo"), then: { $ref: "#/$defs/todoTool" } },
|
|
190
210
|
{ if: typeIs("backbone.skill"), then: { $ref: "#/$defs/skillTool" } },
|
|
211
|
+
{ if: typeIs("backbone.bash"), then: { $ref: "#/$defs/bashTool" } },
|
|
191
212
|
{ if: typeIs("function"), then: { $ref: "#/$defs/functionTool" } },
|
|
192
213
|
{
|
|
193
214
|
// The validator reports these with the reason from NON_AUTHORABLE_TOOL_TYPES; this keeps the published schema honest.
|
|
@@ -6,7 +6,7 @@ import { BackboneApiError, CliUsageError } from "../lib/errors.js";
|
|
|
6
6
|
import { withSpinner } from "../lib/output.js";
|
|
7
7
|
import { resolveAgent, splitAgentRef, suggestAgents } from "../lib/agent-lookup.js";
|
|
8
8
|
import { buildRunEnvelope, EXIT_CODES, MODE_OPTION_HELP, modeItem, parseModeOption, printRunText, stripControl, } from "../lib/agent-run.js";
|
|
9
|
-
import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals,
|
|
9
|
+
import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, parseToolAnswers, planContinuation, } from "../lib/agent-decide.js";
|
|
10
10
|
import { PromptAbortedError, promptApprovals, readlineAsk } from "../lib/approval-prompt.js";
|
|
11
11
|
import { withModelOverride } from "../lib/agent-models.js";
|
|
12
12
|
import { readStdinInput } from "./ai.js";
|
|
@@ -171,9 +171,11 @@ export function makeAgentRunCommand() {
|
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
|
+
/** Repeatable `--output` / `--fail`: a variadic `<v...>` would swallow the arguments that follow it. */
|
|
175
|
+
const collect = (value, previous = []) => [...previous, value];
|
|
174
176
|
export function makeAgentDecideCommand() {
|
|
175
177
|
return new Command("decide")
|
|
176
|
-
.description("
|
|
178
|
+
.description("Answer a paused run: decide every pending approval and answer every relayed tool call in one call, then continue it")
|
|
177
179
|
.argument("<agent>", "Agent name or id, optionally with @label and #model (use the same @label and #model the run used)")
|
|
178
180
|
.requiredOption("--response <responseId>", "The paused response id (responseId in the run envelope)")
|
|
179
181
|
.option("--approve <ids...>", "Approval ids to approve")
|
|
@@ -182,11 +184,15 @@ export function makeAgentDecideCommand() {
|
|
|
182
184
|
.option("--reject-all", "Reject every pending approval of the response")
|
|
183
185
|
.option("--reason <text>", "Reason recorded on each decision (the model sees it on rejects)")
|
|
184
186
|
.option("--remember", "Also approve later calls of the same tool in this conversation")
|
|
187
|
+
.option("--output <callId=value>", "Answer a relayed tool call: <callId>=<text>, <callId>=@<file> or <callId>=@- for stdin; within one hour of the pause (repeatable)", collect)
|
|
188
|
+
.option("--fail <callId=message>", "Answer a relayed tool call as failed: <callId>=<message> (same @file / @- forms; repeatable)", collect)
|
|
185
189
|
.option("--mode <mode>", MODE_OPTION_HELP)
|
|
186
190
|
.option("--raw", "Print the untouched API response")
|
|
187
191
|
.action(async (agentRef, opts, command) => {
|
|
188
192
|
await runAction(command, async () => {
|
|
189
193
|
const mode = parseModeOption(opts.mode);
|
|
194
|
+
// Syntax, duplicates and file reads are checked before any request (#671 D4).
|
|
195
|
+
const toolAnswers = parseToolAnswers(opts.output, opts.fail);
|
|
190
196
|
const client = getClient(command);
|
|
191
197
|
const { name, label, model } = splitAgentRef(agentRef);
|
|
192
198
|
// Dropped silently, an empty override would continue the run on the default model.
|
|
@@ -194,15 +200,15 @@ export function makeAgentDecideCommand() {
|
|
|
194
200
|
throw new CliUsageError(`Empty #model in '${agentRef}': use the model the run used, or drop the '#'.`);
|
|
195
201
|
const agent = await resolveAgent(client, name);
|
|
196
202
|
const pending = await fetchPendingApprovals(client, String(agent.id), opts.response);
|
|
197
|
-
const decisions =
|
|
203
|
+
const decisions = planContinuation(pending, {
|
|
198
204
|
approve: opts.approve,
|
|
199
205
|
reject: opts.reject,
|
|
200
206
|
approveAll: opts.approveAll,
|
|
201
207
|
rejectAll: opts.rejectAll,
|
|
202
208
|
reason: opts.reason,
|
|
203
209
|
remember: opts.remember,
|
|
204
|
-
}, opts.response);
|
|
205
|
-
const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model, mode));
|
|
210
|
+
}, toolAnswers, opts.response);
|
|
211
|
+
const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model, mode, toolAnswers));
|
|
206
212
|
await finishRun(command, client, { name, label, model, mode }, result, { raw: !!opts.raw, interactive: false });
|
|
207
213
|
});
|
|
208
214
|
});
|
package/dist/commands/ai.js
CHANGED
|
@@ -106,11 +106,11 @@ export function makeAiCommand() {
|
|
|
106
106
|
.option("--agent <id>", "Invoke a stored agent by id or name, optionally with @label")
|
|
107
107
|
.option("--model <id>", "Model in provider/model format; with --agent, one of the agent's models to run this request on")
|
|
108
108
|
.option("--conversation <id>", "Conversation ID this response belongs to")
|
|
109
|
-
.option("--stream", "Stream the response (not supported by the
|
|
109
|
+
.option("--stream", "Stream the response (not supported by the CLI yet)")
|
|
110
110
|
.action(async (input, opts, command) => {
|
|
111
111
|
await runAction(command, async () => {
|
|
112
112
|
if (opts.stream) {
|
|
113
|
-
throw new CliUsageError("Streaming is not supported by the
|
|
113
|
+
throw new CliUsageError("Streaming is not supported by the CLI yet. Run without --stream.");
|
|
114
114
|
}
|
|
115
115
|
const model = resolveModel(opts);
|
|
116
116
|
const inputText = input ?? readStdinInput();
|
package/dist/commands/billing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { getClient, runAction } from "../lib/client.js";
|
|
3
3
|
import { isJsonOutput } from "../lib/config.js";
|
|
4
|
+
import { CliUsageError } from "../lib/errors.js";
|
|
4
5
|
import { formatDetail, formatList } from "../lib/output.js";
|
|
5
6
|
import chalk from "chalk";
|
|
6
7
|
export function makeBillingCommand() {
|
|
@@ -133,6 +134,107 @@ export function makeBillingCommand() {
|
|
|
133
134
|
}
|
|
134
135
|
});
|
|
135
136
|
});
|
|
137
|
+
cmd.addCommand(makeOverageCommand());
|
|
138
|
+
cmd.addCommand(makeTopUpsCommand());
|
|
139
|
+
return cmd;
|
|
140
|
+
}
|
|
141
|
+
/** Whole euros from cents, e.g. 1234 → "€12.34". Amounts are net of VAT (#973 D-O). */
|
|
142
|
+
function euros(cents) {
|
|
143
|
+
return `€${((cents ?? 0) / 100).toFixed(2)}`;
|
|
144
|
+
}
|
|
145
|
+
function parseWhole(value, name, min, max) {
|
|
146
|
+
const n = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
147
|
+
if (!Number.isInteger(n) || n < min || n > max) {
|
|
148
|
+
throw new CliUsageError(`${name} must be a whole number between ${min} and ${max}, not '${value}'`);
|
|
149
|
+
}
|
|
150
|
+
return n;
|
|
151
|
+
}
|
|
152
|
+
/** `bb billing overage get|set` — the opt-in overage cap (#973, plan D-O). Reads need a member login. */
|
|
153
|
+
function makeOverageCommand() {
|
|
154
|
+
const cmd = new Command("overage").description("Show or set the opt-in overage cap (euros, net of VAT)");
|
|
155
|
+
cmd
|
|
156
|
+
.command("get")
|
|
157
|
+
.description("Show the overage setting and this period's overage (needs `bb auth login`)")
|
|
158
|
+
.action(async (_opts, command) => {
|
|
159
|
+
await runAction(command, async () => {
|
|
160
|
+
const client = getClient(command);
|
|
161
|
+
const { data } = await client.GET("/v1/billing/overage");
|
|
162
|
+
if (isJsonOutput(command)) {
|
|
163
|
+
console.log(JSON.stringify(data, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(`Overage: ${data?.enabled ? chalk.green("on") : chalk.dim("off")}`);
|
|
167
|
+
console.log(`Cap: €${data?.capEuros ?? 0} (max €${data?.maxCapEuros ?? 0})`);
|
|
168
|
+
console.log(`Available: ${data?.available ? chalk.green("yes") : chalk.yellow(`no (${data?.unavailableReason ?? "unknown"})`)}`);
|
|
169
|
+
console.log(`Spent: ${euros(data?.spentCents)}`);
|
|
170
|
+
console.log(`Reserved: ${euros(data?.reservedCents)}`);
|
|
171
|
+
if (data?.periodStart || data?.periodEnd) {
|
|
172
|
+
console.log(chalk.dim(`Period: ${data?.periodStart?.slice(0, 10) ?? "?"} → ${data?.periodEnd?.slice(0, 10) ?? "?"}`));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
cmd
|
|
177
|
+
.command("set")
|
|
178
|
+
.description("Turn overage on with a cap, or off (admin)")
|
|
179
|
+
.option("--cap <euros>", "Monthly cap in whole euros, 1-100; turns overage on")
|
|
180
|
+
.option("--off", "Turn overage off, keeping the stored cap")
|
|
181
|
+
.action(async (opts, command) => {
|
|
182
|
+
await runAction(command, async () => {
|
|
183
|
+
if (opts.off === Boolean(opts.cap)) {
|
|
184
|
+
throw new CliUsageError("Pass exactly one of --cap <euros> or --off");
|
|
185
|
+
}
|
|
186
|
+
// --off sends no cap: the backend keeps the stored one, and no read is needed (the read
|
|
187
|
+
// refuses API keys, the write admits an admin's).
|
|
188
|
+
const body = opts.off
|
|
189
|
+
? { enabled: false }
|
|
190
|
+
: { enabled: true, capEuros: parseWhole(String(opts.cap), "cap", 1, 100) };
|
|
191
|
+
const client = getClient(command);
|
|
192
|
+
const { data } = await client.PUT("/v1/billing/overage", { body });
|
|
193
|
+
formatDetail(data, command);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
return cmd;
|
|
197
|
+
}
|
|
198
|
+
/** `bb billing top-ups list|buy` — prepaid credit packs (#973, plan D-E). Never opens a browser. */
|
|
199
|
+
function makeTopUpsCommand() {
|
|
200
|
+
const cmd = new Command("top-ups").description("List or buy prepaid credit (packs of 1,000 credits at €15)");
|
|
201
|
+
cmd
|
|
202
|
+
.command("list")
|
|
203
|
+
.description("List unexpired prepaid credit (needs `bb auth login`)")
|
|
204
|
+
.action(async (_opts, command) => {
|
|
205
|
+
await runAction(command, async () => {
|
|
206
|
+
const client = getClient(command);
|
|
207
|
+
const { data } = await client.GET("/v1/billing/top-ups");
|
|
208
|
+
if (isJsonOutput(command)) {
|
|
209
|
+
console.log(JSON.stringify(data, null, 2));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const rows = (data ?? []).map((lot) => ({
|
|
213
|
+
kind: lot.kind,
|
|
214
|
+
purchased: euros(lot.purchasedCents),
|
|
215
|
+
remaining: euros(lot.remainingCents),
|
|
216
|
+
expires: lot.expiresAt?.slice(0, 10),
|
|
217
|
+
}));
|
|
218
|
+
formatList(rows, command, ["kind", "purchased", "remaining", "expires"]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
cmd
|
|
222
|
+
.command("buy")
|
|
223
|
+
.description("Start a Stripe Checkout for top-up packs and print its URL (admin)")
|
|
224
|
+
.requiredOption("--packs <n>", "Number of packs, 1-20")
|
|
225
|
+
.action(async (opts, command) => {
|
|
226
|
+
await runAction(command, async () => {
|
|
227
|
+
const packs = parseWhole(String(opts.packs), "packs", 1, 20);
|
|
228
|
+
const client = getClient(command);
|
|
229
|
+
const { data } = await client.POST("/v1/billing/top-ups", { body: { packs } });
|
|
230
|
+
if (isJsonOutput(command)) {
|
|
231
|
+
console.log(JSON.stringify(data, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
console.log(`Open this page to pay: ${data?.checkoutUrl}`);
|
|
235
|
+
console.log(chalk.dim(`Expires: ${data?.expiresAt ?? "?"}`));
|
|
236
|
+
});
|
|
237
|
+
});
|
|
136
238
|
return cmd;
|
|
137
239
|
}
|
|
138
240
|
//# sourceMappingURL=billing.js.map
|
package/dist/commands/convert.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { getClient,
|
|
5
|
-
import {
|
|
6
|
-
import { BackboneApiError } from "../lib/errors.js";
|
|
4
|
+
import { getClient, runAction } from "../lib/client.js";
|
|
5
|
+
import { isJsonOutput } from "../lib/config.js";
|
|
7
6
|
import { formatDetail, withSpinner } from "../lib/output.js";
|
|
8
7
|
import { fileToBlob, getMimeType } from "../lib/multipart.js";
|
|
9
8
|
function mapFormat(format) {
|
|
@@ -136,34 +135,19 @@ function buildFormData(paths, opts) {
|
|
|
136
135
|
formData.append("options", new Blob([JSON.stringify(pipelineOptions)], { type: "application/json" }));
|
|
137
136
|
return formData;
|
|
138
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* POST the multipart body through the typed client. The generated type describes `files` as string[],
|
|
140
|
+
* which FormData cannot match, so the placeholder body satisfies the type and bodySerializer sends the
|
|
141
|
+
* real parts; the client's auth and error middleware apply.
|
|
142
|
+
*/
|
|
139
143
|
async function multipartConvert(command, formData, endpoint, pipeline) {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: `${baseUrl}${endpoint}`;
|
|
146
|
-
const res = await fetch(url, {
|
|
147
|
-
method: "POST",
|
|
148
|
-
headers: { Authorization: authHeader },
|
|
149
|
-
body: formData,
|
|
144
|
+
const client = getClient(command);
|
|
145
|
+
const { data } = await client.POST(endpoint, {
|
|
146
|
+
params: { query: { pipeline } },
|
|
147
|
+
body: { files: [] },
|
|
148
|
+
bodySerializer: () => formData,
|
|
150
149
|
});
|
|
151
|
-
|
|
152
|
-
let body;
|
|
153
|
-
try {
|
|
154
|
-
body = await res.json();
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
body = {
|
|
158
|
-
error: res.statusText,
|
|
159
|
-
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
160
|
-
status: res.status,
|
|
161
|
-
timestamp: new Date().toISOString(),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
throw new BackboneApiError(body);
|
|
165
|
-
}
|
|
166
|
-
return (await res.json());
|
|
150
|
+
return data;
|
|
167
151
|
}
|
|
168
152
|
export function makeConvertCommand() {
|
|
169
153
|
const cmd = new Command("convert").description("Convert documents");
|
|
@@ -16,8 +16,18 @@ function formatComparisonCell(result) {
|
|
|
16
16
|
return chalk.red("ERROR");
|
|
17
17
|
const scores = result.scores ?? [];
|
|
18
18
|
if (scores.length > 0) {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// A SKIP carries a placeholder 0.0 that is not a rating, and a comment-only
|
|
20
|
+
// score has no number; neither takes part in the average (#693).
|
|
21
|
+
const rated = scores.filter((s) => s.label !== "SKIP" && s.score != null);
|
|
22
|
+
if (rated.length > 0) {
|
|
23
|
+
return (rated.reduce((sum, s) => sum + s.score, 0) / rated.length).toFixed(2);
|
|
24
|
+
}
|
|
25
|
+
// A label-only verdict (#1163) has no number; show its label, as the console does.
|
|
26
|
+
const verdict = scores.find((s) => s.label && s.label !== "SKIP");
|
|
27
|
+
if (verdict)
|
|
28
|
+
return String(verdict.label);
|
|
29
|
+
if (scores.every((s) => s.label === "SKIP"))
|
|
30
|
+
return chalk.dim("skipped");
|
|
21
31
|
}
|
|
22
32
|
return result.durationMs != null ? `${result.durationMs}ms` : chalk.dim("done");
|
|
23
33
|
}
|
|
@@ -392,8 +402,14 @@ export function makeExperimentsCommand() {
|
|
|
392
402
|
console.log(chalk.dim(`Page ${(page.number ?? 0) + 1} of ${page.totalPages ?? 0} (${page.totalElements ?? 0} total)`));
|
|
393
403
|
}
|
|
394
404
|
if (aggregates.length > 0) {
|
|
405
|
+
const skippedOf = (agg) => Object.entries((agg.skippedEvaluatorCounts ?? {}))
|
|
406
|
+
.filter(([, n]) => n > 0)
|
|
407
|
+
.map(([evaluatorId, n]) => `${evaluatorId}: ${n}`)
|
|
408
|
+
.join(", ");
|
|
409
|
+
// Only when something was skipped, so a clean run keeps its narrow table (#693).
|
|
410
|
+
const showSkipped = aggregates.some((agg) => skippedOf(agg) !== "");
|
|
395
411
|
const aggTable = new Table({
|
|
396
|
-
head: ["Variant", "Items", "Avg Duration", "Avg Tokens", "Total Cost"].map((h) => chalk.cyan(h)),
|
|
412
|
+
head: ["Variant", "Items", "Avg Duration", "Avg Tokens", "Total Cost", ...(showSkipped ? ["Skipped"] : [])].map((h) => chalk.cyan(h)),
|
|
397
413
|
wordWrap: true,
|
|
398
414
|
});
|
|
399
415
|
for (const agg of aggregates) {
|
|
@@ -403,6 +419,7 @@ export function makeExperimentsCommand() {
|
|
|
403
419
|
agg.avgDurationMs != null ? `${Math.round(agg.avgDurationMs)}ms` : "—",
|
|
404
420
|
agg.avgTokens != null ? Math.round(agg.avgTokens) : "—",
|
|
405
421
|
agg.totalCost != null ? `$${agg.totalCost}` : "—",
|
|
422
|
+
...(showSkipped ? [skippedOf(agg) || "—"] : []),
|
|
406
423
|
]);
|
|
407
424
|
}
|
|
408
425
|
console.log(chalk.bold("\nAggregates:"));
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { getClient, runAction } from "../lib/client.js";
|
|
3
|
+
import { CliUsageError } from "../lib/errors.js";
|
|
3
4
|
import { formatPage, formatDetail, formatList, formatSuccess } from "../lib/output.js";
|
|
4
5
|
import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
|
|
6
|
+
const MIN_MONTHLY_PERCENT = 0;
|
|
7
|
+
const MAX_MONTHLY_PERCENT = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* Validated inside the action rather than as commander's argument parser, so the refusal
|
|
10
|
+
* travels through `runAction` and exits 2 like every other usage error (see `parsePort` in
|
|
11
|
+
* connectors.ts).
|
|
12
|
+
*/
|
|
13
|
+
function parseMonthlyPercent(value) {
|
|
14
|
+
const percent = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
15
|
+
if (!Number.isInteger(percent) || percent < MIN_MONTHLY_PERCENT || percent > MAX_MONTHLY_PERCENT) {
|
|
16
|
+
throw new CliUsageError(`percent must be a whole number between ${MIN_MONTHLY_PERCENT} and ${MAX_MONTHLY_PERCENT}, not '${value}'`);
|
|
17
|
+
}
|
|
18
|
+
return percent;
|
|
19
|
+
}
|
|
5
20
|
export function makeInstallationsCommand() {
|
|
6
21
|
const cmd = new Command("installations").description("Manage surface installations");
|
|
7
22
|
const list = new Command("list").description("List installations");
|
|
@@ -172,6 +187,36 @@ export function makeInstallationsCommand() {
|
|
|
172
187
|
});
|
|
173
188
|
});
|
|
174
189
|
cmd.addCommand(keys);
|
|
190
|
+
const budget = new Command("budget").description("Manage an installation's monthly budget: a share of the organisation's pool");
|
|
191
|
+
budget
|
|
192
|
+
.command("get")
|
|
193
|
+
.description("Read an installation's current budget: percent, period, and usage")
|
|
194
|
+
.argument("<id>", "Installation ID")
|
|
195
|
+
.action(async (id, _opts, command) => {
|
|
196
|
+
await runAction(command, async () => {
|
|
197
|
+
const client = getClient(command);
|
|
198
|
+
const { data } = await client.GET("/v1/installations/{id}/budget", {
|
|
199
|
+
params: { path: { id } },
|
|
200
|
+
});
|
|
201
|
+
formatDetail(data, command);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
budget
|
|
205
|
+
.command("set")
|
|
206
|
+
.description("Set or change an installation's monthly budget share, 0-1000 percent")
|
|
207
|
+
.argument("<id>", "Installation ID")
|
|
208
|
+
.argument("<percent>", "Monthly percent of the organisation's pool, 0-1000")
|
|
209
|
+
.action(async (id, percent, _opts, command) => {
|
|
210
|
+
await runAction(command, async () => {
|
|
211
|
+
const client = getClient(command);
|
|
212
|
+
const { data } = await client.PUT("/v1/installations/{id}/budget", {
|
|
213
|
+
params: { path: { id } },
|
|
214
|
+
body: { monthlyPercent: parseMonthlyPercent(percent) },
|
|
215
|
+
});
|
|
216
|
+
formatDetail(data, command);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
cmd.addCommand(budget);
|
|
175
220
|
return cmd;
|
|
176
221
|
}
|
|
177
222
|
//# sourceMappingURL=installations.js.map
|
|
@@ -37,7 +37,7 @@ export function makeKnowledgeCommand() {
|
|
|
37
37
|
.description("Create a knowledge base and its first configuration version")
|
|
38
38
|
.requiredOption("-n, --name <name>", "Knowledge base name")
|
|
39
39
|
.requiredOption("--slug <slug>", "Unique slug")
|
|
40
|
-
.requiredOption("--embedding-provider <id>", "Embedding provider ID")
|
|
40
|
+
.requiredOption("--embedding-provider <id>", "Embedding provider: a provider ID, or builtin for the platform's built-in models (text-embedding-3-small, text-embedding-3-large; charged per input token)")
|
|
41
41
|
.requiredOption("--embedding-model <model>", "Embedding model")
|
|
42
42
|
.requiredOption("--embedding-dim <n>", "Embedding dimension", parseInt)
|
|
43
43
|
.option("-d, --description <text>", "Description")
|
|
@@ -47,6 +47,7 @@ export function makeKnowledgeCommand() {
|
|
|
47
47
|
.option("--parent-chunk-size <n>", "Parent chunk size (hierarchical chunking)", parseInt)
|
|
48
48
|
.option("--reranker-provider <id>", "Reranker provider ID")
|
|
49
49
|
.option("--hybrid-search <bool>", "Enable hybrid search (true/false)")
|
|
50
|
+
.option("--text-search-language <name>", "Text-search language for the lexical leg (default simple)")
|
|
50
51
|
.action(async (opts, command) => {
|
|
51
52
|
await runAction(command, async () => {
|
|
52
53
|
const client = getClient(command);
|
|
@@ -64,6 +65,7 @@ export function makeKnowledgeCommand() {
|
|
|
64
65
|
parentChunkSize: opts.parentChunkSize,
|
|
65
66
|
rerankerProviderId: opts.rerankerProvider,
|
|
66
67
|
hybridSearchEnabled: opts.hybridSearch !== undefined ? opts.hybridSearch === "true" : undefined,
|
|
68
|
+
textSearchLanguage: opts.textSearchLanguage,
|
|
67
69
|
},
|
|
68
70
|
});
|
|
69
71
|
formatDetail(data, command);
|
|
@@ -75,7 +77,7 @@ export function makeKnowledgeCommand() {
|
|
|
75
77
|
.argument("<id>", "Knowledge base ID")
|
|
76
78
|
.option("-n, --name <name>", "New name")
|
|
77
79
|
.option("-d, --description <text>", "New description")
|
|
78
|
-
.option("--embedding-provider <id>", "New embedding provider ID")
|
|
80
|
+
.option("--embedding-provider <id>", "New embedding provider ID, or builtin. Re-embeds nothing: stored vectors stay comparable only if the new provider serves the same model at the same width; the safe move is a new knowledge base with the documents uploaded again")
|
|
79
81
|
.option("--embedding-model <model>", "New embedding model")
|
|
80
82
|
.option("--chunking-strategy <strategy>", "New chunking strategy (AUTO, FIXED, HIERARCHICAL, CUSTOM)")
|
|
81
83
|
.option("--chunk-size <n>", "New chunk size", parseInt)
|
|
@@ -83,6 +85,7 @@ export function makeKnowledgeCommand() {
|
|
|
83
85
|
.option("--parent-chunk-size <n>", "New parent chunk size", parseInt)
|
|
84
86
|
.option("--reranker-provider <id>", "New reranker provider ID")
|
|
85
87
|
.option("--hybrid-search <bool>", "Enable hybrid search (true/false)")
|
|
88
|
+
.option("--text-search-language <name>", "Change the text-search language (reindexes in the background); omit to leave it untouched")
|
|
86
89
|
.action(async (id, opts, command) => {
|
|
87
90
|
await runAction(command, async () => {
|
|
88
91
|
const client = getClient(command);
|
|
@@ -107,6 +110,8 @@ export function makeKnowledgeCommand() {
|
|
|
107
110
|
body.rerankerProviderId = opts.rerankerProvider;
|
|
108
111
|
if (opts.hybridSearch !== undefined)
|
|
109
112
|
body.hybridSearchEnabled = opts.hybridSearch === "true";
|
|
113
|
+
if (opts.textSearchLanguage)
|
|
114
|
+
body.textSearchLanguage = opts.textSearchLanguage;
|
|
110
115
|
const { data } = await client.PATCH("/v1/knowledge-bases/{id}", {
|
|
111
116
|
params: { path: { id } },
|
|
112
117
|
body: body,
|
|
@@ -173,7 +178,7 @@ export function makeKnowledgeCommand() {
|
|
|
173
178
|
cmd
|
|
174
179
|
.command("embedding-models")
|
|
175
180
|
.description("List embedding models a provider serves that fit a provisioned dimension")
|
|
176
|
-
.argument("<providerId>", "Provider ID")
|
|
181
|
+
.argument("<providerId>", "Provider ID, or builtin for the platform's built-in embedding models")
|
|
177
182
|
.action(async (providerId, _opts, command) => {
|
|
178
183
|
await runAction(command, async () => {
|
|
179
184
|
const client = getClient(command);
|
package/dist/commands/skills.js
CHANGED
|
@@ -149,18 +149,16 @@ export function makeSkillsCommand() {
|
|
|
149
149
|
}
|
|
150
150
|
({ blob, filename } = fileToBlob(path));
|
|
151
151
|
}
|
|
152
|
-
const config = resolveConfig(command);
|
|
153
|
-
const authHeader = await resolveAuthHeader(config);
|
|
154
152
|
const formData = new FormData();
|
|
155
153
|
formData.append("file", blob, filename);
|
|
154
|
+
const client = getClient(command);
|
|
156
155
|
const data = await withSpinner("Importing skill...", async () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
// Placeholder body for the generated type; bodySerializer sends the real part.
|
|
157
|
+
const result = await client.POST("/v1/skills/import", {
|
|
158
|
+
body: { file: "" },
|
|
159
|
+
bodySerializer: () => formData,
|
|
161
160
|
});
|
|
162
|
-
|
|
163
|
-
return res.json();
|
|
161
|
+
return result.data;
|
|
164
162
|
});
|
|
165
163
|
formatDetail(data, command);
|
|
166
164
|
});
|
package/dist/commands/tracing.js
CHANGED
|
@@ -77,6 +77,7 @@ export function makeTracingCommand() {
|
|
|
77
77
|
const sessions = new Command("sessions").description("Session-level trace views (spans grouped by exporter-stamped session id)");
|
|
78
78
|
const sessionsList = new Command("list").description("List trace sessions");
|
|
79
79
|
sessionsList.option("--search <query>", "Free-text search across session id and name");
|
|
80
|
+
sessionsList.option("--session-id <sessionId>", "Exact match on the session.id the exporter stamped (slashes allowed)");
|
|
80
81
|
sessionsList.option("--from <iso>", "Start of time range (ISO-8601)");
|
|
81
82
|
sessionsList.option("--to <iso>", "End of time range (ISO-8601)");
|
|
82
83
|
sessionsList.option("--page <n>", "Page index (0-based)", "0");
|
|
@@ -88,6 +89,7 @@ export function makeTracingCommand() {
|
|
|
88
89
|
params: {
|
|
89
90
|
query: {
|
|
90
91
|
search: opts.search,
|
|
92
|
+
sessionId: opts.sessionId,
|
|
91
93
|
from: opts.from,
|
|
92
94
|
to: opts.to,
|
|
93
95
|
page: parseInt(opts.page, 10),
|
|
@@ -96,17 +98,17 @@ export function makeTracingCommand() {
|
|
|
96
98
|
},
|
|
97
99
|
});
|
|
98
100
|
const page = data;
|
|
99
|
-
formatList((page?.content ?? []), command, ["sessionId", "name", "startTime", "durationMs", "turnCount", "errorCount", "costUsd"]);
|
|
101
|
+
formatList((page?.content ?? []), command, ["id", "sessionId", "name", "startTime", "durationMs", "turnCount", "errorCount", "costUsd"]);
|
|
100
102
|
});
|
|
101
103
|
});
|
|
102
104
|
sessions.addCommand(sessionsList);
|
|
103
105
|
const sessionsGet = new Command("get").description("Get all spans of a session by id, sorted chronologically");
|
|
104
|
-
sessionsGet.argument("<
|
|
105
|
-
sessionsGet.action(async (
|
|
106
|
+
sessionsGet.argument("<id>", "Session id: the id column of `sessions list` (find one by session.id with --session-id)");
|
|
107
|
+
sessionsGet.action(async (id, _opts, command) => {
|
|
106
108
|
await runAction(command, async () => {
|
|
107
109
|
const client = getClient(command);
|
|
108
|
-
const { data } = await client.GET("/v1/traces/sessions/{
|
|
109
|
-
params: { path: {
|
|
110
|
+
const { data } = await client.GET("/v1/traces/sessions/{id}", {
|
|
111
|
+
params: { path: { id } },
|
|
110
112
|
});
|
|
111
113
|
formatDetail(data, command);
|
|
112
114
|
});
|