@2kw/ai 6.3.0-dev.8 → 6.3.0-dev.84

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 (44) hide show
  1. package/README.md +3 -0
  2. package/dist/agent-config/schema.d.ts +3 -0
  3. package/dist/agent-config/schema.js +7 -1
  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 +37 -17
  10. package/dist/commands/agents.js +15 -1
  11. package/dist/commands/ai.js +2 -2
  12. package/dist/commands/auth.js +6 -1
  13. package/dist/commands/billing.js +5 -3
  14. package/dist/commands/config.d.ts +1 -1
  15. package/dist/commands/config.js +16 -2
  16. package/dist/commands/connectors.js +2 -1
  17. package/dist/commands/conversations.js +16 -0
  18. package/dist/commands/experiments.js +2 -2
  19. package/dist/commands/files.js +10 -28
  20. package/dist/commands/knowledge-documents.js +12 -32
  21. package/dist/commands/memory.d.ts +10 -0
  22. package/dist/commands/memory.js +132 -0
  23. package/dist/commands/settings.d.ts +13 -0
  24. package/dist/commands/settings.js +80 -0
  25. package/dist/commands/skill-versions.js +27 -1
  26. package/dist/commands/skills.js +29 -0
  27. package/dist/commands/tracing.js +5 -12
  28. package/dist/index.js +4 -0
  29. package/dist/lib/agent-decide.d.ts +3 -2
  30. package/dist/lib/agent-decide.js +5 -2
  31. package/dist/lib/agent-run.d.ts +49 -3
  32. package/dist/lib/agent-run.js +152 -9
  33. package/dist/lib/approval-prompt.js +31 -1
  34. package/dist/lib/client.d.ts +8 -0
  35. package/dist/lib/client.js +20 -1
  36. package/dist/lib/config.d.ts +16 -0
  37. package/dist/lib/config.js +39 -0
  38. package/dist/lib/errors.d.ts +6 -0
  39. package/dist/lib/errors.js +5 -2
  40. package/dist/lib/skills-apply-preview.d.ts +19 -0
  41. package/dist/lib/skills-apply-preview.js +94 -0
  42. package/dist/lib/tracing-settings.d.ts +26 -0
  43. package/dist/lib/tracing-settings.js +25 -0
  44. package/package.json +1 -1
@@ -1,4 +1,36 @@
1
1
  import chalk from "chalk";
