@2kw/ai 6.2.0-dev.14 → 6.2.0-dev.25

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 (40) hide show
  1. package/README.md +2 -2
  2. package/dist/agent-config/export.d.ts +31 -0
  3. package/dist/agent-config/export.js +87 -0
  4. package/dist/agent-config/file.d.ts +20 -0
  5. package/dist/agent-config/file.js +137 -0
  6. package/dist/agent-config/plan.d.ts +33 -0
  7. package/dist/agent-config/plan.js +100 -0
  8. package/dist/agent-config/schema.d.ts +420 -0
  9. package/dist/agent-config/schema.js +222 -0
  10. package/dist/agent-config/template.d.ts +6 -0
  11. package/dist/agent-config/template.js +59 -0
  12. package/dist/agent-config/validate.d.ts +29 -0
  13. package/dist/agent-config/validate.js +123 -0
  14. package/dist/commands/agent-apply.d.ts +9 -0
  15. package/dist/commands/agent-apply.js +176 -0
  16. package/dist/commands/agent-export.d.ts +3 -0
  17. package/dist/commands/agent-export.js +100 -0
  18. package/dist/commands/agent-init.d.ts +4 -0
  19. package/dist/commands/agent-init.js +54 -0
  20. package/dist/commands/agent-run.d.ts +5 -0
  21. package/dist/commands/agent-run.js +187 -0
  22. package/dist/commands/agent-versions.js +12 -7
  23. package/dist/commands/agents.js +24 -9
  24. package/dist/commands/ai.d.ts +5 -37
  25. package/dist/commands/ai.js +29 -136
  26. package/dist/lib/agent-decide.d.ts +40 -0
  27. package/dist/lib/agent-decide.js +128 -0
  28. package/dist/lib/agent-lookup.d.ts +14 -0
  29. package/dist/lib/agent-lookup.js +70 -0
  30. package/dist/lib/agent-models.d.ts +28 -0
  31. package/dist/lib/agent-models.js +53 -0
  32. package/dist/lib/agent-run.d.ts +83 -0
  33. package/dist/lib/agent-run.js +170 -0
  34. package/dist/lib/approval-prompt.d.ts +18 -0
  35. package/dist/lib/approval-prompt.js +84 -0
  36. package/dist/lib/client.d.ts +2 -0
  37. package/dist/lib/client.js +13 -2
  38. package/dist/lib/errors.d.ts +21 -2
  39. package/dist/lib/errors.js +46 -2
  40. package/package.json +5 -2
