@2kw/ai 6.3.0-dev.8 → 6.3.0-dev.85

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 (44) hide show
  1. package/README.md +3 -0
  2. package/dist/agent-config/schema.d.ts +3 -0
  3. package/dist/agent-config/schema.js +7 -1
  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 +37 -17
  10. package/dist/commands/agents.js +15 -1
  11. package/dist/commands/ai.js +2 -2
  12. package/dist/commands/auth.js +6 -1
  13. package/dist/commands/billing.js +5 -3
  14. package/dist/commands/config.d.ts +1 -1
  15. package/dist/commands/config.js +16 -2
  16. package/dist/commands/connectors.js +2 -1
  17. package/dist/commands/conversations.js +16 -0
  18. package/dist/commands/experiments.js +2 -2
  19. package/dist/commands/files.js +10 -28
  20. package/dist/commands/knowledge-documents.js +12 -32
  21. package/dist/commands/memory.d.ts +10 -0
  22. package/dist/commands/memory.js +132 -0
  23. package/dist/commands/settings.d.ts +13 -0
  24. package/dist/commands/settings.js +80 -0
  25. package/dist/commands/skill-versions.js +27 -1
  26. package/dist/commands/skills.js +29 -0
  27. package/dist/commands/tracing.js +5 -12
  28. package/dist/index.js +4 -0
  29. package/dist/lib/agent-decide.d.ts +3 -2
  30. package/dist/lib/agent-decide.js +5 -2
  31. package/dist/lib/agent-run.d.ts +49 -3
  32. package/dist/lib/agent-run.js +152 -9
  33. package/dist/lib/approval-prompt.js +31 -1
  34. package/dist/lib/client.d.ts +8 -0
  35. package/dist/lib/client.js +20 -1
  36. package/dist/lib/config.d.ts +16 -0
  37. package/dist/lib/config.js +39 -0
  38. package/dist/lib/errors.d.ts +6 -0
  39. package/dist/lib/errors.js +5 -2
  40. package/dist/lib/skills-apply-preview.d.ts +19 -0
  41. package/dist/lib/skills-apply-preview.js +94 -0
  42. package/dist/lib/tracing-settings.d.ts +26 -0
  43. package/dist/lib/tracing-settings.js +25 -0
  44. 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,6 +108,7 @@ 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 |
@@ -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: {
@@ -84,7 +84,13 @@ export const agentSchema = {
84
84
  annotations: {
85
85
  type: "object",
86
86
  required: ["readOnlyHint", "destructiveHint"],
87
- properties: { readOnlyHint: { type: "boolean" }, destructiveHint: { type: "boolean" } },
87
+ properties: {
88
+ readOnlyHint: { type: "boolean" },
89
+ destructiveHint: { type: "boolean" },
90
+ // Optional. With destructiveHint: false it declares the tool harmless, so a write under
91
+ // `auto` may reach the approver (#938).
92
+ openWorldHint: { type: "boolean" },
93
+ },
88
94
  },
89
95
  fileSearchTool: {
90
96
  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";
8
+ import { buildRunEnvelope, EXIT_CODES, MODE_OPTION_HELP, modeItem, parseModeOption, printRunText, stripControl, } from "../lib/agent-run.js";
9
9
  import { continueWithDecisions, decisionsFromAnswers, fetchPendingApprovals, planDecisions, } 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,7 +164,7 @@ 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
  });
@@ -164,9 +182,11 @@ export function makeAgentDecideCommand() {
164
182
  .option("--reject-all", "Reject every pending approval of the response")
165
183
  .option("--reason <text>", "Reason recorded on each decision (the model sees it on rejects)")
166
184
  .option("--remember", "Also approve later calls of the same tool in this conversation")
185
+ .option("--mode <mode>", MODE_OPTION_HELP)
167
186
  .option("--raw", "Print the untouched API response")
168
187
  .action(async (agentRef, opts, command) => {
169
188
  await runAction(command, async () => {
189
+ const mode = parseModeOption(opts.mode);
170
190
  const client = getClient(command);
171
191
  const { name, label, model } = splitAgentRef(agentRef);
172
192
  // Dropped silently, an empty override would continue the run on the default model.
@@ -182,8 +202,8 @@ export function makeAgentDecideCommand() {
182
202
  reason: opts.reason,
183
203
  remember: opts.remember,
184
204
  }, 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 });
205
+ const result = await withSpinner("Continuing...", () => continueWithDecisions(client, String(agent.id), opts.response, decisions, label, model, mode));
206
+ await finishRun(command, client, { name, label, model, mode }, result, { raw: !!opts.raw, interactive: false });
187
207
  });
188
208
  });