2
+ import { DEFAULT_CHAT_URL } from "./config.js";
3
+ import { CliUsageError } from "./errors.js";
4
+ export const CONVERSATION_MODES = ["plan", "ask", "auto"];
5
+ /** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
6
+ export const MODE_OPTION_HELP = "Conversation mode: plan (read-only), ask (no automatic approver; calls that need approval wait for you), " +
7
+ "auto (the operator's policy as written). Without it, the conversation keeps its mode";
8
+ function isConversationMode(value) {
9
+ return typeof value === "string" && CONVERSATION_MODES.includes(value);
10
+ }
11
+ /**
12
+ * The `--mode` value, checked before any request is sent: anything but the three exact values is a
13
+ * usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
14
+ */
15
+ export function parseModeOption(raw) {
16
+ if (raw === undefined)
17
+ return undefined;
18
+ if (isConversationMode(raw))
19
+ return raw;
20
+ throw new CliUsageError(`Invalid --mode '${raw}': use plan, ask or auto.`);
21
+ }
22
+ /** The `backbone:mode` input item (S1 D7); always appended last. */
23
+ export function modeItem(mode) {
24
+ return { type: "backbone:mode", mode };
25
+ }
26
+ /**
27
+ * The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
28
+ * and any value other than the three read as null, which also means "none set".
29
+ */
30
+ export function responseMode(result) {
31
+ const raw = result?.conversation_mode;
32
+ return isConversationMode(raw) ? raw : null;
33
+ }
2
34
  export const EXIT_CODES = {
3
35
  completed: 0,
4
36
  requires_approval: 3,
@@ -60,7 +92,78 @@ export function shellQuote(value) {
60
92
  return value;
61
93
  return `'${value.replace(/'/g, `'\\''`)}'`;
62
94
  }
63
- export function buildRunEnvelope(result, agentRef) {
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
+ /**
109
+ * The call ids a connect pause withholds from the caller: the call each request names, and every
110
+ * `mcp__<label>__connect` call for a pending connector (the model may call it more than once; one
111
+ * request stands for all of them).
112
+ */
113
+ function connectCallIds(output, connections) {
114
+ const connectNames = new Set(connections.map((c) => `mcp__${c.serverLabel}__connect`));
115
+ const ids = new Set();
116
+ for (const item of output) {
117
+ if (item.type === "backbone:connector_auth_request" && (item.status === undefined || item.status === "in_progress")) {
118
+ ids.add(String(item.call_id));
119
+ }
120
+ else if (item.type === "function_call" && connectNames.has(String(item.name))) {
121
+ ids.add(String(item.call_id));
122
+ }
123
+ }
124
+ return ids;
125
+ }
126
+ function connectionPhrase(c) {
127
+ const target = `${c.serverLabel} (${c.host})`;
128
+ if (c.reason === "allow")
129
+ return `allow the agent to use ${target}`;
130
+ if (c.reason === "reconnect")
131
+ return `reconnect ${target}`;
132
+ return `connect ${target}`;
133
+ }
134
+ /** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
135
+ export function connectionsSentence(connections) {
136
+ const phrases = connections.map(connectionPhrase);
137
+ const joined = phrases.length > 1 ? `${phrases.slice(0, -1).join(", ")} and ${phrases.at(-1)}` : phrases[0] ?? "";
138
+ return joined.charAt(0).toUpperCase() + joined.slice(1);
139
+ }
140
+ /** Joins a connect pause's instruction to the command that continues it; {@link printRunText} breaks the line there. */
141
+ const THEN_RUN = ", then run: ";
142
+ /**
143
+ * Output item types the envelope decodes, or that never hold a run: text, reasoning and finished
144
+ * connector calls. A pause on anything else (a connector approval's `mcp_approval_request`) is named.
145
+ */
146
+ const DECODED_ITEM_TYPES = new Set([
147
+ "message",
148
+ "reasoning",
149
+ "function_call",
150
+ "function_call_output",
151
+ "backbone:approval_request",
152
+ "backbone:connector_auth_request",
153
+ "mcp_call",
154
+ "mcp_list_tools",
155
+ ]);
156
+ function undecodedPause(output) {
157
+ const types = [...new Set(output.map((i) => String(i.type)).filter((t) => !DECODED_ITEM_TYPES.has(t)))];
158
+ return types.length > 0
159
+ ? `This CLI cannot show or answer ${types.join(", ")}.`
160
+ : "This CLI cannot tell what the run waits for.";
161
+ }
162
+ /**
163
+ * @param chatUrl the chat web host of the API the run went to ({@link chatUrlFor}); a connect pause's
164
+ * `next` sends the user to its Connectors page.
165
+ */
166
+ export function buildRunEnvelope(result, agentRef, chatUrl = DEFAULT_CHAT_URL) {
64
167
  const output = result?.output ?? [];
65
168
  const { agent, version } = parseAgentModel(result?.model);
66
169
  // call_id → status of its tool output; a failed server-side tool run is marked `incomplete`.
@@ -80,8 +183,10 @@ export function buildRunEnvelope(result, agentRef) {
80
183
  arguments: parseArguments(i.arguments),
81
184
  policyClass: String(i.policy_class),
82
185
  reason: typeof i.reason === "string" && i.reason ? i.reason : null,
186
+ preview: i.preview && typeof i.preview === "object" && !Array.isArray(i.preview) ? i.preview : null,
83
187
  }));
84
- const withheldCallIds = new Set(pendingApprovals.map((a) => a.callId));
188
+ const pendingConnections = pendingConnectionsOf(output);
189
+ const withheldCallIds = new Set([...pendingApprovals.map((a) => a.callId), ...connectCallIds(output, pendingConnections)]);
85
190
  const toolCalls = [];
86
191
  const pendingToolCalls = [];
87
192
  for (const item of output) {
@@ -105,19 +210,38 @@ export function buildRunEnvelope(result, agentRef) {
105
210
  status = "incomplete";
106
211
  break;
107
212
  case "requires_action":
108
- status = pendingToolCalls.length > 0 || pendingApprovals.length === 0 ? "requires_tool_output" : "requires_approval";
213
+ // A connect pause keeps exit 4 (§8, CLI agents D14): the CLI cannot answer it either.
214
+ status =
215
+ pendingToolCalls.length > 0 || pendingConnections.length > 0 || pendingApprovals.length === 0
216
+ ? "requires_tool_output"
217
+ : "requires_approval";
109
218
  break;
110
219
  default:
111
220
  throw new Error(`Unexpected response status: ${result?.status}`);
112
221
  }
113
222
  const responseId = result?.id ?? null;
114
223
  const ref = agentRef ?? agent;
115
- const next = status === "requires_approval" && ref && responseId
116
- ? `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`
117
- : null;
224
+ let next = null;
225
+ if (status === "requires_approval" && ref && responseId) {
226
+ next = `2kw agents decide ${shellQuote(ref)} --response ${responseId} --approve-all`;
227
+ }
228
+ else if (status === "requires_tool_output" && pendingConnections.length > 0 && pendingToolCalls.length === 0 && responseId) {
229
+ // Not with a relayed call beside it: a continuation without that call's output is refused.
230
+ // `agents run --continue` needs no input: the continuation re-checks access itself.
231
+ const resume = ref
232
+ ? `${THEN_RUN}2kw agents run ${shellQuote(ref)} --continue ${shellQuote(responseId)}`
233
+ : `, then run the agent again with --continue ${shellQuote(responseId)}`;
234
+ next = `${connectionsSentence(pendingConnections)} in ${chatUrl}/connectors${resume}`;
235
+ }
236
+ else if (status === "requires_tool_output" && pendingConnections.length === 0 && pendingToolCalls.length === 0 && responseId) {
237
+ // Paused on nothing the envelope decodes: say so, rather than exit 4 without a word.
238
+ next =
239
+ `${undecodedPause(output)} Response ${responseId} stays paused: answer it in ${chatUrl}/ ` +
240
+ "or from a client that continues it by previous_response_id.";
241
+ }
118
242
  return {
119
243
  status,
120
- mode: null,
244
+ mode: responseMode(result),
121
245
  agent,
122
246
  version,
123
247
  responseId,
@@ -126,6 +250,7 @@ export function buildRunEnvelope(result, agentRef) {
126
250
  toolCalls,
127
251
  pendingApprovals,
128
252
  pendingToolCalls,
253
+ pendingConnections,
129
254
  incompleteReason: result?.incomplete_details?.reason ?? null,
130
255
  usage: result?.usage
131
256
  ? { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0 }
@@ -160,8 +285,26 @@ export function printRunText(env) {
160
285
  console.log(`\nDecide with:\n ${s(env.next)}`);
161
286
  }
162
287
  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)})`));