@@ -0,0 +1,84 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import chalk from "chalk";
3
+ import { stripControl } from "./agent-run.js";
4
+ /** Raised when the prompt's input closes (EOF, Ctrl+C, Ctrl+D) before an answer arrives. */
5
+ export class PromptAbortedError extends Error {
6
+ constructor() {
7
+ super("Approval prompt closed");
8
+ this.name = "PromptAbortedError";
9
+ }
10
+ }
11
+ /**
12
+ * Prompts go to stderr so stdout stays the agent's answer. A closed input never
13
+ * leaves a question hanging: every pending and later `ask` rejects with
14
+ * {@link PromptAbortedError}.
15
+ */
16
+ export function readlineAsk(input = process.stdin, output = process.stderr) {
17
+ const rl = createInterface({ input, output, terminal: Boolean(input.isTTY) });
18
+ let closed = false;
19
+ let abortPending;
20
+ rl.once("close", () => {
21
+ closed = true;
22
+ abortPending?.();
23
+ });
24
+ const ask = (question) => new Promise((resolve, reject) => {
25
+ if (closed) {
26
+ reject(new PromptAbortedError());
27
+ return;
28
+ }
29
+ let settled = false;
30
+ const abort = () => settle(() => reject(new PromptAbortedError()));
31
+ const settle = (finish) => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ if (abortPending === abort)
36
+ abortPending = undefined;
37
+ finish();
38
+ };
39
+ abortPending = abort;
40
+ rl.question(question).then((answer) => settle(() => resolve(answer)), (err) => settle(() => reject(err?.name === "AbortError" ? new PromptAbortedError() : err)));
41
+ });
42
+ return {
43
+ ask,
44
+ close: () => {
45
+ if (!closed)
46
+ rl.close();
47
+ },
48
+ };
49
+ }
50
+ export async function promptApprovals(pending, ask, write = (line) => console.error(line)) {
51
+ const answers = new Map();
52
+ const s = stripControl;
53
+ for (const [index, a] of pending.entries()) {
54
+ const destructive = a.policyClass.toLowerCase() === "destructive";
55
+ write(chalk.yellow(`\nApproval ${index + 1}/${pending.length}: ${s(a.tool)} [${s(a.policyClass)}]`));
56
+ // JSON.stringify escapes C0 controls but leaves DEL and C1 raw.
57
+ write(chalk.dim(s(JSON.stringify(a.arguments, null, 2))));
58
+ if (a.reason)
59
+ write(chalk.dim(`reason: ${s(a.reason)}`));
60
+ const question = destructive ? "[a]pprove / [r]eject? " : "[a]pprove / [r]eject / approve and [R]emember? ";
61
+ const hint = destructive
62
+ ? "Answer a (approve), r (reject)"
63
+ : "Answer a (approve), r (reject), or R (approve and remember)";
64
+ for (;;) {
65
+ const reply = (await ask(question)).trim();
66
+ if (reply === "a") {
67
+ answers.set(a.approvalId, { decision: "approve" });
68
+ break;
69
+ }
70
+ if (reply === "R" && !destructive) {
71
+ answers.set(a.approvalId, { decision: "approve", remember: true });
72
+ break;
73
+ }
74
+ if (reply === "r") {
75
+ const reason = (await ask("Reason (optional): ")).trim();
76
+ answers.set(a.approvalId, reason ? { decision: "reject", reason } : { decision: "reject" });
77
+ break;
78
+ }
79
+ write(hint);
80
+ }
81
+ }
82
+ return answers;
83
+ }
84
+ //# sourceMappingURL=approval-prompt.js.map
@@ -23,6 +23,8 @@ export interface SessionAuthDeps {
23
23
  }
24
24
  /**
25
25
  * Error-handling middleware: intercepts non-ok responses and throws BackboneApiError.
26
+ * The api-key filter now returns the OpenAI envelope (#658); the surface
27
+ * filter still sends the flat shape, so both are normalized here.
26
28
  */
27
29
  export declare const errorMiddleware: Middleware;
28
30
  /**
@@ -9,6 +9,8 @@ import { BackboneApiError, handleError } from "./errors.js";
9
9
  export const RETRY_MARKER = "x-2kw-auth-retried";
10
10
  /**
11
11
  * Error-handling middleware: intercepts non-ok responses and throws BackboneApiError.
12
+ * The api-key filter now returns the OpenAI envelope (#658); the surface
13
+ * filter still sends the flat shape, so both are normalized here.
12
14
  */
