@2kw/ai 6.3.0-dev.14 → 6.3.0-dev.144

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.
Files changed (49) hide show
  1. package/README.md +4 -1
  2. package/dist/agent-config/schema.d.ts +6 -3
  3. package/dist/agent-config/schema.js +19 -4
  4. package/dist/agent-config/template.js +1 -1
  5. package/dist/commands/agent-apply.js +1 -1
  6. package/dist/commands/agent-policy.d.ts +2 -1
  7. package/dist/commands/agent-policy.js +9 -2
  8. package/dist/commands/agent-run.d.ts +5 -0
  9. package/dist/commands/agent-run.js +47 -21
  10. package/dist/commands/agents.js +18 -5
  11. package/dist/commands/ai.js +5 -11
  12. package/dist/commands/auth.js +6 -1
  13. package/dist/commands/billing.js +107 -3
  14. package/dist/commands/config.d.ts +1 -1
  15. package/dist/commands/config.js +16 -2
  16. package/dist/commands/conversations.js +16 -0
  17. package/dist/commands/datasets.js +17 -15
  18. package/dist/commands/experiments.js +36 -26
  19. package/dist/commands/files.js +10 -28
  20. package/dist/commands/installations.js +46 -1
  21. package/dist/commands/knowledge-documents.js +24 -32
  22. package/dist/commands/knowledge.js +5 -0
  23. package/dist/commands/memory.d.ts +10 -0
  24. package/dist/commands/memory.js +132 -0
  25. package/dist/commands/prompts.js +9 -8
  26. package/dist/commands/schemas.js +15 -6
  27. package/dist/commands/skill-versions.js +27 -1
  28. package/dist/commands/skills.js +31 -1
  29. package/dist/commands/tracing.js +12 -17
  30. package/dist/index.js +2 -0
  31. package/dist/lib/agent-decide.d.ts +29 -2
  32. package/dist/lib/agent-decide.js +100 -3
  33. package/dist/lib/agent-run.d.ts +50 -3
  34. package/dist/lib/agent-run.js +168 -16
  35. package/dist/lib/approval-prompt.js +31 -1
  36. package/dist/lib/client.d.ts +8 -0
  37. package/dist/lib/client.js +18 -1
  38. package/dist/lib/config.d.ts +16 -0
  39. package/dist/lib/config.js +26 -1
  40. package/dist/lib/connect-pause.d.ts +71 -0
  41. package/dist/lib/connect-pause.js +147 -0
  42. package/dist/lib/errors.js +3 -1
  43. package/dist/lib/overlay.d.ts +10 -0
  44. package/dist/lib/overlay.js +20 -0
  45. package/dist/lib/skills-apply-preview.d.ts +19 -0
  46. package/dist/lib/skills-apply-preview.js +94 -0
  47. package/dist/lib/tracing-settings.d.ts +26 -0
  48. package/dist/lib/tracing-settings.js +25 -0
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -88,6 +88,8 @@ kubectl-style contexts switch between organizations and environments:
88
88
  2kw context create staging # Create a new context
89
89
  ```
90
90
 
91
+ `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.
92
+
91
93
  ## Commands
92
94
 
93
95
  | Command | Description |
@@ -106,10 +108,11 @@ kubectl-style contexts switch between organizations and environments:
106
108
  | `evaluators` | Manage evaluator templates |
107
109
  | `scores` | Record and inspect quality scores |
108
110
  | `tracing` | Inspect request traces and tracing settings |
111
+ | `memory` | Your agent memory files (list, read, write, delete, forget); member totals and erasure for admins |
109
112
  | `providers` | Manage BYOK AI providers |
110
113
  | `analytics` | Usage analytics: spend, quality, providers, errors |
111
114
  | `billing` | Check subscription tier and usage limits |
112
- | `agents` | Run agents and decide approvals; apply, export and scaffold agent.yaml; manage versions, labels, approvals and tool catalogs |
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 |
113
116
  | `skills` | Import and export SKILL.md bundles; manage skill versions and labels |
