@2kw/ai 6.2.0-dev.42 → 6.2.0-dev.49
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 +2 -0
- package/dist/commands/agent-policy.d.ts +11 -0
- package/dist/commands/agent-policy.js +64 -0
- package/dist/commands/agents.js +17 -0
- package/dist/commands/skills.js +35 -10
- package/dist/lib/skill-bundle.d.ts +15 -0
- package/dist/lib/skill-bundle.js +80 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -110,6 +110,8 @@ kubectl-style contexts switch between organizations and environments:
|
|
|
110
110
|
| `analytics` | Usage analytics: spend, quality, providers, errors |
|
|
111
111
|
| `billing` | Check subscription tier and usage limits |
|
|
112
112
|
| `agents` | Run agents and decide approvals; apply, export and scaffold agent.yaml; manage versions, labels, approvals and tool catalogs |
|
|
113
|
+
| `skills` | Import and export SKILL.md bundles; manage skill versions and labels |
|
|
114
|
+
| `plugins` | Install plugin repositories, sync them, and inspect their reports |
|
|
113
115
|
| `conversations` | Create and manage conversations and their items |
|
|
114
116
|
| `knowledge` | Manage knowledge bases, documents, search, and citations |
|
|
115
117
|
| `files` | Upload, download, and manage files |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `agents policy <agent>` — what the policy gate would do to a call of each tool
|
|
4
|
+
* a version configures, without running the agent (&56 S6, #662).
|
|
5
|
+
*
|
|
6
|
+
* Answers the question an operator otherwise has to answer by running the agent
|
|
7
|
+
* and reading the transcript. The version defaults to the agent's latest, which
|
|
8
|
+
* is the one an unlabelled reference resolves to.
|
|
9
|
+
*/
|
|
10
|
+
export declare function makeAgentPolicyCommand(): Command;
|
|
11
|
+
//# sourceMappingURL=agent-policy.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getClient, runAction } from "../lib/client.js";
|
|
4
|
+
import { isJsonOutput } from "../lib/config.js";
|
|
5
|
+
import { formatDetail, formatList } from "../lib/output.js";
|
|
6
|
+
import { resolveAgent } from "../lib/agent-lookup.js";
|
|
7
|
+
import { CliUsageError } from "../lib/errors.js";
|
|
8
|
+
import { getLatestVersion } from "./agent-apply.js";
|
|
9
|
+
/**
|
|
10
|
+
* `agents policy <agent>` — what the policy gate would do to a call of each tool
|
|
11
|
+
* a version configures, without running the agent (&56 S6, #662).
|
|
12
|
+
*
|
|
13
|
+
* Answers the question an operator otherwise has to answer by running the agent
|
|
14
|
+
* and reading the transcript. The version defaults to the agent's latest, which
|
|
15
|
+
* is the one an unlabelled reference resolves to.
|
|
16
|
+
*/
|
|
17
|
+
export function makeAgentPolicyCommand() {
|
|
18
|
+
return new Command("policy")
|
|
19
|
+
.description("Show the effective tool policy of an agent version")
|
|
20
|
+
.argument("<agent>", "Agent ID or name")
|
|
21
|
+
.option("--version <id>", "Agent version ID (default: the agent's latest version)")
|
|
22
|
+
.option("--tool <name>", "Answer for one tool instead of every configured tool")
|
|
23
|
+
.option("--installation <id>", "Resolve the module tool catalog of this installation")
|
|
24
|
+
.action(async (agentRef, opts, command) => {
|
|
25
|
+
await runAction(command, async () => {
|
|
26
|
+
const client = getClient(command);
|
|
27
|
+
const agent = await resolveAgent(client, agentRef);
|
|
28
|
+
const agentId = agent.id;
|
|
29
|
+
const versionId = opts.version ?? (await getLatestVersion(client, agentId))?.id;
|
|
30
|
+
if (!versionId) {
|
|
31
|
+
throw new CliUsageError(`Agent '${agent.name}' has no versions yet, so it has no policy to read. Create a version first.`);
|
|
32
|
+
}
|
|
33
|
+
const { data } = await client.GET("/v1/agents/{agentId}/versions/{versionId}/policy", {
|
|
34
|
+
params: {
|
|
35
|
+
path: { agentId, versionId },
|
|
36
|
+
query: { tool: opts.tool, installation: opts.installation },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (isJsonOutput(command)) {
|
|
40
|
+
formatDetail(data, command);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Both deployment switches decide whether a JUDGE row ever reaches a
|
|
44
|
+
// model, so they belong above the table rather than in a footnote.
|
|
45
|
+
const auto = data?.autoApproval;
|
|
46
|
+
const parts = [
|
|
47
|
+
`auto-approval ${auto?.enabled ? "on" : "off (every auto pauses for a human)"}`,
|
|
48
|
+
`judge ${auto?.judgeConfigured ? "configured" : "not configured (every candidate escalates)"}`,
|
|
49
|
+
];
|
|
50
|
+
if (data?.catalogResolvedAt)
|
|
51
|
+
parts.push(`catalog resolved ${data.catalogResolvedAt}`);
|
|
52
|
+
console.log(chalk.dim(parts.join(", ")));
|
|
53
|
+
formatList(data?.tools, command, [
|
|
54
|
+
"tool",
|
|
55
|
+
"verdict",
|
|
56
|
+
"action",
|
|
57
|
+
"policyClass",
|
|
58
|
+
"source",
|
|
59
|
+
"matchedRules",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=agent-policy.js.map
|
package/dist/commands/agents.js
CHANGED
|
@@ -7,6 +7,7 @@ import { makeAgentLabelsCommand } from "./agent-labels.js";
|
|
|
7
7
|
import { makeAgentDecideCommand, makeAgentRunCommand } from "./agent-run.js";
|
|
8
8
|
import { makeAgentApplyCommand } from "./agent-apply.js";
|
|
9
9
|
import { makeAgentExportCommand } from "./agent-export.js";
|
|
10
|
+
import { makeAgentPolicyCommand } from "./agent-policy.js";
|
|
10
11
|
import { makeAgentInitCommand, makeAgentSchemaCommand } from "./agent-init.js";
|
|
11
12
|
import { maskSecrets } from "../agent-config/file.js";
|
|
12
13
|
import { collectModel, MODEL_HELP, MODELS_HELP, modelFieldsFromOptions } from "../lib/agent-models.js";
|
|
@@ -26,6 +27,21 @@ export function makeAgentsCommand() {
|
|
|
26
27
|
});
|
|
27
28
|
});
|
|
28
29
|
cmd.addCommand(list);
|
|
30
|
+
// The chat-facing read (#814): published agents, {id, name, description} only, and the
|
|
31
|
+
// one agent endpoint a chat-only USER key may call. `list` needs VIEWER or higher.
|
|
32
|
+
const catalog = new Command("catalog").description("List published agents (id, name, description)");
|
|
33
|
+
addPaginationOptions(catalog);
|
|
34
|
+
catalog.option("-s, --search <term>", "Filter by name");
|
|
35
|
+
catalog.action(async (opts, command) => {
|
|
36
|
+
await runAction(command, async () => {
|
|
37
|
+
const client = getClient(command);
|
|
38
|
+
const { data } = await client.GET("/v1/agents/catalog", {
|
|
39
|
+
params: { query: { search: opts.search, ...paginationParams(opts) } },
|
|
40
|
+
});
|
|
41
|
+
formatPage(data, command, ["id", "name", "description"]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
cmd.addCommand(catalog);
|
|
29
45
|
cmd
|
|
30
46
|
.command("get")
|
|
31
47
|
.description("Get an agent by ID")
|
|
@@ -162,6 +178,7 @@ export function makeAgentsCommand() {
|
|
|
162
178
|
cmd.addCommand(makeAgentExportCommand());
|
|
163
179
|
cmd.addCommand(makeAgentInitCommand());
|
|
164
180
|
cmd.addCommand(makeAgentSchemaCommand());
|
|
181
|
+
cmd.addCommand(makeAgentPolicyCommand());
|
|
165
182
|
cmd.addCommand(makeAgentVersionsCommand());
|
|
166
183
|
cmd.addCommand(makeAgentLabelsCommand());
|
|
167
184
|
return cmd;
|
package/dist/commands/skills.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
|
-
import { statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, extname } from "node:path";
|
|
4
4
|
import { getClient, resolveAuthHeader, runAction } from "../lib/client.js";
|
|
5
5
|
import { resolveConfig } from "../lib/config.js";
|
|
6
6
|
import { BackboneApiError } from "../lib/errors.js";
|
|
7
7
|
import { fileToBlob } from "../lib/multipart.js";
|
|
8
|
+
import { packSkillDirectory, unpackSkillBundle } from "../lib/skill-bundle.js";
|
|
8
9
|
import { formatPage, formatDetail, formatSuccess, withSpinner } from "../lib/output.js";
|
|
9
10
|
import { addPaginationOptions, paginationParams } from "../lib/pagination.js";
|
|
10
11
|
import { makeSkillVersionsCommand } from "./skill-versions.js";
|
|
@@ -98,18 +99,29 @@ export function makeSkillsCommand() {
|
|
|
98
99
|
});
|
|
99
100
|
cmd
|
|
100
101
|
.command("import")
|
|
101
|
-
.description("Import a local SKILL.md or zip archive")
|
|
102
|
-
.argument("<path>", "Local SKILL.md or .zip file")
|
|
102
|
+
.description("Import a local skill directory, SKILL.md or zip archive")
|
|
103
|
+
.argument("<path>", "Local skill directory, SKILL.md or .zip file")
|
|
103
104
|
.action(async (path, _opts, command) => {
|
|
104
105
|
await runAction(command, async () => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
106
|
+
let blob;
|
|
107
|
+
let filename;
|
|
108
|
+
if (statSync(path).isDirectory()) {
|
|
109
|
+
const bundle = packSkillDirectory(path);
|
|
110
|
+
if (bundle.skipped.length > 0) {
|
|
111
|
+
// The server reports what it was sent; only the CLI can name what never left the disk.
|
|
112
|
+
console.error(`Left out of the bundle (the importer takes SKILL.md, references/, assets/, scripts/): ${bundle.skipped.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
blob = new Blob([bundle.bytes], { type: "application/zip" });
|
|
115
|
+
filename = bundle.filename;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
if (basename(path) !== "SKILL.md" && extname(path).toLowerCase() !== ".zip") {
|
|
119
|
+
throw new Error("Import requires a skill directory, a SKILL.md or a .zip file.");
|
|
120
|
+
}
|
|
121
|
+
({ blob, filename } = fileToBlob(path));
|
|
109
122
|
}
|
|
110
123
|
const config = resolveConfig(command);
|
|
111
124
|
const authHeader = await resolveAuthHeader(config);
|
|
112
|
-
const { blob, filename } = fileToBlob(path);
|
|
113
125
|
const formData = new FormData();
|
|
114
126
|
formData.append("file", blob, filename);
|
|
115
127
|
const data = await withSpinner("Importing skill...", async () => {
|
|
@@ -126,14 +138,21 @@ export function makeSkillsCommand() {
|
|
|
126
138
|
});
|
|
127
139
|
cmd
|
|
128
140
|
.command("export")
|
|
129
|
-
.description("Export a skill version as a zip archive")
|
|
141
|
+
.description("Export a skill version as a zip archive or into a directory")
|
|
130
142
|
.argument("<id>", "Skill ID")
|
|
131
143
|
.argument("<versionNumber>", "Version number")
|
|
132
144
|
.option("-o, --output <file>", "Output path (default: <skill-name>-v<n>.zip)")
|
|
145
|
+
.option("-d, --extract-to <dir>", "Unpack the bundle into this directory instead of writing a zip")
|
|
146
|
+
.option("--force", "Write into a non-empty --extract-to directory")
|
|
133
147
|
.action(async (id, versionNumber, opts, command) => {
|
|
134
148
|
await runAction(command, async () => {
|
|
149
|
+
if (opts.output && opts.extractTo)
|
|
150
|
+
throw new Error("Pass either --output or --extract-to, not both.");
|
|
151
|
+
if (opts.extractTo && existsSync(opts.extractTo) && readdirSync(opts.extractTo).length > 0 && !opts.force) {
|
|
152
|
+
throw new Error(`${opts.extractTo} is not empty. Pass --force to write into it.`);
|
|
153
|
+
}
|
|
135
154
|
let outputPath = opts.output;
|
|
136
|
-
if (!outputPath) {
|
|
155
|
+
if (!outputPath && !opts.extractTo) {
|
|
137
156
|
const client = getClient(command);
|
|
138
157
|
const { data } = await client.GET("/v1/skills/{id}", { params: { path: { id } } });
|
|
139
158
|
if (!data?.name)
|
|
@@ -150,6 +169,12 @@ export function makeSkillsCommand() {
|
|
|
150
169
|
await checkResponse(res);
|
|
151
170
|
return Buffer.from(await res.arrayBuffer());
|
|
152
171
|
});
|
|
172
|
+
if (opts.extractTo) {
|
|
173
|
+
mkdirSync(opts.extractTo, { recursive: true });
|
|
174
|
+
const written = unpackSkillBundle(new Uint8Array(bytes), opts.extractTo);
|
|
175
|
+
formatSuccess(`Wrote ${written.length} file(s) to ${opts.extractTo}: ${written.join(", ")}`, command);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
153
178
|
writeFileSync(outputPath, bytes);
|
|
154
179
|
formatSuccess(`Wrote ${bytes.length} bytes to ${outputPath}`, command);
|
|
155
180
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PackedSkill {
|
|
2
|
+
/** Pinned to an ArrayBuffer so the bytes go straight into a Blob without a cast. */
|
|
3
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
4
|
+
/** Upload filename: the importer dispatches on the `.zip` suffix, not on the content type. */
|
|
5
|
+
filename: string;
|
|
6
|
+
/** Bundle paths that went in, SKILL.md first. */
|
|
7
|
+
packed: string[];
|
|
8
|
+
/** Directory-relative entries left out, directories with a trailing slash. */
|
|
9
|
+
skipped: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Zip a local skill directory in the layout `POST /v1/skills/import` expects. */
|
|
12
|
+
export declare function packSkillDirectory(dir: string): PackedSkill;
|
|
13
|
+
/** Unpack an exported skill bundle into a directory. Returns the bundle paths written. */
|
|
14
|
+
export declare function unpackSkillBundle(bytes: Uint8Array, dir: string): string[];
|
|
15
|
+
//# sourceMappingURL=skill-bundle.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { unzipSync, zipSync } from "fflate";
|
|
4
|
+
/**
|
|
5
|
+
* The importer takes SKILL.md plus these three directories and reports anything else as
|
|
6
|
+
* `unsupported-path` (spec §4). Packing only these keeps a stray .git or node_modules out of
|
|
7
|
+
* the upload instead of spending the 20 MB bundle cap on it.
|
|
8
|
+
*/
|
|
9
|
+
const RESOURCE_DIRS = ["references", "assets", "scripts"];
|
|
10
|
+
function byName(left, right) {
|
|
11
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
12
|
+
}
|
|
13
|
+
/** Collect one resource directory. A stable order keeps the same directory hashing the same. */
|
|
14
|
+
function collect(dir, prefix, files, packed, skipped) {
|
|
15
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort(byName)) {
|
|
16
|
+
const path = `${prefix}${entry.name}`;
|
|
17
|
+
if (entry.isDirectory())
|
|
18
|
+
collect(join(dir, entry.name), `${path}/`, files, packed, skipped);
|
|
19
|
+
// Symlinks, sockets and devices are reported rather than followed: reading one can leave
|
|
20
|
+
// the directory the user named.
|
|
21
|
+
else if (!entry.isFile())
|
|
22
|
+
skipped.push(path);
|
|
23
|
+
else {
|
|
24
|
+
files[path] = new Uint8Array(readFileSync(join(dir, entry.name)));
|
|
25
|
+
packed.push(path);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Zip a local skill directory in the layout `POST /v1/skills/import` expects. */
|
|
30
|
+
export function packSkillDirectory(dir) {
|
|
31
|
+
const root = readdirSync(dir, { withFileTypes: true }).sort(byName);
|
|
32
|
+
if (!root.some((entry) => entry.isFile() && entry.name === "SKILL.md")) {
|
|
33
|
+
throw new Error(`${join(dir, "SKILL.md")} not found: a skill directory holds SKILL.md at its root.`);
|
|
34
|
+
}
|
|
35
|
+
const files = {
|
|
36
|
+
"SKILL.md": new Uint8Array(readFileSync(join(dir, "SKILL.md"))),
|
|
37
|
+
};
|
|
38
|
+
const packed = ["SKILL.md"];
|
|
39
|
+
const skipped = [];
|
|
40
|
+
for (const entry of root) {
|
|
41
|
+
if (entry.isFile() && entry.name === "SKILL.md")
|
|
42
|
+
continue;
|
|
43
|
+
if (entry.isDirectory() && RESOURCE_DIRS.includes(entry.name)) {
|
|
44
|
+
collect(join(dir, entry.name), `${entry.name}/`, files, packed, skipped);
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
skipped.push(entry.isDirectory() ? `${entry.name}/` : entry.name);
|
|
48
|
+
}
|
|
49
|
+
return { bytes: new Uint8Array(zipSync(files)), filename: `${basename(resolve(dir))}.zip`, packed, skipped };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Refuse what the backend refuses on import (SkillBundleReader.validatePath), so a crafted
|
|
53
|
+
* archive cannot write outside the directory the user named.
|
|
54
|
+
*/
|
|
55
|
+
function assertSafePath(path) {
|
|
56
|
+
const parts = path.split("/");
|
|
57
|
+
if (path === "" ||
|
|
58
|
+
path.startsWith("/") ||
|
|
59
|
+
path.includes("\\") ||
|
|
60
|
+
/^[A-Za-z]:/.test(path) ||
|
|
61
|
+
parts.some((part) => part === ".." || part === ".") ||
|
|
62
|
+
[...path].some((character) => character.charCodeAt(0) < 32)) {
|
|
63
|
+
throw new Error(`The archive holds an unsafe path and was not written: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Unpack an exported skill bundle into a directory. Returns the bundle paths written. */
|
|
67
|
+
export function unpackSkillBundle(bytes, dir) {
|
|
68
|
+
const written = [];
|
|
69
|
+
for (const [path, content] of Object.entries(unzipSync(bytes))) {
|
|
70
|
+
if (path.endsWith("/"))
|
|
71
|
+
continue;
|
|
72
|
+
assertSafePath(path);
|
|
73
|
+
const target = join(dir, ...path.split("/"));
|
|
74
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
75
|
+
writeFileSync(target, content);
|
|
76
|
+
written.push(path);
|
|
77
|
+
}
|
|
78
|
+
return written.sort();
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=skill-bundle.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2kw/ai",
|
|
3
|
-
"version": "6.2.0-dev.
|
|
3
|
+
"version": "6.2.0-dev.49",
|
|
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",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"cli-table3": "^0.6.5",
|
|
52
52
|
"commander": "^13.1.0",
|
|
53
53
|
"conf": "^13.1.0",
|
|
54
|
+
"fflate": "^0.8.3",
|
|
54
55
|
"open": "^10.2.0",
|
|
55
56
|
"openapi-fetch": "^0.13.5",
|
|
56
57
|
"ora": "^8.2.0",
|