13
15
  export const errorMiddleware = {
14
16
  async onResponse({ response }) {
@@ -21,11 +23,20 @@ export const errorMiddleware = {
21
23
  catch {
22
24
  body = {};
23
25
  }
26
+ // Gateway endpoints and the api-key filter answer in the OpenAI shape
27
+ // {error: {message, type, param, code}}; its code is also kept on its own.
28
+ const nested = body && typeof body.error === "object" && body.error !== null ? body.error : undefined;
24
29
  throw new BackboneApiError({
25
30
  status: body.status ?? response.status,
26
- error: body.title ?? body.error ?? response.statusText,
27
- message: body.detail ?? body.message ?? `HTTP ${response.status}: ${response.statusText}`,
31
+ error: typeof body.error === "string"
32
+ ? body.error
33
+ : (nested?.code ?? nested?.type ?? body.title ?? response.statusText),
34
+ message: body.detail ??
35
+ body.message ??
36
+ nested?.message ??
37
+ `HTTP ${response.status}: ${response.statusText}`,
28
38
  timestamp: body.timestamp ?? new Date().toISOString(),
39
+ code: typeof nested?.code === "string" ? nested.code : undefined,
29
40
  });
30
41
  },
31
42
  };
@@ -1,14 +1,31 @@
1
1
  export interface ApiErrorBody {
2
2
  status: number;
3
- error: string;
3
+ error?: string | {
4
+ message?: string;
5
+ type?: string;
6
+ param?: string | null;
7
+ code?: string | null;
8
+ };
4
9
  message: string;
5
10
  timestamp: string;
11
+ /** Machine-readable code from an OpenAI-style gateway error, e.g. `approval_hmac_mismatch`. */
12
+ code?: string;
6
13
  }
7
14
  export declare class BackboneApiError extends Error {
8
15
  readonly status: number;
9
16
  readonly errorType: string;
10
17
  readonly timestamp: string;
11
- constructor(body: ApiErrorBody);
18
+ readonly code?: string;
19
+ constructor(body: Omit<ApiErrorBody, "error"> & {
20
+ error: string;
21
+ });
22
+ }
23
+ /**
24
+ * Invalid usage, invalid config, or a local check that failed before any request.
25
+ * `handleError` maps it to exit code 2 so scripted callers can tell it from an API failure.
26
+ */
27
+ export declare class CliUsageError extends Error {
28
+ constructor(message: string);
12
29
  }
13
30
  /**
14
31
  * Discriminated cause of a failed credential check. Lets callers give a
@@ -28,5 +45,7 @@ export type AuthFailureKind = "UNAUTHORIZED" | "FORBIDDEN" | "UNREACHABLE" | "UN
28
45
  * - Everything else → UNKNOWN.
29
46
  */
30
47
  export declare function classifyAuthFailure(err: unknown): AuthFailureKind;
48
+ /** The most specific hint for an API error: gateway code, then known messages, then the status table. */
49
+ export declare function hintFor(err: BackboneApiError): string | undefined;
31
50
  export declare function handleError(err: unknown, json: boolean): void;
32
51
  //# sourceMappingURL=errors.d.ts.map
@@ -6,12 +6,24 @@ export class BackboneApiError extends Error {
6
6
  status;
7
7
  errorType;
8
8
  timestamp;
9
+ code;
9
10
  constructor(body) {
10
11
  super(body.message);
11
12
  this.name = "BackboneApiError";
12
13
  this.status = body.status;
13
14
  this.errorType = body.error;
14
15
  this.timestamp = body.timestamp;
16
+ this.code = body.code;
17
+ }
18
+ }
19
+ /**
20
+ * Invalid usage, invalid config, or a local check that failed before any request.
21
+ * `handleError` maps it to exit code 2 so scripted callers can tell it from an API failure.
22
+ */
23
+ export class CliUsageError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "CliUsageError";
15
27
  }
16
28
  }
17
29
  // Node/undici surface network faults through the error's `code`. A DNS or
@@ -77,6 +89,37 @@ const HINTS = {
77
89
  422: "Validation error. Check the input values.",
78
90
  429: "Rate limit exceeded. Wait a moment and try again.",
79
91
  };
