@2kw/ai 7.4.0-dev.9 → 8.0.0-dev.11

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.
@@ -571,7 +571,7 @@ export declare const agentSchema: {
571
571
  };
572
572
  readonly additionalProperties: {
573
573
  readonly type: "string";
574
- readonly enum: readonly ["read", "write", "destructive", "approve", "auto", "deny"];
574
+ readonly enum: readonly ["read", "write", "destructive", "approve", "deny"];
575
575
  };
576
576
  };
577
577
  };
@@ -105,8 +105,8 @@ export const agentSchema = {
105
105
  properties: {
106
106
  readOnlyHint: { type: "boolean" },
107
107
  destructiveHint: { type: "boolean" },
108
- // Optional. With destructiveHint: false it declares the tool harmless, so a write under
109
- // `auto` may reach the approver (#938).
108
+ // Optional. With destructiveHint: false it declares the tool harmless, so a write that needs
109
+ // approval may be decided by the automatic approver in the Auto mode (#938, #1645).
110
110
  openWorldHint: { type: "boolean" },
111
111
  },
112
112
  },
@@ -290,17 +290,17 @@ export const agentSchema = {
290
290
  type: "object",
291
291
  additionalProperties: false,
292
292
  properties: {
293
- read: oneOfLowercase("allow", "auto", "approve", "block", "deny"),
294
- write: oneOfLowercase("auto", "approve", "block", "deny"),
295
- destructive: oneOfLowercase("block", "deny"),
293
+ read: oneOfLowercase("allow", "approve", "block", "deny"),
294
+ write: oneOfLowercase("approve", "block", "deny"),
295
+ destructive: oneOfLowercase("approve", "block", "deny"),
296
296
  },
297
297
  },
298
298
  tools: {
299
- // Value is a policy class or an action. Actions (approve, auto, deny) are exact lowercase;
299
+ // Value is a policy class or an action. Actions (approve, deny) are exact lowercase;
300
300
  // classes (read, write, destructive) are case-insensitive and lowercased by the validator.
301
301
  type: "object",
302
302
  propertyNames: { pattern: "\\S" },
303
- additionalProperties: { type: "string", enum: ["read", "write", "destructive", "approve", "auto", "deny"] },
303
+ additionalProperties: { type: "string", enum: ["read", "write", "destructive", "approve", "deny"] },
304
304
  },
305
305
  },
306
306
  },
