@2kw/ai 6.3.0-dev.2 → 6.3.0-dev.24

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.
@@ -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());
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Manage the connector settings an organization decides centrally (#805).
4
+ *
5
+ * Only the approved public hosts live here: which MCP servers this organization's agents
6
+ * may reach directly. The connectors themselves are declared per agent, in the agent
7
+ * version's `tools`.
8
+ */
9
+ export declare function makeConnectorsCommand(): Command;
10
+ //# sourceMappingURL=connectors.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { Command } from "commander";
2
+ import { getClient, runAction } from "../lib/client.js";
3
+ import { CliUsageError } from "../lib/errors.js";
4
+ import { formatDetail, formatList, formatSuccess } from "../lib/output.js";
5
+ /**
6
+ * These three endpoints take a member sign-in only (plan ruling R10): approving a
7
+ * destination is the human gate against exfiltration, so an API key — however admin it is —
8
+ * is refused. Said in every description, because `--help` is where a reader looks before
9
+ * the 403.
10
+ */
11
+ const SESSION_ONLY = "Signed-in admins only: run 2kw auth login and sign in through the browser; an API key is refused";
12
+ /** The backend's limit, so a too-long host fails here instead of after a round trip. */
13
+ const MAX_HOST_LENGTH = 253;
14
+ const MIN_PORT = 1;
15
+ const MAX_PORT = 65535;
16
+ /**
17
+ * Parse `--port` strictly, and only when one was given.
18
+ *
19
+ * Commander's house pattern is a bare `parseInt`, which turns `--port abc` into `NaN`,
20
+ * `JSON.stringify` turns that into `null`, and the backend reads a null port as "not
21
+ * given" and approves 443. Silently approving a port nobody asked for is not a wart worth
22
+ * inheriting on a trust decision.
23
+ *
24
+ * Validated inside the action rather than as commander's option parser, so the refusal
25
+ * travels through `runAction` and exits 2 like every other usage error.
26
+ */
27
+ function parsePort(value) {
28
+ if (value === undefined) {
29
+ return undefined;
30
+ }
31
+ // Digits only: `Number` alone accepts " 8443 ", "0x20FB" and "8.443e3".
32
+ const port = /^\d{1,5}$/.test(value) ? Number(value) : NaN;
33
+ if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
34
+ throw new CliUsageError(`--port must be a whole number between ${MIN_PORT} and ${MAX_PORT}, not '${value}'`);
35
+ }
36
+ return port;
37
+ }
38
+ function requireHost(host) {
39
+ if (host.length > MAX_HOST_LENGTH) {
40
+ throw new CliUsageError(`A host name is at most ${MAX_HOST_LENGTH} characters`);
41
+ }
42
+ return host;
43
+ }
44
+ /**
45
+ * Manage the connector settings an organization decides centrally (#805).
46
+ *
47
+ * Only the approved public hosts live here: which MCP servers this organization's agents
48
+ * may reach directly. The connectors themselves are declared per agent, in the agent
49
+ * version's `tools`.
50
+ */
51
+ export function makeConnectorsCommand() {
52
+ const cmd = new Command("connectors").description("Manage MCP connector settings");
53
+ const publicHosts = new Command("public-hosts").description(`Approve the public MCP hosts this organization's agents may reach. ${SESSION_ONLY}`);
54
+ publicHosts
55
+ .command("list")
56
+ .description(`List the approved public MCP hosts. ${SESSION_ONLY}`)
57
+ .action(async (_opts, command) => {
58
+ await runAction(command, async () => {
59
+ const client = getClient(command);
60
+ const { data } = await client.GET("/v1/connectors/public-hosts");
61
+ formatList(data, command, [
62
+ "id",
63
+ "host",
64
+ "port",
65
+ "createdBy",
66
+ "createdAt",
67
+ ]);
68
+ });
69
+ });
70
+ publicHosts
71
+ .command("add")
72
+ .description(`Approve a host — every path and every account on it becomes reachable. ${SESSION_ONLY}`)
73
+ .argument("<host>", "Plain host name: no scheme, path, port, wildcard or IP literal")
74
+ .option("--port <n>", "Port to approve; 443 when omitted")
75
+ .action(async (host, opts, command) => {
76
+ await runAction(command, async () => {
77
+ const body = { host: requireHost(host), port: parsePort(opts.port) };
78
+ const client = getClient(command);
79
+ const { data } = await client.POST("/v1/connectors/public-hosts", { body });
80
+ formatDetail(data, command);
81
+ });
82
+ });
83
+ publicHosts
84
+ .command("remove")
85
+ .description(`Withdraw an approved host; it takes effect on the next request. ${SESSION_ONLY}`)
86
+ .argument("<id>", "Approved host ID, from `connectors public-hosts list`")
87
+ .action(async (id, _opts, command) => {
88
+ await runAction(command, async () => {
89
+ const client = getClient(command);
90
+ await client.DELETE("/v1/connectors/public-hosts/{id}", { params: { path: { id } } });
91
+ formatSuccess(`Host ${id} withdrawn.`, command);
92
+ });
93
+ });
94
+ cmd.addCommand(publicHosts);
95
+ return cmd;
96
+ }
97
+ //# sourceMappingURL=connectors.js.map
@@ -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
@@ -0,0 +1,13 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Parses the value a setting takes: a JSON object. The server checks its shape per key
4
+ * (for agents.default: {"agentId": "<id>"}); this only refuses what can never be valid.
5
+ */
6
+ export declare function parseSettingValue(raw: string): Record<string, unknown>;
7
+ /**
8
+ * Organization and member settings (agent-first chat spec §3.1, §4). Without --org every
9
+ * write targets the caller's own member tier; --org writes the organization's, which needs
10
+ * the key's write role (ADMIN or OWNER for agents.default).
11
+ */
12
+ export declare function makeSettingsCommand(): Command;
13
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { Command } from "commander";
2
+ import { getClient, runAction } from "../lib/client.js";
3
+ import { CliUsageError } from "../lib/errors.js";
4
+ import { formatDetail } from "../lib/output.js";
5
+ /**
6
+ * Parses the value a setting takes: a JSON object. The server checks its shape per key
7
+ * (for agents.default: {"agentId": "<id>"}); this only refuses what can never be valid.
8
+ */
9
+ export function parseSettingValue(raw) {
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(raw);
13
+ }
14
+ catch {
15
+ throw new CliUsageError(`Setting value is not valid JSON: ${raw}`);
16
+ }
17
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
18
+ throw new CliUsageError('Setting value must be a JSON object, e.g. \'{"agentId":"agt_123"}\'');
19
+ }
20
+ return parsed;
21
+ }
22
+ /**
23
+ * Organization and member settings (agent-first chat spec §3.1, §4). Without --org every
24
+ * write targets the caller's own member tier; --org writes the organization's, which needs
25
+ * the key's write role (ADMIN or OWNER for agents.default).
26
+ */
27
+ export function makeSettingsCommand() {
28
+ const cmd = new Command("settings").description("Organization and member settings (e.g. agents.default)");
29
+ cmd
30
+ .command("get <key>")
31
+ .description("Show a setting: the effective value, its source and both tiers")
32
+ .action(async (key, _opts, command) => {
33
+ await runAction(command, async () => {
34
+ const client = getClient(command);
35
+ const { data } = await client.GET("/v1/settings/{key}", {
36
+ params: { path: { key } },
37
+ });
38
+ formatDetail(data, command);
39
+ });
40
+ });
41
+ cmd
42
+ .command("set <key> <json>")
43
+ .description("Set your own value, or the organization's with --org (ADMIN+)")
44
+ .option("--org", "Write the organization's value instead of your own")
45
+ .action(async (key, json, opts, command) => {
46
+ await runAction(command, async () => {
47
+ const client = getClient(command);
48
+ const body = { value: parseSettingValue(json) };
49
+ const { data } = opts.org
50
+ ? await client.PUT("/v1/settings/{key}/org", {
51
+ params: { path: { key } },
52
+ body,
53
+ })
54
+ : await client.PUT("/v1/settings/{key}/member", {
55
+ params: { path: { key } },
56
+ body,
57
+ });
58
+ formatDetail(data, command);
59
+ });
60
+ });
61
+ cmd
62
+ .command("unset <key>")
63
+ .description("Clear your own value, or the organization's with --org (ADMIN+)")
64
+ .option("--org", "Clear the organization's value instead of your own")
65
+ .action(async (key, opts, command) => {
66
+ await runAction(command, async () => {
67
+ const client = getClient(command);
68
+ const { data } = opts.org
69
+ ? await client.DELETE("/v1/settings/{key}/org", {
70
+ params: { path: { key } },
71
+ })
72
+ : await client.DELETE("/v1/settings/{key}/member", {
73
+ params: { path: { key } },
74
+ });
75
+ formatDetail(data, command);
76
+ });
77
+ });
78
+ return cmd;
79
+ }
80
+ //# sourceMappingURL=settings.js.map
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { makeDocsCommand } from "./commands/docs.js";
21
21
  import { makeContextCommand } from "./commands/context.js";
