@2kw/ai 6.3.0-dev.14 → 6.3.0-dev.142

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.
Files changed (49) hide show
  1. package/README.md +4 -1
  2. package/dist/agent-config/schema.d.ts +6 -3
  3. package/dist/agent-config/schema.js +19 -4
  4. package/dist/agent-config/template.js +1 -1
  5. package/dist/commands/agent-apply.js +1 -1
  6. package/dist/commands/agent-policy.d.ts +2 -1
  7. package/dist/commands/agent-policy.js +9 -2
  8. package/dist/commands/agent-run.d.ts +5 -0
  9. package/dist/commands/agent-run.js +47 -21
  10. package/dist/commands/agents.js +18 -5
  11. package/dist/commands/ai.js +3 -9
  12. package/dist/commands/auth.js +6 -1
  13. package/dist/commands/billing.js +107 -3
  14. package/dist/commands/config.d.ts +1 -1
  15. package/dist/commands/config.js +16 -2
  16. package/dist/commands/conversations.js +16 -0
  17. package/dist/commands/datasets.js +17 -15
  18. package/dist/commands/experiments.js +36 -26
  19. package/dist/commands/files.js +10 -28
  20. package/dist/commands/installations.js +46 -1
  21. package/dist/commands/knowledge-documents.js +24 -32
  22. package/dist/commands/knowledge.js +5 -0
  23. package/dist/commands/memory.d.ts +10 -0
  24. package/dist/commands/memory.js +132 -0
  25. package/dist/commands/prompts.js +9 -8
  26. package/dist/commands/schemas.js +15 -6
  27. package/dist/commands/skill-versions.js +27 -1
  28. package/dist/commands/skills.js +31 -1
  29. package/dist/commands/tracing.js +12 -17
  30. package/dist/index.js +2 -0
  31. package/dist/lib/agent-decide.d.ts +29 -2
  32. package/dist/lib/agent-decide.js +100 -3
  33. package/dist/lib/agent-run.d.ts +50 -3
  34. package/dist/lib/agent-run.js +168 -16
  35. package/dist/lib/approval-prompt.js +31 -1
  36. package/dist/lib/client.d.ts +8 -0
  37. package/dist/lib/client.js +18 -1
  38. package/dist/lib/config.d.ts +16 -0
  39. package/dist/lib/config.js +26 -1
  40. package/dist/lib/connect-pause.d.ts +71 -0
  41. package/dist/lib/connect-pause.js +147 -0
  42. package/dist/lib/errors.js +3 -1
  43. package/dist/lib/overlay.d.ts +10 -0
  44. package/dist/lib/overlay.js +20 -0
  45. package/dist/lib/skills-apply-preview.d.ts +19 -0
  46. package/dist/lib/skills-apply-preview.js +94 -0
  47. package/dist/lib/tracing-settings.d.ts +26 -0
  48. package/dist/lib/tracing-settings.js +25 -0
  49. package/package.json +1 -1
@@ -1,4 +1,37 @@
1
1
  import chalk from "chalk";