92
+ const PENDING_APPROVALS_HINT = "The approval was already decided or its response was superseded. See what is still pending: 2kw agents approvals --agent <agent-id> --status pending";
93
+ const CODE_HINTS = {
94
+ approval_hmac_mismatch: PENDING_APPROVALS_HINT,
95
+ unknown_approval_id: PENDING_APPROVALS_HINT,
96
+ 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.",
97
+ conversation_agent_mismatch: "This conversation belongs to another agent. Start a new conversation or run the agent that owns it.",
98
+ };
99
+ /** The most specific hint for an API error: gateway code, then known messages, then the status table. */
100
+ export function hintFor(err) {
101
+ if (err.code === "invalid_approval_decision" && /decided automatically/.test(err.message)) {
102
+ return "This request was already decided by the automatic approver; nothing to decide.";
103
+ }
104
+ if (err.code === "invalid_approval_decision" && /decided by a conversation grant/.test(err.message)) {
105
+ return "This request was already approved by a conversation grant; nothing to decide.";
106
+ }
107
+ if (err.code && CODE_HINTS[err.code])
108
+ return CODE_HINTS[err.code];
109
+ // Anchored on the agent service's wording: prompts and schemas send the same
110
+ // "Label '<x>' not found" prefix, and must keep the generic 404 hint.
111
+ if (err.status === 404 && /^Label 'latest' not found (on agent|for this agent)/.test(err.message)) {
112
+ return "The agent has no published version. Run: 2kw agents apply -f agent.yaml (or 2kw agents versions create).";
113
+ }
114
+ if (err.status === 404 && /^Label '.+' not found (on agent|for this agent)/.test(err.message)) {
115
+ return "That label does not exist on the agent. List labels: 2kw agents labels list --agent <agent-id>";
116
+ }
117
+ // Only a skill-binding resolution failure, not any 422 that mentions a skill.
118
+ if (err.status === 422 && /^Skill '.+' (cannot be resolved|is archived)/.test(err.message)) {
119
+ return "Check the skill exists, is active, and has that label or version: 2kw skills list";
120
+ }
121
+ return HINTS[err.status];
122
+ }
80
123
  /**
81
124
  * Drop the cached org JWT of the active session context.
82
125
  *
@@ -114,13 +157,14 @@ export function handleError(err, json) {
114
157
  console.error(JSON.stringify({
115
158
  error: err.errorType,
116
159
  status: err.status,
160
+ ...(err.code ? { code: err.code } : {}),
117
161
  message: err.message,
118
162
  timestamp: err.timestamp,
119
163
  }));
120
164
  }
121
165
  else {
122
166
  console.error(chalk.red(`Error ${err.status}: ${err.message}`));
123
- const hint = HINTS[err.status];
167
+ const hint = hintFor(err);
124
168
  if (hint) {
125
169
  console.error(chalk.yellow(`Hint: ${hint}`));
126
170
  }
@@ -151,6 +195,6 @@ export function handleError(err, json) {
151
195
  else {
152
196
  console.error(chalk.red(`Unknown error: ${String(err)}`));
153
197
  }
154
- process.exitCode = 1;
198
+ process.exitCode = err instanceof CliUsageError ? 2 : 1;
155
199
  }
156
200
  //# sourceMappingURL=errors.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.2.0-dev.14",
3
+ "version": "6.2.0-dev.25",
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",
@@ -38,6 +38,7 @@
38
38
  "check:generated": "tsx openapi/scripts/check-generated.ts",
39
39
  "fix:generated": "tsx openapi/scripts/check-generated.ts --write",
40
40
  "check:coverage": "tsx openapi/scripts/check-coverage.ts",
41
+ "sync:agent-schema": "tsx scripts/sync-agent-schema.ts",
41
42
  "typecheck": "tsc --noEmit",
42
43
  "test": "vitest",
43
44
  "test:run": "vitest run",
@@ -45,13 +46,15 @@
45
46
  "test:contract": "vitest run tests/contract"
46
47
  },
47
48
  "dependencies": {
49
+ "ajv": "^8.20.0",
48
50
  "chalk": "^5.6.2",
49
51
  "cli-table3": "^0.6.5",
50
52
  "commander": "^13.1.0",
51
53
  "conf": "^13.1.0",
52
54
  "open": "^10.2.0",
53
55
  "openapi-fetch": "^0.13.5",
54
- "ora": "^8.2.0"
56
+ "ora": "^8.2.0",
57
+ "yaml": "^2.9.1"
55
58
  },
56
59
  "devDependencies": {
57
60
  "@types/node": "^22.20.1",