@2kw/ai 6.2.2 → 6.3.0-dev.106

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 (53) hide show
  1. package/README.md +3 -0
  2. package/dist/agent-config/schema.d.ts +6 -3
  3. package/dist/agent-config/schema.js +13 -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 +37 -17
  10. package/dist/commands/agents.js +33 -6
  11. package/dist/commands/ai.js +3 -9
  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.d.ts +10 -0
  17. package/dist/commands/connectors.js +97 -0
  18. package/dist/commands/conversations.js +16 -0
  19. package/dist/commands/datasets.js +17 -15
  20. package/dist/commands/experiments.js +36 -26
  21. package/dist/commands/files.js +10 -28
  22. package/dist/commands/installations.js +1 -1
  23. package/dist/commands/knowledge-documents.js +24 -32
  24. package/dist/commands/memory.d.ts +10 -0
  25. package/dist/commands/memory.js +132 -0
  26. package/dist/commands/prompts.js +9 -8
  27. package/dist/commands/relays.d.ts +3 -0
  28. package/dist/commands/relays.js +135 -0
  29. package/dist/commands/schemas.js +15 -6
  30. package/dist/commands/settings.d.ts +13 -0
  31. package/dist/commands/settings.js +80 -0
  32. package/dist/commands/skill-versions.js +27 -1
  33. package/dist/commands/skills.js +31 -1
  34. package/dist/commands/tracing.js +5 -12
  35. package/dist/index.js +8 -0
  36. package/dist/lib/agent-decide.d.ts +3 -2
  37. package/dist/lib/agent-decide.js +5 -2
  38. package/dist/lib/agent-run.d.ts +49 -3
  39. package/dist/lib/agent-run.js +152 -9
  40. package/dist/lib/approval-prompt.js +31 -1
  41. package/dist/lib/client.d.ts +8 -0
  42. package/dist/lib/client.js +20 -1
  43. package/dist/lib/config.d.ts +16 -0
  44. package/dist/lib/config.js +40 -1
  45. package/dist/lib/errors.d.ts +6 -0
  46. package/dist/lib/errors.js +9 -0
  47. package/dist/lib/overlay.d.ts +10 -0
  48. package/dist/lib/overlay.js +20 -0
  49. package/dist/lib/skills-apply-preview.d.ts +19 -0
  50. package/dist/lib/skills-apply-preview.js +94 -0
  51. package/dist/lib/tracing-settings.d.ts +26 -0
  52. package/dist/lib/tracing-settings.js +25 -0
  53. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Parses the value a setting takes: a JSON object. The server checks its shape per key
4
+ * (for agents.default: {"agentId": "<id>"}); this only refuses what can never be valid.
5
+ */
6
+ export declare function parseSettingValue(raw: string): Record<string, unknown>;
7
+ /**
8
+ * Organization and member settings (agent-first chat spec §3.1, §4). Without --org every
9
+ * write targets the caller's own member tier; --org writes the organization's, which needs
10
+ * the key's write role (ADMIN or OWNER for agents.default).
11
+ */
12
+ export declare function makeSettingsCommand(): Command;
13
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { Command } from "commander";
2
+ import { getClient, runAction } from "../lib/client.js";
3
+ import { CliUsageError } from "../lib/errors.js";
4
+ import { formatDetail } from "../lib/output.js";
5
+ /**
6
+ * Parses the value a setting takes: a JSON object. The server checks its shape per key
7
+ * (for agents.default: {"agentId": "<id>"}); this only refuses what can never be valid.
8
+ */
9
+ export function parseSettingValue(raw) {
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(raw);
13
+ }
14
+ catch {
15
+ throw new CliUsageError(`Setting value is not valid JSON: ${raw}`);
16
+ }
17
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
18
+ throw new CliUsageError('Setting value must be a JSON object, e.g. \'{"agentId":"agt_123"}\'');
19
+ }
20
+ return parsed;
21
+ }
22
+ /**
23
+ * Organization and member settings (agent-first chat spec §3.1, §4). Without --org every
24
+ * write targets the caller's own member tier; --org writes the organization's, which needs
25
+ * the key's write role (ADMIN or OWNER for agents.default).
26
+ */
27
+ export function makeSettingsCommand() {
28
+ const cmd = new Command("settings").description("Organization and member settings (e.g. agents.default)");
29
+ cmd
30
+ .command("get <key>")
31
+ .description("Show a setting: the effective value, its source and both tiers")
32
+ .action(async (key, _opts, command) => {
33
+ await runAction(command, async () => {
34
+ const client = getClient(command);
35
+ const { data } = await client.GET("/v1/settings/{key}", {
36
+ params: { path: { key } },
37
+ });
38
+ formatDetail(data, command);
39
+ });
40
+ });
41
+ cmd
42
+ .command("set <key> <json>")
43
+ .description("Set your own value, or the organization's with --org (ADMIN+)")
44
+ .option("--org", "Write the organization's value instead of your own")
45
+ .action(async (key, json, opts, command) => {
46
+ await runAction(command, async () => {
47
+ const client = getClient(command);
48
+ const body = { value: parseSettingValue(json) };
49
+ const { data } = opts.org
50
+ ? await client.PUT("/v1/settings/{key}/org", {
51
+ params: { path: { key } },
52
+ body,
53
+ })
54
+ : await client.PUT("/v1/settings/{key}/member", {
55
+ params: { path: { key } },
56
+ body,
57
+ });
58
+ formatDetail(data, command);
59
+ });
60
+ });
61
+ cmd
62
+ .command("unset <key>")
63
+ .description("Clear your own value, or the organization's with --org (ADMIN+)")
64
+ .option("--org", "Clear the organization's value instead of your own")
65
+ .action(async (key, opts, command) => {
66
+ await runAction(command, async () => {
67
+ const client = getClient(command);
68
+ const { data } = opts.org
69
+ ? await client.DELETE("/v1/settings/{key}/org", {
70
+ params: { path: { key } },
71
+ })
72
+ : await client.DELETE("/v1/settings/{key}/member", {
73
+ params: { path: { key } },
74
+ });
75
+ formatDetail(data, command);
76
+ });
77
+ });
78
+ return cmd;
79
+ }
80
+ //# sourceMappingURL=settings.js.map
@@ -1,4 +1,4 @@
1
- import { Command } from "commander";
1
+ import { Command, InvalidArgumentError } from "commander";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { getClient, runAction } from "../lib/client.js";
4
4
  import { formatPage, formatDetail } from "../lib/output.js";
