@2kw/ai 6.3.0-dev.98 → 6.4.0-dev.10
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 +1 -1
- package/dist/agent-config/schema.d.ts +31 -0
- package/dist/agent-config/schema.js +21 -0
- package/dist/agent-config/template.js +2 -0
- package/dist/commands/agent-run.js +11 -5
- package/dist/commands/ai.js +2 -2
- package/dist/commands/billing.js +102 -0
- package/dist/commands/convert.js +13 -29
- package/dist/commands/experiments.js +20 -3
- package/dist/commands/installations.js +45 -0
- package/dist/commands/knowledge.js +8 -3
- package/dist/commands/skills.js +6 -8
- package/dist/commands/tracing.js +7 -5
- package/dist/commands/transcribe.js +9 -27
- package/dist/lib/agent-decide.d.ts +27 -1
- package/dist/lib/agent-decide.js +98 -4
- package/dist/lib/agent-run.d.ts +5 -1
- package/dist/lib/agent-run.js +145 -67
- package/dist/lib/approval-prompt.js +11 -4
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.js +5 -19
- package/dist/lib/connect-pause.d.ts +71 -0
- package/dist/lib/connect-pause.js +147 -0
- package/dist/lib/errors.js +3 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -22,21 +22,47 @@ export interface PromptAnswer {
|
|
|
22
22
|
reason?: string;
|
|
23
23
|
remember?: boolean;
|
|
24
24
|
}
|
|
25
|
+
/** One relayed tool call answered by `--output` or `--fail` (#671 D2, D3). */
|
|
26
|
+
export interface ToolAnswer {
|
|
27
|
+
callId: string;
|
|
28
|
+
/** Sent verbatim as the item's `output` string. */
|
|
29
|
+
output: string;
|
|
30
|
+
failed: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface ToolAnswerIo {
|
|
33
|
+
readFile: (path: string) => string;
|
|
34
|
+
readStdin: () => string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `--output` and `--fail` values as `<callId>=<value>`, split on the first `=`. `@<path>` reads a file and
|
|
38
|
+
* `@-` reads stdin, both verbatim. Only syntax is checked here: without stored state the CLI cannot list the
|
|
39
|
+
* calls a response released, so completeness is the server's check (#671 D4). Every check runs before any read.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseToolAnswers(outputs?: string[], fails?: string[], io?: ToolAnswerIo): ToolAnswer[];
|
|
25
42
|
export declare function fetchPendingApprovals(client: ApiClient, agentId: string, responseId: string): Promise<ApprovalRow[]>;
|
|
26
43
|
/**
|
|
27
44
|
* The backend refuses a continuation that leaves any pending approval of the paused
|
|
28
45
|
* response undecided (400 incomplete_tool_outputs), so the whole set is checked here first.
|
|
29
46
|
*/
|
|
30
47
|
export declare function planDecisions(pending: ApprovalRow[], opts: DecideOptions, responseId: string): Decision[];
|
|
48
|
+
/**
|
|
49
|
+
* The decisions a `decide` call sends beside its tool answers (#671 D5). A pure relay pause has no pending
|
|
50
|
+
* approval, so the approval flags are optional there: `--approve-all` / `--reject-all` add nothing (P1) and
|
|
51
|
+
* only explicit ids are refused. A mixed pause needs its approvals decided in the same call.
|
|
52
|
+
*/
|
|
53
|
+
export declare function planContinuation(pending: ApprovalRow[], opts: DecideOptions, toolAnswers: ToolAnswer[], responseId: string): Decision[];
|
|
31
54
|
export declare function decisionsFromAnswers(pending: ApprovalRow[], answers: Map<string, PromptAnswer>): Decision[];
|
|
32
55
|
/** Same item shape as n8n Decide Approval (#660). */
|
|
33
56
|
export declare function toApprovalItems(decisions: Decision[]): Record<string, string>[];
|
|
57
|
+
/** `failed: true` is Backbone's extension (#480): the tool's span ends ERROR. */
|
|
58
|
+
export declare function toToolOutputItems(answers: ToolAnswer[]): Record<string, string | boolean>[];
|
|
34
59
|
/**
|
|
35
60
|
* Continues on `agent/<agentId>[@<label>][#<model>]` with the label and model override the run used: the
|
|
36
61
|
* server resolves the continuation's tools, instructions and model from this reference, so both must match
|
|
37
62
|
* the run's or the continuation drops to another version or the agent's default model.
|
|
38
63
|
* Never the echoed `name@<versionNumber>` (it would 404 as a label).
|
|
39
64
|
* With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
|
|
65
|
+
* Tool answers go first, in flag order (#671 D6): the server replays accepted outputs ahead of other items.
|
|
40
66
|
*/
|
|
41
|
-
export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string, mode?: ConversationMode): Promise<ResponsesResult>;
|
|
67
|
+
export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string, mode?: ConversationMode, toolAnswers?: ToolAnswer[]): Promise<ResponsesResult>;
|
|
42
68
|
//# sourceMappingURL=agent-decide.d.ts.map
|
package/dist/lib/agent-decide.js
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { modeItem } from "./agent-run.js";
|
|
2
3
|
import { CliUsageError } from "./errors.js";
|
|
3
4
|
import { paginationParams } from "./pagination.js";
|
|
5
|
+
const defaultIo = {
|
|
6
|
+
readFile: (path) => readFileSync(path, "utf-8"),
|
|
7
|
+
// Not readStdinInput: that one trims, and a tool result is sent as read (P5).
|
|
8
|
+
readStdin: () => readFileSync(0, "utf-8"),
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* `--output` and `--fail` values as `<callId>=<value>`, split on the first `=`. `@<path>` reads a file and
|
|
12
|
+
* `@-` reads stdin, both verbatim. Only syntax is checked here: without stored state the CLI cannot list the
|
|
13
|
+
* calls a response released, so completeness is the server's check (#671 D4). Every check runs before any read.
|
|
14
|
+
*/
|
|
15
|
+
export function parseToolAnswers(outputs = [], fails = [], io = defaultIo) {
|
|
16
|
+
const entries = [
|
|
17
|
+
...outputs.map((raw) => ({ raw, flag: "--output", noun: "value", failed: false })),
|
|
18
|
+
...fails.map((raw) => ({ raw, flag: "--fail", noun: "message", failed: true })),
|
|
19
|
+
].map(({ raw, flag, noun, failed }) => {
|
|
20
|
+
const eq = raw.indexOf("=");
|
|
21
|
+
if (eq < 0)
|
|
22
|
+
throw new CliUsageError(`Invalid ${flag} '${raw}': use <callId>=<${noun}>.`);
|
|
23
|
+
const callId = raw.slice(0, eq);
|
|
24
|
+
if (!callId)
|
|
25
|
+
throw new CliUsageError(`Invalid ${flag} '${raw}': the call id is empty.`);
|
|
26
|
+
return { callId, value: raw.slice(eq + 1), failed };
|
|
27
|
+
});
|
|
28
|
+
const seen = new Map();
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
const before = seen.get(e.callId);
|
|
31
|
+
if (before === undefined) {
|
|
32
|
+
seen.set(e.callId, e.failed);
|
|
33
|
+
}
|
|
34
|
+
else if (before === e.failed) {
|
|
35
|
+
throw new CliUsageError(`Tool call ${e.callId} is answered more than once.`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw new CliUsageError(`Tool call ${e.callId} is answered by both --output and --fail.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (entries.filter((e) => e.value === "@-").length > 1) {
|
|
42
|
+
throw new CliUsageError("Only one --output or --fail value can read stdin (@-).");
|
|
43
|
+
}
|
|
44
|
+
return entries.map(({ callId, value, failed }) => ({ callId, output: readAnswerValue(value, io), failed }));
|
|
45
|
+
}
|
|
46
|
+
function readAnswerValue(value, io) {
|
|
47
|
+
if (value === "@-") {
|
|
48
|
+
try {
|
|
49
|
+
return io.readStdin();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// A closed or non-blocking stdin (EAGAIN, EOF) is a usage error like an unreadable file.
|
|
53
|
+
throw new CliUsageError(`stdin: cannot read (${err.code ?? err.message})`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!value.startsWith("@"))
|
|
57
|
+
return value;
|
|
58
|
+
const path = value.slice(1);
|
|
59
|
+
try {
|
|
60
|
+
return io.readFile(path);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const code = err.code;
|
|
64
|
+
throw new CliUsageError(`${path}: cannot read file (${code ?? err.message})`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
4
67
|
const MAX_PAGES = 50;
|
|
5
68
|
export async function fetchPendingApprovals(client, agentId, responseId) {
|
|
6
69
|
const rows = [];
|
|
@@ -41,7 +104,8 @@ export function planDecisions(pending, opts, responseId) {
|
|
|
41
104
|
throw new CliUsageError("Use either --approve-all / --reject-all or explicit --approve / --reject ids, not both.");
|
|
42
105
|
}
|
|
43
106
|
if (!usesAll && approve.length === 0 && reject.length === 0) {
|
|
44
|
-
throw new CliUsageError("Choose a decision: --approve <ids...>, --reject <ids...>, --approve-all or --reject-all
|
|
107
|
+
throw new CliUsageError("Choose a decision: --approve <ids...>, --reject <ids...>, --approve-all or --reject-all " +
|
|
108
|
+
"(and answer relayed tool calls with --output / --fail).");
|
|
45
109
|
}
|
|
46
110
|
const choice = new Map();
|
|
47
111
|
if (usesAll) {
|
|
@@ -84,6 +148,26 @@ export function planDecisions(pending, opts, responseId) {
|
|
|
84
148
|
};
|
|
85
149
|
});
|
|
86
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The decisions a `decide` call sends beside its tool answers (#671 D5). A pure relay pause has no pending
|
|
153
|
+
* approval, so the approval flags are optional there: `--approve-all` / `--reject-all` add nothing (P1) and
|
|
154
|
+
* only explicit ids are refused. A mixed pause needs its approvals decided in the same call.
|
|
155
|
+
*/
|
|
156
|
+
export function planContinuation(pending, opts, toolAnswers, responseId) {
|
|
157
|
+
if (toolAnswers.length === 0)
|
|
158
|
+
return planDecisions(pending, opts, responseId);
|
|
159
|
+
const ids = [...(opts.approve ?? []), ...(opts.reject ?? [])];
|
|
160
|
+
if (pending.length === 0) {
|
|
161
|
+
if (ids.length)
|
|
162
|
+
throw new CliUsageError(`Not pending on ${responseId}: ${ids.join(", ")}`);
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
if (!opts.approveAll && !opts.rejectAll && ids.length === 0) {
|
|
166
|
+
throw new CliUsageError(`${responseId} also waits for approvals: every relayed tool call and every pending approval must be answered ` +
|
|
167
|
+
`in one call. Undecided: ${pending.map((r) => r.id).join(", ")} (add --approve-all, --reject-all or the ids).`);
|
|
168
|
+
}
|
|
169
|
+
return planDecisions(pending, opts, responseId);
|
|
170
|
+
}
|
|
87
171
|
export function decisionsFromAnswers(pending, answers) {
|
|
88
172
|
return pending.map((r) => {
|
|
89
173
|
const answer = answers.get(String(r.id));
|
|
@@ -109,20 +193,30 @@ export function toApprovalItems(decisions) {
|
|
|
109
193
|
...(d.remember ? { remember: d.remember } : {}),
|
|
110
194
|
}));
|
|
111
195
|
}
|
|
196
|
+
/** `failed: true` is Backbone's extension (#480): the tool's span ends ERROR. */
|
|
197
|
+
export function toToolOutputItems(answers) {
|
|
198
|
+
return answers.map((a) => ({
|
|
199
|
+
type: "function_call_output",
|
|
200
|
+
call_id: a.callId,
|
|
201
|
+
output: a.output,
|
|
202
|
+
...(a.failed ? { failed: true } : {}),
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
112
205
|
/**
|
|
113
206
|
* Continues on `agent/<agentId>[@<label>][#<model>]` with the label and model override the run used: the
|
|
114
207
|
* server resolves the continuation's tools, instructions and model from this reference, so both must match
|
|
115
208
|
* the run's or the continuation drops to another version or the agent's default model.
|
|
116
209
|
* Never the echoed `name@<versionNumber>` (it would 404 as a label).
|
|
117
210
|
* With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
|
|
211
|
+
* Tool answers go first, in flag order (#671 D6): the server replays accepted outputs ahead of other items.
|
|
118
212
|
*/
|
|
119
|
-
export async function continueWithDecisions(client, agentId, responseId, decisions, label, model, mode) {
|
|
120
|
-
const
|
|
213
|
+
export async function continueWithDecisions(client, agentId, responseId, decisions, label, model, mode, toolAnswers = []) {
|
|
214
|
+
const items = [...toToolOutputItems(toolAnswers), ...toApprovalItems(decisions)];
|
|
121
215
|
const body = {
|
|
122
216
|
model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
|
|
123
217
|
previous_response_id: responseId,
|
|
124
218
|
stream: false,
|
|
125
|
-
input: mode ? [...
|
|
219
|
+
input: mode ? [...items, modeItem(mode)] : items,
|
|
126
220
|
};
|
|
127
221
|
// The generated body type does not model backbone: extension items, hence the cast.
|
|
128
222
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
package/dist/lib/agent-run.d.ts
CHANGED
|
@@ -28,7 +28,8 @@ export interface PendingToolCall {
|
|
|
28
28
|
/**
|
|
29
29
|
* A connector the run waits for the user to connect, allow or reconnect in chat.2kw.ai: an open
|
|
30
30
|
* `backbone:connector_auth_request` (#807 R12). The caller never answers the connect call; the
|
|
31
|
-
* continuation re-checks access itself.
|
|
31
|
+
* continuation re-checks access itself. Decoded by `connect-pause.ts` (#1086), whose `id` and
|
|
32
|
+
* `callId` the envelope does not publish.
|
|
32
33
|
*/
|
|
33
34
|
export interface PendingConnection {
|
|
34
35
|
serverLabel: string;
|
|
@@ -115,6 +116,9 @@ export declare function parseAgentModel(model?: string): {
|
|
|
115
116
|
* clipboard. Newlines and tabs stay. JSON output is never passed through this.
|
|
116
117
|
*/
|
|
117
118
|
export declare function stripControl(s: string): string;
|
|
119
|
+
export declare function hasInvisible(s: string): boolean;
|
|
120
|
+
/** Replaces each hidden code point with a visible `<U+XXXX>`, so approving a diff means having seen it. */
|
|
121
|
+
export declare function revealInvisible(s: string): string;
|
|
118
122
|
export declare function shellQuote(value: string): string;
|
|
119
123
|
/** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
|
|
120
124
|
export declare function connectionsSentence(connections: PendingConnection[]): string;
|
package/dist/lib/agent-run.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { DEFAULT_CHAT_URL } from "./config.js";
|
|
3
|
+
import { connectCallIds, connectionsPhrase, pendingConnectionsOf } from "./connect-pause.js";
|
|
3
4
|
import { CliUsageError } from "./errors.js";
|
|
5
|
+
import { previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
|
|
4
6
|
export const CONVERSATION_MODES = ["plan", "ask", "auto"];
|
|
5
7
|
/** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
|
|
6
8
|
export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
|
|
@@ -87,61 +89,91 @@ const CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
|
87
89
|
export function stripControl(s) {
|
|
88
90
|
return s.replace(CONTROL_CHARS, "");
|
|
89
91
|
}
|
|
90
|
-
export function shellQuote(value) {
|
|
91
|
-
if (/^[A-Za-z0-9._@/:-]+$/.test(value))
|
|
92
|
-
return value;
|
|
93
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
94
|
-
}
|
|
95
|
-
/** An open connector consent request; a continuation's projection carries another status. */
|
|
96
|
-
function pendingConnectionsOf(output) {
|
|
97
|
-
return output
|
|
98
|
-
.filter((i) => i.type === "backbone:connector_auth_request" && (i.status === undefined || i.status === "in_progress"))
|
|
99
|
-
.map((i) => ({
|
|
100
|
-
serverLabel: String(i.server_label),
|
|
101
|
-
host: String(i.host),
|
|
102
|
-
reason: String(i.reason),
|
|
103
|
-
...(Array.isArray(i.destinations) && i.destinations.length > 0
|
|
104
|
-
? { destinations: i.destinations.map((d) => String(d)) }
|
|
105
|
-
: {}),
|
|
106
|
-
}));
|
|
107
|
-
}
|
|
108
92
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
93
|
+
* Code points that print as nothing, or reorder what surrounds them, yet reach the model: Unicode
|
|
94
|
+
* tags, zero-width characters, directional marks, embeddings, overrides and isolates, the word
|
|
95
|
+
* joiner and the byte-order mark (#1027). As numbers, not an escaped regex, so no tool can fold
|
|
96
|
+
* them into the source. Kept in step with `surface/src/core/skills-apply-preview.ts`.
|
|
112
97
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
const INVISIBLE_RANGES = [
|
|
99
|
+
[0xe0000, 0xe007f],
|
|
100
|
+
[0x200b, 0x200f],
|
|
101
|
+
[0x202a, 0x202e],
|
|
102
|
+
[0x2060, 0x2060],
|
|
103
|
+
[0x2066, 0x2069],
|
|
104
|
+
[0x061c, 0x061c],
|
|
105
|
+
[0xfeff, 0xfeff],
|
|
106
|
+
];
|
|
107
|
+
function isInvisible(codePoint) {
|
|
108
|
+
return INVISIBLE_RANGES.some(([low, high]) => codePoint >= low && codePoint <= high);
|
|
109
|
+
}
|
|
110
|
+
export function hasInvisible(s) {
|
|
111
|
+
for (const char of s)
|
|
112
|
+
if (isInvisible(char.codePointAt(0)))
|
|
113
|
+
return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
/** Replaces each hidden code point with a visible `<U+XXXX>`, so approving a diff means having seen it. */
|
|
117
|
+
export function revealInvisible(s) {
|
|
118
|
+
let out = "";
|
|
119
|
+
for (const char of s) {
|
|
120
|
+
const codePoint = char.codePointAt(0);
|
|
121
|
+
out += isInvisible(codePoint) ? `<U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}>` : char;
|
|
123
122
|
}
|
|
124
|
-
return
|
|
123
|
+
return out;
|
|
125
124
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (c.reason === "reconnect")
|
|
131
|
-
return `reconnect ${target}`;
|
|
132
|
-
return `connect ${target}`;
|
|
125
|
+
export function shellQuote(value) {
|
|
126
|
+
if (/^[A-Za-z0-9._@/:-]+$/.test(value))
|
|
127
|
+
return value;
|
|
128
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
133
129
|
}
|
|
134
130
|
/** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
|
|
135
131
|
export function connectionsSentence(connections) {
|
|
136
|
-
const
|
|
137
|
-
const joined = phrases.length > 1 ? `${phrases.slice(0, -1).join(", ")} and ${phrases.at(-1)}` : phrases[0] ?? "";
|
|
132
|
+
const joined = connectionsPhrase(connections);
|
|
138
133
|
return joined.charAt(0).toUpperCase() + joined.slice(1);
|
|
139
134
|
}
|
|
140
135
|
/** Joins a connect pause's instruction to the command that continues it; {@link printRunText} breaks the line there. */
|
|
141
136
|
const THEN_RUN = ", then run: ";
|
|
137
|
+
/**
|
|
138
|
+
* The `decide` command that answers a relay pause (#671 D7): one `--output <callId>=@<file>` per pending call,
|
|
139
|
+
* plus `--approve-all` when approvals wait too. The call id is chosen by the model, so the suggested file name
|
|
140
|
+
* keeps only `[A-Za-z0-9_-]` (P2) and never repeats: it can be neither `../…` nor an absolute path.
|
|
141
|
+
*/
|
|
142
|
+
function relayDecideCommand(ref, responseId, calls, withApprovals) {
|
|
143
|
+
const used = new Set();
|
|
144
|
+
const outputs = calls.map((c) => {
|
|
145
|
+
const safe = c.callId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
146
|
+
let file = safe;
|
|
147
|
+
// Compared without case: on Windows and macOS `callA.out` and `calla.out` are one file.
|
|
148
|
+
for (let n = 2; used.has(file.toLowerCase()); n++)
|
|
149
|
+
file = `${safe}-${n}`;
|
|
150
|
+
used.add(file.toLowerCase());
|
|
151
|
+
return ` --output ${shellQuote(`${c.callId}=@${file}.out`)}`;
|
|
152
|
+
});
|
|
153
|
+
return (`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)}${outputs.join("")}` +
|
|
154
|
+
(withApprovals ? " --approve-all" : ""));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A connector tool's open approval, decoded from the standard `mcp_approval_request` item (#949). The item
|
|
158
|
+
* is the MCP twin of `backbone:approval_request` (#805): it names the server and the tool, carries no call
|
|
159
|
+
* id, policy class or status, and exists only while the decision is open, so its id doubles as the call id
|
|
160
|
+
* and the tool is named as the approval row stores it, `mcp__<server_label>__<tool>`.
|
|
161
|
+
*/
|
|
162
|
+
function connectorApproval(i) {
|
|
163
|
+
return {
|
|
164
|
+
approvalId: String(i.id),
|
|
165
|
+
callId: String(i.id),
|
|
166
|
+
tool: `mcp__${String(i.server_label)}__${String(i.name)}`,
|
|
167
|
+
arguments: parseArguments(i.arguments),
|
|
168
|
+
policyClass: "unknown",
|
|
169
|
+
reason: null,
|
|
170
|
+
preview: null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
142
173
|
/**
|
|
143
174
|
* Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
|
|
144
|
-
* connector calls. A pause on anything else
|
|
175
|
+
* connector calls. A pause on anything else is named.
|
|
176
|
+
* `backbone:tool_image` is an image a finished `ViewImage` call attached (#1244): it never holds a run.
|
|
145
177
|
*/
|
|
146
178
|
const DECODED_ITEM_TYPES = new Set([
|
|
147
179
|
"message",
|
|
@@ -150,6 +182,8 @@ const DECODED_ITEM_TYPES = new Set([
|
|
|
150
182
|
"function_call_output",
|
|
151
183
|
"backbone:approval_request",
|
|
152
184
|
"backbone:connector_auth_request",
|
|
185
|
+
"backbone:tool_image",
|
|
186
|
+
"mcp_approval_request",
|
|
153
187
|
"mcp_call",
|
|
154
188
|
"mcp_list_tools",
|
|
155
189
|
]);
|
|
@@ -173,20 +207,30 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
173
207
|
String(i.call_id),
|
|
174
208
|
i.status === "incomplete" ? "incomplete" : "completed",
|
|
175
209
|
]));
|
|
176
|
-
const pendingApprovals =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
const pendingApprovals = [
|
|
211
|
+
...output
|
|
212
|
+
// Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
|
|
213
|
+
.filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
|
|
214
|
+
.map((i) => ({
|
|
215
|
+
approvalId: String(i.id),
|
|
216
|
+
callId: String(i.call_id),
|
|
217
|
+
tool: String(i.tool),
|
|
218
|
+
arguments: parseArguments(i.arguments),
|
|
219
|
+
policyClass: String(i.policy_class),
|
|
220
|
+
reason: typeof i.reason === "string" && i.reason ? i.reason : null,
|
|
221
|
+
preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
|
|
222
|
+
})),
|
|
223
|
+
...output.filter((i) => i.type === "mcp_approval_request").map(connectorApproval),
|
|
224
|
+
];
|
|
225
|
+
// The one decoding rule (#1086): only an open request with its id, call id, label and host.
|
|
226
|
+
const connectRequests = pendingConnectionsOf(output);
|
|
227
|
+
const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
|
|
228
|
+
serverLabel,
|
|
229
|
+
host,
|
|
230
|
+
reason,
|
|
231
|
+
...(destinations ? { destinations } : {}),
|
|
187
232
|
}));
|
|
188
|
-
const
|
|
189
|
-
const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, pendingConnections)]);
|
|
233
|
+
const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, connectRequests)]);
|
|
190
234
|
const toolCalls = [];
|
|
191
235
|
const pendingToolCalls = [];
|
|
192
236
|
for (const item of output) {
|
|
@@ -225,8 +269,23 @@ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
|
225
269
|
if (status === "requires_approval" && ref && responseId) {
|
|
226
270
|
next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
|
|
227
271
|
}
|
|
228
|
-
else if (status === "requires_tool_output" &&
|
|
229
|
-
//
|
|
272
|
+
else if (status === "requires_tool_output" && pendingToolCalls.length > 0) {
|
|
273
|
+
// The engine answers the connect call itself on the continuation, so connecting first is enough.
|
|
274
|
+
if (ref && responseId) {
|
|
275
|
+
const decide = relayDecideCommand(ref, responseId, pendingToolCalls, pendingApprovals.length > 0);
|
|
276
|
+
next = pendingConnections.length > 0
|
|
277
|
+
? `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}${decide}`
|
|
278
|
+
: decide;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingApprovals.length > 0 && ref && responseId) {
|
|
282
|
+
// A continuation must decide every pending approval (400 incomplete_tool_outputs otherwise), so
|
|
283
|
+
// `--continue` would fail; decide answers them and the engine re-checks access itself (#949 review).
|
|
284
|
+
next =
|
|
285
|
+
`${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}` +
|
|
286
|
+
`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)} --approve-all`;
|
|
287
|
+
}
|
|
288
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
|
|
230
289
|
// `agents run --continue` needs no input: the continuation re-checks access itself.
|
|
231
290
|
const resume = ref
|
|
232
291
|
? `${THEN_RUN}2kw agents run ${shellQuote(ref)} --continue ${shellQuote(responseId)}`
|
|
@@ -272,19 +331,39 @@ export function printRunText(env) {
|
|
|
272
331
|
console.log(chalk.dim(`\n── ${s(head.join(" · "))}`));
|
|
273
332
|
if (env.toolCalls.length)
|
|
274
333
|
console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
|
|
275
|
-
|
|
276
|
-
console.log(
|
|
277
|
-
|
|
278
|
-
|
|
334
|
+
const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
|
|
335
|
+
console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
|
|
336
|
+
// A SkillsApply change reads as its diff (#1028), like the interactive prompt; an incomplete
|
|
337
|
+
// preview keeps the arguments, the one place a member can read what would persist.
|
|
338
|
+
if (a.preview)
|
|
339
|
+
for (const line of previewLines(a.preview))
|
|
340
|
+
console.log(chalk.dim(` ${s(line)}`));
|
|
341
|
+
if (!a.preview || previewIsIncomplete(a.preview)) {
|
|
279
342
|
// JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
|
|
280
343
|
console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
344
|
+
}
|
|
345
|
+
if (a.reason)
|
|
346
|
+
console.log(chalk.dim(` reason: ${s(a.reason)}`));
|
|
347
|
+
});
|
|
348
|
+
if (env.status === "requires_approval") {
|
|
349
|
+
console.log(chalk.yellow("\nPaused for approval:"));
|
|
350
|
+
printApprovals();
|
|
284
351
|
if (env.next)
|
|
285
352
|
console.log(`\nDecide with:\n ${s(env.next)}`);
|
|
286
353
|
}
|
|
287
354
|
else if (env.status === "requires_tool_output") {
|
|
355
|
+
if (env.pendingToolCalls.length) {
|
|
356
|
+
console.log(chalk.yellow("\nPaused: the agent waits for client-side tool output:"));
|
|
357
|
+
env.pendingToolCalls.forEach((c) => {
|
|
358
|
+
console.log(` - ${s(c.tool)} (${s(c.callId)})`);
|
|
359
|
+
console.log(chalk.dim(` ${s(JSON.stringify(c.arguments))}`));
|
|
360
|
+
});
|
|
361
|
+
// The printed command carries --approve-all, so show what it approves (#671 P3).
|
|
362
|
+
if (env.pendingApprovals.length) {
|
|
363
|
+
console.log(chalk.yellow("\nIt also waits for approval:"));
|
|
364
|
+
printApprovals();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
288
367
|
if (env.pendingConnections.length) {
|
|
289
368
|
console.log(chalk.yellow("\nPaused: the agent needs you to connect in chat:"));
|
|
290
369
|
env.pendingConnections.forEach((c) => {
|
|
@@ -296,9 +375,8 @@ export function printRunText(env) {
|
|
|
296
375
|
if (env.next)
|
|
297
376
|
console.log(`\n${s(env.next.replace(THEN_RUN, ", then run:\n "))}`);
|
|
298
377
|
}
|
|
299
|
-
if (env.pendingToolCalls.length) {
|
|
300
|
-
console.log(
|
|
301
|
-
env.pendingToolCalls.forEach((c) => console.log(` - ${s(c.tool)} (${s(c.callId)})`));
|
|
378
|
+
else if (env.pendingToolCalls.length && env.next) {
|
|
379
|
+
console.log(`\nAnswer with:\n ${s(env.next)}`);
|
|
302
380
|
}
|
|
303
381
|
if (!env.pendingConnections.length && !env.pendingToolCalls.length) {
|
|
304
382
|
console.log(chalk.yellow("\nPaused on something this CLI cannot show:"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { stripControl } from "./agent-run.js";
|
|
3
|
+
import { hasInvisible, revealInvisible, stripControl } from "./agent-run.js";
|
|
4
4
|
import { REMEMBER_NOTE, previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
|
|
5
5
|
/** Raised when the prompt's input closes (EOF, Ctrl+C, Ctrl+D) before an answer arrives. */
|
|
6
6
|
export class PromptAbortedError extends Error {
|
|
@@ -50,9 +50,11 @@ export function readlineAsk(input = process.stdin, output = process.stderr) {
|
|
|
50
50
|
}
|
|
51
51
|
export async function promptApprovals(pending, ask, write = (line) => console.error(line)) {
|
|
52
52
|
const answers = new Map();
|
|
53
|
-
const s = stripControl;
|
|
53
|
+
const s = (value) => revealInvisible(stripControl(value));
|
|
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))));
|
|
@@ -60,7 +62,12 @@ export async function promptApprovals(pending, ask, write = (line) => console.er
|
|
|
60
62
|
// A SkillsApply change reads as its diff (#782, spec §10). When the preview leaves changes
|
|
61
63
|
// out, the full arguments follow it: this prompt never runs with --json, so it is the only
|
|
62
64
|
// place the member can read what would persist (spec D7).
|
|
63
|
-
|
|
65
|
+
const lines = previewLines(a.preview);
|
|
66
|
+
// Hidden characters are shown as <U+XXXX> below; the warning says why (#1027, spec D7).
|
|
67
|
+
if (lines.some(hasInvisible)) {
|
|
68
|
+
write(chalk.yellow("Hidden characters found: shown as <U+XXXX>. A model reads them; you would not see them. Check each marked line before you approve."));
|
|
69
|
+
}
|
|
70
|
+
for (const line of lines)
|
|
64
71
|
write(colourPreviewLine(s(line)));
|
|
65
72
|
if (previewIsIncomplete(a.preview)) {
|
|
66
73
|
write(chalk.dim("Full request (the preview leaves changes out):"));
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -52,12 +52,12 @@ export declare const DEFAULT_BASE_URL: string;
|
|
|
52
52
|
*/
|
|
53
53
|
export declare const DEFAULT_AUTH_URL: string;
|
|
54
54
|
export declare function defaultAuthUrlFor(baseUrl: string): string;
|
|
55
|
-
|
|
56
|
-
export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
55
|
+
export { DEFAULT_CHAT_URL } from "./connect-pause.js";
|
|
57
56
|
/**
|
|
58
57
|
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
59
58
|
* (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
|
|
60
|
-
* does not know; a trailing slash is dropped so callers can append a path.
|
|
59
|
+
* does not know; a trailing slash is dropped so callers can append a path. The mapping itself
|
|
60
|
+
* is `connect-pause.ts`'s {@link chatOriginFor}, shared with the MCP server and n8n (#1086).
|
|
61
61
|
*/
|
|
62
62
|
export declare function chatUrlFor(baseUrl: string, env?: NodeJS.ProcessEnv): string;
|
|
63
63
|
declare const store: Conf<BackboneConfigStore>;
|