@2kw/ai 6.3.0-dev.3 → 6.3.0-dev.36
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.
- package/README.md +3 -0
- package/dist/commands/agents.js +15 -1
- package/dist/commands/auth.js +6 -1
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +16 -2
- package/dist/commands/connectors.d.ts +10 -0
- package/dist/commands/connectors.js +97 -0
- package/dist/commands/files.js +10 -28
- package/dist/commands/knowledge-documents.js +12 -32
- package/dist/commands/memory.d.ts +10 -0
- package/dist/commands/memory.js +132 -0
- package/dist/commands/settings.d.ts +13 -0
- package/dist/commands/settings.js +80 -0
- package/dist/index.js +6 -0
- package/dist/lib/client.d.ts +8 -0
- package/dist/lib/client.js +20 -1
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.js +11 -0
- package/dist/lib/errors.d.ts +6 -0
- package/dist/lib/errors.js +9 -0
- 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 |
|
package/dist/commands/agents.js
CHANGED
|
@@ -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());
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
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}`));
|
|
@@ -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.
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
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));
|
|
@@ -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
|
package/dist/commands/files.js
CHANGED
|
@@ -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.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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,
|
|
4
|
-
import {
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
@@ -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,8 @@ 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";
|
|
25
|
+
import { makeMemoryCommand } from "./commands/memory.js";
|
|
24
26
|
import { makeEvaluatorsCommand } from "./commands/evaluators.js";
|
|
25
27
|
import { makeScoresCommand } from "./commands/scores.js";
|
|
26
28
|
import { makeQueuesCommand } from "./commands/queues.js";
|
|
@@ -29,6 +31,7 @@ import { makeConversationsCommand } from "./commands/conversations.js";
|
|
|
29
31
|
import { makeKnowledgeCommand } from "./commands/knowledge.js";
|
|
30
32
|
import { makeFilesCommand } from "./commands/files.js";
|
|
31
33
|
import { makeInstallationsCommand } from "./commands/installations.js";
|
|
34
|
+
import { makeConnectorsCommand } from "./commands/connectors.js";
|
|
32
35
|
import { makeRelaysCommand } from "./commands/relays.js";
|
|
33
36
|
import { checkForUpdates } from "./lib/update-notifier.js";
|
|
34
37
|
const updater = checkForUpdates(pkg.version);
|
|
@@ -61,11 +64,14 @@ program.addCommand(makeEvaluatorsCommand());
|
|
|
61
64
|
program.addCommand(makeScoresCommand());
|
|
62
65
|
program.addCommand(makeQueuesCommand());
|
|
63
66
|
program.addCommand(makeTracingCommand());
|
|
67
|
+
program.addCommand(makeSettingsCommand());
|
|
68
|
+
program.addCommand(makeMemoryCommand());
|
|
64
69
|
program.addCommand(makeAgentsCommand());
|
|
65
70
|
program.addCommand(makeConversationsCommand());
|
|
66
71
|
program.addCommand(makeKnowledgeCommand());
|
|
67
72
|
program.addCommand(makeFilesCommand());
|
|
68
73
|
program.addCommand(makeInstallationsCommand());
|
|
74
|
+
program.addCommand(makeConnectorsCommand());
|
|
69
75
|
program.addCommand(makeRelaysCommand());
|
|
70
76
|
program.addCommand(makeDocsCommand());
|
|
71
77
|
program.parseAsync().then(() => updater.notify());
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export declare function apiKeyAuthMiddleware(config: ApiKeyConfig): Middleware;
|
|
|
39
39
|
* fires and cannot be replayed safely.
|
|
40
40
|
*/
|
|
41
41
|
export declare function sessionAuthMiddleware(config: SessionConfig, deps?: SessionAuthDeps): Middleware;
|
|
42
|
+
/** The per-request opt-in to agent memory for API-key runs (spec §3.3, D6, D7). */
|
|
43
|
+
export declare const MEMORY_HEADER = "X-Backbone-Memory";
|
|
44
|
+
/**
|
|
45
|
+
* Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses`, the call that starts a run
|
|
46
|
+
* and that also carries approval decisions (`lib/agent-decide.ts`). No other request gets
|
|
47
|
+
* it; the server reads it only there.
|
|
48
|
+
*/
|
|
49
|
+
export declare const memoryHeaderMiddleware: Middleware;
|
|
42
50
|
/**
|
|
43
51
|
* The middleware stack for a resolved config, in registration order.
|
|
44
52
|
*
|
package/dist/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createClient from "openapi-fetch";
|
|
2
|
-
import { resolveConfig, isJsonOutput } from "./config.js";
|
|
2
|
+
import { resolveConfig, isJsonOutput, memoryEnabled } from "./config.js";
|
|
3
3
|
import { ensureJwt } from "./auth-session.js";
|
|
4
4
|
import { BackboneApiError, handleError } from "./errors.js";
|
|
5
5
|
/**
|
|
@@ -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
|
};
|
|
@@ -83,6 +85,21 @@ export function sessionAuthMiddleware(config, deps = {}) {
|
|
|
83
85
|
},
|
|
84
86
|
};
|
|
85
87
|
}
|
|
88
|
+
/** The per-request opt-in to agent memory for API-key runs (spec §3.3, D6, D7). */
|
|
89
|
+
export const MEMORY_HEADER = "X-Backbone-Memory";
|
|
90
|
+
/**
|
|
91
|
+
* Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses`, the call that starts a run
|
|
92
|
+
* and that also carries approval decisions (`lib/agent-decide.ts`). No other request gets
|
|
93
|
+
* it; the server reads it only there.
|
|
94
|
+
*/
|
|
95
|
+
export const memoryHeaderMiddleware = {
|
|
96
|
+
onRequest({ request }) {
|
|
97
|
+
if (request.method === "POST" && new URL(request.url).pathname.endsWith("/v1/responses")) {
|
|
98
|
+
request.headers.set(MEMORY_HEADER, "enabled");
|
|
99
|
+
}
|
|
100
|
+
return request;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
86
103
|
/**
|
|
87
104
|
* The middleware stack for a resolved config, in registration order.
|
|
88
105
|
*
|
|
@@ -123,6 +140,8 @@ export function getClient(command) {
|
|
|
123
140
|
baseUrl: config.baseUrl.replace(/\/+$/, ""),
|
|
124
141
|
});
|
|
125
142
|
client.use(...buildMiddleware(config));
|
|
143
|
+
if (memoryEnabled())
|
|
144
|
+
client.use(memoryHeaderMiddleware);
|
|
126
145
|
return client;
|
|
127
146
|
}
|
|
128
147
|
/**
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ContextEntry {
|
|
|
14
14
|
cachedJwt?: string;
|
|
15
15
|
/** Epoch millis when cachedJwt expires. */
|
|
16
16
|
cachedJwtExp?: number;
|
|
17
|
+
/** Send `X-Backbone-Memory: enabled` on responses calls (`2kw config set memory true`, #721). */
|
|
18
|
+
memory?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export interface BackboneConfigStore {
|
|
19
21
|
activeContext: string;
|
|
@@ -55,6 +57,12 @@ export { store };
|
|
|
55
57
|
export declare function validateContextName(name: string): void;
|
|
56
58
|
export declare function getActiveContextName(): string;
|
|
57
59
|
export declare function getActiveContext(): ContextEntry | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Whether responses calls opt into agent memory (spec §3.3, D6). `AI_2KW_MEMORY` wins when
|
|
62
|
+
* set (`true` on, anything else off), so CI can switch it without touching the store;
|
|
63
|
+
* otherwise the active context's `memory` key decides. Off by default.
|
|
64
|
+
*/
|
|
65
|
+
export declare function memoryEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
58
66
|
export declare function getAllContexts(): Record<string, ContextEntry>;
|
|
59
67
|
export declare function getContextCount(): number;
|
|
60
68
|
export declare function setContext(name: string, entry: ContextEntry): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -142,6 +142,17 @@ export function getActiveContext() {
|
|
|
142
142
|
const contexts = store.get("contexts") ?? {};
|
|
143
143
|
return contexts[name];
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Whether responses calls opt into agent memory (spec §3.3, D6). `AI_2KW_MEMORY` wins when
|
|
147
|
+
* set (`true` on, anything else off), so CI can switch it without touching the store;
|
|
148
|
+
* otherwise the active context's `memory` key decides. Off by default.
|
|
149
|
+
*/
|
|
150
|
+
export function memoryEnabled(env = process.env) {
|
|
151
|
+
const fromEnv = env.AI_2KW_MEMORY;
|
|
152
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
153
|
+
return fromEnv === "true";
|
|
154
|
+
return getActiveContext()?.memory === true;
|
|
155
|
+
}
|
|
145
156
|
export function getAllContexts() {
|
|
146
157
|
return store.get("contexts") ?? {};
|
|
147
158
|
}
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -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
|
});
|
package/dist/lib/errors.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "6.3.0-dev.36",
|
|
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",
|