@@ -24,7 +24,8 @@ export function makeAgentPolicyCommand() {
24
24
  .option("--tool <name>", "Answer for one tool instead of every configured tool")
25
25
  .option("--installation <id>", "Resolve the module tool catalog of this installation")
26
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)")
27
+ "ask (a person decides every call that needs approval), auto (the automatic approver decides reads and " +
28
+ "writes the server marks safe). Without it, the agent's default mode")
28
29
  .action(async (agentRef, opts, command) => {
29
30
  await runAction(command, async () => {
30
31
  const mode = parseModeOption(opts.mode);
@@ -49,11 +50,14 @@ export function makeAgentPolicyCommand() {
49
50
  // model, so they belong above the table rather than in a footnote.
50
51
  const auto = data?.autoApproval;
51
52
  const parts = [
52
- `auto-approval ${auto?.enabled ? "on" : "off (every auto pauses for a human)"}`,
53
- `judge ${auto?.judgeConfigured ? "configured" : "not configured (every candidate escalates)"}`,
53
+ `auto-approval ${auto?.enabled ? "on" : "off (every call the approver would decide pauses for a human)"}`,
54
+ `approver ${auto?.judgeConfigured ? "configured" : "not configured (every candidate escalates)"}`,
54
55
  ];
55
- if (mode)
56
- parts.push(`mode ${mode}`);
56
+ if (auto?.instructionsExceedJudgeBound) {
57
+ parts.push("instructions longer than the approver reads (every candidate escalates)");
58
+ }
59
+ if (data?.effectiveMode)
60
+ parts.push(`mode ${data.effectiveMode} (${data.effectiveModeSource})`);
57
61
  if (data?.catalogResolvedAt)
58
62
  parts.push(`catalog resolved ${data.catalogResolvedAt}`);
59
63
  console.log(chalk.dim(parts.join(", ")));
@@ -97,7 +97,6 @@ export function makeDatasetVersionsCommand() {
97
97
  const versionId = opts.versionId ?? (await resolveLatestVersionId(client, opts.dataset));
98
98
  await client.DELETE("/v1/datasets/{id}/versions/{versionId}/items/{itemId}", {
99
99
  params: {
100
- // OpenAPI spec omits `id` in declared path params though `{id}` is in the URL template.
101
100
  path: { id: opts.dataset, versionId, itemId },
102
101
  },
103
102
  });
@@ -120,7 +119,6 @@ export function makeDatasetVersionsCommand() {
120
119
  body.expectedOutput = JSON.parse(opts.expectedOutput);
121
120
  const { data } = await client.PATCH("/v1/datasets/{id}/versions/{versionId}/items/{itemId}/expected-output", {
122
121
  params: {
123
- // OpenAPI spec omits `id` in declared path params though `{id}` is in the URL template.
124
122
  path: { id: opts.dataset, versionId, itemId },
125
123
  },
126
124
  body: body,
@@ -138,7 +138,6 @@ export function makeDatasetsCommand() {
138
138
  body.metadata = JSON.parse(opts.metadata);
139
139
  const { data } = await client.POST("/v1/datasets/{id}/versions/{versionId}/items", {
140
140
  params: {
141
- // OpenAPI spec omits `id` in declared path params though `{id}` is in the URL template.
142
141
  path: { id: opts.dataset, versionId },
143
142
  },
144
143
  body: body,
@@ -158,7 +157,6 @@ export function makeDatasetsCommand() {
158
157
  const items = JSON.parse(opts.items);
159
158
  const { data } = await client.POST("/v1/datasets/{id}/versions/{versionId}/items/bulk", {
160
159
  params: {
161
- // OpenAPI spec omits `id` in declared path params though `{id}` is in the URL template.
162
160
  path: { id: opts.dataset, versionId },
163
161
  },
164
162
  body: items,
@@ -335,7 +335,7 @@ export function makeExperimentsCommand() {
335
335
  },
336
336
  });
337
337
  formatPage(data, command, [
338
- "datasetItemId", "durationMs", "inputTokens", "outputTokens", "estimatedCost", "error",
338
+ "datasetItemId", "durationMs", "inputTokens", "outputTokens", "error",
339
339
  ]);
340
340
  });
341
341
  });
@@ -418,7 +418,7 @@ export function makeExperimentsCommand() {
418
418
  // Only when something was skipped, so a clean run keeps its narrow table (#693).
419
419
  const showSkipped = aggregates.some((agg) => skippedOf(agg) !== "");
420
420
  const aggTable = new Table({
421
- head: ["Variant", "Items", "Avg Duration", "Avg Tokens", "Total Cost", ...(showSkipped ? ["Skipped"] : [])].map((h) => chalk.cyan(h)),
421
+ head: ["Variant", "Items", "Avg Duration", "Avg Tokens", ...(showSkipped ? ["Skipped"] : [])].map((h) => chalk.cyan(h)),
422
422
  wordWrap: true,
423
423
  });
424
424
  for (const agg of aggregates) {
@@ -427,7 +427,6 @@ export function makeExperimentsCommand() {
427
427
  agg.itemCount ?? "—",
428
428
  agg.avgDurationMs != null ? `${Math.round(agg.avgDurationMs)}ms` : "—",
429
429
  agg.avgTokens != null ? Math.round(agg.avgTokens) : "—",
430
- agg.totalCost != null ? `$${agg.totalCost}` : "—",
431
430
  ...(showSkipped ? [skippedOf(agg) || "—"] : []),
432
431
  ]);
433
432
  }
@@ -2,10 +2,9 @@ import { Command } from "commander";
2
2
  import { writeFileSync } from "node:fs";
3
3
  import chalk from "chalk";
4
4
  import { getClient, resolveAuthHeader, runAction } from "../lib/client.js";
5
- import { resolveConfig } from "../lib/config.js";
5
+ import { isJsonOutput, resolveConfig } from "../lib/config.js";
6
6
  import { BackboneApiError } from "../lib/errors.js";
7
- import { formatPage, formatDetail, formatSuccess, withSpinner } from "../lib/output.js";
8
- import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
7
+ import { formatDetail, formatList, formatSuccess, withSpinner } from "../lib/output.js";
9
8
  import { fileToBlob } from "../lib/multipart.js";
10
9
  /**
11
10
  * Upload a single file with purpose=agent_input|knowledge. Both are multipart
@@ -59,15 +58,31 @@ async function downloadFile(command, fileId) {
59
58
  export function makeFilesCommand() {
60
59
  const cmd = new Command("files").description("Manage uploaded files");
61
60
  const list = new Command("list").description("List files");
62
- addPaginationOptions(list);
63
61
  list.option("--purpose <purpose>", "Filter by purpose (agent_input, knowledge)");
62
+ list.option("--limit <number>", "How many files to list (1-100)", "20");
63
+ list.option("--after <fileId>", "List the files after this one (the previous list's last id)");
64
+ list.option("--order <order>", "asc (oldest first) or desc (newest first)", "desc");
64
65
  list.action(async (opts, command) => {
65
66
  await runAction(command, async () => {
66
67
  const client = getClient(command);
67
68
  const { data } = await client.GET("/v1/files", {
68
- params: { query: { purpose: opts.purpose, ...paginationParams(opts) } },
69
+ params: {
70
+ query: {
71
+ purpose: opts.purpose,
72
+ limit: parseInt(opts.limit, 10),
73
+ after: opts.after,
74
+ order: opts.order,
75
+ },
76
+ },
69
77
  });
70
- formatPage(data, command, ["id", "filename", "purpose", "bytes", "created_at"]);
78
+ if (isJsonOutput(command)) {
79
+ console.log(JSON.stringify(data, null, 2));
80
+ return;
81
+ }
82
+ formatList(data?.data, command, ["id", "filename", "purpose", "bytes", "created_at"]);
83
+ if (data?.has_more) {
84
+ console.log(chalk.dim(`More files: --after ${data.last_id}`));
85
+ }
71
86
  });
72
87
  });
73
88
  cmd.addCommand(list);
@@ -86,10 +86,23 @@ export function makeKnowledgeCommand() {
86
86
  .option("--reranker-provider <id>", "New reranker provider ID")
87
87
  .option("--hybrid-search <bool>", "Enable hybrid search (true/false)")
88
88
  .option("--text-search-language <name>", "Change the text-search language (reindexes in the background); omit to leave it untouched")
89
+ .option("--cancel-text-search-reindex", "Abandon the text-search reindex in progress and keep the active language")
89
90
  .action(async (id, opts, command) => {
90
91
  await runAction(command, async () => {
91
92
  const client = getClient(command);
92
- const body = {};
93
+ // The update takes the full body: name, slug, provider, model and dimension are required on
94
+ // every PATCH, so start from the stored values and overlay the flags. Optional fields left
95
+ // out keep their stored values server-side.
96
+ const { data: current } = await client.GET("/v1/knowledge-bases/{id}", {
97
+ params: { path: { id } },
98
+ });
99
+ const body = {
100
+ name: current?.name,
101
+ slug: current?.slug,
102
+ embeddingProviderId: current?.embeddingProviderId,
103
+ embeddingModel: current?.embeddingModel,
104
+ embeddingDim: current?.embeddingDim,
105
+ };
93
106
  if (opts.name)
94
107
  body.name = opts.name;
95
108
  if (opts.description !== undefined)
@@ -112,6 +125,8 @@ export function makeKnowledgeCommand() {
112
125
  body.hybridSearchEnabled = opts.hybridSearch === "true";
113
126
  if (opts.textSearchLanguage)
114
127
  body.textSearchLanguage = opts.textSearchLanguage;
128
+ if (opts.cancelTextSearchReindex)
129
+ body.cancelTextSearchReindex = true;
115
130
  const { data } = await client.PATCH("/v1/knowledge-bases/{id}", {
116
131
  params: { path: { id } },
117
132
  body: body,
@@ -138,7 +138,7 @@ export function makeSkillsCommand() {
138
138
  const bundle = packSkillDirectory(path);
139
139
  if (bundle.skipped.length > 0) {
140
140
  // The server reports what it was sent; only the CLI can name what never left the disk.
141
- console.error(`Left out of the bundle (the importer takes SKILL.md, references/, assets/, scripts/): ${bundle.skipped.join(", ")}`);
141
+ console.error(`Left out of the bundle (the importer takes SKILL.md, references/, assets/, scripts/, examples/): ${bundle.skipped.join(", ")}`);
142
142
  }
143
143
  blob = new Blob([bundle.bytes], { type: "application/zip" });
144
144
  filename = bundle.filename;
@@ -171,6 +171,7 @@ export function makeSkillsCommand() {
171
171
  .option("-o, --output <file>", "Output path (default: <skill-name>-v<n>.zip)")
172
172
  .option("-d, --extract-to <dir>", "Unpack the bundle into this directory instead of writing a zip")
173
173
  .option("--force", "Write into a non-empty --extract-to directory")
174
+ .option("--include-examples", "Keep the confidential examples/ files (needs ADMIN or OWNER)")
174
175
  .action(async (id, versionNumber, opts, command) => {
175
176
  await runAction(command, async () => {
176
177
  if (opts.output && opts.extractTo)
@@ -189,14 +190,21 @@ export function makeSkillsCommand() {
189
190
  }
190
191
  const config = resolveConfig(command);
191
192
  const authHeader = await resolveAuthHeader(config);
193
+ let omitted = 0;
192
194
  const bytes = await withSpinner("Exporting skill...", async () => {
193
195
  const baseUrl = config.baseUrl.replace(/\/+$/, "");
194
- const res = await fetch(`${baseUrl}/v1/skills/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionNumber)}/export`, {
196
+ const query = opts.includeExamples ? "?includeExamples=true" : "";
197
+ const res = await fetch(`${baseUrl}/v1/skills/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionNumber)}/export${query}`, {
195
198
  headers: { Authorization: authHeader },
196
199
  });
197
200
  await checkResponse(res);
201
+ omitted = Number(res.headers.get("X-Skill-Examples-Omitted") ?? 0);
198
202
  return Buffer.from(await res.arrayBuffer());
199
203
  });
204
+ if (omitted > 0) {
205
+ // #926: a re-import of this bundle would drop them, so say so where the user looks.
206
+ console.error(`${omitted} confidential example file(s) left out; --include-examples (admin) keeps them`);
207
+ }
200
208
  if (opts.extractTo) {
201
209
  mkdirSync(opts.extractTo, { recursive: true });
202
210
  const written = unpackSkillBundle(new Uint8Array(bytes), opts.extractTo);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { Command } from "commander";
3
+ import { Command, CommanderError } from "commander";
4
4
  import { makeAuthCommand } from "./commands/auth.js";
5
5
  const require = createRequire(import.meta.url);
6
6
  const pkg = require("../package.json");
@@ -78,5 +78,20 @@ program.addCommand(makeConnectorsCommand());
78
78
  program.addCommand(makeRelaysCommand());
79
79
  program.addCommand(makeBridgeCommand());
80
80
  program.addCommand(makeDocsCommand());
81
- program.parseAsync().then(() => updater.notify());
81
+ // Commander exits 1 on its own parse errors, and 1 is the exit code for API and network failures.
82
+ // Every command throws instead, so a usage error exits 2 like a CliUsageError (#697).
83
+ const throwOnExit = (cmd) => {
84
+ cmd.exitOverride();
85
+ cmd.commands.forEach(throwOnExit);
86
+ };
87
+ throwOnExit(program);
88
+ program
89
+ .parseAsync()
90
+ .then(() => updater.notify())
91
+ .catch((err) => {
92
+ if (!(err instanceof CommanderError))
93
+ throw err;
94
+ // --help, --version and a command group's help without a subcommand succeed.
95
+ process.exit(err.exitCode === 0 || err.code === "commander.help" ? 0 : 2);
96
+ });
82
97
  //# sourceMappingURL=index.js.map
@@ -60,7 +60,7 @@ export declare const CONVERSATION_MODES: readonly ConversationMode[];
60
60
  export declare const MODE_OPTION_HELP: string;
61
61
  /**
62
62
  * The `--mode` value, checked before any request is sent: anything but the three exact values is a
63
- * usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
63
+ * usage error (exit 2).
64
64
  */
65
65
  export declare function parseModeOption(raw: string | undefined): ConversationMode | undefined;
66
66
  /** The `backbone:mode` input item (S1 D7); always appended last. */
@@ -5,14 +5,15 @@ import { CliUsageError } from "./errors.js";
5
5
  import { previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
6
6
  export const CONVERSATION_MODES = ["plan", "ask", "auto"];
7
7
  /** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
8
- export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
9
- "auto (the operator's policy as written). Without it, the conversation keeps its mode";
8
+ export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (calls that need approval wait for you), " +
9
+ "auto (the automatic approver decides reads and writes the server marks safe; the rest wait for you). " +
10
+ "Without it, the conversation keeps its mode, else the agent's default; a mode the agent does not offer is refused";
10
11
  function isConversationMode(value) {
11
12
  return typeof value === "string" && CONVERSATION_MODES.includes(value);
12
13
  }
13
14
  /**
14
15
  * The `--mode` value, checked before any request is sent: anything but the three exact values is a
15
- * usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
16
+ * usage error (exit 2).
16
17
  */
17
18
  export function parseModeOption(raw) {
18
19
  if (raw === undefined)
@@ -102,6 +102,7 @@ const CODE_HINTS = {
102
102
  "Check pendingToolCalls and pendingApprovals in the run envelope.",
103
103
  unknown_tool_output: "That call id was not released by this response. Use the responseId of the latest envelope.",
104
104
  conversation_agent_mismatch: "This conversation belongs to another agent. Start a new conversation or run the agent that owns it.",
105
+ conversation_mode_not_offered: "The agent does not offer that mode. Use one of the modes the message lists (plan is always offered), or leave --mode out for the agent's default.",
105
106
  provider_quota_exhausted: "The model provider account is out of credits or at its spend limit. Add credits or raise the limit with the provider; retrying will not help.",
106
107
  };
107
108
  /** The most specific hint for an API error: gateway code, then known messages, then the status table. */
@@ -117,6 +118,9 @@ export function hintFor(err) {
117
118
  }
118
119
  if (err.code && Object.hasOwn(CODE_HINTS, err.code))
119
120
  return CODE_HINTS[err.code];
121
+ // The REST layer (the explain view) spells the same refusal as an errorCode.
122
+ if (err.errorCode === "CONVERSATION_MODE_NOT_OFFERED")
123
+ return CODE_HINTS.conversation_mode_not_offered;
120
124
  // The gateway answers `agent_not_published` (code), the REST layer AGENT_NOT_PUBLISHED (errorCode).
121
125
  if (err.code === "agent_not_published" || err.errorCode === "AGENT_NOT_PUBLISHED") {
122
126
  return NOT_PUBLISHED_HINT;
@@ -2,11 +2,12 @@ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import { unzipSync, zipSync } from "fflate";
4
4
  /**
5
- * The importer takes SKILL.md plus these three directories and reports anything else as
5
+ * The importer takes SKILL.md plus these four directories and reports anything else as
6
6
  * `unsupported-path` (spec §4). Packing only these keeps a stray .git or node_modules out of
7
- * the upload instead of spending the 20 MB bundle cap on it.
7
+ * the upload instead of spending the 20 MB bundle cap on it. `examples/` holds confidential
8
+ * files (#926): imported like the rest, left out of a plain export.
8
9
  */
9
- const RESOURCE_DIRS = ["references", "assets", "scripts"];
10
+ const RESOURCE_DIRS = ["references", "assets", "scripts", "examples"];
10
11
  function byName(left, right) {
11
12
  return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
12
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "7.4.0-dev.9",
3
+ "version": "8.0.0-dev.11",
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",