22
22
  import { makeExperimentsCommand } from "./commands/experiments.js";
23
23
  import { makeTracingCommand } from "./commands/tracing.js";
24
+ import { makeSettingsCommand } from "./commands/settings.js";
24
25
  import { makeEvaluatorsCommand } from "./commands/evaluators.js";
25
26
  import { makeScoresCommand } from "./commands/scores.js";
26
27
  import { makeQueuesCommand } from "./commands/queues.js";
@@ -29,6 +30,8 @@ import { makeConversationsCommand } from "./commands/conversations.js";
29
30
  import { makeKnowledgeCommand } from "./commands/knowledge.js";
30
31
  import { makeFilesCommand } from "./commands/files.js";
31
32
  import { makeInstallationsCommand } from "./commands/installations.js";
33
+ import { makeConnectorsCommand } from "./commands/connectors.js";
34
+ import { makeRelaysCommand } from "./commands/relays.js";
32
35
  import { checkForUpdates } from "./lib/update-notifier.js";
33
36
  const updater = checkForUpdates(pkg.version);
34
37
  const program = new Command();
@@ -60,11 +63,14 @@ program.addCommand(makeEvaluatorsCommand());
60
63
  program.addCommand(makeScoresCommand());
61
64
  program.addCommand(makeQueuesCommand());