2
+ import { DEFAULT_CHAT_URL } from "./config.js";
3
+ import { connectCallIds, connectionsPhrase, pendingConnectionsOf } from "./connect-pause.js";
4
+ import { CliUsageError } from "./errors.js";
5
+ export const CONVERSATION_MODES = ["plan", "ask", "auto"];
6
+ /** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
7
+ export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
8
+ "auto (the operator's policy as written). Without it, the conversation keeps its mode";
9
+ function isConversationMode(value) {
10
+ return typeof value === "string" && CONVERSATION_MODES.includes(value);
11
+ }
12
+ /**
13
+ * The `--mode` value, checked before any request is sent: anything but the three exact values is a
14
+ * usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
15
+ */
16
+ export function parseModeOption(raw) {
17
+ if (raw === undefined)
18
+ return undefined;
19
+ if (isConversationMode(raw))
20
+ return raw;
21
+ throw new CliUsageError(`Invalid --mode '${raw}': use plan, ask or auto.`);
22
+ }
23
+ /** The `backbone:mode` input item (S1 D7); always appended last. */
24
+ export function modeItem(mode) {
25
+ return { type: "backbone:mode", mode };
26
+ }
27
+ /**
28
+ * The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
29
+ * and any value other than the three read as null, which also means "none set".
30
+ */
31
+ export function responseMode(result) {
32
+ const raw = result?.conversation_mode;
33
+ return isConversationMode(raw) ? raw : null;
34
+ }
2
35
  export const EXIT_CODES = {
3
36
  completed: 0,
4
37
  requires_approval: 3,
@@ -60,7 +93,59 @@ export function shellQuote(value) {
60
93
  return value;
61
94
  return `'${value.replace(/'/g, `'\\''`)}'`;
62
95
  }
63
- export function buildRunEnvelope(result, agentRef) {
96
+ /** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
97
+ export function connectionsSentence(connections) {
98
+ const joined = connectionsPhrase(connections);
99
+ return joined.charAt(0).toUpperCase() + joined.slice(1);
100
+ }
101
+ /** Joins a connect pause's instruction to the command that continues it; {@link printRunText} breaks the line there. */
102
+ const THEN_RUN = ", then run: ";
103
+ /**
104
+ * The `decide` command that answers a relay pause (#671 D7): one `--output <callId>=@<file>` per pending call,
105
+ * plus `--approve-all` when approvals wait too. The call id is chosen by the model, so the suggested file name
106
+ * keeps only `[A-Za-z0-9_-]` (P2) and never repeats: it can be neither `../…` nor an absolute path.
107
+ */
108
+ function relayDecideCommand(ref, responseId, calls, withApprovals) {
109
+ const used = new Set();
110
+ const outputs = calls.map((c) => {
111
+ const safe = c.callId.replace(/[^A-Za-z0-9_-]/g, "_");
112
+ let file = safe;
113
+ // Compared without case: on Windows and macOS `callA.out` and `calla.out` are one file.
114
+ for (let n = 2; used.has(file.toLowerCase()); n++)
115
+ file = `${safe}-${n}`;
116
+ used.add(file.toLowerCase());
117
+ return ` --output ${shellQuote(`${c.callId}=@${file}.out`)}`;
118
+ });
119
+ return (`2kw agents decide ${shellQuote(ref)} --response ${shellQuote(responseId)}${outputs.join("")}` +
120
+ (withApprovals ? " --approve-all" : ""));
121
+ }
122
+ /**
123
+ * Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
124
+ * connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
125
+ * `backbone:tool_image` is an image a finished `ViewImage` call attached (#1244): it never holds a run.
126
+ */
127
+ const DECODED_ITEM_TYPES = new Set([
128
+ "message",
129
+ "reasoning",
130
+ "function_call",
131
+ "function_call_output",
132
+ "backbone:approval_request",
133
+ "backbone:connector_auth_request",
134
+ "backbone:tool_image",
135
+ "mcp_call",
136
+ "mcp_list_tools",
137
+ ]);
138
+ function undecodedPause(output) {
139
+ const types = [...new Set(output.map((i) => String(i.type)).filter((t) => !DECODED_ITEM_TYPES.has(t)))];
140
+ return types.length > 0
141
+ ? `This CLI cannot show or answer ${types.join(", ")}.`
142
+ : "This CLI cannot tell what the run waits for.";
143
+ }
144
+ /**
145
+ * @param chatUrl the chat web host of the API the run went to ({@link chatUrlFor}); a connect pause's
146
+ * `next` sends the user to its Connectors page.
147
+ */
148
+ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
64
149
  const output = result?.output ?? [];
65
150
  const { agent, version } = parseAgentModel(result?.model);
66
151
  // call_id → status of its tool output; a failed server-side tool run is marked `incomplete`.
@@ -80,8 +165,17 @@ export function buildRunEnvelope(result, agentRef) {
80
165
  arguments: parseArguments(i.arguments),
81
166
  policyClass: String(i.policy_class),
82
167
  reason: typeof i.reason === "string" && i.reason ? i.reason : null,
168
+ preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
83
169
  }));
84
- const withheldCallIds = new Set(pendingApprovals.map((a) => a.callId));
170
+ // The one decoding rule (#1086): only an open request with its id, call id, label and host.
171
+ const connectRequests = pendingConnectionsOf(output);
172
+ const pendingConnections = connectRequests.map(({ serverLabel, host, reason, destinations }) => ({
173
+ serverLabel,
174
+ host,
175
+ reason,
176
+ ...(destinations ? { destinations } : {}),
177
+ }));
178
+ const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, connectRequests)]);
85
179
  const toolCalls = [];
86
180
  const pendingToolCalls = [];
87
181
  for (const item of output) {
@@ -105,19 +199,46 @@ export function buildRunEnvelope(result, agentRef) {
105
199
  status = "incomplete";
106
200
  break;
107
201
  case "requires_action":
108
- status = pendingToolCalls.length > 0 || pendingApprovals.length === 0 ? "requires_tool_output" : "requires_approval";
202
+ // A connect pause keeps exit 4 (§8, CLI agents D14): the CLI cannot answer it either.
203
+ status =
204
+ pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
205
+ ? "requires_tool_output"
206
+ : "requires_approval";
109
207
  break;
110
208
  default:
111
209
  throw new Error(`Unexpected response status: ${result?.status}`);
112
210
  }
113
211
  const responseId = result?.id ?? null;
114
212
  const ref = agentRef ?? agent;
115
- const next = status === "requires_approval" && ref && responseId
116
- ? `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`
117
- : null;
213
+ let next = null;
214
+ if (status === "requires_approval" && ref && responseId) {
215
+ next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
216
+ }
217
+ else if (status === "requires_tool_output" && pendingToolCalls.length > 0) {
218
+ // The engine answers the connect call itself on the continuation, so connecting first is enough.
219
+ if (ref && responseId) {
220
+ const decide = relayDecideCommand(ref, responseId, pendingToolCalls, pendingApprovals.length > 0);
221
+ next = pendingConnections.length > 0
222
+ ? `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${THEN_RUN}${decide}`
223
+ : decide;
224
+ }
225
+ }
226
+ else if (status === "requires_tool_output" && pendingConnections.length > 0 && responseId) {
227
+ // `agents run --continue` needs no input: the continuation re-checks access itself.
228
+ const resume = ref
229
+ ? `${THEN_RUN}2kw agents run ${shellQuote(ref)} --continue ${shellQuote(responseId)}`
230
+ : `, then run the agent again with --continue ${shellQuote(responseId)}`;
231
+ next = `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${resume}`;
232
+ }
233
+ else if (status === "requires_tool_output" && pendingConnections.length === 0 && pendingToolCalls.length === 0 && responseId) {
234
+ // Paused on nothing the envelope decodes: say so, rather than exit 4 without a word.
235
+ next =
236
+ `${undecodedPause(output)} Response ${responseId} stays paused: answer it in ${chatUrl}/ ` +
237
+ "or from a client that continues it by previous_response_id.";
238
+ }
118
239
  return {
119
240
  status,
120
- mode: null,
241
+ mode: responseMode(result),
121
242
  agent,
122
243
  version,
123
244
  responseId,
@@ -126,6 +247,7 @@ export function buildRunEnvelope(result, agentRef) {
126
247
  toolCalls,
127
248
  pendingApprovals,
128
249
  pendingToolCalls,
250
+ pendingConnections,
129
251
  incompleteReason: result?.incomplete_details?.reason ?? null,
130
252
  usage: result?.usage
131
253
  ? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
@@ -147,21 +269,51 @@ export function printRunText(env) {
147
269
  console.log(chalk.dim(`\n── ${s(head.join(" · "))}`));
148
270
  if (env.toolCalls.length)
149
271
  console.log(chalk.dim(` tools: ${env.toolCalls.map((c) => `${s(c.tool)} ${c.status === "incomplete" ? "✗" : "✓"}`).join(" ")}`));
272
+ const printApprovals = () => env.pendingApprovals.forEach((a, i) => {
273
+ console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
274
+ // JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
275
+ console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
276
+ if (a.reason)
277
+ console.log(chalk.dim(` reason: ${s(a.reason)}`));
278
+ });
150
279
  if (env.status === "requires_approval") {
151
280
  console.log(chalk.yellow("\nPaused for approval:"));
152
- env.pendingApprovals.forEach((a, i) => {
153
- console.log(` ${i + 1}. ${s(a.tool)} [${s(a.policyClass)}] ${s(a.approvalId)}`);
154
- // JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
155
- console.log(chalk.dim(` ${s(JSON.stringify(a.arguments))}`));
156
- if (a.reason)
157
- console.log(chalk.dim(` reason: ${s(a.reason)}`));
158
- });
281
+ printApprovals();
159
282
  if (env.next)
160
283
  console.log(`\nDecide with:\n ${s(env.next)}`);
161
284
  }
162
285
  else if (env.status === "requires_tool_output") {
163
- console.log(chalk.yellow("\nPaused: the agent needs client-side tool output this CLI cannot provide:"));
164
- env.pendingToolCalls.forEach((c) => console.log(` - ${s(c.tool)} (${s(c.callId)})`));
286
+ if (env.pendingToolCalls.length) {
287
+ console.log(chalk.yellow("\nPaused: the agent waits for client-side tool output:"));
288
+ env.pendingToolCalls.forEach((c) => {
289
+ console.log(` - ${s(c.tool)} (${s(c.callId)})`);
290
+ console.log(chalk.dim(` ${s(JSON.stringify(c.arguments))}`));
291
+ });
292
+ // The printed command carries --approve-all, so show what it approves (#671 P3).
293
+ if (env.pendingApprovals.length) {
294
+ console.log(chalk.yellow("\nIt also waits for approval:"));
295
+ printApprovals();
296
+ }
297
+ }
298
+ if (env.pendingConnections.length) {
299
+ console.log(chalk.yellow("\nPaused: the agent needs you to connect in chat:"));
300
+ env.pendingConnections.forEach((c) => {
301
+ console.log(` - ${s(connectionsSentence([c]))}`);
302
+ if (c.destinations?.length)
303
+ console.log(chalk.dim(` new destinations: ${s(c.destinations.join(", "))}`));
304
+ });
305
+ // The continue command on its own line, ready to copy.
306
+ if (env.next)
307
+ console.log(`\n${s(env.next.replace(THEN_RUN, ", then run:\n "))}`);
308
+ }
309
+ else if (env.pendingToolCalls.length && env.next) {
310
+ console.log(`\nAnswer with:\n ${s(env.next)}`);
311
+ }
312
+ if (!env.pendingConnections.length && !env.pendingToolCalls.length) {
313
+ console.log(chalk.yellow("\nPaused on something this CLI cannot show:"));
314
+ if (env.next)
315
+ console.log(` ${s(env.next)}`);
316
+ }
165
317
  }
166
318
  else if (env.status === "incomplete") {
167
319
  console.log(chalk.yellow(`\nIncomplete: ${s(env.incompleteReason ?? "unknown reason")}`));
@@ -1,6 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import chalk from "chalk";
3
3
  import { stripControl } from "./agent-run.js";
4
+ import { REMEMBER_NOTE, previewIsIncomplete, previewLines } from "./skills-apply-preview.js";
4
5
  /** Raised when the prompt's input closes (EOF, Ctrl+C, Ctrl+D) before an answer arrives. */
5
6
  export class PromptAbortedError extends Error {
6
7
  constructor() {
@@ -54,7 +55,23 @@ export async function promptApprovals(pending, ask, write = (line) => console.er
54
55
  const destructive = a.policyClass.toLowerCase() === "destructive";
55
56
  write(chalk.yellow(`\nApproval ${index + 1}/${pending.length}: ${s(a.tool)} [${s(a.policyClass)}]`));
56
57
  // JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
57
- write(chalk.dim(s(JSON.stringify(a.arguments, null, 2))));
58
+ const request = () => write(chalk.dim(s(JSON.stringify(a.arguments, null, 2))));
59
+ if (a.preview) {
60
+ // A SkillsApply change reads as its diff (#782, spec §10). When the preview leaves changes
61
+ // out, the full arguments follow it: this prompt never runs with --json, so it is the only
62
+ // place the member can read what would persist (spec D7).
63
+ for (const line of previewLines(a.preview))
64
+ write(colourPreviewLine(s(line)));
65
+ if (previewIsIncomplete(a.preview)) {
66
+ write(chalk.dim("Full request (the preview leaves changes out):"));
67
+ request();
68
+ }
69
+ if (!destructive)
70
+ write(chalk.dim(REMEMBER_NOTE));
71
+ }
72
+ else {
73
+ request();
74
+ }
58
75
  if (a.reason)
59
76
  write(chalk.dim(`reason: ${s(a.reason)}`));
60
77
  const question = destructive ? "[a]pprove / [r]eject? " : "[a]pprove / [r]eject / approve and [R]emember? ";
@@ -81,4 +98,17 @@ export async function promptApprovals(pending, ask, write = (line) => console.er
81
98
  }
82
99
  return answers;
83
100
  }
101
+ /** Diff lines sit indented six spaces under their file; only those are coloured. */
102
+ function colourPreviewLine(line) {
103
+ if (!line.startsWith(" "))
104
+ return line;
105
+ const body = line.slice(6);
106
+ if (body.startsWith("+"))
107
+ return chalk.green(line);
108
+ if (body.startsWith("-"))
109
+ return chalk.red(line);
110
+ if (body.startsWith("@@"))
111
+ return chalk.cyan(line);
112
+ return chalk.dim(line);
113
+ }
84
114
  //# sourceMappingURL=approval-prompt.js.map
@@ -39,6 +39,14 @@ export declare function apiKeyAuthMiddleware(config: ApiKeyConfig): Middleware;
39
39
  * fires and cannot be replayed safely.
40
40
  */
41
41
  export declare function sessionAuthMiddleware(config: SessionConfig, deps?: SessionAuthDeps): Middleware;
42
+ /** The per-request opt-in to agent memory for API-key runs (spec §3.3, D6, D7). */
43
+ export declare const MEMORY_HEADER = "X-Backbone-Memory";
44
+ /**
45
+ * Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses`, the call that starts a run
46
+ * and that also carries approval decisions (`lib/agent-decide.ts`). No other request gets
47
+ * it; the server reads it only there.
48
+ */
49
+ export declare const memoryHeaderMiddleware: Middleware;
42
50
  /**
43
51
  * The middleware stack for a resolved config, in registration order.
44
52
  *
@@ -1,5 +1,5 @@
1
1
  import createClient from "openapi-fetch";
2
- import { resolveConfig, isJsonOutput } from "./config.js";
2
+ import { resolveConfig, isJsonOutput, memoryEnabled } from "./config.js";
3
3
  import { ensureJwt } from "./auth-session.js";
4
4
  import { BackboneApiError, handleError } from "./errors.js";
5
5
  /**
@@ -85,6 +85,21 @@ export function sessionAuthMiddleware(config, deps = {}) {
85
85
  },
86
86
  };
87
87
  }
88
+ /** The per-request opt-in to agent memory for API-key runs (spec §3.3, D6, D7). */
89
+ export const MEMORY_HEADER = "X-Backbone-Memory";
90
+ /**
91
+ * Adds `X-Backbone-Memory: enabled` to `POST …/v1/responses`, the call that starts a run
92
+ * and that also carries approval decisions (`lib/agent-decide.ts`). No other request gets
93
+ * it; the server reads it only there.
94
+ */
95
+ export const memoryHeaderMiddleware = {
96
+ onRequest({ request }) {
97
+ if (request.method === "POST" && new URL(request.url).pathname.endsWith("/v1/responses")) {
98
+ request.headers.set(MEMORY_HEADER, "enabled");
99
+ }
100
+ return request;
101
+ },
102
+ };
88
103
  /**
89
104
  * The middleware stack for a resolved config, in registration order.
90
105
  *
@@ -125,6 +140,8 @@ export function getClient(command) {
125
140
  baseUrl: config.baseUrl.replace(/\/+$/, ""),
126
141
  });
127
142
  client.use(...buildMiddleware(config));
143
+ if (memoryEnabled())
144
+ client.use(memoryHeaderMiddleware);
128
145
  return client;
129
146
  }
130
147
  /**
@@ -14,6 +14,8 @@ export interface ContextEntry {
14
14
  cachedJwt?: string;
15
15
  /** Epoch millis when cachedJwt expires. */
16
16
  cachedJwtExp?: number;
17
+ /** Send `X-Backbone-Memory: enabled` on responses calls (`2kw config set memory true`, #721). */
18
+ memory?: boolean;
17
19
  }
18
20
  export interface BackboneConfigStore {
19
21
  activeContext: string;
@@ -50,11 +52,25 @@ export declare const DEFAULT_BASE_URL: string;
50
52
  */
51
53
  export declare const DEFAULT_AUTH_URL: string;
52
54
  export declare function defaultAuthUrlFor(baseUrl: string): string;
55
+ export { DEFAULT_CHAT_URL } from "./connect-pause.js";
56
+ /**
57
+ * The chat web host that belongs to an API base URL, where a member connects a connector
58
+ * (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
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
+ */
62
+ export declare function chatUrlFor(baseUrl: string, env?: NodeJS.ProcessEnv): string;
53
63
  declare const store: Conf<BackboneConfigStore>;
54
64
  export { store };
55
65
  export declare function validateContextName(name: string): void;
56
66
  export declare function getActiveContextName(): string;
57
67
  export declare function getActiveContext(): ContextEntry | undefined;
68
+ /**
69
+ * Whether responses calls opt into agent memory (spec §3.3, D6). `AI_2KW_MEMORY` wins when
70
+ * set (`true` on, anything else off), so CI can switch it without touching the store;
71
+ * otherwise the active context's `memory` key decides. Off by default.
72
+ */
73
+ export declare function memoryEnabled(env?: NodeJS.ProcessEnv): boolean;
58
74
  export declare function getAllContexts(): Record<string, ContextEntry>;
59
75
  export declare function getContextCount(): number;
60
76
  export declare function setContext(name: string, entry: ContextEntry): void;
@@ -1,6 +1,7 @@
1
1
  import Conf from "conf";
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
+ import { chatOriginFor } from "./connect-pause.js";
4
5
  /**
5
6
  * Fallback base URL when the user has no context, env var, or local config.
6
7
  * Override at runtime by setting AI_2KW_DEFAULT_BASE_URL (or the legacy
@@ -9,7 +10,7 @@ import { resolve } from "node:path";
9
10
  */
10
11
  export const DEFAULT_BASE_URL = process.env.AI_2KW_DEFAULT_BASE_URL ??
11
12
  process.env.BACKBONE_DEFAULT_BASE_URL ??
12
- "https://backbone.manfred-kunze.dev/api";
13
+ "https://api.2kw.ai";
13
14
  /**
14
15
  * Fallback auth-service URL used when the API host is not one we recognise.
15
16
  * Override at runtime with AI_2KW_DEFAULT_AUTH_URL.
@@ -36,6 +37,19 @@ export function defaultAuthUrlFor(baseUrl) {
36
37
  return DEFAULT_AUTH_URL;
37
38
  }
38
39
  }
40
+ export { DEFAULT_CHAT_URL } from "./connect-pause.js";
41
+ /**
42
+ * The chat web host that belongs to an API base URL, where a member connects a connector
43
+ * (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
44
+ * does not know; a trailing slash is dropped so callers can append a path. The mapping itself
45
+ * is `connect-pause.ts`'s {@link chatOriginFor}, shared with the MCP server and n8n (#1086).
46
+ */
47
+ export function chatUrlFor(baseUrl, env = process.env) {
48
+ const override = env.AI_2KW_CHAT_URL?.trim();
49
+ if (override)
50
+ return override.replace(/\/+$/, "");
51
+ return chatOriginFor(baseUrl);
52
+ }
39
53
  // One-shot flag so we only print the deprecation warning once per process,
40
54
  // even if resolveConfig is called multiple times across commands.
41
55
  let legacyEnvWarned = false;
@@ -142,6 +156,17 @@ export function getActiveContext() {
142
156
  const contexts = store.get("contexts") ?? {};
143
157
  return contexts[name];
144
158
  }
159
+ /**
160
+ * Whether responses calls opt into agent memory (spec §3.3, D6). `AI_2KW_MEMORY` wins when
161
+ * set (`true` on, anything else off), so CI can switch it without touching the store;
162
+ * otherwise the active context's `memory` key decides. Off by default.
163
+ */
164
+ export function memoryEnabled(env = process.env) {
165
+ const fromEnv = env.AI_2KW_MEMORY;
166
+ if (fromEnv !== undefined && fromEnv !== "")
167
+ return fromEnv === "true";
168
+ return getActiveContext()?.memory === true;
169
+ }
145
170
  export function getAllContexts() {
146
171
  return store.get("contexts") ?? {};
147
172
  }
@@ -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,147 @@
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 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 const CHAT_URL_BY_API_HOST = {
20
+ "api.2kw.ai": "https://chat.2kw.ai",
21
+ "api-dev.2kw.ai": "https://chat-dev.2kw.ai",
22
+ "backbone.manfred-kunze.dev": "https://chat.2kw.ai",
23
+ "localhost:8080": "http://localhost:3000",
24
+ "127.0.0.1:8080": "http://localhost:3000",
25
+ };
26
+ /**
27
+ * The chat web host that belongs to an API base URL, where a member connects a connector
28
+ * (`<chat>/connectors`). An unknown host, an unparsable URL or no URL at all is chat.2kw.ai.
29
+ */
30
+ export function chatOriginFor(baseUrl) {
31
+ try {
32
+ const host = new URL(String(baseUrl ?? "")).host;
33
+ // hasOwn, not a bare index: a host named after an Object.prototype key must not resolve.
34
+ return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] ?? DEFAULT_CHAT_URL : DEFAULT_CHAT_URL;
35
+ }
36
+ catch {
37
+ return DEFAULT_CHAT_URL;
38
+ }
39
+ }
40
+ export const CONNECT_REASONS = ["connect", "allow", "reconnect"];
41
+ const CONNECT_REQUEST = "backbone:connector_auth_request";
42
+ function itemsOf(output) {
43
+ if (!Array.isArray(output))
44
+ return [];
45
+ return output.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item));
46
+ }
47
+ function text(value) {
48
+ return typeof value === "string" && value.length > 0 ? value : undefined;
49
+ }
50
+ /** The synthetic tool a connector's connect pause is raised through (#806). */
51
+ export function connectToolName(serverLabel) {
52
+ return `mcp__${serverLabel}__connect`;
53
+ }
54
+ /**
55
+ * The connectors a response's `output` still waits on: every `in_progress` connect request whose
56
+ * id no other-status projection in the same list resolved, one per id, in order. An item without
57
+ * a non-empty `id`, `call_id`, `server_label` or `host` cannot be acted on and is skipped; a
58
+ * `reason` outside the three reads as `connect`, the action that always applies; only the string
59
+ * entries of `destinations` are kept, and none at all omits the field.
60
+ */
61
+ export function pendingConnectionsOf(output) {
62
+ const items = itemsOf(output);
63
+ const resolved = new Set();
64
+ for (const item of items) {
65
+ const id = text(item.id);
66
+ if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
67
+ resolved.add(id);
68
+ }
69
+ const pending = new Map();
70
+ for (const item of items) {
71
+ if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
72
+ continue;
73
+ const id = text(item.id);
74
+ const callId = text(item.call_id);
75
+ const serverLabel = text(item.server_label);
76
+ const host = text(item.host);
77
+ if (!id || !callId || !serverLabel || !host || resolved.has(id) || pending.has(id))
78
+ continue;
79
+ const reason = text(item.reason);
80
+ const destinations = Array.isArray(item.destinations)
81
+ ? item.destinations.filter((entry) => typeof entry === "string")
82
+ : [];
83
+ pending.set(id, {
84
+ id,
85
+ callId,
86
+ serverLabel,
87
+ host,
88
+ reason: reason && CONNECT_REASONS.includes(reason) ? reason : "connect",
89
+ ...(destinations.length > 0 ? { destinations } : {}),
90
+ });
91
+ }
92
+ return [...pending.values()];
93
+ }
94
+ /**
95
+ * The call ids a connect pause withholds from the caller: the call each pending request names;
96
+ * the own `call_id` of every open connect request, even one {@link pendingConnectionsOf} could not
97
+ * describe (a missing `server_label` or `host`), since a connect call is the engine's to answer,
98
+ * never the client's (R7); and every `mcp__<label>__connect` call for a connector in
99
+ * `connections` (the model may call it more than once; one request stands for all of them).
100
+ */
101
+ export function connectCallIds(output, connections) {
102
+ const names = new Set(connections.map((c) => connectToolName(c.serverLabel)));
103
+ const ids = new Set(connections.map((c) => c.callId));
104
+ const items = itemsOf(output);
105
+ // Every open connect request's own call id is withheld, even one pendingConnectionsOf could
106
+ // not fully describe (a missing server_label or host): the paired function_call must never be
107
+ // left for the caller to fabricate an output for (R7).
108
+ const resolved = new Set();
109
+ for (const item of items) {
110
+ const id = text(item.id);
111
+ if (item.type === CONNECT_REQUEST && id && item.status !== "in_progress")
112
+ resolved.add(id);
113
+ }
114
+ for (const item of items) {
115
+ if (item.type !== CONNECT_REQUEST || item.status !== "in_progress")
116
+ continue;
117
+ const id = text(item.id);
118
+ if (id && resolved.has(id))
119
+ continue;
120
+ const callId = text(item.call_id);
121
+ if (callId)
122
+ ids.add(callId);
123
+ }
124
+ for (const item of items) {
125
+ const callId = text(item.call_id);
126
+ if (item.type === "function_call" && callId && typeof item.name === "string" && names.has(item.name))
127
+ ids.add(callId);
128
+ }
129
+ return ids;
130
+ }
131
+ /** "connect erp (erp.example.com)", "allow the agent to use …" or "reconnect …". */
132
+ export function connectionPhrase(c) {
133
+ const target = `${c.serverLabel} (${c.host})`;
134
+ if (c.reason === "allow")
135
+ return `allow the agent to use ${target}`;
136
+ if (c.reason === "reconnect")
137
+ return `reconnect ${target}`;
138
+ return `connect ${target}`;
139
+ }
140
+ /** "connect a (h), allow the agent to use b (h) and reconnect c (h)"; empty for none. */
141
+ export function connectionsPhrase(connections) {
142
+ const phrases = connections.map(connectionPhrase);
143
+ if (phrases.length < 2)
144
+ return phrases[0] ?? "";
145
+ return `${phrases.slice(0, -1).join(", ")} and ${phrases[phrases.length - 1]}`;
146
+ }
147
+ //# sourceMappingURL=connect-pause.js.map
@@ -95,7 +95,9 @@ const PENDING_APPROVALS_HINT = "The approval was already decided or its response
95
95
  const CODE_HINTS = {
96
96
  approval_hmac_mismatch: PENDING_APPROVALS_HINT,
97
97
  unknown_approval_id: PENDING_APPROVALS_HINT,
98
- incomplete_tool_outputs: "Every pending approval of a paused response must be decided in one call. Use --approve-all or --reject-all, or name every id.",
98
+ incomplete_tool_outputs: "Every released tool call and every pending approval of a paused response must be answered in one call. " +
99
+ "Check pendingToolCalls and pendingApprovals in the run envelope.",
100
+ unknown_tool_output: "That call id was not released by this response. Use the responseId of the latest envelope.",
99
101
  conversation_agent_mismatch: "This conversation belongs to another agent. Start a new conversation or run the agent that owns it.",
100
102
  };
101
103
  /** The most specific hint for an API error: gateway code, then known messages, then the status table. */