@2kw/ai 6.4.0-dev.7 → 6.4.0-dev.8
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/dist/commands/convert.js +13 -29
- package/dist/commands/skills.js +6 -8
- package/dist/commands/transcribe.js +9 -27
- package/dist/lib/agent-run.js +51 -15
- package/dist/lib/approval-prompt.js +3 -1
- package/package.json +1 -1
package/dist/commands/convert.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { getClient,
|
|
5
|
-
import {
|
|
6
|
-
import { BackboneApiError } from "../lib/errors.js";
|
|
4
|
+
import { getClient, runAction } from "../lib/client.js";
|
|
5
|
+
import { isJsonOutput } from "../lib/config.js";
|
|
7
6
|
import { formatDetail, withSpinner } from "../lib/output.js";
|
|
8
7
|
import { fileToBlob, getMimeType } from "../lib/multipart.js";
|
|
9
8
|
function mapFormat(format) {
|
|
@@ -136,34 +135,19 @@ function buildFormData(paths, opts) {
|
|
|
136
135
|
formData.append("options", new Blob([JSON.stringify(pipelineOptions)], { type: "application/json" }));
|
|
137
136
|
return formData;
|
|
138
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* POST the multipart body through the typed client. The generated type describes `files` as string[],
|
|
140
|
+
* which FormData cannot match, so the placeholder body satisfies the type and bodySerializer sends the
|
|
141
|
+
* real parts; the client's auth and error middleware apply.
|
|
142
|
+
*/
|
|
139
143
|
async function multipartConvert(command, formData, endpoint, pipeline) {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: `${baseUrl}${endpoint}`;
|
|
146
|
-
const res = await fetch(url, {
|
|
147
|
-
method: "POST",
|
|
148
|
-
headers: { Authorization: authHeader },
|
|
149
|
-
body: formData,
|
|
144
|
+
const client = getClient(command);
|
|
145
|
+
const { data } = await client.POST(endpoint, {
|
|
146
|
+
params: { query: { pipeline } },
|
|
147
|
+
body: { files: [] },
|
|
148
|
+
bodySerializer: () => formData,
|
|
150
149
|
});
|
|
151
|
-
|
|
152
|
-
let body;
|
|
153
|
-
try {
|
|
154
|
-
body = await res.json();
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
body = {
|
|
158
|
-
error: res.statusText,
|
|
159
|
-
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
160
|
-
status: res.status,
|
|
161
|
-
timestamp: new Date().toISOString(),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
throw new BackboneApiError(body);
|
|
165
|
-
}
|
|
166
|
-
return (await res.json());
|
|
150
|
+
return data;
|
|
167
151
|
}
|
|
168
152
|
export function makeConvertCommand() {
|
|
169
153
|
const cmd = new Command("convert").description("Convert documents");
|
package/dist/commands/skills.js
CHANGED
|
@@ -149,18 +149,16 @@ export function makeSkillsCommand() {
|
|
|
149
149
|
}
|
|
150
150
|
({ blob, filename } = fileToBlob(path));
|
|
151
151
|
}
|
|
152
|
-
const config = resolveConfig(command);
|
|
153
|
-
const authHeader = await resolveAuthHeader(config);
|
|
154
152
|
const formData = new FormData();
|
|
155
153
|
formData.append("file", blob, filename);
|
|
154
|
+
const client = getClient(command);
|
|
156
155
|
const data = await withSpinner("Importing skill...", async () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
// Placeholder body for the generated type; bodySerializer sends the real part.
|
|
157
|
+
const result = await client.POST("/v1/skills/import", {
|
|
158
|
+
body: { file: "" },
|
|
159
|
+
bodySerializer: () => formData,
|
|
161
160
|
});
|
|
162
|
-
|
|
163
|
-
return res.json();
|
|
161
|
+
return result.data;
|
|
164
162
|
});
|
|
165
163
|
formatDetail(data, command);
|
|
166
164
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { getClient } from "../lib/client.js";
|
|
3
|
+
import { isJsonOutput } from "../lib/config.js";
|
|
4
4
|
import { handleError } from "../lib/errors.js";
|
|
5
5
|
import { formatDetail, withSpinner } from "../lib/output.js";
|
|
6
6
|
import { fileToBlob } from "../lib/multipart.js";
|
|
7
|
-
import { BackboneApiError } from "../lib/errors.js";
|
|
8
7
|
export function makeTranscribeCommand() {
|
|
9
8
|
const cmd = new Command("transcribe")
|
|
10
9
|
.description("Transcribe an audio file")
|
|
@@ -17,8 +16,6 @@ export function makeTranscribeCommand() {
|
|
|
17
16
|
.action(async (file, opts, command) => {
|
|
18
17
|
const json = isJsonOutput(command);
|
|
19
18
|
try {
|
|
20
|
-
const config = resolveConfig(command);
|
|
21
|
-
const authHeader = await resolveAuthHeader(config);
|
|
22
19
|
const { blob, filename } = fileToBlob(file);
|
|
23
20
|
const formData = new FormData();
|
|
24
21
|
formData.append("file", blob, filename);
|
|
@@ -31,30 +28,15 @@ export function makeTranscribeCommand() {
|
|
|
31
28
|
formData.append("response_format", opts.format);
|
|
32
29
|
if (opts.temperature)
|
|
33
30
|
formData.append("temperature", opts.temperature);
|
|
34
|
-
const
|
|
35
|
-
const url = `${baseUrl}/v1/audio/transcriptions`;
|
|
31
|
+
const client = getClient(command);
|
|
36
32
|
const data = await withSpinner("Transcribing...", async () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
body:
|
|
33
|
+
// The generated type describes "file" as a binary string, which FormData cannot match;
|
|
34
|
+
// the placeholder satisfies it and bodySerializer sends the real parts.
|
|
35
|
+
const result = await client.POST("/v1/audio/transcriptions", {
|
|
36
|
+
body: { file: "", model: opts.model },
|
|
37
|
+
bodySerializer: () => formData,
|
|
41
38
|
});
|
|
42
|
-
|
|
43
|
-
let body;
|
|
44
|
-
try {
|
|
45
|
-
body = await res.json();
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
body = {
|
|
49
|
-
error: res.statusText,
|
|
50
|
-
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
51
|
-
status: res.status,
|
|
52
|
-
timestamp: new Date().toISOString(),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
throw new BackboneApiError(body);
|
|
56
|
-
}
|
|
57
|
-
return res.json();
|
|
39
|
+
return result.data;
|
|
58
40
|
});
|
|
59
41
|
formatDetail(data, command);
|
|
60
42
|
}
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { DEFAULT_CHAT_URL } from "./config.js";
|
|
3
3
|
import { connectCallIds, connectionsPhrase, pendingConnectionsOf } from "./connect-pause.js";
|
|
4
4
|
import { CliUsageError } from "./errors.js";
|
|
5
|
+
import { previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
|
|
5
6
|
export const CONVERSATION_MODES = ["plan", "ask", "auto"];
|
|
6
7
|
/** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
|
|
7
8
|
export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
|
|
@@ -119,9 +120,26 @@ function relayDecideCommand(ref, responseId, calls, withApprovals) {
|
|
|
119
120
|
return (`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)}${outputs.join("")}` +
|
|
120
121
|
(withApprovals ? " --approve-all" : ""));
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* A connector tool's open approval, decoded from the standard `mcp_approval_request` item (#949). The item
|
|
125
|
+
* is the MCP twin of `backbone:approval_request` (#805): it names the server and the tool, carries no call
|
|
126
|
+
* id, policy class or status, and exists only while the decision is open, so its id doubles as the call id
|
|
127
|
+
* and the tool is named as the approval row stores it, `mcp__<server_label>__<tool>`.
|
|
128
|
+
*/
|
|
129
|
+
function connectorApproval(i) {
|
|
130
|
+
return {
|
|
131
|
+
approvalId: String(i.id),
|
|
132
|
+
callId: String(i.id),
|
|
133
|
+
tool: `mcp__${String(i.server_label)}__${String(i.name)}`,
|
|
134
|
+
arguments: parseArguments(i.arguments),
|
|
135
|
+
policyClass: "unknown",
|
|
136
|
+
reason: null,
|
|
137
|
+
preview: null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
122
140
|
/**
|
|
123
141
|
* Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
|
|
124
|
-
* connector calls. A pause on anything else
|
|
142
|
+
* connector calls. A pause on anything else is named.
|
|
125
143
|
* `backbone:tool_image` is an image a finished `ViewImage` call attached (#1244): it never holds a run.
|
|
126
144
|
*/
|
|
127
145
|
const DECODED_ITEM_TYPES = new Set([
|
|
@@ -132,6 +150,7 @@ const DECODED_ITEM_TYPES = new Set([
|
|
|
132
150
|
"backbone:approval_request",
|
|
133
151
|
"backbone:connector_auth_request",
|
|
134
152
|
"backbone:tool_image",
|
|
153
|
+
"mcp_approval_request",
|
|
135
154
|
"mcp_call",
|
|
136
155
|
"mcp_list_tools",
|
|
137
156
|
]);
|
|
@@ -155,18 +174,21 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
155
174
|
String(i.call_id),
|
|
156
175
|
i.status === "incomplete" ? "incomplete" : "completed",
|
|
157
176
|
]));
|
|
158
|
-
const pendingApprovals =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
const pendingApprovals = [
|
|
178
|
+
...output
|
|
179
|
+
// Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
|
|
180
|
+
.filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
|
|
181
|
+
.map((i) => ({
|
|
182
|
+
approvalId: String(i.id),
|
|
183
|
+
callId: String(i.call_id),
|
|
184
|
+
tool: String(i.tool),
|
|
185
|
+
arguments: parseArguments(i.arguments),
|
|
186
|
+
policyClass: String(i.policy_class),
|
|
187
|
+
reason: typeof i.reason === "string" && i.reason ? i.reason : null,
|
|
188
|
+
preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
|
|
189
|
+
})),
|
|
190
|
+
...output.filter((i) => i.type === "mcp_approval_request").map(connectorApproval),
|
|
191
|
+
];
|
|
170
192
|
// The one decoding rule (#1086): only an open request with its id, call id, label and host.
|
|
171
193
|
const connectRequests = pendingConnectionsOf(output);
|
|
172
194
|
const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
|
|
@@ -223,6 +245,13 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
223
245
|
: decide;
|
|
224
246
|
}
|
|
225
247
|
}
|
|
248
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingApprovals.length > 0 && ref && responseId) {
|
|
249
|
+
// A continuation must decide every pending approval (400 incomplete_tool_outputs otherwise), so
|
|
250
|
+
// `--continue` would fail; decide answers them and the engine re-checks access itself (#949 review).
|
|
251
|
+
next =
|
|
252
|
+
`${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}` +
|
|
253
|
+
`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)} --approve-all`;
|
|
254
|
+
}
|
|
226
255
|
else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
|
|
227
256
|
// `agents run --continue` needs no input: the continuation re-checks access itself.
|
|
228
257
|
const resume = ref
|
|
@@ -271,8 +300,15 @@ export function printRunText(env) {
|
|
|
271
300
|
console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
|
|
272
301
|
const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
|
|
273
302
|
console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
|
|
274
|
-
//
|
|
275
|
-
|
|
303
|
+
// A SkillsApply change reads as its diff (#1028), like the interactive prompt; an incomplete
|
|
304
|
+
// preview keeps the arguments, the one place a member can read what would persist.
|
|
305
|
+
if (a.preview)
|
|
306
|
+
for (const line of previewLines(a.preview))
|
|
307
|
+
console.log(chalk.dim(` ${s(line)}`));
|
|
308
|
+
if (!a.preview || previewIsIncomplete(a.preview)) {
|
|
309
|
+
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
310
|
+
console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
|
|
311
|
+
}
|
|
276
312
|
if (a.reason)
|
|
277
313
|
console.log(chalk.dim(` reason: ${s(a.reason)}`));
|
|
278
314
|
});
|
|
@@ -52,7 +52,9 @@ export async function promptApprovals(pending, ask, write = (line) => console.er
|
|
|
52
52
|
const answers = new Map();
|
|
53
53
|
const s = stripControl;
|
|
54
54
|
for (const [index, a] of pending.entries()) {
|
|
55
|
-
|
|
55
|
+
// A connector approval carries no policy class (#949): "unknown" may be destructive, so it is never
|
|
56
|
+
// offered a standing grant here; the server drops one for a destructive row anyway.
|
|
57
|
+
const destructive = ["destructive", "unknown"].includes(a.policyClass.toLowerCase());
|
|
56
58
|
write(chalk.yellow(`\nApproval ${index + 1}/${pending.length}: ${s(a.tool)} [${s(a.policyClass)}]`));
|
|
57
59
|
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
58
60
|
const request = () => write(chalk.dim(s(JSON.stringify(a.arguments, null, 2))));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2kw/ai",
|
|
3
|
-
"version": "6.4.0-dev.
|
|
3
|
+
"version": "6.4.0-dev.8",
|
|
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",
|