@2kw/ai-mcp-server 6.3.0-dev.13 → 6.3.0-dev.135
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 -1
- package/dist/client.d.ts +9 -2
- package/dist/client.js +15 -1
- package/dist/index.js +5 -53
- package/dist/lib/agent-decide.d.ts +94 -0
- package/dist/lib/agent-decide.js +216 -0
- package/dist/lib/agent-run.d.ts +125 -0
- package/dist/lib/agent-run.js +259 -0
- package/dist/lib/connect-pause.d.ts +71 -0
- package/dist/lib/connect-pause.js +149 -0
- package/dist/lib/overlay.d.ts +10 -0
- package/dist/lib/overlay.js +20 -0
- package/dist/tools/agents.js +118 -4
- package/dist/tools/ai-gateway.js +25 -11
- package/dist/tools/conversations.js +26 -0
- package/dist/tools/datasets.js +10 -15
- package/dist/tools/experiments.js +24 -28
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +190 -0
- package/dist/tools/knowledge.js +41 -23
- package/dist/tools/memory.d.ts +9 -0
- package/dist/tools/memory.js +43 -0
- package/dist/tools/prompts.js +10 -9
- package/dist/tools/schemas.js +14 -7
- package/dist/tools/tracing.js +21 -8
- package/package.json +5 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run envelope of `bb agents run --json` (spec 2026-09-14-cli-agents-design.md §4.3), copied
|
|
3
|
+
* from `cli/src/lib/agent-run.ts` because cli/ and mcp/ share no package (#667). Keep the two in step;
|
|
4
|
+
* only the text of `next` differs, because an MCP caller decides through a tool, not a shell command.
|
|
5
|
+
* The connect pause is not copied by hand: `connect-pause.ts` is a guarded byte copy of the CLI's (#1086).
|
|
6
|
+
*/
|
|
7
|
+
import { chatOriginFor, connectCallIds, connectionsPhrase, DEFAULT_CHAT_URL, pendingConnectionsOf, } from "./connect-pause.js";
|
|
8
|
+
export { DEFAULT_CHAT_URL };
|
|
9
|
+
/**
|
|
10
|
+
* The chat web host that belongs to the server's API base URL (`AI_2KW_BASE_URL`), where a member
|
|
11
|
+
* connects a connector. `AI_2KW_CHAT_URL` overrides the mapping; a trailing slash is dropped. The
|
|
12
|
+
* mapping itself is the shared `connect-pause.ts`'s {@link chatOriginFor} (#1086).
|
|
13
|
+
*/
|
|
14
|
+
export function chatUrlFor(baseUrl, env = process.env) {
|
|
15
|
+
const override = env.AI_2KW_CHAT_URL?.trim();
|
|
16
|
+
if (override)
|
|
17
|
+
return override.replace(/\/+$/, "");
|
|
18
|
+
return chatOriginFor(baseUrl);
|
|
19
|
+
}
|
|
20
|
+
export const CONVERSATION_MODES = ["plan", "ask", "auto"];
|
|
21
|
+
/**
|
|
22
|
+
* Who may change the mode (#656 D8), stated in both tool descriptions: the mode outlives the request
|
|
23
|
+
* that sets it, so a model that switched on its own would lift the user's choice for every later turn.
|
|
24
|
+
*/
|
|
25
|
+
export const MODE_RULE = "Set `mode` only when the user explicitly asks for that mode. Never set it to retry a refused approval " +
|
|
26
|
+
"or to leave plan mode on your own: ask the user first. plan = read-only; ask = no automatic approver, " +
|
|
27
|
+
"calls that need approval wait for a person; auto = the operator's policy as written. Leave it out to " +
|
|
28
|
+
"keep the conversation's mode.";
|
|
29
|
+
function isConversationMode(value) {
|
|
30
|
+
return typeof value === "string" && CONVERSATION_MODES.includes(value);
|
|
31
|
+
}
|
|
32
|
+
/** The `backbone:mode` input item (S1 D7); always appended last. */
|
|
33
|
+
export function modeItem(mode) {
|
|
34
|
+
return { type: "backbone:mode", mode };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
|
|
38
|
+
* and any value other than the three read as null, which also means "none set".
|
|
39
|
+
*/
|
|
40
|
+
export function responseMode(result) {
|
|
41
|
+
const raw = result?.conversation_mode;
|
|
42
|
+
return isConversationMode(raw) ? raw : null;
|
|
43
|
+
}
|
|
44
|
+
/** The assistant's text: every `output_text` part of every `message` item, in order. */
|
|
45
|
+
export function extractResponseText(result) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
for (const item of result?.output ?? []) {
|
|
48
|
+
if (item.type !== "message")
|
|
49
|
+
continue;
|
|
50
|
+
for (const part of item.content ?? []) {
|
|
51
|
+
if (part.type === "output_text" && part.text)
|
|
52
|
+
parts.push(part.text);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return parts.join("\n");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The response echoes `model` as `agent/{name}@{versionNumber}` (all digits), optionally
|
|
59
|
+
* followed by a `#<model>` override. Only a trailing all-digit `@N` is read as a version;
|
|
60
|
+
* any other `@` suffix stays part of the name.
|
|
61
|
+
*/
|
|
62
|
+
export function parseAgentModel(model) {
|
|
63
|
+
// Strip the `#<model>` override (from the last `#`, as the server splits it) before matching.
|
|
64
|
+
const hash = (model ?? "").lastIndexOf("#");
|
|
65
|
+
const ref = hash >= 0 ? (model ?? "").slice(0, hash) : model ?? "";
|
|
66
|
+
const withVersion = /^agent\/(.+)@(\d+)$/.exec(ref);
|
|
67
|
+
if (withVersion)
|
|
68
|
+
return { agent: withVersion[1], version: Number(withVersion[2]) };
|
|
69
|
+
const bare = /^agent\/(.+)$/.exec(ref);
|
|
70
|
+
if (bare)
|
|
71
|
+
return { agent: bare[1], version: null };
|
|
72
|
+
return { agent: null, version: null };
|
|
73
|
+
}
|
|
74
|
+
function parseArguments(raw) {
|
|
75
|
+
if (typeof raw !== "string")
|
|
76
|
+
return raw ?? {};
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
|
|
86
|
+
* connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
|
|
87
|
+
*/
|
|
88
|
+
const DECODED_ITEM_TYPES = new Set([
|
|
89
|
+
"message",
|
|
90
|
+
"reasoning",
|
|
91
|
+
"function_call",
|
|
92
|
+
"function_call_output",
|
|
93
|
+
"backbone:approval_request",
|
|
94
|
+
"backbone:connector_auth_request",
|
|
95
|
+
"mcp_call",
|
|
96
|
+
"mcp_list_tools",
|
|
97
|
+
]);
|
|
98
|
+
/**
|
|
99
|
+
* The relay pause's `next` sentence (#1254 M7, M9): names every relayed call (JSON-quoted, P2: call
|
|
100
|
+
* ids and tool names are the agent's model's own, not this server's) and the decide call that answers
|
|
101
|
+
* them within the relay window.
|
|
102
|
+
*/
|
|
103
|
+
function relayNext(pendingToolCalls, ref, responseId) {
|
|
104
|
+
const calls = pendingToolCalls.map((c) => `${JSON.stringify(c.callId)} (tool ${JSON.stringify(c.tool)})`).join(", ");
|
|
105
|
+
return (`The agent waits for client-side tool output: ${calls}. Within one hour, answer each one: run it only when it clearly maps ` +
|
|
106
|
+
"onto something you can do here, and ask the user first when it has side effects; otherwise answer it with `failed: true`. " +
|
|
107
|
+
`Then call 2kw_decide_agent_approvals with agent ${JSON.stringify(ref)}, responseId ${JSON.stringify(responseId)} and one ` +
|
|
108
|
+
"`outputs` entry per call.");
|
|
109
|
+
}
|
|
110
|
+
/** Output item types this server does not decode into anything the envelope shows (e.g. a connector-tool approval's `mcp_approval_request`). */
|
|
111
|
+
function extraUndecodedTypes(output) {
|
|
112
|
+
return [...new Set(output.map((i) => String(i.type)).filter((t) => !DECODED_ITEM_TYPES.has(t)))];
|
|
113
|
+
}
|
|
114
|
+
function undecodedPause(output) {
|
|
115
|
+
const types = extraUndecodedTypes(output);
|
|
116
|
+
return types.length > 0
|
|
117
|
+
? `This server cannot show or answer ${types.join(", ")}.`
|
|
118
|
+
: "This server cannot tell what the run waits for.";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* @param agentRef the reference the run was started with (`name[@label][#model]`); `next` names it so
|
|
122
|
+
* the decision continues on the same label and model (D11). Falls back to the echoed agent name.
|
|
123
|
+
* @param chatUrl the chat web host of the API ({@link chatUrlFor}); a connect pause's `next` names its
|
|
124
|
+
* Connectors page.
|
|
125
|
+
*/
|
|
126
|
+
export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
|
|
127
|
+
const output = result?.output ?? [];
|
|
128
|
+
const { agent, version } = parseAgentModel(result?.model);
|
|
129
|
+
// call_id → status of its tool output; a failed server-side tool run is marked `incomplete`.
|
|
130
|
+
const outputStatuses = new Map(output
|
|
131
|
+
.filter((i) => i.type === "function_call_output")
|
|
132
|
+
.map((i) => [
|
|
133
|
+
String(i.call_id),
|
|
134
|
+
i.status === "incomplete" ? "incomplete" : "completed",
|
|
135
|
+
]));
|
|
136
|
+
const pendingApprovals = output
|
|
137
|
+
// Only an open request is pending; judge- or grant-decided requests (#634, #629) carry another status.
|
|
138
|
+
.filter((i) => i.type === "backbone:approval_request" && (i.status === undefined || i.status === "in_progress"))
|
|
139
|
+
.map((i) => ({
|
|
140
|
+
approvalId: String(i.id),
|
|
141
|
+
callId: String(i.call_id),
|
|
142
|
+
tool: String(i.tool),
|
|
143
|
+
arguments: parseArguments(i.arguments),
|
|
144
|
+
policyClass: String(i.policy_class),
|
|
145
|
+
reason: typeof i.reason === "string" && i.reason ? i.reason : null,
|
|
146
|
+
}));
|
|
147
|
+
// The one decoding rule (#1086): only an open request with its id, call id, label and host.
|
|
148
|
+
const connectRequests = pendingConnectionsOf(output);
|
|
149
|
+
const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
|
|
150
|
+
serverLabel,
|
|
151
|
+
host,
|
|
152
|
+
reason,
|
|
153
|
+
...(destinations ? { destinations } : {}),
|
|
154
|
+
}));
|
|
155
|
+
const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, connectRequests)]);
|
|
156
|
+
const toolCalls = [];
|
|
157
|
+
const pendingToolCalls = [];
|
|
158
|
+
for (const item of output) {
|
|
159
|
+
if (item.type !== "function_call")
|
|
160
|
+
continue;
|
|
161
|
+
const callId = String(item.call_id);
|
|
162
|
+
const outputStatus = outputStatuses.get(callId);
|
|
163
|
+
if (outputStatus) {
|
|
164
|
+
toolCalls.push({ tool: String(item.name), callId, status: outputStatus });
|
|
165
|
+
}
|
|
166
|
+
else if (!withheldCallIds.has(callId)) {
|
|
167
|
+
pendingToolCalls.push({ tool: String(item.name), callId, arguments: parseArguments(item.arguments) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let status;
|
|
171
|
+
switch (result?.status) {
|
|
172
|
+
case "completed":
|
|
173
|
+
status = "completed";
|
|
174
|
+
break;
|
|
175
|
+
case "incomplete":
|
|
176
|
+
status = "incomplete";
|
|
177
|
+
break;
|
|
178
|
+
case "requires_action":
|
|
179
|
+
// A connect pause is requires_tool_output too (#807 R12): no tool here can answer it.
|
|
180
|
+
status =
|
|
181
|
+
pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
|
|
182
|
+
? "requires_tool_output"
|
|
183
|
+
: "requires_approval";
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
throw new Error(`Unexpected response status: ${result?.status}`);
|
|
187
|
+
}
|
|
188
|
+
const responseId = result?.id ?? null;
|
|
189
|
+
const ref = agentRef ?? agent;
|
|
190
|
+
let next = null;
|
|
191
|
+
if (status === "requires_approval" && ref && responseId) {
|
|
192
|
+
// Never an "approve all" hint: the assistant decides only what the user decided (D12).
|
|
193
|
+
next = `Show the pending approvals to the user; after they decide each one, call 2kw_decide_agent_approvals with agent ${JSON.stringify(ref)} and responseId ${JSON.stringify(responseId)}.`;
|
|
194
|
+
}
|
|
195
|
+
else if (status === "requires_tool_output" && pendingToolCalls.length > 0 && ref && responseId) {
|
|
196
|
+
// The engine answers a connect call itself on the continuation, so connecting first is enough
|
|
197
|
+
// to also resolve a relay + connect pause (#1254 M7).
|
|
198
|
+
const relay = relayNext(pendingToolCalls, ref, responseId);
|
|
199
|
+
// The relay window started at the pause, not at the connection (#1254 review): connecting takes
|
|
200
|
+
// however long it takes, and does not reset the clock the relay text below names.
|
|
201
|
+
const connectPrefix = pendingConnections.length > 0
|
|
202
|
+
? `Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors; once they have, continue as follows ` +
|
|
203
|
+
"without delay — the window below started at the pause, not at the connection. "
|
|
204
|
+
: "";
|
|
205
|
+
// Never a decideAll hint: the assistant decides only what the user decided (D12, #667). The
|
|
206
|
+
// approval also has to fit inside the relay window named above (#1254 review).
|
|
207
|
+
const approvalsSuffix = pendingApprovals.length > 0
|
|
208
|
+
? " Show the pending approvals to the user and pass their decisions in the same call, within the same window."
|
|
209
|
+
: "";
|
|
210
|
+
// A connector-tool approval (mcp_approval_request) never decodes into pendingApprovals, so this
|
|
211
|
+
// envelope can under-report what is actually pending; say so rather than sound falsely complete
|
|
212
|
+
// (#1254 review). The continuation itself still refuses an incomplete answer (400
|
|
213
|
+
// incomplete_tool_outputs, decideErrorText's hint), this only tells the assistant to expect that.
|
|
214
|
+
const undecodedTypes = extraUndecodedTypes(output);
|
|
215
|
+
const undecodedSuffix = undecodedTypes.length > 0
|
|
216
|
+
? ` This server also cannot show or answer ${undecodedTypes.join(", ")} on this response — it may hold more than ` +
|
|
217
|
+
"the relayed calls above (for example another pending approval); if the continuation answers " +
|
|
218
|
+
"400 incomplete_tool_outputs, that is why."
|
|
219
|
+
: "";
|
|
220
|
+
next = `${connectPrefix}${relay}${approvalsSuffix}${undecodedSuffix}`;
|
|
221
|
+
}
|
|
222
|
+
else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingToolCalls.length === 0 && responseId) {
|
|
223
|
+
// A connector-only pause (no relayed call, so 2kw_decide_agent_approvals has nothing to decide,
|
|
224
|
+
// spec M11) has no tool here to continue it by id, and a connect pause cannot be continued by
|
|
225
|
+
// conversation (the gateway answers 400), so the honest ways on are a fresh request or a client
|
|
226
|
+
// that continues by response id (#1254 review: continueWithDecisions does send previous_response_id
|
|
227
|
+
// for every other pause this server can decide).
|
|
228
|
+
next =
|
|
229
|
+
`Tell the user to ${connectionsPhrase(pendingConnections)} in ${chatUrl}/connectors. ` +
|
|
230
|
+
`This server cannot continue response ${JSON.stringify(responseId)}: once they have connected, send the request again ` +
|
|
231
|
+
"with 2kw_create_response without `conversation` (a connect pause cannot be continued by conversation), " +
|
|
232
|
+
"or continue the response by its id from n8n (Previous Response ID) or any client that sends previous_response_id.";
|
|
233
|
+
}
|
|
234
|
+
else if (status === "requires_tool_output" && pendingConnections.length === 0 && pendingToolCalls.length === 0 && responseId) {
|
|
235
|
+
// Paused on nothing the envelope decodes: say so, rather than hand back a pause with no way on.
|
|
236
|
+
next =
|
|
237
|
+
`${undecodedPause(output)} Response ${JSON.stringify(responseId)} stays paused: tell the user to answer it ` +
|
|
238
|
+
`in ${chatUrl}/, or continue it from a client that sends previous_response_id.`;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
status,
|
|
242
|
+
mode: responseMode(result),
|
|
243
|
+
agent,
|
|
244
|
+
version,
|
|
245
|
+
responseId,
|
|
246
|
+
conversationId: result?.conversation?.id ?? null,
|
|
247
|
+
text: extractResponseText(result),
|
|
248
|
+
toolCalls,
|
|
249
|
+
pendingApprovals,
|
|
250
|
+
pendingToolCalls,
|
|
251
|
+
pendingConnections,
|
|
252
|
+
incompleteReason: result?.incomplete_details?.reason ?? null,
|
|
253
|
+
usage: result?.usage
|
|
254
|
+
? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
|
|
255
|
+
: null,
|
|
256
|
+
next,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=agent-run.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
|
|
3
|
+
* `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
|
|
4
|
+
* connect, allow or reconnect in chat.2kw.ai.
|
|
5
|
+
*
|
|
6
|
+
* This file is canonical. `mcp/src/lib/connect-pause.ts` and
|
|
7
|
+
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
8
|
+
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
9
|
+
* Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
|
|
10
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
|
|
11
|
+
*
|
|
12
|
+
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
13
|
+
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
14
|
+
* `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
|
|
15
|
+
*/
|
|
16
|
+
/** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
|
|
17
|
+
export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
18
|
+
/** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
|
|
19
|
+
export declare const CHAT_URL_BY_API_HOST: Readonly<Record<string, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
22
|
+
* (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
|
|
23
|
+
*/
|
|
24
|
+
export declare function chatOriginFor(baseUrl: unknown): string;
|
|
25
|
+
/** What the user is asked to do: connect, allow the agent, or reconnect. */
|
|
26
|
+
export type ConnectReason = "connect" | "allow" | "reconnect";
|
|
27
|
+
export declare const CONNECT_REASONS: readonly ConnectReason[];
|
|
28
|
+
/**
|
|
29
|
+
* One open connect request. `id` (`cauth_<call id>`) and `callId` (the first connect call for
|
|
30
|
+
* this connector) are what a client needs to withhold the connect calls; the CLI and the MCP
|
|
31
|
+
* server drop both from what they publish.
|
|
32
|
+
*/
|
|
33
|
+
export interface PendingConnection {
|
|
34
|
+
id: string;
|
|
35
|
+
callId: string;
|
|
36
|
+
serverLabel: string;
|
|
37
|
+
/** The MCP server's host: what the user is asked to connect to. */
|
|
38
|
+
host: string;
|
|
39
|
+
reason: ConnectReason;
|
|
40
|
+
/** The egress difference an allow is asked again for (#807 R9); absent otherwise, never empty. */
|
|
41
|
+
destinations?: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Enough of a connection to name it in a sentence. */
|
|
44
|
+
export interface ConnectTarget {
|
|
45
|
+
serverLabel: string;
|
|
46
|
+
host: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
/** The synthetic tool a connector's connect pause is raised through (#806). */
|
|
50
|
+
export declare function connectToolName(serverLabel: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The connectors a response's `output` still waits on: every `in_progress` connect request whose
|
|
53
|
+
* id no other-status projection in the same list resolved, one per id, in order. An item without
|
|
54
|
+
* a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
|
|
55
|
+
* `reason` outside the three reads as `connect`, the action that always applies; only the string
|
|
56
|
+
* entries of `destinations` are kept, and none at all omits the field.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pendingConnectionsOf(output: unknown): PendingConnection[];
|
|
59
|
+
/**
|
|
60
|
+
* The call ids a connect pause withholds from the caller: the call each pending request names;
|
|
61
|
+
* the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
|
|
62
|
+
* describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
|
|
63
|
+
* never the client's (R7); and every `mcp__<label>__connect` call for a connector in
|
|
64
|
+
* `connections` (the model may call it more than once; one request stands for all of them).
|
|
65
|
+
*/
|
|
66
|
+
export declare function connectCallIds(output: unknown, connections: readonly Pick<PendingConnection, "callId" | "serverLabel">[]): Set<string>;
|
|
67
|
+
/** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
|
|
68
|
+
export declare function connectionPhrase(c: ConnectTarget): string;
|
|
69
|
+
/** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
|
|
70
|
+
export declare function connectionsPhrase(connections: readonly ConnectTarget[]): string;
|
|
71
|
+
//# sourceMappingURL=connect-pause.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// GENERATED COPY of cli/src/lib/connect-pause.ts (#1086). Do not edit here: edit the CLI's file,
|
|
2
|
+
// then run `npm run sync:connect-pause` in mcp/. `npm run check:connect-pause` fails on any drift.
|
|
3
|
+
/**
|
|
4
|
+
* The connect pause, decoded one way for every client (#1086, spec §4, D4): an open
|
|
5
|
+
* `backbone:connector_auth_request` (#807 R12) is a connector the run waits for the user to
|
|
6
|
+
* connect, allow or reconnect in chat.2kw.ai.
|
|
7
|
+
*
|
|
8
|
+
* This file is canonical. `mcp/src/lib/connect-pause.ts` and
|
|
9
|
+
* `n8n/nodes/TwoKw/operations/connect-pause.ts` are byte copies of it under a header: edit it
|
|
10
|
+
* here, then run `npm run sync:connect-pause` in mcp/ and `npm run sync-connect-pause` in n8n/.
|
|
11
|
+
* Its golden fixture, `cli/tests/fixtures/connect-pause-cases.json`, is copied the same way into
|
|
12
|
+
* mcp/, n8n/ and surface/, and every client's decoder test runs every case of it.
|
|
13
|
+
*
|
|
14
|
+
* Plain TypeScript with no imports, no `process`, no Node types and no timers, so n8n's source
|
|
15
|
+
* scanner and its empty `dependencies` accept the copy. The CLI and the MCP server keep their
|
|
16
|
+
* `AI_2KW_CHAT_URL` override in their own code, around {@link chatOriginFor}.
|
|
17
|
+
*/
|
|
18
|
+
/** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
|
|
19
|
+
export const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
|
|
20
|
+
/** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
|
|
21
|
+
export const CHAT_URL_BY_API_HOST = {
|
|
22
|
+
"api.2kw.ai": "https://chat.2kw.ai",
|
|
23
|
+
"api-dev.2kw.ai": "https://chat-dev.2kw.ai",
|
|
24
|
+
"backbone.manfred-kunze.dev": "https://chat.2kw.ai",
|
|
25
|
+
"localhost:8080": "http://localhost:3000",
|
|
26
|
+
"127.0.0.1:8080": "http://localhost:3000",
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The chat web host that belongs to an API base URL, where a member connects a connector
|
|
30
|
+
* (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
|
|
31
|
+
*/
|
|
32
|
+
export function chatOriginFor(baseUrl) {
|
|
33
|
+
try {
|
|
34
|
+
const host = new URL(String(baseUrl ?? "")).host;
|
|
35
|
+
// hasOwn, not a bare index: a host named after an Object.prototype key must not resolve.
|
|
36
|
+
return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] ?? DEFAULT_CHAT_URL : DEFAULT_CHAT_URL;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return DEFAULT_CHAT_URL;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export const CONNECT_REASONS = ["connect", "allow", "reconnect"];
|
|
43
|
+
const CONNECT_REQUEST = "backbone:connector_auth_request";
|
|
44
|
+
function itemsOf(output) {
|
|
45
|
+
if (!Array.isArray(output))
|
|
46
|
+
return [];
|
|
47
|
+
return output.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item));
|
|
48
|
+
}
|
|
49
|
+
function text(value) {
|
|
50
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** The synthetic tool a connector's connect pause is raised through (#806). */
|
|
53
|
+
export function connectToolName(serverLabel) {
|
|
54
|
+
return `mcp__${serverLabel}__connect`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The connectors a response's `output` still waits on: every `in_progress` connect request whose
|
|
58
|
+
* id no other-status projection in the same list resolved, one per id, in order. An item without
|
|
59
|
+
* a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
|
|
60
|
+
* `reason` outside the three reads as `connect`, the action that always applies; only the string
|
|
61
|
+
* entries of `destinations` are kept, and none at all omits the field.
|
|
62
|
+
*/
|
|
63
|
+
export function pendingConnectionsOf(output) {
|
|
64
|
+
const items = itemsOf(output);
|
|
65
|
+
const resolved = new Set();
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
const id = text(item.id);
|
|
68
|
+
if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
|
|
69
|
+
resolved.add(id);
|
|
70
|
+
}
|
|
71
|
+
const pending = new Map();
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
|
|
74
|
+
continue;
|
|
75
|
+
const id = text(item.id);
|
|
76
|
+
const callId = text(item.call_id);
|
|
77
|
+
const serverLabel = text(item.server_label);
|
|
78
|
+
const host = text(item.host);
|
|
79
|
+
if (!id || !callId || !serverLabel || !host || resolved.has(id) || pending.has(id))
|
|
80
|
+
continue;
|
|
81
|
+
const reason = text(item.reason);
|
|
82
|
+
const destinations = Array.isArray(item.destinations)
|
|
83
|
+
? item.destinations.filter((entry) => typeof entry === "string")
|
|
84
|
+
: [];
|
|
85
|
+
pending.set(id, {
|
|
86
|
+
id,
|
|
87
|
+
callId,
|
|
88
|
+
serverLabel,
|
|
89
|
+
host,
|
|
90
|
+
reason: reason && CONNECT_REASONS.includes(reason) ? reason : "connect",
|
|
91
|
+
...(destinations.length > 0 ? { destinations } : {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return [...pending.values()];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The call ids a connect pause withholds from the caller: the call each pending request names;
|
|
98
|
+
* the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
|
|
99
|
+
* describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
|
|
100
|
+
* never the client's (R7); and every `mcp__<label>__connect` call for a connector in
|
|
101
|
+
* `connections` (the model may call it more than once; one request stands for all of them).
|
|
102
|
+
*/
|
|
103
|
+
export function connectCallIds(output, connections) {
|
|
104
|
+
const names = new Set(connections.map((c) => connectToolName(c.serverLabel)));
|
|
105
|
+
const ids = new Set(connections.map((c) => c.callId));
|
|
106
|
+
const items = itemsOf(output);
|
|
107
|
+
// Every open connect request's own call id is withheld, even one pendingConnectionsOf could
|
|
108
|
+
// not fully describe (a missing server_label or host): the paired function_call must never be
|
|
109
|
+
// left for the caller to fabricate an output for (R7).
|
|
110
|
+
const resolved = new Set();
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
const id = text(item.id);
|
|
113
|
+
if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
|
|
114
|
+
resolved.add(id);
|
|
115
|
+
}
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
|
|
118
|
+
continue;
|
|
119
|
+
const id = text(item.id);
|
|
120
|
+
if (id && resolved.has(id))
|
|
121
|
+
continue;
|
|
122
|
+
const callId = text(item.call_id);
|
|
123
|
+
if (callId)
|
|
124
|
+
ids.add(callId);
|
|
125
|
+
}
|
|
126
|
+
for (const item of items) {
|
|
127
|
+
const callId = text(item.call_id);
|
|
128
|
+
if (item.type === "function_call" && callId && typeof item.name === "string" && names.has(item.name))
|
|
129
|
+
ids.add(callId);
|
|
130
|
+
}
|
|
131
|
+
return ids;
|
|
132
|
+
}
|
|
133
|
+
/** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
|
|
134
|
+
export function connectionPhrase(c) {
|
|
135
|
+
const target = `${c.serverLabel} (${c.host})`;
|
|
136
|
+
if (c.reason === "allow")
|
|
137
|
+
return `allow the agent to use ${target}`;
|
|
138
|
+
if (c.reason === "reconnect")
|
|
139
|
+
return `reconnect ${target}`;
|
|
140
|
+
return `connect ${target}`;
|
|
141
|
+
}
|
|
142
|
+
/** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
|
|
143
|
+
export function connectionsPhrase(connections) {
|
|
144
|
+
const phrases = connections.map(connectionPhrase);
|
|
145
|
+
if (phrases.length < 2)
|
|
146
|
+
return phrases[0] ?? "";
|
|
147
|
+
return `${phrases.slice(0, -1).join(", ")} and ${phrases[phrases.length - 1]}`;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=connect-pause.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The body for a PUT that replaces the whole row: the stored resource as its GET returned it, with
|
|
3
|
+
* every change the caller gave written over it. A change that is `undefined` was not given and keeps
|
|
4
|
+
* the stored value; the backend would store it as null otherwise (#1195). Server-managed fields in
|
|
5
|
+
* `current` (a dataset's or prompt's latestVersionId) go back as read, which is what keeps them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function overlay<T extends object>(current: T, changes: Record<string, unknown>): T;
|
|
8
|
+
/** Refuses a name that is present but blank, which a full-replace PUT would otherwise store. */
|
|
9
|
+
export declare function assertNameNotBlank(name: string | undefined): void;
|
|
10
|
+
//# sourceMappingURL=overlay.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The body for a PUT that replaces the whole row: the stored resource as its GET returned it, with
|
|
3
|
+
* every change the caller gave written over it. A change that is `undefined` was not given and keeps
|
|
4
|
+
* the stored value; the backend would store it as null otherwise (#1195). Server-managed fields in
|
|
5
|
+
* `current` (a dataset's or prompt's latestVersionId) go back as read, which is what keeps them.
|
|
6
|
+
*/
|
|
7
|
+
export function overlay(current, changes) {
|
|
8
|
+
const body = { ...current };
|
|
9
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
10
|
+
if (value !== undefined)
|
|
11
|
+
body[key] = value;
|
|
12
|
+
}
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
/** Refuses a name that is present but blank, which a full-replace PUT would otherwise store. */
|
|
16
|
+
export function assertNameNotBlank(name) {
|
|
17
|
+
if (name !== undefined && !name.trim())
|
|
18
|
+
throw new Error("name must not be empty.");
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=overlay.js.map
|