@@ -48,6 +48,32 @@ export function makeSkillVersionsCommand() {
48
48
  formatDetail(data, command);
49
49
  });
50
50
  });
51
+ cmd
52
+ .command("restore")
53
+ .description("Restore a version as a new version; latest moves to it (the newest version is reused when it already has the content)")
54
+ .argument("<id>", "Skill ID")
55
+ .argument("<versionNumber>", "Version number to restore", parseVersionNumber)
56
+ .action(async (id, versionNumber, _opts, command) => {
57
+ await runAction(command, async () => {
58
+ const client = getClient(command);
59
+ const { data, response } = await client.POST("/v1/skills/{id}/versions/{versionNumber}/restore", {
60
+ params: { path: { id, versionNumber } },
61
+ });
62
+ // 200, not 201: the newest version already had the content and latest moved to it.
63
+ if (response.status === 200 && data) {
64
+ console.error(`No new version: v${data.versionNumber} already has the content of v${versionNumber}; latest now points at v${data.versionNumber}.`);
65
+ }
66
+ formatDetail(data, command);
67
+ });
68
+ });
51
69
  return cmd;
52
70
  }
71
+ /** A restore names an existing version by number; refuse anything else before calling the API (#783). */
72
+ function parseVersionNumber(value) {
73
+ const n = Number(value);
74
+ if (!/^\d+$/.test(value) || n < 1) {
75
+ throw new InvalidArgumentError("Version number must be a positive integer.");
76
+ }
77
+ return n;
78
+ }
53
79
  //# sourceMappingURL=skill-versions.js.map
@@ -21,6 +21,17 @@ async function checkResponse(res) {
21
21
  timestamp: body.timestamp ?? new Date().toISOString(),
22
22
  });
23
23
  }
24
+ const MIN_ROLES = ["user", "viewer", "member", "admin", "owner"];
25
+ /** A role word as the API's `Role`, or null for `none` (#785). */
26
+ function parseMinRole(word) {
27
+ const lower = word.trim().toLowerCase();
28
+ if (lower === "none")
29
+ return null;
30
+ if (MIN_ROLES.includes(lower)) {
31
+ return lower.toUpperCase();
32
+ }
33
+ throw new Error(`Role must be one of: ${MIN_ROLES.join(", ")}, none`);
34
+ }
24
35
  /** Manage skills, versions and labels (#642). */
25
36
  export function makeSkillsCommand() {
26
37
  const cmd = new Command("skills").description("Manage skills");
@@ -75,6 +86,24 @@ export function makeSkillsCommand() {
75
86
  formatDetail(data, command);
76
87
  });
77
88
  });
