@2kw/ai 7.1.0-dev.1 → 7.1.0-dev.16
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 +2 -1
- package/dist/agent-config/export.d.ts +2 -2
- package/dist/agent-config/export.js +2 -2
- package/dist/agent-config/schema.d.ts +83 -0
- package/dist/agent-config/schema.js +34 -1
- package/dist/agent-config/template.js +3 -0
- package/dist/agent-config/validate.js +33 -0
- package/dist/commands/agent-apply.js +22 -20
- package/dist/commands/agents.js +26 -5
- package/dist/commands/auth.d.ts +5 -0
- package/dist/commands/auth.js +7 -3
- package/dist/commands/billing.js +93 -0
- package/dist/commands/context.d.ts +6 -1
- package/dist/commands/context.js +22 -4
- 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/agent-run.js +3 -2
- package/dist/lib/errors.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,8 @@ kubectl-style contexts switch between organizations and environments:
|
|
|
85
85
|
```bash
|
|
86
86
|
2kw context list # List all contexts
|
|
87
87
|
2kw context use acme-corp # Switch active context
|
|
88
|
-
2kw context create staging
|
|
88
|
+
2kw context create staging --base-url https://api.staging.example # Create a context, sign in through the browser
|
|
89
|
+
2kw context create ci --api-key sk_... # Or store an API key instead
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
`2kw config set memory true` makes an API-key context send `X-Backbone-Memory: enabled` on `ai respond` and `agents run` calls, which opts those runs into agent memory; `AI_2KW_MEMORY=true|false` overrides it per shell. Runs from a browser-login (session) context are always eligible for agent memory regardless of this setting, so `memory false` only affects API-key contexts.
|
|
@@ -11,8 +11,8 @@ export declare function redactSecrets(tools: Record<string, unknown>[] | undefin
|
|
|
11
11
|
envNames: string[];
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
* Tool entries the backend stores but never runs (planner
|
|
15
|
-
* e.g.
|
|
14
|
+
* Tool entries the backend stores but never runs as authored (planner- or runtime-injected,
|
|
15
|
+
* e.g. `backbone.tool_search`). agent.yaml cannot author them, so export drops them.
|
|
16
16
|
*/
|
|
17
17
|
export declare function dropInertTools(tools: Record<string, unknown>[] | undefined): {
|
|
18
18
|
tools: Record<string, unknown>[] | undefined;
|
|
@@ -43,8 +43,8 @@ export function redactSecrets(tools) {
|
|
|
43
43
|
return { tools: redacted, envNames: [...owners.keys()] };
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* Tool entries the backend stores but never runs (planner
|
|
47
|
-
* e.g.
|
|
46
|
+
* Tool entries the backend stores but never runs as authored (planner- or runtime-injected,
|
|
47
|
+
* e.g. `backbone.tool_search`). agent.yaml cannot author them, so export drops them.
|
|
48
48
|
*/
|
|
49
49
|
export function dropInertTools(tools) {
|
|
50
50
|
if (!tools)
|
|
@@ -138,6 +138,77 @@ export declare const agentSchema: {
|
|
|
138
138
|
};
|
|
139
139
|
};
|
|
140
140
|
};
|
|
141
|
+
readonly webSearchTool: {
|
|
142
|
+
readonly type: "object";
|
|
143
|
+
readonly additionalProperties: false;
|
|
144
|
+
readonly required: readonly ["type"];
|
|
145
|
+
readonly properties: {
|
|
146
|
+
readonly type: {
|
|
147
|
+
readonly const: "web_search";
|
|
148
|
+
};
|
|
149
|
+
readonly config: {
|
|
150
|
+
readonly type: "object";
|
|
151
|
+
readonly properties: {
|
|
152
|
+
readonly source: {
|
|
153
|
+
readonly enum: readonly ["builtin", "external", "native"];
|
|
154
|
+
};
|
|
155
|
+
readonly searchProviderId: {
|
|
156
|
+
readonly type: readonly ["string", "null"];
|
|
157
|
+
};
|
|
158
|
+
readonly fallback: {
|
|
159
|
+
readonly enum: readonly ["builtin", "external"];
|
|
160
|
+
};
|
|
161
|
+
readonly allowedDomains: {
|
|
162
|
+
readonly type: "array";
|
|
163
|
+
readonly items: {
|
|
164
|
+
readonly type: "string";
|
|
165
|
+
readonly pattern: "^(?=.{1,253}$)([\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?\\.)+\\p{L}(?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?$";
|
|
166
|
+
};
|
|
167
|
+
readonly maxItems: 10;
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
readonly if: {
|
|
171
|
+
readonly anyOf: readonly [{
|
|
172
|
+
readonly required: readonly ["source"];
|
|
173
|
+
readonly properties: {
|
|
174
|
+
readonly source: {
|
|
175
|
+
readonly const: "external";
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}, {
|
|
179
|
+
readonly required: readonly ["source", "fallback"];
|
|
180
|
+
readonly properties: {
|
|
181
|
+
readonly source: {
|
|
182
|
+
readonly const: "native";
|
|
183
|
+
};
|
|
184
|
+
readonly fallback: {
|
|
185
|
+
readonly const: "external";
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
}];
|
|
189
|
+
};
|
|
190
|
+
readonly then: {
|
|
191
|
+
readonly required: readonly ["searchProviderId"];
|
|
192
|
+
readonly properties: {
|
|
193
|
+
readonly searchProviderId: {
|
|
194
|
+
readonly type: "string";
|
|
195
|
+
readonly pattern: "\\S";
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
readonly else: {
|
|
200
|
+
readonly properties: {
|
|
201
|
+
readonly searchProviderId: {
|
|
202
|
+
readonly type: "null";
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
readonly requiresConfirmation: {
|
|
208
|
+
readonly type: "boolean";
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
};
|
|
141
212
|
readonly extractionTool: {
|
|
142
213
|
readonly type: "object";
|
|
143
214
|
readonly additionalProperties: false;
|
|
@@ -306,6 +377,18 @@ export declare const agentSchema: {
|
|
|
306
377
|
readonly then: {
|
|
307
378
|
readonly $ref: "#/$defs/fileSearchTool";
|
|
308
379
|
};
|
|
380
|
+
}, {
|
|
381
|
+
readonly if: {
|
|
382
|
+
required: string[];
|
|
383
|
+
properties: {
|
|
384
|
+
type: {
|
|
385
|
+
const: string;
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
readonly then: {
|
|
390
|
+
readonly $ref: "#/$defs/webSearchTool";
|
|
391
|
+
};
|
|
309
392
|
}, {
|
|
310
393
|
readonly if: {
|
|
311
394
|
required: string[];
|
|
@@ -16,6 +16,9 @@ const MODEL_REF = {
|
|
|
16
16
|
maxLength: 255,
|
|
17
17
|
pattern: "^(?:[^/#]+|[A-Za-z0-9-]+/[^#]+)$",
|
|
18
18
|
};
|
|
19
|
+
// A plain hostname (no scheme, path, port or wildcard): dot-separated labels of letters, digits and
|
|
20
|
+
// inner hyphens, Unicode or ASCII. Mirrors WebToolEntryValidator.HOST; needs a Unicode-aware regex engine.
|
|
21
|
+
const HOSTNAME = "^(?=.{1,253}$)([\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?\\.)+\\p{L}(?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?$";
|
|
19
22
|
const oneOfLowercase = (...values) => ({ type: "string", enum: values });
|
|
20
23
|
/** Types the backend injects or cannot execute; authoring them has no effect or breaks the run. */
|
|
21
24
|
export const NON_AUTHORABLE_TOOL_TYPES = {
|
|
@@ -27,10 +30,10 @@ export const NON_AUTHORABLE_TOOL_TYPES = {
|
|
|
27
30
|
"backbone.deliver": "it is added at run time while sandbox outputs are held",
|
|
28
31
|
// #1244: injected wherever backbone.bash is advertised; an authored copy is dropped.
|
|
29
32
|
"backbone.view_image": "it is added at run time wherever the sandboxed shell is",
|
|
30
|
-
web_search: "it has no executor and is skipped",
|
|
31
33
|
};
|
|
32
34
|
const BUILTIN_TYPES = [
|
|
33
35
|
"file_search",
|
|
36
|
+
"web_search",
|
|
34
37
|
"backbone.extraction",
|
|
35
38
|
"backbone.document_convert",
|
|
36
39
|
"backbone.todo",
|
|
@@ -118,6 +121,35 @@ export const agentSchema = {
|
|
|
118
121
|
},
|
|
119
122
|
},
|
|
120
123
|
},
|
|
124
|
+
// A missing config, `config: {}` or a missing source runs the built-in search. Mirrors
|
|
125
|
+
// WebToolEntryValidator; `config` stays open so later sources' fields do not break older CLIs.
|
|
126
|
+
webSearchTool: {
|
|
127
|
+
type: "object",
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
required: ["type"],
|
|
130
|
+
properties: {
|
|
131
|
+
type: { const: "web_search" },
|
|
132
|
+
config: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
source: { enum: ["builtin", "external", "native"] },
|
|
136
|
+
searchProviderId: { type: ["string", "null"] },
|
|
137
|
+
fallback: { enum: ["builtin", "external"] },
|
|
138
|
+
allowedDomains: { type: "array", items: { type: "string", pattern: HOSTNAME }, maxItems: 10 },
|
|
139
|
+
},
|
|
140
|
+
// A provider id is required exactly when an external provider serves the search.
|
|
141
|
+
if: {
|
|
142
|
+
anyOf: [
|
|
143
|
+
{ required: ["source"], properties: { source: { const: "external" } } },
|
|
144
|
+
{ required: ["source", "fallback"], properties: { source: { const: "native" }, fallback: { const: "external" } } },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
then: { required: ["searchProviderId"], properties: { searchProviderId: { type: "string", pattern: "\\S" } } },
|
|
148
|
+
else: { properties: { searchProviderId: { type: "null" } } },
|
|
149
|
+
},
|
|
150
|
+
requiresConfirmation: { type: "boolean" },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
121
153
|
extractionTool: {
|
|
122
154
|
type: "object",
|
|
123
155
|
additionalProperties: false,
|
|
@@ -204,6 +236,7 @@ export const agentSchema = {
|
|
|
204
236
|
type: "object",
|
|
205
237
|
allOf: [
|
|
206
238
|
{ if: typeIs("file_search"), then: { $ref: "#/$defs/fileSearchTool" } },
|
|
239
|
+
{ if: typeIs("web_search"), then: { $ref: "#/$defs/webSearchTool" } },
|
|
207
240
|
{ if: typeIs("backbone.extraction"), then: { $ref: "#/$defs/extractionTool" } },
|
|
208
241
|
{ if: typeIs("backbone.document_convert"), then: { $ref: "#/$defs/documentConvertTool" } },
|
|
209
242
|
{ if: typeIs("backbone.todo"), then: { $ref: "#/$defs/todoTool" } },
|
|
@@ -6,6 +6,8 @@ const validate = ajv.compile(agentSchema);
|
|
|
6
6
|
const NULLABLE_KEYS = ["description", "models", "instructions", "tools", "hitlPolicy", "options", "skills"];
|
|
7
7
|
/** Tool-rule values the backend matches case-insensitively; the actions (approve, auto, deny) are exact. */
|
|
8
8
|
const TOOL_RULE_CLASSES = new Set(["read", "write", "destructive"]);
|
|
9
|
+
/** web_search config keys whose explicit `null` the backend reads as absent. */
|
|
10
|
+
const WEB_SEARCH_NULLABLE_KEYS = ["source", "fallback", "searchProviderId", "allowedDomains"];
|
|
9
11
|
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10
12
|
/**
|
|
11
13
|
* A deep copy of `doc` in the form the backend actually checks: explicit nulls it treats as absent
|
|
@@ -31,6 +33,8 @@ export function normalizeForValidation(doc) {
|
|
|
31
33
|
for (const tool of copy.tools) {
|
|
32
34
|
if (isPlainObject(tool) && tool.annotations === null)
|
|
33
35
|
delete tool.annotations;
|
|
36
|
+
if (isPlainObject(tool) && tool.type === "web_search")
|
|
37
|
+
normalizeWebSearch(tool);
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
const policy = copy.hitlPolicy;
|
|
@@ -50,6 +54,21 @@ export function normalizeForValidation(doc) {
|
|
|
50
54
|
}
|
|
51
55
|
return copy;
|
|
52
56
|
}
|
|
57
|
+
/** WebToolEntryValidator reads a null config and null config fields as absent, and trims each domain. */
|
|
58
|
+
function normalizeWebSearch(tool) {
|
|
59
|
+
if (tool.config === null)
|
|
60
|
+
delete tool.config;
|
|
61
|
+
const config = tool.config;
|
|
62
|
+
if (!isPlainObject(config))
|
|
63
|
+
return;
|
|
64
|
+
for (const key of WEB_SEARCH_NULLABLE_KEYS) {
|
|
65
|
+
if (config[key] === null)
|
|
66
|
+
delete config[key];
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(config.allowedDomains)) {
|
|
69
|
+
config.allowedDomains = config.allowedDomains.map((d) => (typeof d === "string" ? d.trim() : d));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
53
72
|
function describe(e) {
|
|
54
73
|
// if/then wrappers restate the real error one level up; drop them. `not` only marks
|
|
55
74
|
// non-authorable tool types, which validateAgentConfig reports with the reason.
|
|
@@ -75,6 +94,13 @@ function describe(e) {
|
|
|
75
94
|
if (e.keyword === "const") {
|
|
76
95
|
return { pointer, message: `must be ${JSON.stringify(params.allowedValue)}` };
|
|
77
96
|
}
|
|
97
|
+
// The `else` branch of webSearchTool's provider rule: an id where no external provider serves the search.
|
|
98
|
+
if (e.keyword === "type" && params.type === "null" && pointer.endsWith("/config/searchProviderId")) {
|
|
99
|
+
return {
|
|
100
|
+
pointer,
|
|
101
|
+
message: "only allowed when an external provider serves the search (source external, or native with fallback external)",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
78
104
|
return { pointer, message: e.message ?? e.keyword };
|
|
79
105
|
}
|
|
80
106
|
export function validateAgentConfig(raw) {
|
|
@@ -98,11 +124,18 @@ export function validateAgentConfig(raw) {
|
|
|
98
124
|
}
|
|
99
125
|
}
|
|
100
126
|
if (Array.isArray(record.tools)) {
|
|
127
|
+
let webSearchSeen = false;
|
|
101
128
|
record.tools.forEach((tool, i) => {
|
|
102
129
|
const type = tool?.type;
|
|
103
130
|
if (typeof type === "string" && Object.hasOwn(NON_AUTHORABLE_TOOL_TYPES, type)) {
|
|
104
131
|
issues.push({ pointer: `/tools/${i}/type`, message: `type "${type}" cannot be authored: ${NON_AUTHORABLE_TOOL_TYPES[type]}` });
|
|
105
132
|
}
|
|
133
|
+
// The backend refuses a second envelope (WebToolEntryValidator).
|
|
134
|
+
if (type === "web_search") {
|
|
135
|
+
if (webSearchSeen)
|
|
136
|
+
issues.push({ pointer: `/tools/${i}/type`, message: "an agent may carry only one web_search tool" });
|
|
137
|
+
webSearchSeen = true;
|
|
138
|
+
}
|
|
106
139
|
});
|
|
107
140
|
}
|
|
108
141
|
if (Array.isArray(record.skills)) {
|
|
@@ -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/auth.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ type SessionConfig = Extract<ResolvedConfig, {
|
|
|
14
14
|
* or a code that already carries its own separator) is shown untouched.
|
|
15
15
|
*/
|
|
16
16
|
export declare function formatUserCode(code: string): string;
|
|
17
|
+
/** Trailing slashes make `${authUrl}/api/auth/...` double up — drop them once, here. */
|
|
18
|
+
export declare function stripTrailingSlash(url: string): string;
|
|
17
19
|
/** Injection points for {@link deviceFlowLogin}; all default to the real thing. */
|
|
18
20
|
export interface DeviceLoginDeps {
|
|
19
21
|
requestCode?: typeof requestDeviceCode;
|
|
@@ -33,6 +35,9 @@ export interface DeviceLoginTarget {
|
|
|
33
35
|
contextName: string;
|
|
34
36
|
baseUrl: string;
|
|
35
37
|
authUrl: string;
|
|
38
|
+
/** Make the context active once stored. Defaults to true; `context create`
|
|
39
|
+
* turns it off for every context but the first. */
|
|
40
|
+
activate?: boolean;
|
|
36
41
|
}
|
|
37
42
|
/**
|
|
38
43
|
* The browser login, end to end: ask for a device code, wait for the user to
|
package/dist/commands/auth.js
CHANGED
|
@@ -27,7 +27,7 @@ export function formatUserCode(code) {
|
|
|
27
27
|
return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
|
|
28
28
|
}
|
|
29
29
|
/** Trailing slashes make `${authUrl}/api/auth/...` double up — drop them once, here. */
|
|
30
|
-
function stripTrailingSlash(url) {
|
|
30
|
+
export function stripTrailingSlash(url) {
|
|
31
31
|
return url.replace(/\/+$/, "");
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
@@ -65,7 +65,7 @@ async function tryOpenBrowser(url) {
|
|
|
65
65
|
*/
|
|
66
66
|
export async function deviceFlowLogin(target, deps = {}) {
|
|
67
67
|
const { requestCode = requestDeviceCode, poll = pollDeviceToken, runPolling = runDevicePolling, sessionInfo = getSessionInfo, listOrgs = listOrganizations, chooseOrg = selectOrganization, setActiveOrg = setActiveOrganization, revoke = revokeSession, openBrowser = tryOpenBrowser, saveContext = setContext, activateContext = setActiveContext, } = deps;
|
|
68
|
-
const { contextName, baseUrl, authUrl } = target;
|
|
68
|
+
const { contextName, baseUrl, authUrl, activate = true } = target;
|
|
69
69
|
const device = await requestCode(authUrl, CLI_CLIENT_ID);
|
|
70
70
|
console.log("");
|
|
71
71
|
console.log(`Open ${chalk.cyan(device.verification_uri_complete)}`);
|
|
@@ -121,10 +121,14 @@ export async function deviceFlowLogin(target, deps = {}) {
|
|
|
121
121
|
}
|
|
122
122
|
// Outside the guard on purpose: the session is on disk now, so a failure to
|
|
123
123
|
// mark the context active still leaves a credential the user can reach.
|
|
124
|
-
|
|
124
|
+
if (activate)
|
|
125
|
+
activateContext(contextName);
|
|
125
126
|
console.log(chalk.green(`Logged in as ${email}`));
|
|
126
127
|
console.log(chalk.dim(` — organization: ${org.name}`));
|
|
127
128
|
console.log(chalk.dim(`Config stored at: ${store.path}`));
|
|
129
|
+
if (!activate) {
|
|
130
|
+
console.log(chalk.dim(`Switch to it with "2kw context use ${contextName}".`));
|
|
131
|
+
}
|
|
128
132
|
}
|
|
129
133
|
/**
|
|
130
134
|
* The pre-device-flow login: take an API key from the flag or the terminal and
|
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,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { updateContext } from "../lib/config.js";
|
|
3
|
+
import { type DeviceLoginDeps } from "./auth.js";
|
|
3
4
|
import { listOrganizations, setActiveOrganization } from "../lib/auth-service.js";
|
|
4
5
|
import { selectOrganization, type Asker } from "../lib/auth-session.js";
|
|
5
6
|
/** Injection points for {@link performSetOrg}; all default to the real thing. */
|
|
@@ -24,5 +25,9 @@ export interface SetOrgDeps {
|
|
|
24
25
|
* @param orgArg an organization id or slug; omitted means "prompt".
|
|
25
26
|
*/
|
|
26
27
|
export declare function performSetOrg(orgArg: string | undefined, deps?: SetOrgDeps): Promise<void>;
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param deviceDeps forwarded to {@link deviceFlowLogin} by `context create` —
|
|
30
|
+
* a seam for tests, as on `makeAuthCommand`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function makeContextCommand(deviceDeps?: DeviceLoginDeps): Command;
|
|
28
33
|
//# sourceMappingURL=context.d.ts.map
|
package/dist/commands/context.js
CHANGED
|
@@ -2,7 +2,8 @@ import { Command } from "commander";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import Table from "cli-table3";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { store, isJsonOutput, getAllContexts, getActiveContext, getActiveContextName, getContextCount, setContext, deleteContext, renameContext, setActiveContext, updateContext, validateContextName, DEFAULT_BASE_URL, } from "../lib/config.js";
|
|
5
|
+
import { store, isJsonOutput, getAllContexts, getActiveContext, getActiveContextName, getContextCount, setContext, deleteContext, renameContext, setActiveContext, updateContext, validateContextName, defaultAuthUrlFor, DEFAULT_BASE_URL, } from "../lib/config.js";
|
|
6
|
+
import { deviceFlowLogin, stripTrailingSlash } from "./auth.js";
|
|
6
7
|
import { listOrganizations, setActiveOrganization, } from "../lib/auth-service.js";
|
|
7
8
|
import { selectOrganization } from "../lib/auth-session.js";
|
|
8
9
|
import { runAction } from "../lib/client.js";
|
|
@@ -76,7 +77,11 @@ export async function performSetOrg(orgArg, deps = {}) {
|
|
|
76
77
|
}
|
|
77
78
|
console.log(chalk.green(`Active organization: ${org.name} (${org.slug})`));
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @param deviceDeps forwarded to {@link deviceFlowLogin} by `context create` —
|
|
82
|
+
* a seam for tests, as on `makeAuthCommand`.
|
|
83
|
+
*/
|
|
84
|
+
export function makeContextCommand(deviceDeps = {}) {
|
|
80
85
|
const cmd = new Command("context").description("Manage CLI contexts for multiple environments");
|
|
81
86
|
cmd
|
|
82
87
|
.command("list")
|
|
@@ -125,10 +130,12 @@ export function makeContextCommand() {
|
|
|
125
130
|
});
|
|
126
131
|
cmd
|
|
127
132
|
.command("create")
|
|
128
|
-
.description("Create a new context")
|
|
133
|
+
.description("Create a new context and sign in through the browser (or with an API key)")
|
|
129
134
|
.argument("<name>", "Context name")
|
|
130
135
|
.option("--base-url <url>", "Base URL")
|
|
131
|
-
.option("--
|
|
136
|
+
.option("--auth-url <url>", "Auth service URL (defaults to the one matching the base URL)")
|
|
137
|
+
.option("--api-key <key>", "Save an API key instead of signing in through the browser")
|
|
138
|
+
.option("--manual", "Prompt for an API key instead of signing in through the browser")
|
|
132
139
|
.action(async (name, _opts, command) => {
|
|
133
140
|
validateContextName(name);
|
|
134
141
|
// Merged, not own — the root program declares --base-url and --api-key
|
|
@@ -140,6 +147,17 @@ export function makeContextCommand() {
|
|
|
140
147
|
if (contexts[name]) {
|
|
141
148
|
throw new Error(`Context "${name}" already exists. Delete it first or choose a different name.`);
|
|
142
149
|
}
|
|
150
|
+
// The same browser login as `auth login`, written into the new context
|
|
151
|
+
// instead of the active one. Only the first context becomes active, as
|
|
152
|
+
// with an API key below: creating one is not switching to it.
|
|
153
|
+
if (!opts.apiKey && !opts.manual) {
|
|
154
|
+
const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
155
|
+
const authUrl = stripTrailingSlash(opts.authUrl ?? defaultAuthUrlFor(baseUrl));
|
|
156
|
+
const activate = getContextCount() === 0;
|
|
157
|
+
console.log(chalk.dim(`Auth service: ${authUrl} (override with --auth-url)`));
|
|
158
|
+
await runAction(command, () => deviceFlowLogin({ contextName: name, baseUrl, authUrl, activate }, deviceDeps));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
143
161
|
let baseUrl = opts.baseUrl;
|
|
144
162
|
let apiKey = opts.apiKey;
|
|
145
163
|
if (!baseUrl || !apiKey) {
|
|
@@ -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/agent-run.js
CHANGED
|
@@ -156,7 +156,8 @@ function relayDecideCommand(ref, responseId, calls, withApprovals) {
|
|
|
156
156
|
/**
|
|
157
157
|
* A connector tool's open approval, decoded from the standard `mcp_approval_request` item (#949). The item
|
|
158
158
|
* is the MCP twin of `backbone:approval_request` (#805): it names the server and the tool, carries no call
|
|
159
|
-
* id
|
|
159
|
+
* id or status (its policy class is an optional extension, #1084; "unknown" without it), and exists only
|
|
160
|
+
* while the decision is open, so its id doubles as the call id
|
|
160
161
|
* and the tool is named as the approval row stores it, `mcp__<server_label>__<tool>`.
|
|
161
162
|
*/
|
|
162
163
|
function connectorApproval(i) {
|
|
@@ -165,7 +166,7 @@ function connectorApproval(i) {
|
|
|
165
166
|
callId: String(i.id),
|
|
166
167
|
tool: `mcp__${String(i.server_label)}__${String(i.name)}`,
|
|
167
168
|
arguments: parseArguments(i.arguments),
|
|
168
|
-
policyClass: "unknown",
|
|
169
|
+
policyClass: typeof i.policy_class === "string" ? i.policy_class : "unknown",
|
|
169
170
|
reason: null,
|
|
170
171
|
preview: null,
|
|
171
172
|
};
|
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-dev.
|
|
3
|
+
"version": "7.1.0-dev.16",
|
|
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",
|