189
209
  }
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { getClient, runAction } from "../lib/client.js";
3
- import { formatPage, formatDetail, formatSuccess } from "../lib/output.js";
3
+ import { formatPage, formatDetail, formatList, formatSuccess } from "../lib/output.js";
4
4
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
5
5
  import { makeAgentVersionsCommand } from "./agent-versions.js";
6
6
  import { makeAgentLabelsCommand } from "./agent-labels.js";
@@ -172,6 +172,20 @@ export function makeAgentsCommand() {
172
172
  });
173
173
  });
174
174
  cmd.addCommand(toolCatalogs);
175
+ // The skills the latest published version binds (#827) -- readable by a chat-only USER key,
176
+ // which cannot read the version itself. No published version answers [] rather than 404.
177
+ const skills = new Command("skills").description("List the skills an agent's latest published version binds");
178
+ skills.requiredOption("--agent <id>", "Agent ID");
179
+ skills.action(async (opts, command) => {
180
+ await runAction(command, async () => {
181
+ const client = getClient(command);
182
+ const { data } = await client.GET("/v1/agents/{agentId}/skills", {
183
+ params: { path: { agentId: opts.agent } },
184
+ });
185
+ formatList(data, command, ["name", "description", "versionNumber", "ref", "pluginName"]);
186
+ });
187
+ });
188
+ cmd.addCommand(skills);
175
189
  cmd.addCommand(makeAgentRunCommand());
176
190
  cmd.addCommand(makeAgentDecideCommand());
177
191
  cmd.addCommand(makeAgentApplyCommand());
@@ -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]`));
@@ -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}`));
@@ -49,10 +49,12 @@ export function makeBillingCommand() {
49
49
  }
50
50
  console.log();
51
51
  }