89
+ cmd
90
+ .command("min-role")
91
+ .description("Set the minimum organization role that sees this skill in agent runs (admin only); none removes it")
92
+ .argument("<id>", "Skill ID")
93
+ .argument("<role>", "user, viewer, member, admin, owner or none")
94
+ .action(async (id, role, _opts, command) => {
95
+ await runAction(command, async () => {
96
+ const minRole = parseMinRole(role);
97
+ const client = getClient(command);
98
+ const { data } = await client.PUT("/v1/skills/{id}/min-role", {
99
+ params: { path: { id } },
100
+ // The spec publishes minRole without null (springdoc marks no record component
101
+ // nullable); the operation documents null as "remove the minimum", and it is sent as such.
102
+ body: { minRole: minRole },
103
+ });
104
+ formatDetail(data, command);
105
+ });
106
+ });
78
107
  cmd
79
108
  .command("delete")
80
109
  .description("Delete a skill")
@@ -157,7 +186,8 @@ export function makeSkillsCommand() {
157
186
  const { data } = await client.GET("/v1/skills/{id}", { params: { path: { id } } });
158
187
  if (!data?.name)
159
188
  throw new Error("Skill name missing; specify an output path with -o.");
160
- outputPath = `${basename(data.name)}-v${versionNumber}.zip`;
189
+ // #788: ":" (a plugin skill) becomes "." before basename, which on Windows reads "a:" as a drive.
190
+ outputPath = `${basename(data.name.replace(/:/g, "."))}-v${versionNumber}.zip`;
161
191
  }
162
192
  const config = resolveConfig(command);
163
193
  const authHeader = await resolveAuthHeader(config);
@@ -1,10 +1,11 @@
1
1
  import { Command } from "commander";
2
2
  import { getClient, runAction } from "../lib/client.js";
3
3
  import { formatDetail, formatList } from "../lib/output.js";
