@2kw/ai 6.3.0-dev.91 → 6.3.0-dev.96

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.
@@ -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";
@@ -216,7 +216,7 @@ export declare const agentSchema: {
216
216
  readonly uniqueItems: true;
217
217
  readonly items: {
218
218
  readonly type: "string";
219
- 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._-]+)?$";
220
220
  };
221
221
  };
222
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,
@@ -74,7 +77,7 @@ export const agentSchema = {
74
77
  additionalProperties: false,
75
78
  required: ["name"],
76
79
  properties: {
77
- name: { type: "string", pattern: SKILL_NAME, maxLength: 100 },
80
+ name: { type: "string", pattern: SKILL_NAME, maxLength: SKILL_NAME_MAX },
78
81
  ref: { type: "string", pattern: "\\S", description: "Label name or version number; default latest." },
79
82
  },
80
83
  },
@@ -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)
@@ -3,6 +3,7 @@ import { getClient, runAction } from "../lib/client.js";
3
3
  import { resolveLatestVersionId } from "../lib/datasets.js";
4
4
  import { formatPage, formatDetail, formatList, formatSuccess } from "../lib/output.js";
5
5
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
6
+ import { assertNameNotBlank, overlay } from "../lib/overlay.js";
6
7
  import { makeDatasetVersionsCommand } from "./dataset-versions.js";