52
+ // A null maximum is "unlimited" (schemas and prompts in every tier since #971), not 0.
53
+ const max = (value) => (value == null ? "unlimited" : String(value));
52
54
  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}`);
55
+ console.log(` Schemas: ${data?.currentSchemas ?? 0} / ${max(data?.maxSchemas)}`);
56
+ console.log(` Prompts: ${data?.currentPrompts ?? 0} / ${max(data?.maxPrompts)}`);
57
+ console.log(` Team members: ${data?.currentTeamMembers ?? 0} / ${max(data?.maxTeamMembers)}`);
56
58
  console.log();
57
59
  console.log(chalk.bold("Features:"));
58
60
  console.log(` BYOK: ${data?.byokEnabled ? chalk.green("yes") : chalk.dim("no")}`);
@@ -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));
@@ -28,7 +28,8 @@ function parsePort(value) {
28
28
  if (value === undefined) {
29
29
  return undefined;
30
30
  }
31
- const port = Number(value);
31
+ // Digits only: `Number` alone accepts " 8443 ", "0x20FB" and "8.443e3".
32
+ const port = /^\d{1,5}$/.test(value) ? Number(value) : NaN;
32
33
  if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
33
34
  throw new CliUsageError(`--port must be a whole number between ${MIN_PORT} and ${MAX_PORT}, not '${value}'`);
34
35
  }
@@ -98,6 +98,22 @@ export function makeConversationsCommand() {
98
98
  formatSuccess(`Conversation ${conversationId} deleted.`, command);
99
99
  });
100
100
  });
101
+ cmd
102
+ .command("cancel")
103
+ .description("Ask a running turn of a conversation to stop")
104
+ .argument("<conversationId>", "Conversation ID")
105
+ .requiredOption("--turn <turnId>", "The turn id the client sent as the Backbone-Turn-Id header")
106
+ .action(async (conversationId, opts, command) => {
107
+ await runAction(command, async () => {
108
+ const client = getClient(command);
109
+ await client.POST("/v1/conversations/{conversationId}/cancel", {
110
+ params: { path: { conversationId } },
111
+ body: { turn_id: opts.turn },
112
+ });
113
+ // Accepted, not performed: the turn's own response says whether it stopped.
114
+ formatSuccess(`Stop requested for turn ${opts.turn} of conversation ${conversationId}.`, command);
115
+ });
116
+ });
101
117
  const items = new Command("items").description("List a conversation's items");
102
118
  items
103
119
  .command("list")
@@ -131,7 +131,7 @@ export function makeExperimentsCommand() {
131
131
  .description("Add a variant to an experiment")
132
132
  .requiredOption("--experiment <id>", "Experiment ID")
133
133
  .requiredOption("-n, --name <name>", "Variant name")
134
- .requiredOption("--task-type <type>", "Task type")
134
+ .requiredOption("--task-type <type>", "Task type: extraction or agent")
135
135
  .requiredOption("--config <json>", "Variant configuration (JSON)")
136
136
  .option("-d, --description <text>", "Variant description")
137
137
  .option("--sort-order <n>", "Sort order", parseInt)
@@ -179,7 +179,7 @@ export function makeExperimentsCommand() {
179
179
  .argument("<variantId>", "Variant ID")
180
180
  .requiredOption("--experiment <id>", "Experiment ID")
181
181
  .option("-n, --name <name>", "Variant name")
182
- .option("--task-type <type>", "Task type")
182
+ .option("--task-type <type>", "Task type; must equal the variant's current one (it cannot change)")
183
183
  .option("--config <json>", "Variant configuration (JSON)")
184
184
  .option("-d, --description <text>", "Variant description")
185
185
  .option("--sort-order <n>", "Sort order", parseInt)
@@ -8,40 +8,22 @@ import { formatPage, formatDetail, formatSuccess, withSpinner } from "../lib/out
8
8
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
9
9
  import { fileToBlob } from "../lib/multipart.js";
10
10
  /**
11
- * Upload a single file with purpose=agent_input|knowledge. Bypasses the typed
12
- * client the same way convert.ts and transcribe.ts do: the generated types
13
- * describe the multipart body as an opaque `file: string`, which is not
14
- * something FormData can produce a match for.
11
+ * Upload a single file with purpose=agent_input|knowledge. The generated type
12
+ * describes the "file" part as a binary string, which FormData cannot match,
13
+ * so the placeholder body satisfies the type and bodySerializer sends the
14
+ * real part.
15
15
  */
16
16
  async function uploadFile(command, filePath, purpose) {
17
- const config = resolveConfig(command);
18
- const authHeader = await resolveAuthHeader(config);
19
17
  const { blob, filename } = fileToBlob(filePath);
20
18
  const formData = new FormData();
21
19
  formData.append("file", blob, filename);
22
- const baseUrl = config.baseUrl.replace(/\/+$/, "");
23
- const url = `${baseUrl}/v1/files?purpose=${encodeURIComponent(purpose)}`;
24
- const res = await fetch(url, {
25
- method: "POST",
26
- headers: { Authorization: authHeader },
27
- body: formData,
20
+ const client = getClient(command);
21
+ const { data } = await client.POST("/v1/files", {
22
+ params: { query: { purpose } },
23
+ body: { file: "" },
24
+ bodySerializer: () => formData,
28
25
  });
29
- if (!res.ok) {
30
- let body;
31
- try {
32
- body = await res.json();
33
- }
34
- catch {
35
- body = {
36
- error: res.statusText,
37
- message: `HTTP ${res.status}: ${res.statusText}`,
38
- status: res.status,
39
- timestamp: new Date().toISOString(),
40
- };
41
- }
42
- throw new BackboneApiError(body);
43
- }
44
- return (await res.json());
26
+ return data;
45
27
  }
46
28
  /** Stream a file's bytes from /v1/files/{fileId}/content via a raw fetch. */
47
29
  async function downloadFile(command, fileId) {
@@ -1,49 +1,29 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
- import { getClient, resolveAuthHeader, runAction } from "../lib/client.js";
4
- import { resolveConfig, isJsonOutput } from "../lib/config.js";
5
- import { BackboneApiError } from "../lib/errors.js";
3
+ import { getClient, runAction } from "../lib/client.js";
4
+ import { isJsonOutput } from "../lib/config.js";
6
5
  import { formatPage, formatDetail, formatSuccess, withSpinner } from "../lib/output.js";
7
6
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
8
7
  import { fileToBlob } from "../lib/multipart.js";
9
8
  /**
10
- * Upload one or more local files to a knowledge base. Bypasses the typed
11
- * client the same way convert.ts's multipartConvert does: springdoc mis-shapes
12
- * this endpoint's `files` parameter as a query array of binary strings, but the
13
- * controller (`KnowledgeDocumentController.upload`) actually reads it off a
14
- * multipart/form-data body with repeated "files" parts.
9
+ * Upload one or more local files to a knowledge base as repeated "files"
10
+ * parts of a multipart/form-data body. The generated type describes each part
11
+ * as a binary string, which FormData cannot match, so the placeholder body
12
+ * satisfies the type and bodySerializer sends the real parts.
15
13
  */
16
14
  async function uploadDocuments(command, knowledgeBaseId, paths) {
17
- const config = resolveConfig(command);
18
- const authHeader = await resolveAuthHeader(config);
19
15
  const formData = new FormData();
20
16
  for (const p of paths) {
21
17
  const { blob, filename } = fileToBlob(p);
22
18
  formData.append("files", blob, filename);
23
19
  }
24
- const baseUrl = config.baseUrl.replace(/\/+$/, "");
25
- const url = `${baseUrl}/v1/knowledge-bases/${encodeURIComponent(knowledgeBaseId)}/documents`;
26
- const res = await fetch(url, {
27
- method: "POST",
28
- headers: { Authorization: authHeader },
29
- body: formData,
20
+ const client = getClient(command);
21
+ const { data } = await client.POST("/v1/knowledge-bases/{knowledgeBaseId}/documents", {
22
+ params: { path: { knowledgeBaseId } },
23
+ body: { files: [] },
24
+ bodySerializer: () => formData,
30
25
  });
31
- if (!res.ok) {
32
- let body;
33
- try {
34
- body = await res.json();
35
- }
36
- catch {
37
- body = {
38
- error: res.statusText,
39
- message: `HTTP ${res.status}: ${res.statusText}`,
40
- status: res.status,
41
- timestamp: new Date().toISOString(),
42
- };
43
- }
44
- throw new BackboneApiError(body);
45
- }
46
- return (await res.json());
26
+ return data ?? [];
47
27
  }
48
28
  function printUploadResults(results, command) {
49
29
  if (isJsonOutput(command)) {
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ /** The If-Match value for a version a GET returned: the quoted ETag the API expects. */
3
+ export declare function ifMatchHeader(version: string): string;
4
+ /**
5
+ * Agent memory (agent memory spec §7.1, §7.4). `ls`, `cat`, `put`, `rm` and `forget` act on
6
+ * the caller's own memory; `members` and `erase` are for ADMIN and OWNER and never show
7
+ * paths or content. Content is lossless: `cat` prints it raw, `put` sends the file as is.
8
+ */
9
+ export declare function makeMemoryCommand(): Command;
10
+ //# sourceMappingURL=memory.d.ts.map