4
+ import { tracingSettingsBody } from "../lib/tracing-settings.js";
4
5
  export function makeTracingCommand() {
5
6
  const cmd = new Command("tracing").description("Traces and per-org tracing settings (prompt / completion capture)");
6
7
  // ── settings ──────────────────────────────────────────────────
7
- const settings = new Command("settings").description("Manage per-org tracing settings (includePrompts / includeCompletions)");
8
+ const settings = new Command("settings").description("Manage per-org tracing settings (PII capture and trace retention)");
8
9
  settings
9
10
  .command("get")
10
11
  .description("Show current tracing settings")
@@ -20,6 +21,7 @@ export function makeTracingCommand() {
20
21
  .description("Update tracing settings. Both toggles control PII capture — default is false for both.")
21
22
  .option("--include-prompts <bool>", "Retain prompt content on ingested spans (true/false)")
22
23
  .option("--include-completions <bool>", "Retain completion content on ingested spans (true/false)")
24
+ .option("--retention-days <days>", 'Trace retention in days, at most your plan\'s limit, or "plan" to follow the plan. Shortening deletes older traces at the next daily sweep.')
23
25
  .action(async (opts, command) => {
24
26
  await runAction(command, async () => {
25
27
  const client = getClient(command);
@@ -27,17 +29,8 @@ export function makeTracingCommand() {
27
29
  // resetting the other. Otherwise an unset flag defaults to false
28
30
  // and silently disables capture the admin had already enabled.
29
31
  const { data: current } = await client.GET("/v1/tracing/settings");
30
- const body = {
31
- includePrompts: opts.includePrompts !== undefined
32
- ? opts.includePrompts === "true"
33
- : (current?.includePrompts ?? false),
34
- includeCompletions: opts.includeCompletions !== undefined
35
- ? opts.includeCompletions === "true"
36
- : (current?.includeCompletions ?? false),
37
- };
38
- const { data } = await client.PUT("/v1/tracing/settings", {
39
- body,
40
- });
32
+ const body = tracingSettingsBody(current, opts);
33
+ const { data } = await client.PUT("/v1/tracing/settings", { body });
41
34
  formatDetail(data, command);
42
35
  });
43
36
  });
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ import { makeDocsCommand } from "./commands/docs.js";
21
21
  import { makeContextCommand } from "./commands/context.js";
22
22
  import { makeExperimentsCommand } from "./commands/experiments.js";
23
23
  import { makeTracingCommand } from "./commands/tracing.js";
24
+ import { makeSettingsCommand } from "./commands/settings.js";
25
+ import { makeMemoryCommand } from "./commands/memory.js";
24
26
  import { makeEvaluatorsCommand } from "./commands/evaluators.js";
25
27
  import { makeScoresCommand } from "./commands/scores.js";
26
28
  import { makeQueuesCommand } from "./commands/queues.js";
@@ -29,6 +31,8 @@ import { makeConversationsCommand } from "./commands/conversations.js";
29
31
  import { makeKnowledgeCommand } from "./commands/knowledge.js";
30
32
  import { makeFilesCommand } from "./commands/files.js";
31
33
  import { makeInstallationsCommand } from "./commands/installations.js";
34
+ import { makeConnectorsCommand } from "./commands/connectors.js";
35
+ import { makeRelaysCommand } from "./commands/relays.js";
32
36
  import { checkForUpdates } from "./lib/update-notifier.js";
33
37
  const updater = checkForUpdates(pkg.version);
34
38
  const program = new Command();
@@ -60,11 +64,15 @@ program.addCommand(makeEvaluatorsCommand());
60
64
  program.addCommand(makeScoresCommand());
61
65
  program.addCommand(makeQueuesCommand());
62
66
  program.addCommand(makeTracingCommand());
67
+ program.addCommand(makeSettingsCommand());
68
+ program.addCommand(makeMemoryCommand());
63
69
  program.addCommand(makeAgentsCommand());
64
70
  program.addCommand(makeConversationsCommand());
65
71
  program.addCommand(makeKnowledgeCommand());
66
72
  program.addCommand(makeFilesCommand());
67
73
  program.addCommand(makeInstallationsCommand());
74
+ program.addCommand(makeConnectorsCommand());
75
+ program.addCommand(makeRelaysCommand());
68
76
  program.addCommand(makeDocsCommand());
69
77
  program.parseAsync().then(() => updater.notify());
70
78
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  import type { components } from "../generated/openapi.js";
2
2
  import type { ApiClient } from "./agent-lookup.js";
3
- import type { ResponsesResult } from "./agent-run.js";
3
+ import { type ConversationMode, type ResponsesResult } from "./agent-run.js";
4
4
  export type ApprovalRow = components["schemas"]["ToolApprovalDTO"];
5
5
  export interface DecideOptions {
6
6
  approve?: string[];
@@ -36,6 +36,7 @@ export declare function toApprovalItems(decisions: Decision[]): Record<string, s
36
36
  * server resolves the continuation's tools, instructions and model from this reference, so both must match
37
37
  * the run's or the continuation drops to another version or the agent's default model.
38
38
  * Never the echoed `name@<versionNumber>` (it would 404 as a label).
39
+ * With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
39
40
  */
40
- export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string): Promise<ResponsesResult>;
41
+ export declare function continueWithDecisions(client: ApiClient, agentId: string, responseId: string, decisions: Decision[], label?: string, model?: string, mode?: ConversationMode): Promise<ResponsesResult>;
41
42
  //# sourceMappingURL=agent-decide.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { modeItem } from "./agent-run.js";
1
2
  import { CliUsageError } from "./errors.js";
2
3
  import { paginationParams } from "./pagination.js";
3
4
  const MAX_PAGES = 50;
@@ -113,13 +114,15 @@ export function toApprovalItems(decisions) {
113
114
  * server resolves the continuation's tools, instructions and model from this reference, so both must match
114
115
  * the run's or the continuation drops to another version or the agent's default model.
115
116
  * Never the echoed `name@<versionNumber>` (it would 404 as a label).
117
+ * With `mode`, the `backbone:mode` item follows the decisions (#656 D7); without it, nothing is added (D2).
116
118
  */
117
- export async function continueWithDecisions(client, agentId, responseId, decisions, label, model) {
119
+ export async function continueWithDecisions(client, agentId, responseId, decisions, label, model, mode) {
120
+ const approvals = toApprovalItems(decisions);
118
121
  const body = {
119
122
  model: `agent/${agentId}${label ? `@${label}` : ""}${model ? `#${model}` : ""}`,
120
123
  previous_response_id: responseId,
121
124
  stream: false,
122
- input: toApprovalItems(decisions),
125
+ input: mode ? [...approvals, modeItem(mode)] : approvals,
123
126
  };
124
127
  // The generated body type does not model backbone: extension items, hence the cast.
125
128
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,5 +1,8 @@
1
+ import type { components } from "../generated/openapi.js";
1
2
  type AnyRecord = Record<string, any>;
2
3
  export type RunStatus = "completed" | "requires_approval" | "requires_tool_output" | "incomplete";
4
+ /** A built-in SkillsApply approval's preview (#781, spec §6); the generated type, fields all optional. */
5
+ export type SkillsApplyPreview = components["schemas"]["SkillsApplyPreview"];
3
6
  export interface PendingApproval {
4
7
  approvalId: string;
5
8
  callId: string;
@@ -8,6 +11,8 @@ export interface PendingApproval {
8
11
  policyClass: string;
9
12
  /** Why the automatic approver escalated this request to a human (#634); null when it did not. */
10
13
  reason: string | null;
14
+ /** What a built-in SkillsApply change does (#781); null for every other approval. Keyed on presence, not on the tool name (#948). */
15
+ preview: SkillsApplyPreview | null;
11
16
  }
12
17
  export interface ToolCallSummary {
13
18
  tool: string;
@@ -20,10 +25,43 @@ export interface PendingToolCall {
20
25
  callId: string;
21
26
  arguments: unknown;
22
27
  }
28
+ /**
29
+ * A connector the run waits for the user to connect, allow or reconnect in chat.2kw.ai: an open
30
+ * `backbone:connector_auth_request` (#807 R12). The caller never answers the connect call; the
31
+ * continuation re-checks access itself.
32
+ */
33
+ export interface PendingConnection {
34
+ serverLabel: string;
35
+ host: string;
36
+ /** `connect`, `allow` or `reconnect`. */
37
+ reason: string;
38
+ /** The egress difference an allow is re-asked for; absent otherwise. */
39
+ destinations?: string[];
40
+ }
41
+ /** The end user's conversation mode (epic &59); the server matches these three values exactly. */
42
+ export type ConversationMode = "plan" | "ask" | "auto";
43
+ export declare const CONVERSATION_MODES: readonly ConversationMode[];
44
+ /** `--mode` help text, in the words of the embedded chat's mode chip (#655). */
45
+ export declare const MODE_OPTION_HELP: string;
46
+ /**
47
+ * The `--mode` value, checked before any request is sent: anything but the three exact values is a
48
+ * usage error (exit 2). Commander's `.choices()` is not used because it exits 1 through `process.exit`.
49
+ */
50
+ export declare function parseModeOption(raw: string | undefined): ConversationMode | undefined;
51
+ /** The `backbone:mode` input item (S1 D7); always appended last. */
52
+ export declare function modeItem(mode: ConversationMode): {
53
+ type: "backbone:mode";
54
+ mode: ConversationMode;
55
+ };
56
+ /**
57
+ * The mode the response ran under (`conversation_mode`, #656). An absent key (a server before #656)
58
+ * and any value other than the three read as null, which also means "none set".
59
+ */
60
+ export declare function responseMode(result: ResponsesResult | undefined): ConversationMode | null;
23
61
  export interface RunEnvelope {
24
62
  status: RunStatus;
25
- /** Reserved for the server-side conversation mode (#656); always null until then. */
26
- mode: null;
63
+ /** The conversation mode the request ran under (`conversation_mode`); null when none is set. */
64
+ mode: ConversationMode | null;
27
65
  agent: string | null;
28
66
  version: number | null;
29
67
  responseId: string | null;
@@ -32,6 +70,7 @@ export interface RunEnvelope {
32
70
  toolCalls: ToolCallSummary[];
33
71
  pendingApprovals: PendingApproval[];
34
72
  pendingToolCalls: PendingToolCall[];
73
+ pendingConnections: PendingConnection[];
35
74
  incompleteReason: string | null;
36
75
  usage: {
37
76
  inputTokens: number;
@@ -56,6 +95,7 @@ export interface ResponsesResult {
56
95
  incomplete_details?: {
57
96
  reason?: string;
58
97
  } | null;
98
+ conversation_mode?: string | null;
59
99
  }
60
100
  export declare const EXIT_CODES: Record<RunStatus, number>;
61
101
  /** The assistant's text: every `output_text` part of every `message` item, in order. */
@@ -76,7 +116,13 @@ export declare function parseAgentModel(model?: string): {
76
116
  */
77
117
  export declare function stripControl(s: string): string;
78
118
  export declare function shellQuote(value: string): string;
79
- export declare function buildRunEnvelope(result: ResponsesResult | undefined, agentRef?: string): RunEnvelope;
119
+ /** "Connect a (h), allow the agent to use b (h) and reconnect c (h)". */
120
+ export declare function connectionsSentence(connections: PendingConnection[]): string;
121
+ /**
122
+ * @param chatUrl the chat web host of the API the run went to ({@link chatUrlFor}); a connect pause's
123
+ * `next` sends the user to its Connectors page.
124
+ */
125
+ export declare function buildRunEnvelope(result: ResponsesResult | undefined, agentRef?: string, chatUrl?: string): RunEnvelope;
80
126
  /** Human-readable rendering of an envelope on stdout. Every value printed is passed through {@link stripControl}. */
81
127
  export declare function printRunText(env: RunEnvelope): void;
82
128
  export {};
@@ -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