@2kw/ai 6.2.2 → 6.3.0-dev.106

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 (53) hide show
  1. package/README.md +3 -0
  2. package/dist/agent-config/schema.d.ts +6 -3
  3. package/dist/agent-config/schema.js +13 -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 +37 -17
  10. package/dist/commands/agents.js +33 -6
  11. package/dist/commands/ai.js +3 -9
  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.d.ts +10 -0
  17. package/dist/commands/connectors.js +97 -0
  18. package/dist/commands/conversations.js +16 -0
  19. package/dist/commands/datasets.js +17 -15
  20. package/dist/commands/experiments.js +36 -26
  21. package/dist/commands/files.js +10 -28
  22. package/dist/commands/installations.js +1 -1
  23. package/dist/commands/knowledge-documents.js +24 -32
  24. package/dist/commands/memory.d.ts +10 -0
  25. package/dist/commands/memory.js +132 -0
  26. package/dist/commands/prompts.js +9 -8
  27. package/dist/commands/relays.d.ts +3 -0
  28. package/dist/commands/relays.js +135 -0
  29. package/dist/commands/schemas.js +15 -6
  30. package/dist/commands/settings.d.ts +13 -0
  31. package/dist/commands/settings.js +80 -0
  32. package/dist/commands/skill-versions.js +27 -1
  33. package/dist/commands/skills.js +31 -1
  34. package/dist/commands/tracing.js +5 -12
  35. package/dist/index.js +8 -0
  36. package/dist/lib/agent-decide.d.ts +3 -2
  37. package/dist/lib/agent-decide.js +5 -2
  38. package/dist/lib/agent-run.d.ts +49 -3
  39. package/dist/lib/agent-run.js +152 -9
  40. package/dist/lib/approval-prompt.js +31 -1
  41. package/dist/lib/client.d.ts +8 -0
  42. package/dist/lib/client.js +20 -1
  43. package/dist/lib/config.d.ts +16 -0
  44. package/dist/lib/config.js +40 -1
  45. package/dist/lib/errors.d.ts +6 -0
  46. package/dist/lib/errors.js +9 -0
  47. package/dist/lib/overlay.d.ts +10 -0
  48. package/dist/lib/overlay.js +20 -0
  49. package/dist/lib/skills-apply-preview.d.ts +19 -0
  50. package/dist/lib/skills-apply-preview.js +94 -0
  51. package/dist/lib/tracing-settings.d.ts +26 -0
  52. package/dist/lib/tracing-settings.js +25 -0
  53. package/package.json +1 -1
@@ -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
  });
@@ -131,7 +135,7 @@ export function makeExperimentsCommand() {
131
135
  .description("Add a variant to an experiment")
132
136
  .requiredOption("--experiment <id>", "Experiment ID")
133
137
  .requiredOption("-n, --name <name>", "Variant name")
134
- .requiredOption("--task-type <type>", "Task type")
138
+ .requiredOption("--task-type <type>", "Task type: extraction or agent")
135
139
  .requiredOption("--config <json>", "Variant configuration (JSON)")
136
140
  .option("-d, --description <text>", "Variant description")
137
141
  .option("--sort-order <n>", "Sort order", parseInt)
@@ -179,27 +183,33 @@ export function makeExperimentsCommand() {
179
183
  .argument("<variantId>", "Variant ID")
180
184
  .requiredOption("--experiment <id>", "Experiment ID")
181
185
  .option("-n, --name <name>", "Variant name")
182
- .option("--task-type <type>", "Task type")
186
+ .option("--task-type <type>", "Task type; must equal the variant's current one (it cannot change)")
183
187
  .option("--config <json>", "Variant configuration (JSON)")
184
188
  .option("-d, --description <text>", "Variant description")
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
  });
@@ -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) {
@@ -73,7 +73,7 @@ export function makeInstallationsCommand() {
73
73
  body.status = opts.status;
74
74
  const { data } = await client.PUT("/v1/installations/{id}", {
75
75
  params: { path: { id } },
76
- body: body,
76
+ body,
77
77
  });
78
78
  formatDetail(data, command);
79
79
  });