62
65
  program.addCommand(makeTracingCommand());
66
+ program.addCommand(makeSettingsCommand());
63
67
  program.addCommand(makeAgentsCommand());
64
68
  program.addCommand(makeConversationsCommand());
65
69
  program.addCommand(makeKnowledgeCommand());
66
70
  program.addCommand(makeFilesCommand());
67
71
  program.addCommand(makeInstallationsCommand());
72
+ program.addCommand(makeConnectorsCommand());
73
+ program.addCommand(makeRelaysCommand());
68
74
  program.addCommand(makeDocsCommand());
69
75
  program.parseAsync().then(() => updater.notify());
70
76
  //# sourceMappingURL=index.js.map
@@ -37,6 +37,8 @@ export const errorMiddleware = {
37
37
  `HTTP ${response.status}: ${response.statusText}`,
38
38
  timestamp: body.timestamp ?? new Date().toISOString(),
39
39
  code: typeof nested?.code === "string" ? nested.code : undefined,
40
+ // A problem detail names its cause in `errorCode` (GlobalExceptionHandler).
41
+ errorCode: typeof body.errorCode === "string" ? body.errorCode : undefined,
40
42
  });
41
43
  },
42
44
  };
@@ -10,12 +10,18 @@ export interface ApiErrorBody {
10
10
  timestamp: string;
11
11
  /** Machine-readable code from an OpenAI-style gateway error, e.g. `approval_hmac_mismatch`. */
12
12
  code?: string;
13
+ /**
14
+ * Machine-readable code from a problem detail's `errorCode`, e.g. `CONNECTOR_HOST_SIGN_IN_REQUIRED`.
15
+ * Kept apart from {@link code}: that one holds lowercase gateway codes and is printed by `--json`.
16
+ */
17
+ errorCode?: string;
13
18
  }
14
19
  export declare class BackboneApiError extends Error {
15
20
  readonly status: number;
16
21
  readonly errorType: string;
17
22
  readonly timestamp: string;
18
23
  readonly code?: string;
24
+ readonly errorCode?: string;
19
25
  constructor(body: Omit<ApiErrorBody, "error"> & {
20
26
  error: string;
21
27
  });
@@ -7,6 +7,7 @@ export class BackboneApiError extends Error {
7
7
  errorType;
8
8
  timestamp;
9
9
  code;
10
+ errorCode;
10
11
  constructor(body) {
11
12
  super(body.message);
12
13
  this.name = "BackboneApiError";
@@ -14,6 +15,7 @@ export class BackboneApiError extends Error {
14
15
  this.errorType = body.error;
15
16
  this.timestamp = body.timestamp;
16
17
  this.code = body.code;
18
+ this.errorCode = body.errorCode;
17
19
  }
18
20
  }
19
21
  /**
@@ -106,6 +108,13 @@ export function hintFor(err) {
106
108
  }
107
109
  if (err.code && CODE_HINTS[err.code])
108
110
  return CODE_HINTS[err.code];
111
+ // The 403 whose cause is the credential, not the role (#805). Without this the
112
+ // generic 403 hint tells an organization admin to check a role that is already
113
+ // right, and no role would ever have fixed it. Matched on the backend's error code,
114
+ // so a reworded message cannot silently drop the hint.
115
+ if (err.errorCode === "CONNECTOR_HOST_SIGN_IN_REQUIRED") {
116
+ return "Approved hosts need a browser sign-in: run 2kw auth login. An API key cannot manage them, whatever its role.";
117
+ }
109
118
  // Anchored on the agent service's wording: prompts and schemas send the same
110
119
  // "Label '<x>' not found" prefix, and must keep the generic 404 hint.
111
120
  if (err.status === 404 && /^Label 'latest' not found (on agent|for this agent)/.test(err.message)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.3.0-dev.2",
3
+ "version": "6.3.0-dev.24",
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",