288
+ if (env.pendingConnections.length) {
289
+ console.log(chalk.yellow("\nPaused: the agent needs you to connect in chat:"));
290
+ env.pendingConnections.forEach((c) => {
291
+ console.log(` - ${s(connectionsSentence([c]))}`);
292
+ if (c.destinations?.length)
293
+ console.log(chalk.dim(` new destinations: ${s(c.destinations.join(", "))}`));
294
+ });
295
+ // The continue command on its own line, ready to copy.
296
+ if (env.next)
297
+ console.log(`\n${s(env.next.replace(THEN_RUN, ", then run:\n "))}`);
298
+ }
299
+ if (env.pendingToolCalls.length) {
300
+ console.log(chalk.yellow("\nPaused: the agent needs client-side tool output this CLI cannot provide:"));
301
+ env.pendingToolCalls.forEach((c) => console.log(` - ${s(c.tool)} (${s(c.callId)})`));
302
+ }
303
+ if (!env.pendingConnections.length && !env.pendingToolCalls.length) {
304
+ console.log(chalk.yellow("\nPaused on something this CLI cannot show:"));
305
+ if (env.next)
306
+ console.log(` ${s(env.next)}`);
307
+ }
165
308
  }
166
309
  else if (env.status === "incomplete") {
167
310
  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
  /**
@@ -37,6 +37,8 @@ export const errorMiddleware = {
37
37
  `HTTP ${response.status}: ${response.statusText}`,
38
38
  timestamp: body.timestamp ?? new Date().toISOString(),
39
39
  code: typeof nested?.code === "string" ? nested.code : undefined,
40
+ // A problem detail names its cause in `errorCode` (GlobalExceptionHandler).
41
+ errorCode: typeof body.errorCode === "string" ? body.errorCode : undefined,
40
42
  });
41
43
  },
42
44
  };
@@ -83,6 +85,21 @@ export function sessionAuthMiddleware(config, deps = {}) {
83
85
  },
84
86
  };
85
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
+ };
86
103
  /**
87
104
  * The middleware stack for a resolved config, in registration order.
88
105
  *
@@ -123,6 +140,8 @@ export function getClient(command) {
123
140
  baseUrl: config.baseUrl.replace(/\/+$/, ""),
124
141
  });
125
142
  client.use(...buildMiddleware(config));
143
+ if (memoryEnabled())
144
+ client.use(memoryHeaderMiddleware);
126
145
  return client;
127
146
  }
128
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
+ /** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
56
+ export declare const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
57
+ /**
58
+ * The chat web host that belongs to an API base URL, where a member connects a connector
59
+ * (`<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.
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;
@@ -36,6 +36,34 @@ export function defaultAuthUrlFor(baseUrl) {
36
36
  return DEFAULT_AUTH_URL;
37
37
  }
38
38
  }
39
+ /** Fallback chat.2kw.ai origin when the API host is not one we recognise. */
40
+ export const DEFAULT_CHAT_URL = "https://chat.2kw.ai";
41
+ /** Known API-host → chat web host mappings; a connect pause points the user there (#807 R11). */
42
+ const CHAT_URL_BY_API_HOST = {
43
+ "api.2kw.ai": "https://chat.2kw.ai",
44
+ "api-dev.2kw.ai": "https://chat-dev.2kw.ai",
45
+ "backbone.manfred-kunze.dev": "https://chat.2kw.ai",
46
+ "localhost:8080": "http://localhost:3000",
47
+ "127.0.0.1:8080": "http://localhost:3000",
48
+ };
49
+ /**
50
+ * The chat web host that belongs to an API base URL, where a member connects a connector
51
+ * (`<chat>/connectors`). `AI_2KW_CHAT_URL` overrides the mapping, for deployments the map
52
+ * does not know; a trailing slash is dropped so callers can append a path.
53
+ */
54
+ export function chatUrlFor(baseUrl, env = process.env) {
55
+ const override = env.AI_2KW_CHAT_URL?.trim();
56
+ if (override)
57
+ return override.replace(/\/+$/, "");
58
+ try {
59
+ const host = new URL(baseUrl).host;
60
+ // hasOwn, as in defaultAuthUrlFor: a host named after an Object.prototype key must not resolve.
61
+ return Object.hasOwn(CHAT_URL_BY_API_HOST, host) ? CHAT_URL_BY_API_HOST[host] : DEFAULT_CHAT_URL;
62
+ }
63
+ catch {
64
+ return DEFAULT_CHAT_URL;
65
+ }
66
+ }
39
67
  // One-shot flag so we only print the deprecation warning once per process,
40
68
  // even if resolveConfig is called multiple times across commands.
41
69
  let legacyEnvWarned = false;
@@ -142,6 +170,17 @@ export function getActiveContext() {
142
170
  const contexts = store.get("contexts") ?? {};
143
171
  return contexts[name];
144
172
  }
173
+ /**
174
+ * Whether responses calls opt into agent memory (spec §3.3, D6). `AI_2KW_MEMORY` wins when
175
+ * set (`true` on, anything else off), so CI can switch it without touching the store;
176
+ * otherwise the active context's `memory` key decides. Off by default.
177
+ */
178
+ export function memoryEnabled(env = process.env) {
179
+ const fromEnv = env.AI_2KW_MEMORY;
180
+ if (fromEnv !== undefined && fromEnv !== "")
181
+ return fromEnv === "true";
182
+ return getActiveContext()?.memory === true;
183
+ }
145
184
  export function getAllContexts() {
146
185
  return store.get("contexts") ?? {};
147
186
  }
@@ -10,12 +10,18 @@ export interface ApiErrorBody {
10
10
  timestamp: string;
11
11
  /** Machine-readable code from an OpenAI-style gateway error, e.g. `approval_hmac_mismatch`. */
12
12
  code?: string;
13
+ /**
14
+ * Machine-readable code from a problem detail's `errorCode`, e.g. `CONNECTOR_HOST_SIGN_IN_REQUIRED`.
15
+ * Kept apart from {@link code}: that one holds lowercase gateway codes and is printed by `--json`.
16
+ */
17
+ errorCode?: string;
13
18
  }
14
19
  export declare class BackboneApiError extends Error {
15
20
  readonly status: number;
16
21
  readonly errorType: string;
17
22
  readonly timestamp: string;
18
23
  readonly code?: string;
24
+ readonly errorCode?: string;
19
25
  constructor(body: Omit<ApiErrorBody, "error"> & {
20
26
  error: string;
21
27
  });
@@ -7,6 +7,7 @@ export class BackboneApiError extends Error {
7
7
  errorType;
8
8
  timestamp;
9
9
  code;
10
+ errorCode;
10
11
  constructor(body) {
11
12
  super(body.message);
12
13
  this.name = "BackboneApiError";
@@ -14,6 +15,7 @@ export class BackboneApiError extends Error {
14
15
  this.errorType = body.error;
15
16
  this.timestamp = body.timestamp;
16
17
  this.code = body.code;
18
+ this.errorCode = body.errorCode;
17
19
  }
18
20
  }
19
21
  /**
@@ -108,8 +110,9 @@ export function hintFor(err) {
108
110
  return CODE_HINTS[err.code];
109
111
  // The 403 whose cause is the credential, not the role (#805). Without this the
110
112
  // generic 403 hint tells an organization admin to check a role that is already
111
- // right, and no role would ever have fixed it.
112
- if (err.status === 403 && /^Approved connector hosts are managed by a signed-in/.test(err.message)) {
113
+ // right, and no role would ever have fixed it. Matched on the backend's error code,
114
+ // so a reworded message cannot silently drop the hint.
115
+ if (err.errorCode === "CONNECTOR_HOST_SIGN_IN_REQUIRED") {
113
116
  return "Approved hosts need a browser sign-in: run 2kw auth login. An API key cannot manage them, whatever its role.";
114
117
  }
115
118
  // Anchored on the agent service's wording: prompts and schemas send the same
@@ -0,0 +1,19 @@
1
+ import type { SkillsApplyPreview } from "./agent-run.js";
2
+ /** The grant's consequence for a SkillsApply change (spec §6, D13), shown next to "approve and Remember". */
3
+ export declare const REMEMBER_NOTE = "approve and Remember: later skill changes you make in this chat apply without asking";
4
+ /**
5
+ * A SkillsApply preview as plain text, one entry per line (#782, spec §10): per skill where it
6
+ * starts and the labels it moves, each file with its unified diff indented under it, then the
7
+ * followers snapshot and the binding. Lines are unstyled and unstripped; the prompt does both.
8
+ * The generated type marks every field optional and the wire sends nulls, so every read is guarded.
9
+ */
10
+ export declare function previewLines(preview: SkillsApplyPreview): string[];
11
+ /**
12
+ * Whether the preview leaves part of the change unseen (#782, spec D7): a skill with files left
13
+ * out, a diff that was cut, or an add or edit with no diff at all (abandoned as too large, or a
14
+ * base the server could not read). The prompt then prints the full arguments as well, because a
15
+ * change that persists must be readable at the prompt. A `put` or `delete` carries no diff by
16
+ * design and does not count. Guarded like {@link previewLines}: every field optional, nulls on the wire.
17
+ */
18
+ export declare function previewIsIncomplete(preview: SkillsApplyPreview): boolean;
19
+ //# sourceMappingURL=skills-apply-preview.d.ts.map
@@ -0,0 +1,94 @@
1
+ /** The grant's consequence for a SkillsApply change (spec §6, D13), shown next to "approve and Remember". */
2
+ export const REMEMBER_NOTE = "approve and Remember: later skill changes you make in this chat apply without asking";
3
+ const OP_WORDS = { add: "new file", edit: "edited", delete: "deleted", put: "uploaded file" };
4
+ /**
5
+ * A SkillsApply preview as plain text, one entry per line (#782, spec §10): per skill where it
6
+ * starts and the labels it moves, each file with its unified diff indented under it, then the
7
+ * followers snapshot and the binding. Lines are unstyled and unstripped; the prompt does both.
8
+ * The generated type marks every field optional and the wire sends nulls, so every read is guarded.
9
+ */
10
+ export function previewLines(preview) {
11
+ const lines = ["Skill changes:"];
12
+ for (const skill of list(preview.skills)) {
13
+ const name = String(skill.name ?? "");
14
+ const from = typeof skill.from === "number" ? skill.from : null;
15
+ const moves = list(skill.moves).map(String);
16
+ const start = from === null ? `${name}: new skill` : `${name} v${from} -> new version`;
17
+ lines.push(` ${start}${moves.length > 0 ? `, moves ${moves.join(", ")}` : ""}`);
18
+ if (typeof skill.fork_of_plugin === "string" && skill.fork_of_plugin) {
19
+ lines.push(` forks the plugin skill from ${skill.fork_of_plugin}: later plugin syncs no longer move latest`);
20
+ }
21
+ for (const file of list(skill.files))
22
+ lines.push(...fileLines(file));
23
+ const omitted = typeof skill.files_omitted === "number" ? skill.files_omitted : 0;
24
+ if (omitted > 0)
25
+ lines.push(` ${omitted} more ${omitted === 1 ? "file" : "files"} not shown`);
26
+ }
27
+ const followers = preview.followers;
28
+ if (followers) {
29
+ const agents = list(followers.agents).map(String);
30
+ const more = typeof followers.more === "number" ? followers.more : 0;
31
+ const when = typeof followers.as_of === "string" ? ` (as of ${followers.as_of})` : "";
32
+ if (agents.length === 0 && more === 0)
33
+ lines.push(`No agent follows these labels${when}`);
34
+ else
35
+ lines.push(`Agents following these labels${when}: ${agents.length > 0 ? agents.join(", ") : `${more} agents`}${agents.length > 0 && more > 0 ? ` and ${more} more` : ""}`);
36
+ }
37
+ const agent = preview.agent;
38
+ const bind = agent ? list(agent.bind).map(String) : [];
39
+ if (agent && bind.length > 0) {
40
+ const base = typeof agent.from === "number" ? ` (new agent version from v${agent.from})` : "";
41
+ lines.push(`Binds ${bind.join(", ")} to this agent${base}`);
42
+ }
43
+ return lines;
44
+ }
45
+ /**
46
+ * Whether the preview leaves part of the change unseen (#782, spec D7): a skill with files left
47
+ * out, a diff that was cut, or an add or edit with no diff at all (abandoned as too large, or a
48
+ * base the server could not read). The prompt then prints the full arguments as well, because a
49
+ * change that persists must be readable at the prompt. A `put` or `delete` carries no diff by
50
+ * design and does not count. Guarded like {@link previewLines}: every field optional, nulls on the wire.
51
+ */
52
+ export function previewIsIncomplete(preview) {
53
+ for (const skill of list(preview.skills)) {
54
+ if (typeof skill.files_omitted === "number" && skill.files_omitted > 0)
55
+ return true;
56
+ for (const file of list(skill.files)) {
57
+ if (file.truncated === true)
58
+ return true;
59
+ if ((file.op === "add" || file.op === "edit") && typeof file.diff !== "string")
60
+ return true;
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+ function fileLines(file) {
66
+ const facts = [OP_WORDS[String(file.op)] ?? String(file.op)];
67
+ if (typeof file.added === "number" && typeof file.removed === "number")
68
+ facts.push(`+${file.added} -${file.removed}`);
69
+ if (typeof file.bytes === "number")
70
+ facts.push(size(file.bytes));
71
+ const lines = [` ${String(file.path ?? "")}: ${facts.join(", ")}`];
72
+ const diff = typeof file.diff === "string" ? file.diff : null;
73
+ if (diff !== null) {
74
+ for (const line of diff.split("\n"))
75
+ lines.push(` ${line}`);
76
+ if (file.truncated === true)
77
+ lines.push(" (diff cut; the counts cover every changed line)");
78
+ }
79
+ else if (file.truncated === true) {
80
+ lines.push(` too many changes to show: at most +${Number(file.added ?? 0)} -${Number(file.removed ?? 0)} lines`);
81
+ }
82
+ return lines;
83
+ }
84
+ function list(value) {
85
+ return Array.isArray(value) ? value.filter((entry) => entry !== null && entry !== undefined) : [];
86
+ }
87
+ function size(bytes) {
88
+ if (bytes < 1024)
89
+ return `${bytes} B`;
90
+ if (bytes < 1024 * 1024)
91
+ return `${Math.round(bytes / 1024)} KB`;
92
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
93
+ }
94
+ //# sourceMappingURL=skills-apply-preview.js.map
@@ -0,0 +1,26 @@
1
+ /** The options of `tracing settings set`, as commander hands them over. */
2
+ export interface TracingSettingsOptions {
3
+ includePrompts?: string;
4
+ includeCompletions?: string;
5
+ retentionDays?: string;
6
+ }
7
+ /** The fields of a GET this command carries through. */
8
+ export interface TracingSettingsCurrent {
9
+ includePrompts?: boolean;
10
+ includeCompletions?: boolean;
11
+ retentionDays?: number | null;
12
+ }
13
+ export interface TracingSettingsBody {
14
+ includePrompts: boolean;
15
+ includeCompletions: boolean;
16
+ retentionDays: number | null;
17
+ }
18
+ /**
19
+ * The PUT body: the options given, and the current value of everything else. The PUT is a
20
+ * full replace (#1015), so a field left out would reset: a missing retentionDays would
21
+ * follow the plan again and undo the admin's shorter choice.
22
+ */
23
+ export declare function tracingSettingsBody(current: TracingSettingsCurrent | undefined, opts: TracingSettingsOptions): TracingSettingsBody;
24
+ /** `plan` follows the plan's limit; anything else must be a whole number of days, 1 or more. */
25
+ export declare function parseRetentionDays(value: string): number | null;
26
+ //# sourceMappingURL=tracing-settings.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { CliUsageError } from "./errors.js";
2
+ /**
3
+ * The PUT body: the options given, and the current value of everything else. The PUT is a
4
+ * full replace (#1015), so a field left out would reset: a missing retentionDays would
5
+ * follow the plan again and undo the admin's shorter choice.
6
+ */
7
+ export function tracingSettingsBody(current, opts) {
8
+ return {
9
+ includePrompts: opts.includePrompts !== undefined ? opts.includePrompts === "true" : (current?.includePrompts ?? false),
10
+ includeCompletions: opts.includeCompletions !== undefined
11
+ ? opts.includeCompletions === "true"
12
+ : (current?.includeCompletions ?? false),
13
+ retentionDays: opts.retentionDays !== undefined ? parseRetentionDays(opts.retentionDays) : (current?.retentionDays ?? null),
14
+ };
15
+ }
16
+ /** `plan` follows the plan's limit; anything else must be a whole number of days, 1 or more. */
17
+ export function parseRetentionDays(value) {
18
+ if (value === "plan")
19
+ return null;
20
+ if (!/^[1-9]\d*$/.test(value)) {
21
+ throw new CliUsageError(`--retention-days must be a whole number of days (1 or more) or "plan", got "${value}".`);
22
+ }
23
+ return Number(value);
24
+ }
25
+ //# sourceMappingURL=tracing-settings.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.3.0-dev.8",
3
+ "version": "6.3.0-dev.84",
4
4
  "description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
5
  "keywords": [
6
6
  "cli",