114
117
  | `plugins` | Install plugin repositories, sync them, and inspect their reports |
115
118
  | `conversations` | Create and manage conversations and their items |
@@ -83,8 +83,8 @@ export declare const agentSchema: {
83
83
  readonly properties: {
84
84
  readonly name: {
85
85
  readonly type: "string";
86
- readonly pattern: "^[a-z0-9]+(-[a-z0-9]+)*$";
87
- readonly maxLength: 100;
86
+ readonly pattern: "^[a-z0-9]+(-[a-z0-9]+)*(:[a-z0-9]+(-[a-z0-9]+)*)?$";
87
+ readonly maxLength: 201;
88
88
  };
89
89
  readonly ref: {
90
90
  readonly type: "string";
@@ -106,6 +106,9 @@ export declare const agentSchema: {
106
106
  readonly destructiveHint: {
107
107
  readonly type: "boolean";
108
108
  };
109
+ readonly openWorldHint: {
110
+ readonly type: "boolean";
111
+ };
109
112
  };
110
113
  };
111
114
  readonly fileSearchTool: {
@@ -213,7 +216,7 @@ export declare const agentSchema: {
213
216
  readonly uniqueItems: true;
214
217
  readonly items: {
215
218
  readonly type: "string";
216
- readonly pattern: "^[a-z0-9]+(-[a-z0-9]+)*(@[A-Za-z0-9._-]+)?$";
219
+ readonly pattern: "^[a-z0-9]+(-[a-z0-9]+)*(:[a-z0-9]+(-[a-z0-9]+)*)?(@[A-Za-z0-9._-]+)?$";
217
220
  };
218
221
  };
219
222
  };
@@ -5,8 +5,11 @@
5
5
  * Kept as a TS constant: package.json `files` publishes only dist JS/d.ts.
6
6
  */
7
7
  export const AGENT_SCHEMA_ID = "https://docs.2kw.ai/schemas/agent.v1.json";
8
- const SKILL_NAME = "^[a-z0-9]+(-[a-z0-9]+)*$";
9
- const SKILL_REF = "^[a-z0-9]+(-[a-z0-9]+)*(@[A-Za-z0-9._-]+)?$";
8
+ // #788: a skill name is a slug, or <plugin>:<skill> for a skill a plugin import created.
9
+ // Mirrors ValidationLimits.SKILL_NAME_PATTERN; each part is at most 100 characters, 201 in all.
10
+ const SKILL_NAME = "^[a-z0-9]+(-[a-z0-9]+)*(:[a-z0-9]+(-[a-z0-9]+)*)?$";
11
+ const SKILL_REF = "^[a-z0-9]+(-[a-z0-9]+)*(:[a-z0-9]+(-[a-z0-9]+)*)?(@[A-Za-z0-9._-]+)?$";
12
+ const SKILL_NAME_MAX = 201;
10
13
  const MODEL_REF = {
11
14
  type: "string",
12
15
  minLength: 1,
@@ -18,6 +21,12 @@ const oneOfLowercase = (...values) => ({ type: "string", enum: values });
18
21
  export const NON_AUTHORABLE_TOOL_TYPES = {
19
22
  "backbone.tool_search": "the planner adds it automatically",
20
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",
21
30
  web_search: "it has no executor and is skipped",
22
31
  };
23
32
  const BUILTIN_TYPES = [
@@ -74,7 +83,7 @@ export const agentSchema = {
74
83
  additionalProperties: false,
75
84
  required: ["name"],
76
85
  properties: {
77
- name: { type: "string", pattern: SKILL_NAME, maxLength: 100 },
86
+ name: { type: "string", pattern: SKILL_NAME, maxLength: SKILL_NAME_MAX },
78
87
  ref: { type: "string", pattern: "\\S", description: "Label name or version number; default latest." },
79
88
  },
80
89
  },
@@ -84,7 +93,13 @@ export const agentSchema = {
84
93
  annotations: {
85
94
  type: "object",
86
95
  required: ["readOnlyHint", "destructiveHint"],
87
- properties: { readOnlyHint: { type: "boolean" }, destructiveHint: { type: "boolean" } },
96
+ properties: {
97
+ readOnlyHint: { type: "boolean" },
98
+ destructiveHint: { type: "boolean" },
99
+ // Optional. With destructiveHint: false it declares the tool harmless, so a write under
100
+ // `auto` may reach the approver (#938).
101
+ openWorldHint: { type: "boolean" },
102
+ },
88
103
  },
89
104
  fileSearchTool: {
90
105
  type: "object",
@@ -43,7 +43,7 @@ tools: []
43
43
  # properties:
44
44
  # order: { type: string }
45
45
  # required: [order]
46
- # annotations: { readOnlyHint: true, destructiveHint: false }
46
+ # annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
47
47
  # Approval policy. Defaults: read allow, write approve, destructive block. Rules can only tighten.
48
48
  # hitlPolicy:
49
49
  # version: 1
@@ -30,7 +30,7 @@ export function versionBody(config, changeDescription) {
30
30
  /**
31
31
  * The complete agent DTO for POST /v1/agents and PUT /v1/agents/{id}. The backend's PUT maps every DTO field
32
32
  * onto the row without skipping nulls (#624), so an absent key clears the column: `skills` (NOT NULL) must
33
- * always be sent, and a PUT must carry `latestVersionId` or the agent loses its head pointer. Nullable
33
+ * always be sent. `latestVersionId` is server-managed and ignored in a body (#996); it is still sent. Nullable
34
34
  * columns (instructions, options, hitl_policy) are omitted when the file omits them, which clears them to
35
35
  * SQL NULL exactly as createVersion does; an explicit JSON null would arrive as a NullNode instead.
36
36
  */
@@ -5,7 +5,8 @@ import { Command } from "commander";
5
5
  *
6
6
  * Answers the question an operator otherwise has to answer by running the agent
7
7
  * and reading the transcript. The version defaults to the agent's latest, which
8
- * is the one an unlabelled reference resolves to.
8
+ * is the one an unlabelled reference resolves to. `--mode` answers for a conversation in that
9
+ * mode (#1037); a contributing mode shows in the matched rules as `conversation_mode.<mode>`.
9
10
  */
10
11
  export declare function makeAgentPolicyCommand(): Command;
11
12
  //# sourceMappingURL=agent-policy.d.ts.map
@@ -4,6 +4,7 @@ import { getClient, runAction } from "../lib/client.js";
4
4
  import { isJsonOutput } from "../lib/config.js";
5
5
  import { formatDetail, formatList } from "../lib/output.js";
6
6
  import { resolveAgent } from "../lib/agent-lookup.js";
7
+ import { parseModeOption } from "../lib/agent-run.js";
7
8
  import { CliUsageError } from "../lib/errors.js";
8
9
  import { getLatestVersion } from "./agent-apply.js";
9
10
  /**
@@ -12,7 +13,8 @@ import { getLatestVersion } from "./agent-apply.js";
12
13
  *
13
14
  * Answers the question an operator otherwise has to answer by running the agent
14
15
  * and reading the transcript. The version defaults to the agent's latest, which
15
- * is the one an unlabelled reference resolves to.
16
+ * is the one an unlabelled reference resolves to. `--mode` answers for a conversation in that
17
+ * mode (#1037); a contributing mode shows in the matched rules as `conversation_mode.<mode>`.
16
18
  */
17
19
  export function makeAgentPolicyCommand() {
18
20
  return new Command("policy")
@@ -21,8 +23,11 @@ export function makeAgentPolicyCommand() {
21
23
  .option("--version <id>", "Agent version ID (default: the agent's latest version)")
22
24
  .option("--tool <name>", "Answer for one tool instead of every configured tool")
23
25
  .option("--installation <id>", "Resolve the module tool catalog of this installation")
26
+ .option("--mode <mode>", "Answer as a conversation in this mode would be gated: plan (every call that is not read-only is refused), " +
27
+ "ask (a person decides where the policy would ask the judge), auto (the policy as written)")
24
28
  .action(async (agentRef, opts, command) => {
25
29
  await runAction(command, async () => {
30
+ const mode = parseModeOption(opts.mode);
26
31
  const client = getClient(command);
27
32
  const agent = await resolveAgent(client, agentRef);
28
33
  const agentId = agent.id;
@@ -33,7 +38,7 @@ export function makeAgentPolicyCommand() {
33
38
  const { data } = await client.GET("/v1/agents/{agentId}/versions/{versionId}/policy", {
34
39
  params: {
35
40
  path: { agentId, versionId },
36
- query: { tool: opts.tool, installation: opts.installation },
41
+ query: { tool: opts.tool, installation: opts.installation, mode },
37
42
  },
38
43
  });
39
44
  if (isJsonOutput(command)) {
@@ -47,6 +52,8 @@ export function makeAgentPolicyCommand() {
47
52
  `auto-approval ${auto?.enabled ? "on" : "off (every auto pauses for a human)"}`,
48
53
  `judge ${auto?.judgeConfigured ? "configured" : "not configured (every candidate escalates)"}`,
49
54
  ];
55
+ if (mode)
56
+ parts.push(`mode ${mode}`);
50
57
  if (data?.catalogResolvedAt)
51
58
  parts.push(`catalog resolved ${data.catalogResolvedAt}`);
52
59
  console.log(chalk.dim(parts.join(", ")));
@@ -1,5 +1,10 @@
1
1
  import { Command } from "commander";
2
2
  export declare function resolveRunInput(arg: string | undefined, inputFile: string | undefined, readStdin?: () => string | undefined): string;
3
+ /**
4
+ * The task text from the argument, then the file, then stdin; `undefined` when none is given. A
5
+ * `--continue` run may have none: a connect pause is continued with an empty input (#807).
6
+ */
7
+ export declare function readRunInput(arg: string | undefined, inputFile: string | undefined, readStdin?: () => string | undefined): string | undefined;
3
8
  export declare function makeAgentRunCommand(): Command;
4
9
  export declare function makeAgentDecideCommand(): Command;
5
10
  //# sourceMappingURL=agent-run.d.ts.map
@@ -1,16 +1,27 @@
1
1
  import { Command } from "commander";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { getClient, runAction } from "../lib/client.js";
4
- import { isJsonOutput } from "../lib/config.js";
4
+ import { chatUrlFor, isJsonOutput, resolveConfig } from "../lib/config.js";
5
5
  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
- import { buildRunEnvelope, EXIT_CODES, printRunText, stripControl } from "../lib/agent-run.js";
9
- import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, planDecisions, } from "../lib/agent-decide.js";
8
+ import { buildRunEnvelope, EXIT_CODES, MODE_OPTION_HELP, modeItem, parseModeOption, printRunText, stripControl, } from "../lib/agent-run.js";
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";
13
13
  export function resolveRunInput(arg, inputFile, readStdin = readStdinInput) {
14
+ const text = readRunInput(arg, inputFile, readStdin);
15
+ if (text === undefined) {
16
+ throw new CliUsageError("Input is required: pass it as an argument, with --input-file, or via stdin.");
17
+ }
18
+ return text;
19
+ }
20
+ /**
21
+ * The task text from the argument, then the file, then stdin; `undefined` when none is given. A
22
+ * `--continue` run may have none: a connect pause is continued with an empty input (#807).
23
+ */
24
+ export function readRunInput(arg, inputFile, readStdin = readStdinInput) {
14
25
  if (arg)
15
26
  return arg;
16
27
  if (inputFile) {
@@ -29,10 +40,7 @@ export function resolveRunInput(arg, inputFile, readStdin = readStdinInput) {
29
40
  throw new CliUsageError(`Input file ${inputFile} is empty.`);
30
41
  return text;
31
42
  }
32
- const piped = readStdin();
33
- if (piped)
34
- return piped;
35
- throw new CliUsageError("Input is required: pass it as an argument, with --input-file, or via stdin.");
43
+ return readStdin() || undefined;
36
44
  }
37
45
  async function withAgentSuggestions(client, agentRef, err) {
38
46
  if (!(err instanceof BackboneApiError) || err.status !== 404 || !err.message.startsWith("Agent not found")) {
@@ -56,8 +64,9 @@ async function withAgentSuggestions(client, agentRef, err) {
56
64
  */
57
65
  async function finishRun(command, client, target, first, opts) {
58
66
  const agentRef = `${target.name}${target.label ? `@${target.label}` : ""}${target.model ? `#${target.model}` : ""}`;
67
+ const chatUrl = chatUrlFor(resolveConfig(command).baseUrl);
59
68
  let result = first;
60
- let env = buildRunEnvelope(result, agentRef);
69
+ let env = buildRunEnvelope(result, agentRef, chatUrl);
61
70
  if (opts.interactive && env.status === "requires_approval") {
62
71
  try {
63
72
  const agent = await resolveAgent(client, target.name);
@@ -90,8 +99,8 @@ async function finishRun(command, client, target, first, opts) {
90
99
  throw new CliUsageError(`No pending approvals for ${responseId} (already decided or superseded).`);
91
100
  }
92
101
  const decisions = decisionsFromAnswers(rows, answers);
93
- result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), responseId, decisions, target.label, target.model));
94
- env = buildRunEnvelope(result, agentRef);
102
+ result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), responseId, decisions, target.label, target.model, target.mode));
103
+ env = buildRunEnvelope(result, agentRef, chatUrl);
95
104
  }
96
105
  }
97
106
  catch (err) {
@@ -115,21 +124,30 @@ async function finishRun(command, client, target, first, opts) {
115
124
  }
116
125
  export function makeAgentRunCommand() {
117
126
  return new Command("run")
118
- .description("Run an agent on a task (exit 3 = paused for approval, 4 = needs client tool output, 5 = incomplete)")
127
+ .description("Run an agent on a task (exit 3 = paused for approval, 4 = needs client tool output or a connector connected in chat, 5 = incomplete)")
119
128
  .argument("<agent>", "Agent name or id, optionally with @label and #model")
120
- .argument("[input]", "Task text (or use --input-file, or pipe it on stdin)")
129
+ .argument("[input]", "Task text (or use --input-file, or pipe it on stdin); optional with --continue")
121
130
  .option("--input-file <path>", "Read the task text from a file")
122
131
  .option("--conversation <id>", "Continue a conversation")
123
- .option("--continue <responseId>", "Continue from a previous response")
132
+ .option("--continue <responseId>", "Continue from a previous response, with or without new input")
124
133
  .option("--model <model>", "Run this request on another model from the agent's list (sends <agent>#<model>)")
134
+ .option("--mode <mode>", MODE_OPTION_HELP)
125
135
  .option("--no-input", "Never prompt for approvals, even on a terminal")
126
136
  .option("--raw", "Print the untouched API response")
127
137
  .action(async (agentArg, input, opts, command) => {
128
138
  await runAction(command, async () => {
129
139
  const agentRef = withModelOverride(agentArg, opts.model);
130
- const text = resolveRunInput(input, opts.inputFile);
140
+ const mode = parseModeOption(opts.mode);
141
+ // A continuation may send no new turn (#807): the connect pause's `next` is exactly that command.
142
+ const text = opts.continue ? readRunInput(input, opts.inputFile) : resolveRunInput(input, opts.inputFile);
131
143
  const client = getClient(command);
132
- const body = { model: `agent/${agentRef}`, input: text, stream: false };
144
+ const turn = text === undefined ? [] : [{ type: "message", role: "user", content: text }];
145
+ // Without --mode the body stays exactly as before #656 (D2): input is the bare string, or [] without one.
146
+ const body = {
147
+ model: `agent/${agentRef}`,
148
+ input: mode ? [...turn, modeItem(mode)] : text ?? [],
149
+ stream: false,
150
+ };
133
151
  if (opts.conversation)
134
152
  body.conversation = opts.conversation;
135
153
  if (opts.continue)
@@ -146,16 +164,18 @@ export function makeAgentRunCommand() {
146
164
  // The prompt reads stdin and writes to stderr, so those two decide; stdout may be redirected.
147
165
  const interactive = !!process.stdin.isTTY && !!process.stderr.isTTY && !isJsonOutput(command) && opts.input !== false && !opts.raw;
148
166
  const { name, label, model } = splitAgentRef(agentRef);
149
- await finishRun(command, client, { name, label, model }, data, {
167
+ await finishRun(command, client, { name, label, model, mode }, data, {
150
168
  raw: !!opts.raw,
151
169
  interactive,
152
170
  });
153
171
  });
154
172
  });
155
173
  }
174
+ /** Repeatable `--output` / `--fail`: a variadic `<v...>` would swallow the arguments that follow it. */
175
+ const collect = (value, previous = []) => [...previous, value];
156
176
  export function makeAgentDecideCommand() {
157
177
  return new Command("decide")
158
- .description("Decide every pending approval of a paused run and continue it")
178
+ .description("Answer a paused run: decide every pending approval and answer every relayed tool call in one call, then continue it")
159
179
  .argument("<agent>", "Agent name or id, optionally with @label and #model (use the same @label and #model the run used)")
160
180
  .requiredOption("--response <responseId>", "The paused response id (responseId in the run envelope)")
161
181
  .option("--approve <ids...>", "Approval ids to approve")
@@ -164,9 +184,15 @@ export function makeAgentDecideCommand() {
164
184
  .option("--reject-all", "Reject every pending approval of the response")
165
185
  .option("--reason <text>", "Reason recorded on each decision (the model sees it on rejects)")
166
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)
189
+ .option("--mode <mode>", MODE_OPTION_HELP)
167
190
  .option("--raw", "Print the untouched API response")
168
191
  .action(async (agentRef, opts, command) => {
169
192
  await runAction(command, async () => {
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);
170
196
  const client = getClient(command);
171
197
  const { name, label, model } = splitAgentRef(agentRef);
172
198
  // Dropped silently, an empty override would continue the run on the default model.
@@ -174,16 +200,16 @@ export function makeAgentDecideCommand() {
174
200
  throw new CliUsageError(`Empty #model in '${agentRef}': use the model the run used, or drop the '#'.`);
175
201
  const agent = await resolveAgent(client, name);
176
202
  const pending = await fetchPendingApprovals(client, String(agent.id), opts.response);
177
- const decisions = planDecisions(pending, {
203
+ const decisions = planContinuation(pending, {
178
204
  approve: opts.approve,
179
205
  reject: opts.reject,
180
206
  approveAll: opts.approveAll,
181
207
  rejectAll: opts.rejectAll,
182
208
  reason: opts.reason,
183
209
  remember: opts.remember,
184
- }, opts.response);
185
- const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model));
186
- await finishRun(command, client, { name, label, model }, result, { raw: !!opts.raw, interactive: false });
210
+ }, toolAnswers, opts.response);
211
+ const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model, mode, toolAnswers));
212
+ await finishRun(command, client, { name, label, model, mode }, result, { raw: !!opts.raw, interactive: false });
187
213
  });
188
214
  });
189
215
  }
@@ -11,6 +11,7 @@ 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
13
  import { collectModel, MODEL_HELP, MODELS_HELP, modelFieldsFromOptions } from "../lib/agent-models.js";
14
+ import { assertNameNotBlank } from "../lib/overlay.js";
14
15
  export function makeAgentsCommand() {
15
16
  const cmd = new Command("agents").description("Manage agents");
16
17
  const list = new Command("list").description("List agents");
@@ -101,13 +102,25 @@ export function makeAgentsCommand() {
101
102
  .option("--hitl-policy <json>", "New human-in-the-loop policy as JSON")
102
103
  .action(async (id, opts, command) => {
103
104
  await runAction(command, async () => {
105
+ let name = opts.name;
106
+ let description = opts.description;
107
+ assertNameNotBlank(name);
104
108
  const client = getClient(command);
105
- const body = {};
106
- if (opts.name)
107
- body.name = opts.name;
109
+ // PUT /v1/agents/{id} keeps the configuration fields a body omits, but name is required and
110
+ // description is written as sent, so an omitted description would be cleared (#1195).
111
+ if (name === undefined || description === undefined) {
112
+ const { data: current } = await client.GET("/v1/agents/{id}", {
113
+ params: { path: { id } },
114
+ });
115
+ if (!current)
116
+ throw new Error(`Agent ${id} could not be read.`);
117
+ name ??= current.name;
118
+ description ??= current.description;
119
+ }
120
+ const body = { name };
108
121
  Object.assign(body, modelFieldsFromOptions(opts, false));
109
- if (opts.description !== undefined)
110
- body.description = opts.description;
122
+ if (description !== undefined)
123
+ body.description = description;
111
124
  if (opts.instructions !== undefined)
112
125
  body.instructions = opts.instructions;
113
126
  if (opts.options)
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { getClient, runAction } from "../lib/client.js";
4
- import { isJsonOutput } from "../lib/config.js";
4
+ import { chatUrlFor, isJsonOutput, resolveConfig } from "../lib/config.js";
5
5
  import { CliUsageError } from "../lib/errors.js";
6
6
  import { withModelOverride } from "../lib/agent-models.js";
7
7
  import { formatList, withSpinner } from "../lib/output.js";
@@ -46,7 +46,7 @@ function printResponseResult(result, command, agentRef) {
46
46
  console.log(JSON.stringify(result, null, 2));
47
47
  return;
48
48
  }
49
- printRunText(buildRunEnvelope(result, agentRef));
49
+ printRunText(buildRunEnvelope(result, agentRef, chatUrlFor(resolveConfig(command).baseUrl)));
50
50
  if (result?.usage) {
51
51
  const usage = result.usage;
52
52
  console.log(chalk.dim(`[${result.model} | ${usage.input_tokens} in / ${usage.output_tokens} out / ${usage.total_tokens} total tokens]`));
@@ -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 server yet, #148)")
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 server yet (#148). Run without --stream.");
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();
@@ -125,13 +125,7 @@ export function makeAiCommand() {
125
125
  if (opts.conversation)
126
126
  body.conversation = opts.conversation;
127
127
  const client = getClient(command);
128
- const { data } = await withSpinner("Generating response...", () =>
129
- // The wire `input` field also accepts a bare string (shorthand for
130
- // a single user message; see ResponseItemInputDeserializer), which
131
- // the generated type does not model — hence the cast, same as the
132
- // untyped convert.ts source-conversion calls.
133
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
- client.POST("/v1/responses", { body }));
128
+ const { data } = await withSpinner("Generating response...", () => client.POST("/v1/responses", { body }));
135
129
  printResponseResult(data, command, opts.agent);
136
130
  });
137
131
  });
@@ -160,7 +160,12 @@ async function manualLogin(opts) {
160
160
  console.error(chalk.red("API key is required."));
161
161
  process.exit(1);
162
162
  }
163
- setContext(contextName, { apiKey, baseUrl });
163
+ // Carry the memory opt-in over: re-entering a key should not silently turn it off (#721).
164
+ setContext(contextName, {
165
+ apiKey,
166
+ baseUrl,
167
+ ...(currentCtx?.memory !== undefined ? { memory: currentCtx.memory } : {}),
168
+ });
164
169
  setActiveContext(contextName);
165
170
  console.log(chalk.green("Credentials saved successfully."));
166
171
  console.log(chalk.dim(`Config stored at: ${store.path}`));
@@ -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() {
@@ -49,10 +50,12 @@ export function makeBillingCommand() {
49
50
  }
50
51
  console.log();
51
52
  }
53
+ // A null maximum is "unlimited" (schemas and prompts in every tier since #971), not 0.
54
+ const max = (value) => (value == null ? "unlimited" : String(value));
52
55
  console.log(chalk.bold("Usage:"));
53
- console.log(` Schemas: ${data?.currentSchemas ?? 0} / ${data?.maxSchemas ?? 0}`);
54
- console.log(` Prompts: ${data?.currentPrompts ?? 0} / ${data?.maxPrompts ?? 0}`);
55
- console.log(` Team members: ${data?.currentTeamMembers ?? 0} / ${data?.maxTeamMembers ?? 0}`);
56
+ console.log(` Schemas: ${data?.currentSchemas ?? 0} / ${max(data?.maxSchemas)}`);
57
+ console.log(` Prompts: ${data?.currentPrompts ?? 0} / ${max(data?.maxPrompts)}`);
58
+ console.log(` Team members: ${data?.currentTeamMembers ?? 0} / ${max(data?.maxTeamMembers)}`);
56
59
  console.log();
57
60
  console.log(chalk.bold("Features:"));
58
61
  console.log(` BYOK: ${data?.byokEnabled ? chalk.green("yes") : chalk.dim("no")}`);
@@ -131,6 +134,107 @@ export function makeBillingCommand() {
131
134
  }
132
135
  });
133
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
+ });
134
238
  return cmd;
135
239
  }
136
240
  //# sourceMappingURL=billing.js.map
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- declare const ALLOWED_KEYS: readonly ["apiKey", "baseUrl"];
2
+ declare const ALLOWED_KEYS: readonly ["apiKey", "baseUrl", "memory"];
3
3
  type ConfigKey = (typeof ALLOWED_KEYS)[number];
4
4
  /**
5
5
  * Write one config key onto the active context.
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import { store, isJsonOutput, getActiveContext, getActiveContextName, getContextCount, setContext, DEFAULT_BASE_URL, } from "../lib/config.js";
4
4
  import { maskApiKey } from "../lib/redact.js";
5
- const ALLOWED_KEYS = ["apiKey", "baseUrl"];
5
+ const ALLOWED_KEYS = ["apiKey", "baseUrl", "memory"];
6
6
  const KEY_ALIASES = {
7
7
  "api-key": "apiKey",
8
8
  "base-url": "baseUrl",
@@ -43,6 +43,14 @@ export function applyConfigSet(key, value) {
43
43
  setContext(contextName, entry);
44
44
  return { clearedSession };
45
45
  }
46
+ if (key === "memory") {
47
+ if (value !== "true" && value !== "false") {
48
+ throw new Error('memory takes true or false');
49
+ }
50
+ entry.memory = value === "true";
51
+ setContext(contextName, entry);
52
+ return { clearedSession: false };
53
+ }
46
54
  entry[key] = value;
47
55
  setContext(contextName, entry);
48
56
  return { clearedSession: false };
@@ -82,7 +90,12 @@ export function makeConfigCommand() {
82
90
  const validKey = validateKey(key);
83
91
  const ctx = getActiveContext();
84
92
  const raw = ctx?.[validKey];
85
- const value = raw && validKey === "apiKey" ? maskApiKey(raw) : raw;
93
+ // A stored "" apiKey stays "" (shown as "(not set)"); only a real key is masked.
94
+ const value = raw === undefined
95
+ ? undefined
96
+ : validKey === "apiKey" && typeof raw === "string"
97
+ ? raw && maskApiKey(raw)
98
+ : String(raw);
86
99
  if (isJsonOutput(command)) {
87
100
  console.log(JSON.stringify({ key: validKey, value: value ?? null }));
88
101
  }
@@ -103,6 +116,7 @@ export function makeConfigCommand() {
103
116
  const values = {
104
117
  apiKey: ctx?.apiKey ? maskApiKey(ctx.apiKey) : undefined,
105
118
  baseUrl: ctx?.baseUrl,
119
+ memory: ctx?.memory === undefined ? undefined : String(ctx.memory),
106
120
  };
107
121
  if (isJsonOutput(command)) {
108
122
  console.log(JSON.stringify(values, null, 2));