@@ -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)) {
@@ -119,6 +99,18 @@ export function makeKnowledgeDocumentsCommand() {
119
99
  formatDetail(data, command);
120
100
  });
121
101
  });
102
+ cmd
103
+ .command("retry")
104
+ .description("Re-ingest a document whose newest revision failed, from its stored file")
105
+ .argument("<documentId>", "Document ID")
106
+ .requiredOption("--kb <id>", "Knowledge base ID")
107
+ .action(async (documentId, opts, command) => {
108
+ await runAction(command, async () => {
109
+ const client = getClient(command);
110
+ const { data } = await client.POST("/v1/knowledge-bases/{knowledgeBaseId}/documents/{documentId}/retry", { params: { path: { knowledgeBaseId: opts.kb, documentId } } });
111
+ formatDetail(data, command);
112
+ });
113
+ });
122
114
  cmd
123
115
  .command("delete")
124
116
  .description("Delete a document and take every revision out of retrieval")
@@ -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
@@ -0,0 +1,132 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync } from "node:fs";
3
+ import { getClient, runAction } from "../lib/client.js";
4
+ import { isJsonOutput } from "../lib/config.js";
5
+ import { CliUsageError } from "../lib/errors.js";
6
+ import { formatDetail, formatList, formatSuccess } from "../lib/output.js";
7
+ /** The If-Match value for a version a GET returned: the quoted ETag the API expects. */
8
+ export function ifMatchHeader(version) {
9
+ if (!/^\d{1,18}$/.test(version)) {
10
+ throw new CliUsageError(`--if-match takes the file's version, a whole number: ${version}`);
11
+ }
12
+ return `"${version}"`;
13
+ }
14
+ function requireYes(opts, what) {
15
+ if (!opts.yes) {
16
+ throw new CliUsageError(`${what} cannot be undone. Pass --yes to confirm.`);
17
+ }
18
+ }
19
+ /**
20
+ * Agent memory (agent memory spec §7.1, §7.4). `ls`, `cat`, `put`, `rm` and `forget` act on
21
+ * the caller's own memory; `members` and `erase` are for ADMIN and OWNER and never show
22
+ * paths or content. Content is lossless: `cat` prints it raw, `put` sends the file as is.
23
+ */
24
+ export function makeMemoryCommand() {
25
+ const cmd = new Command("memory").description("Your agent memory files; member totals and erasure for admins");
26
+ cmd
27
+ .command("ls")
28
+ .description("List your memory files")
29
+ .action(async (_opts, command) => {
30
+ await runAction(command, async () => {
31
+ const client = getClient(command);
32
+ const { data } = await client.GET("/v1/memories/me/files");
33
+ formatList(data, command, [
34
+ "path",
35
+ "sizeBytes",
36
+ "version",
37
+ "updatedAt",
38
+ "lastWrittenByAgentId",
39
+ ]);
40
+ });
41
+ });
42
+ cmd
43
+ .command("cat <path>")
44
+ .description("Print one of your memory files, raw (--json for the file object)")
45
+ .action(async (path, _opts, command) => {
46
+ await runAction(command, async () => {
47
+ const client = getClient(command);
48
+ const { data } = await client.GET("/v1/memories/me/file", {
49
+ params: { query: { path } },
50
+ });
51
+ if (isJsonOutput(command)) {
52
+ formatDetail(data, command);
53
+ }
54
+ else {
55
+ process.stdout.write(data.content);
56
+ }
57
+ });
58
+ });
59
+ cmd
60
+ .command("put <path>")
61
+ .description("Create or replace one of your memory files from a local UTF-8 file")
62
+ .requiredOption("--file <file>", "Local file whose content is stored")
63
+ .option("--if-match <version>", "Only replace the file if it still has this version")
64
+ .action(async (path, opts, command) => {
65
+ await runAction(command, async () => {
66
+ // An empty --if-match (e.g. an unset shell variable) must be refused, never dropped
67
+ // into an unconditional overwrite.
68
+ const headers = opts.ifMatch !== undefined ? { "If-Match": ifMatchHeader(opts.ifMatch) } : undefined;
69
+ const content = readFileSync(opts.file, "utf8");
70
+ const client = getClient(command);
71
+ const { data } = await client.PUT("/v1/memories/me/file", {
72
+ params: { query: { path } },
73
+ body: { content },
74
+ headers,
75
+ });
76
+ formatDetail(data, command);
77
+ });
78
+ });
79
+ cmd
80
+ .command("rm <path>")
81
+ .description("Delete one of your memory files, or a directory with everything below it")
82
+ .action(async (path, _opts, command) => {
83
+ await runAction(command, async () => {
84
+ const client = getClient(command);
85
+ await client.DELETE("/v1/memories/me/file", { params: { query: { path } } });
86
+ formatSuccess(`Deleted ${path}`, command);
87
+ });
88
+ });
89
+ cmd
90
+ .command("forget")
91
+ .description("Delete your whole memory (cannot be undone)")
92
+ .option("--yes", "Confirm")
93
+ .action(async (opts, command) => {
94
+ await runAction(command, async () => {
95
+ requireYes(opts, "Forgetting your whole memory");
96
+ const client = getClient(command);
97
+ await client.DELETE("/v1/memories/me");
98
+ formatSuccess("Forgot your whole memory", command);
99
+ });
100
+ });
101
+ cmd
102
+ .command("members")
103
+ .description("Memory totals per member, without paths or content (ADMIN+)")
104
+ .action(async (_opts, command) => {
105
+ await runAction(command, async () => {
106
+ const client = getClient(command);
107
+ const { data } = await client.GET("/v1/memories/users");
108
+ formatList(data, command, [
109
+ "userId",
110
+ "fileCount",
111
+ "totalBytes",
112
+ "lastUpdatedAt",
113
+ ]);
114
+ });
115
+ });
116
+ cmd
117
+ .command("erase <userId>")
118
+ .description("Erase a member's whole memory (ADMIN+, cannot be undone)")
119
+ .option("--yes", "Confirm")
120
+ .action(async (userId, opts, command) => {
121
+ await runAction(command, async () => {
122
+ requireYes(opts, "Erasing a member's memory");
123
+ const client = getClient(command);
124
+ await client.DELETE("/v1/memories/users/{userId}", {
125
+ params: { path: { userId } },
126
+ });
127
+ formatSuccess(`Erased the memory of ${userId}`, command);
128
+ });
129
+ });
130
+ return cmd;
131
+ }
132
+ //# sourceMappingURL=memory.js.map
@@ -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
  });
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function makeRelaysCommand(): Command;
3
+ //# sourceMappingURL=relays.d.ts.map
@@ -0,0 +1,135 @@
1
+ import { Command } from "commander";
2
+ import { getClient, runAction } from "../lib/client.js";
3
+ import { isJsonOutput } from "../lib/config.js";
4
+ import { formatDetail, formatPage, formatSuccess } from "../lib/output.js";
5
+ import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
6
+ /**
7
+ * Prints an enrolment response in text mode. The relay record comes first when the response
8
+ * carries one: `create` is the only place the new relay's id is ever shown, and every other
9
+ * subcommand takes that id as its argument.
10
+ */
11
+ function printEnrolment(data, command) {
12
+ if (isJsonOutput(command)) {
13
+ console.log(JSON.stringify(data, null, 2));
14
+ return;
15
+ }
16
+ if (data.relay) {
17
+ formatDetail(data.relay, command);
18
+ console.log("");
19
+ }
20
+ console.log(data.enrolmentExpiresAt
21
+ ? `Enrolment token (shown once, valid until ${data.enrolmentExpiresAt}):`
22
+ : "Enrolment token (shown once):");
23
+ console.log(` ${data.enrolmentToken ?? ""}`);
24
+ console.log("");
25
+ console.log("Run the relay inside your network:");
26
+ console.log(data.dockerRun ?? "");
27
+ }
28
+ export function makeRelaysCommand() {
29
+ const cmd = new Command("relays").description("Manage MCP relays that reach MCP servers on private networks");
30
+ const list = new Command("list").description("List MCP relays");
31
+ addPaginationOptions(list);
32
+ list.action(async (opts, command) => {
33
+ await runAction(command, async () => {
34
+ const client = getClient(command);
35
+ const { data } = await client.GET("/v1/mcp-relays", {
36
+ params: { query: { ...paginationParams(opts) } },
37
+ });
38
+ formatPage(data, command, [
39
+ "id",
40
+ "name",
41
+ "status",
42
+ "online",
43
+ "relayVersion",
44
+ "inventoryReportedAt",
45
+ ]);
46
+ });
47
+ });
48
+ cmd.addCommand(list);
49
+ cmd
50
+ .command("get")
51
+ .description("Show a relay with its inventory")
52
+ .argument("<id>", "Relay ID")
53
+ .action(async (id, _opts, command) => {
54
+ await runAction(command, async () => {
55
+ const client = getClient(command);
56
+ const { data } = await client.GET("/v1/mcp-relays/{id}", {
57
+ params: { path: { id } },
58
+ });
59
+ formatDetail(data, command);
60
+ });
61
+ });
62
+ cmd
63
+ .command("create")
64
+ .description("Create a relay; prints a single-use enrolment token and a docker run snippet")
65
+ .requiredOption("--name <name>", 'Display name, e.g. "Office Berlin"')
66
+ .action(async (opts, command) => {
67
+ await runAction(command, async () => {
68
+ const client = getClient(command);
69
+ const { data } = await client.POST("/v1/mcp-relays", {
70
+ body: { name: opts.name },
71
+ });
72
+ printEnrolment(data ?? {}, command);
73
+ });
74
+ });
75
+ cmd
76
+ .command("rename")
77
+ .description("Rename a relay")
78
+ .argument("<id>", "Relay ID")
79
+ .requiredOption("--name <name>", "New name")
80
+ .action(async (id, opts, command) => {
81
+ await runAction(command, async () => {
82
+ const client = getClient(command);
83
+ const { data } = await client.PATCH("/v1/mcp-relays/{id}", {
84
+ params: { path: { id } },
85
+ body: { name: opts.name },
86
+ });
87
+ formatDetail(data, command);
88
+ });
89
+ });
90
+ for (const [name, disabled, description] of [
91
+ ["disable", true, "Disable a relay: it gets no new work and its enrolment token is voided"],
92
+ ["enable", false, "Enable a disabled relay"],
93
+ ]) {
94
+ cmd
95
+ .command(name)
96
+ .description(description)
97
+ .argument("<id>", "Relay ID")
98
+ .action(async (id, _opts, command) => {
99
+ await runAction(command, async () => {
100
+ const client = getClient(command);
101
+ const { data } = await client.PATCH("/v1/mcp-relays/{id}", {
102
+ params: { path: { id } },
103
+ body: { disabled },
104
+ });
105
+ formatDetail(data, command);
106
+ });
107
+ });
108
+ }
109
+ cmd
110
+ .command("re-enrol")
111
+ .description("Issue a new enrolment token; redeeming it replaces the relay's key")
112
+ .argument("<id>", "Relay ID")
113
+ .action(async (id, _opts, command) => {
114
+ await runAction(command, async () => {
115
+ const client = getClient(command);
116
+ const { data } = await client.POST("/v1/mcp-relays/{id}/enrolment-token", {
117
+ params: { path: { id } },
118
+ });
119
+ printEnrolment(data ?? {}, command);
120
+ });
121
+ });
122
+ cmd
123
+ .command("delete")
124
+ .description("Revoke a relay for good")
125
+ .argument("<id>", "Relay ID")
126
+ .action(async (id, _opts, command) => {
127
+ await runAction(command, async () => {
128
+ const client = getClient(command);
129
+ await client.DELETE("/v1/mcp-relays/{id}", { params: { path: { id } } });
130
+ formatSuccess(`Relay ${id} revoked.`, command);
131
+ });
132
+ });
133
+ return cmd;
134
+ }
135
+ //# sourceMappingURL=relays.js.map
@@ -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
  });