7
8
  export function makeDatasetsCommand() {
8
9
  const cmd = new Command("datasets").description("Manage datasets");
@@ -77,24 +78,25 @@ export function makeDatasetsCommand() {
77
78
  .option("--metadata <json>", "Metadata (JSON)")
78
79
  .action(async (id, opts, command) => {
79
80
  await runAction(command, async () => {
81
+ assertNameNotBlank(opts.name);
82
+ const changes = {
83
+ name: opts.name,
84
+ description: opts.description,
85
+ type: opts.type,
86
+ inputSchema: opts.inputSchema ? JSON.parse(opts.inputSchema) : undefined,
87
+ expectedOutputSchema: opts.expectedOutputSchema ? JSON.parse(opts.expectedOutputSchema) : undefined,
88
+ metadata: opts.metadata ? JSON.parse(opts.metadata) : undefined,
89
+ };
80
90
  const client = getClient(command);
81
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
- const body = {};
83
- if (opts.name)
84
- body.name = opts.name;
85
- if (opts.description !== undefined)
86
- body.description = opts.description;
87
- if (opts.type !== undefined)
88
- body.type = opts.type;
89
- if (opts.inputSchema)
90
- body.inputSchema = JSON.parse(opts.inputSchema);
91
- if (opts.expectedOutputSchema)
92
- body.expectedOutputSchema = JSON.parse(opts.expectedOutputSchema);
93
- if (opts.metadata)
94
- body.metadata = JSON.parse(opts.metadata);
91
+ // PUT /v1/datasets/{id} replaces the whole dataset, so an omitted field would be stored as null.
92
+ const { data: current } = await client.GET("/v1/datasets/{id}", {
93
+ params: { path: { id } },
94
+ });
95
+ if (!current)
96
+ throw new Error(`Dataset ${id} could not be read.`);
95
97
  const { data } = await client.PUT("/v1/datasets/{id}", {
96
98
  params: { path: { id } },
97
- body: body,
99
+ body: overlay(current, changes),
98
100
  });
99
101
  formatDetail(data, command);
100
102
  });
@@ -5,6 +5,7 @@ import Table from "cli-table3";
5
5
  import { getClient, runAction } from "../lib/client.js";
6
6
  import { formatPage, formatDetail, formatList, formatSuccess, withSpinner } from "../lib/output.js";
7
7
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
8
+ import { assertNameNotBlank, overlay } from "../lib/overlay.js";
8
9
  import { isJsonOutput } from "../lib/config.js";
9
10
  const TERMINAL_STATUSES = new Set(["COMPLETED", "FAILED"]);
10
11
  /** Summarize a single cell of the comparison matrix (a variant's result for one item). */
@@ -92,21 +93,24 @@ export function makeExperimentsCommand() {
92
93
  .option("--metadata <json>", "Metadata (JSON)")
93
94
  .action(async (id, opts, command) => {
94
95
  await runAction(command, async () => {
96
+ assertNameNotBlank(opts.name);
97
+ const changes = {
98
+ name: opts.name,
99
+ description: opts.description,
100
+ type: opts.type,
101
+ datasetVersionId: opts.datasetVersion,
102
+ metadata: opts.metadata ? JSON.parse(opts.metadata) : undefined,
103
+ };
95
104
  const client = getClient(command);
96
- const body = {};
97
- if (opts.name)
98
- body.name = opts.name;
99
- if (opts.description !== undefined)
100
- body.description = opts.description;
101
- if (opts.type !== undefined)
102
- body.type = opts.type;
103
- if (opts.datasetVersion !== undefined)
104
- body.datasetVersionId = opts.datasetVersion;
105
- if (opts.metadata)
106
- body.metadata = JSON.parse(opts.metadata);
105
+ // PUT /v1/experiments/{id} replaces the whole experiment, so an omitted field would be stored as null.
106
+ const { data: current } = await client.GET("/v1/experiments/{id}", {
107
+ params: { path: { id } },
108
+ });
109
+ if (!current)
110
+ throw new Error(`Experiment ${id} could not be read.`);
107
111
  const { data } = await client.PUT("/v1/experiments/{id}", {
108
112
  params: { path: { id } },
109
- body: body,
113
+ body: overlay(current, changes),
110
114
  });
111
115
  formatDetail(data, command);
112
116
  });
@@ -185,21 +189,27 @@ export function makeExperimentsCommand() {
185
189
  .option("--sort-order <n>", "Sort order", parseInt)
186
190
  .action(async (variantId, opts, command) => {
187
191
  await runAction(command, async () => {
192
+ assertNameNotBlank(opts.name);
193
+ const changes = {
194
+ name: opts.name,
195
+ taskType: opts.taskType,
196
+ configuration: opts.config ? JSON.parse(opts.config) : undefined,
197
+ description: opts.description,
198
+ sortOrder: opts.sortOrder,
199
+ };
188
200
  const client = getClient(command);
189
- const body = {};
190
- if (opts.name !== undefined)
191
- body.name = opts.name;
192
- if (opts.taskType !== undefined)
193
- body.taskType = opts.taskType;
194
- if (opts.config)
195
- body.configuration = JSON.parse(opts.config);
196
- if (opts.description !== undefined)
197
- body.description = opts.description;
198
- if (opts.sortOrder !== undefined)
199
- body.sortOrder = opts.sortOrder;
201
+ // The variant PUT writes name, description and configuration as sent, so an omitted one would
202
+ // be cleared. There is no single-variant GET: read it from the list. The read's version goes
203
+ // back with the body, so a change made since is refused with 409.
204
+ const { data: variants } = await client.GET("/v1/experiments/{id}/variants", {
205
+ params: { path: { id: opts.experiment } },
206
+ });
207
+ const current = variants?.find((v) => v.id === variantId);
208
+ if (!current)
209
+ throw new Error(`Variant ${variantId} is not a variant of experiment ${opts.experiment}.`);
200
210
  const { data } = await client.PUT("/v1/experiments/{id}/variants/{variantId}", {
201
211
  params: { path: { id: opts.experiment, variantId } },
202
- body: body,
212
+ body: overlay(current, changes),
203
213
  });
204
214
  formatDetail(data, command);
205
215
  });
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import { getClient, runAction } from "../lib/client.js";
3
3
  import { formatPage, formatDetail, formatSuccess } from "../lib/output.js";
4
4
  import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
5
+ import { assertNameNotBlank, overlay } from "../lib/overlay.js";
5
6
  import { makePromptVersionsCommand } from "./prompt-versions.js";
6
7
  import { makePromptLabelsCommand } from "./prompt-labels.js";
7
8
  export function makePromptsCommand() {
@@ -59,17 +60,17 @@ export function makePromptsCommand() {
59
60
  .option("--type <type>", "New type")
60
61
  .action(async (id, opts, command) => {
61
62
  await runAction(command, async () => {
63
+ assertNameNotBlank(opts.name);
62
64
  const client = getClient(command);
63
- const body = {};
64
- if (opts.name)
65
- body.name = opts.name;
66
- if (opts.description !== undefined)
67
- body.description = opts.description;
68
- if (opts.type !== undefined)
69
- body.type = opts.type;
65
+ // PUT /v1/prompts/{id} replaces the whole prompt, so an omitted field would be stored as null.
66
+ const { data: current } = await client.GET("/v1/prompts/{id}", {
67
+ params: { path: { id } },
68
+ });
69
+ if (!current)
70
+ throw new Error(`Prompt ${id} could not be read.`);
70
71
  const { data } = await client.PUT("/v1/prompts/{id}", {
71
72
  params: { path: { id } },
72
- body: body,
73
+ body: overlay(current, { name: opts.name, description: opts.description, type: opts.type }),
73
74
  });
74
75
  formatDetail(data, command);
75
76
  });
@@ -58,15 +58,24 @@ export function makeSchemasCommand() {
58
58
  .option("-d, --description <text>", "New description")
59
59
  .action(async (id, opts, command) => {
60
60
  await runAction(command, async () => {
61
+ let name = opts.name;
62
+ let description = opts.description;
63
+ if (name !== undefined && !name.trim())
64
+ throw new Error("--name must not be empty.");
61
65
  const client = getClient(command);
62
- const body = {};
63
- if (opts.name)
64
- body.name = opts.name;
65
- if (opts.description !== undefined)
66
- body.description = opts.description;
66
+ // PUT /v1/schemas/{id} replaces the whole schema, so an omitted field would be stored as null.
67
+ if (name === undefined || description === undefined) {
68
+ const { data: current } = await client.GET("/v1/schemas/{id}", {
69
+ params: { path: { id } },
70
+ });
71
+ if (!current)
72
+ throw new Error(`Schema ${id} could not be read.`);
73
+ name ??= current.name;
74
+ description ??= current.description;
75
+ }
67
76
  const { data } = await client.PUT("/v1/schemas/{id}", {
68
77
  params: { path: { id } },
69
- body: body,
78
+ body: { name, description },
70
79
  });
71
80
  formatDetail(data, command);
72
81
  });
@@ -186,7 +186,8 @@ export function makeSkillsCommand() {
186
186
  const { data } = await client.GET("/v1/skills/{id}", { params: { path: { id } } });
187
187
  if (!data?.name)
188
188
  throw new Error("Skill name missing; specify an output path with -o.");
189
- outputPath = `${basename(data.name)}-v${versionNumber}.zip`;
189
+ // #788: ":" (a plugin skill) becomes "." before basename, which on Windows reads "a:" as a drive.
190
+ outputPath = `${basename(data.name.replace(/:/g, "."))}-v${versionNumber}.zip`;
190
191
  }
191
192
  const config = resolveConfig(command);
192
193
  const authHeader = await resolveAuthHeader(config);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The body for a PUT that replaces the whole row: the stored resource as its GET returned it, with
3
+ * every change the caller gave written over it. A change that is `undefined` was not given and keeps
4
+ * the stored value; the backend would store it as null otherwise (#1195). Server-managed fields in
5
+ * `current` (a dataset's or prompt's latestVersionId) go back as read, which is what keeps them.
6
+ */
7
+ export declare function overlay<T extends object>(current: T, changes: Record<string, unknown>): T;
8
+ /** Refuses a --name that is present but blank, which a full-replace PUT would otherwise store. */
9
+ export declare function assertNameNotBlank(name: string | undefined): void;
10
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The body for a PUT that replaces the whole row: the stored resource as its GET returned it, with
3
+ * every change the caller gave written over it. A change that is `undefined` was not given and keeps
4
+ * the stored value; the backend would store it as null otherwise (#1195). Server-managed fields in
5
+ * `current` (a dataset's or prompt's latestVersionId) go back as read, which is what keeps them.
6
+ */
7
+ export function overlay(current, changes) {
8
+ const body = { ...current };
9
+ for (const [key, value] of Object.entries(changes)) {
10
+ if (value !== undefined)
11
+ body[key] = value;
12
+ }
13
+ return body;
14
+ }
15
+ /** Refuses a --name that is present but blank, which a full-replace PUT would otherwise store. */
16
+ export function assertNameNotBlank(name) {
17
+ if (name !== undefined && !name.trim())
18
+ throw new Error("--name must not be empty.");
19
+ }
20
+ //# sourceMappingURL=overlay.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.3.0-dev.91",
3
+ "version": "6.3.0-dev.96",
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",