@sweny-ai/core 0.1.102 → 0.1.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +3 -2
- package/dist/browser.js +6 -1
- package/dist/claude.d.ts +66 -3
- package/dist/claude.js +334 -29
- package/dist/cli/check.d.ts +7 -0
- package/dist/cli/check.js +91 -5
- package/dist/cli/config-file.js +5 -1
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +19 -1
- package/dist/cli/e2e.d.ts +14 -1
- package/dist/cli/e2e.js +40 -6
- package/dist/cli/main.d.ts +3 -1
- package/dist/cli/main.js +26 -16
- package/dist/cli/new.js +7 -0
- package/dist/eval/value.js +12 -1
- package/dist/executor.d.ts +16 -0
- package/dist/executor.js +81 -18
- package/dist/index.d.ts +8 -4
- package/dist/index.js +10 -2
- package/dist/inputs.js +18 -0
- package/dist/loader.js +15 -1
- package/dist/model.d.ts +11 -0
- package/dist/model.js +19 -0
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +2 -2
- package/dist/schema.d.ts +163 -6
- package/dist/schema.js +125 -10
- package/dist/skills/custom-loader.d.ts +3 -3
- package/dist/skills/custom-loader.js +50 -14
- package/dist/skills/github.js +8 -2
- package/dist/skills/linear.js +26 -0
- package/dist/skills/notification.d.ts +17 -0
- package/dist/skills/notification.js +67 -5
- package/dist/source-resolver.js +231 -6
- package/dist/sources.d.ts +33 -0
- package/dist/types.d.ts +29 -1
- package/dist/types.js +17 -0
- package/package.json +4 -4
package/dist/browser.d.ts
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* - `buildWorkflow` / `refineWorkflow` — depend on the Claude interface.
|
|
14
14
|
* - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
|
|
15
15
|
*/
|
|
16
|
-
export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, } from "./types.js";
|
|
16
|
+
export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
17
17
|
export { consoleLogger } from "./types.js";
|
|
18
|
+
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
18
19
|
export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
19
20
|
export type { SkillValidationResult } from "./skills/index.js";
|
|
20
|
-
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
|
|
21
|
+
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
21
22
|
export type { WorkflowError } from "./schema.js";
|
|
22
23
|
export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
|
|
23
24
|
export type { FlowNode, SkillNodeData, FlowEdge } from "./studio.js";
|
package/dist/browser.js
CHANGED
|
@@ -14,9 +14,14 @@
|
|
|
14
14
|
* - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
|
|
15
15
|
*/
|
|
16
16
|
export { consoleLogger } from "./types.js";
|
|
17
|
+
// Runtime enum constants + skill-id helpers. Pure data / regex / pure
|
|
18
|
+
// function from types.ts, which is already proven browser-safe (schema.ts
|
|
19
|
+
// imports it). A Studio dropdown rendering evaluator kinds, or validating a
|
|
20
|
+
// skill id, needs these without hardcoding the members.
|
|
21
|
+
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
17
22
|
// Skills (browser-safe — no filesystem access, no `process.env` reads)
|
|
18
23
|
export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
19
24
|
// Schema & validation
|
|
20
|
-
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
|
|
25
|
+
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
21
26
|
// Studio adapter
|
|
22
27
|
export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
|
package/dist/claude.d.ts
CHANGED
|
@@ -9,6 +9,43 @@
|
|
|
9
9
|
* is to use headless Claude Code — never the raw Anthropic API.
|
|
10
10
|
*/
|
|
11
11
|
import type { Claude, Tool, ToolContext, NodeResult, JSONSchema, Logger, McpServerConfig } from "./types.js";
|
|
12
|
+
/** How sweny resolves which credentials reach the Claude Code subprocess. */
|
|
13
|
+
export type SwenyAuthMode = "auto" | "api-key" | "oauth";
|
|
14
|
+
export interface ResolveAuthEnvOpts {
|
|
15
|
+
/** Explicit override; when absent, read from `env.SWENY_AUTH`. Test seam. */
|
|
16
|
+
mode?: SwenyAuthMode;
|
|
17
|
+
/** Logger for the debug line + invalid-value warning. */
|
|
18
|
+
logger?: Pick<Logger, "debug" | "warn">;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide which auth credentials survive into the Claude Code subprocess env.
|
|
22
|
+
*
|
|
23
|
+
* sweny does not select the winning credential (the spawned agent and any
|
|
24
|
+
* on-disk `~/.claude/.credentials.json` do that). This function only controls
|
|
25
|
+
* what is *present* in the env we hand the agent.
|
|
26
|
+
*
|
|
27
|
+
* Modes (`SWENY_AUTH`, default `auto`):
|
|
28
|
+
* - `auto` — today's protective behavior: when an OAuth token is present,
|
|
29
|
+
* strip `ANTHROPIC_API_KEY` so a stray `.env` key cannot
|
|
30
|
+
* silently bill a metered API account. `ANTHROPIC_AUTH_TOKEN`
|
|
31
|
+
* (a bearer token, not a console key) is never touched.
|
|
32
|
+
* - `api-key` — user explicitly authenticates a gateway with a key/bearer:
|
|
33
|
+
* preserve both even when an OAuth token is also present.
|
|
34
|
+
* - `oauth` — user explicitly wants subscription/OAuth: strip both
|
|
35
|
+
* `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` so a leftover
|
|
36
|
+
* credential cannot win at the agent. Fails closed.
|
|
37
|
+
*
|
|
38
|
+
* Base-URL presence is deliberately NOT a signal here: pass-through proxies
|
|
39
|
+
* (corporate egress, observability) set `ANTHROPIC_BASE_URL` while still
|
|
40
|
+
* billing the real key, so inferring "use the key" from a base URL would
|
|
41
|
+
* re-introduce a surprise-billing path. Intent must be explicit.
|
|
42
|
+
*
|
|
43
|
+
* An unrecognized `SWENY_AUTH` value falls back to `auto` with a warning
|
|
44
|
+
* (never throws — an env typo must not crash a CI run).
|
|
45
|
+
*
|
|
46
|
+
* Pure: returns a copy, never mutates the input.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveAuthEnv(env: Record<string, string>, opts?: ResolveAuthEnvOpts): Record<string, string>;
|
|
12
49
|
export interface ClaudeClientOptions {
|
|
13
50
|
/** Model override */
|
|
14
51
|
model?: string;
|
|
@@ -23,6 +60,20 @@ export interface ClaudeClientOptions {
|
|
|
23
60
|
/** External MCP servers (GitHub, Linear, Sentry, etc.) — merged with core skill tools */
|
|
24
61
|
mcpServers?: Record<string, McpServerConfig>;
|
|
25
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Wire an optional timeout + caller signal onto a single AbortController.
|
|
65
|
+
*
|
|
66
|
+
* Returns undefined when neither a timeout nor a signal is supplied so the
|
|
67
|
+
* default code path (no abortController, no timer) is byte-for-byte unchanged.
|
|
68
|
+
*
|
|
69
|
+
* `reason()` reports whether the abort came from the timeout timer or an
|
|
70
|
+
* external signal, so callers can log a distinct timeout message.
|
|
71
|
+
*/
|
|
72
|
+
export declare function makeAbort(timeoutMs?: number, signal?: AbortSignal): {
|
|
73
|
+
controller: AbortController;
|
|
74
|
+
clear: () => void;
|
|
75
|
+
reason: () => "timeout" | "signal" | undefined;
|
|
76
|
+
} | undefined;
|
|
26
77
|
export declare class ClaudeClient implements Claude {
|
|
27
78
|
private model;
|
|
28
79
|
private maxTurns;
|
|
@@ -32,9 +83,8 @@ export declare class ClaudeClient implements Claude {
|
|
|
32
83
|
private mcpServers;
|
|
33
84
|
constructor(opts?: ClaudeClientOptions);
|
|
34
85
|
/**
|
|
35
|
-
* Build env for the Claude Code subprocess.
|
|
36
|
-
*
|
|
37
|
-
* overriding the user's subscription-based auth.
|
|
86
|
+
* Build env for the Claude Code subprocess. Snapshots process.env (dropping
|
|
87
|
+
* nullish values) and applies auth precedence via {@link resolveAuthEnv}.
|
|
38
88
|
*/
|
|
39
89
|
private buildEnv;
|
|
40
90
|
run(opts: {
|
|
@@ -45,6 +95,11 @@ export declare class ClaudeClient implements Claude {
|
|
|
45
95
|
onProgress?: (message: string) => void;
|
|
46
96
|
maxTurns?: number;
|
|
47
97
|
disallowedTools?: string[];
|
|
98
|
+
model?: string;
|
|
99
|
+
/** Abort the query after this many ms. Default: no timeout (back-compat). */
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
/** Caller-supplied abort signal. Aborting it interrupts the query. */
|
|
102
|
+
signal?: AbortSignal;
|
|
48
103
|
}): Promise<NodeResult>;
|
|
49
104
|
evaluate(opts: {
|
|
50
105
|
question: string;
|
|
@@ -53,11 +108,19 @@ export declare class ClaudeClient implements Claude {
|
|
|
53
108
|
id: string;
|
|
54
109
|
description: string;
|
|
55
110
|
}[];
|
|
111
|
+
/** Abort the query after this many ms. Default: no timeout (back-compat). */
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
/** Caller-supplied abort signal. Aborting it interrupts the query. */
|
|
114
|
+
signal?: AbortSignal;
|
|
56
115
|
}): Promise<string>;
|
|
57
116
|
ask(opts: {
|
|
58
117
|
instruction: string;
|
|
59
118
|
context: Record<string, unknown>;
|
|
60
119
|
model?: string;
|
|
120
|
+
/** Abort the query after this many ms. Default: no timeout (back-compat). */
|
|
121
|
+
timeoutMs?: number;
|
|
122
|
+
/** Caller-supplied abort signal. Aborting it interrupts the query. */
|
|
123
|
+
signal?: AbortSignal;
|
|
61
124
|
}): Promise<string>;
|
|
62
125
|
}
|
|
63
126
|
/**
|
package/dist/claude.js
CHANGED
|
@@ -12,6 +12,131 @@ import { query, createSdkMcpServer, tool as sdkTool } from "@anthropic-ai/claude
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { consoleLogger } from "./types.js";
|
|
14
14
|
const SYSTEM_PROMPT = `You are a step in an automated workflow. Execute the instruction precisely using the tools available to you. Be thorough but concise. When you're done, summarize your findings and results.`;
|
|
15
|
+
/**
|
|
16
|
+
* Decide which auth credentials survive into the Claude Code subprocess env.
|
|
17
|
+
*
|
|
18
|
+
* sweny does not select the winning credential (the spawned agent and any
|
|
19
|
+
* on-disk `~/.claude/.credentials.json` do that). This function only controls
|
|
20
|
+
* what is *present* in the env we hand the agent.
|
|
21
|
+
*
|
|
22
|
+
* Modes (`SWENY_AUTH`, default `auto`):
|
|
23
|
+
* - `auto` — today's protective behavior: when an OAuth token is present,
|
|
24
|
+
* strip `ANTHROPIC_API_KEY` so a stray `.env` key cannot
|
|
25
|
+
* silently bill a metered API account. `ANTHROPIC_AUTH_TOKEN`
|
|
26
|
+
* (a bearer token, not a console key) is never touched.
|
|
27
|
+
* - `api-key` — user explicitly authenticates a gateway with a key/bearer:
|
|
28
|
+
* preserve both even when an OAuth token is also present.
|
|
29
|
+
* - `oauth` — user explicitly wants subscription/OAuth: strip both
|
|
30
|
+
* `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` so a leftover
|
|
31
|
+
* credential cannot win at the agent. Fails closed.
|
|
32
|
+
*
|
|
33
|
+
* Base-URL presence is deliberately NOT a signal here: pass-through proxies
|
|
34
|
+
* (corporate egress, observability) set `ANTHROPIC_BASE_URL` while still
|
|
35
|
+
* billing the real key, so inferring "use the key" from a base URL would
|
|
36
|
+
* re-introduce a surprise-billing path. Intent must be explicit.
|
|
37
|
+
*
|
|
38
|
+
* An unrecognized `SWENY_AUTH` value falls back to `auto` with a warning
|
|
39
|
+
* (never throws — an env typo must not crash a CI run).
|
|
40
|
+
*
|
|
41
|
+
* Pure: returns a copy, never mutates the input.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveAuthEnv(env, opts = {}) {
|
|
44
|
+
const out = { ...env };
|
|
45
|
+
const raw = opts.mode ?? env.SWENY_AUTH;
|
|
46
|
+
let mode = "auto";
|
|
47
|
+
if (raw === "auto" || raw === "api-key" || raw === "oauth") {
|
|
48
|
+
mode = raw;
|
|
49
|
+
}
|
|
50
|
+
else if (raw) {
|
|
51
|
+
// truthy but unrecognized — empty string falls through silently to auto
|
|
52
|
+
opts.logger?.warn?.(`SWENY_AUTH="${raw}" is not one of auto|api-key|oauth; falling back to auto`);
|
|
53
|
+
}
|
|
54
|
+
// Truthy checks throughout: action.yml sets these to "" when an input is
|
|
55
|
+
// omitted, and empty string must read as unset.
|
|
56
|
+
const hasOauth = !!out.CLAUDE_CODE_OAUTH_TOKEN;
|
|
57
|
+
if (mode === "oauth") {
|
|
58
|
+
delete out.ANTHROPIC_API_KEY;
|
|
59
|
+
delete out.ANTHROPIC_AUTH_TOKEN;
|
|
60
|
+
}
|
|
61
|
+
else if (mode === "api-key") {
|
|
62
|
+
// preserve key + bearer; do not strip even if an OAuth token is present
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// auto — byte-for-byte the historical behavior
|
|
66
|
+
if (hasOauth) {
|
|
67
|
+
delete out.ANTHROPIC_API_KEY;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Mode only, never credential values.
|
|
71
|
+
opts.logger?.debug?.(`[sweny] auth mode: ${mode}`);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Wire an optional timeout + caller signal onto a single AbortController.
|
|
76
|
+
*
|
|
77
|
+
* Returns undefined when neither a timeout nor a signal is supplied so the
|
|
78
|
+
* default code path (no abortController, no timer) is byte-for-byte unchanged.
|
|
79
|
+
*
|
|
80
|
+
* `reason()` reports whether the abort came from the timeout timer or an
|
|
81
|
+
* external signal, so callers can log a distinct timeout message.
|
|
82
|
+
*/
|
|
83
|
+
export function makeAbort(timeoutMs, signal) {
|
|
84
|
+
if (!timeoutMs && !signal)
|
|
85
|
+
return undefined;
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
let reason;
|
|
88
|
+
let timer;
|
|
89
|
+
const onSignalAbort = () => {
|
|
90
|
+
if (controller.signal.aborted)
|
|
91
|
+
return;
|
|
92
|
+
reason = "signal";
|
|
93
|
+
controller.abort();
|
|
94
|
+
};
|
|
95
|
+
if (signal) {
|
|
96
|
+
if (signal.aborted) {
|
|
97
|
+
reason = "signal";
|
|
98
|
+
controller.abort();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
signal.addEventListener("abort", onSignalAbort, { once: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (timeoutMs && timeoutMs > 0 && !controller.signal.aborted) {
|
|
105
|
+
timer = setTimeout(() => {
|
|
106
|
+
if (controller.signal.aborted)
|
|
107
|
+
return;
|
|
108
|
+
reason = "timeout";
|
|
109
|
+
controller.abort();
|
|
110
|
+
}, timeoutMs);
|
|
111
|
+
// Don't keep the event loop alive just for the abort timer.
|
|
112
|
+
timer?.unref?.();
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
controller,
|
|
116
|
+
reason: () => reason,
|
|
117
|
+
clear: () => {
|
|
118
|
+
if (timer)
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Best-effort interrupt of an SDK query stream. Called from `finally` so an
|
|
126
|
+
* early return / timeout / throw does not leak the underlying subprocess.
|
|
127
|
+
* Swallows errors: the stream may already be done, and interrupt is only
|
|
128
|
+
* supported in streaming-input mode.
|
|
129
|
+
*/
|
|
130
|
+
async function interruptStream(stream) {
|
|
131
|
+
if (!stream || typeof stream.interrupt !== "function")
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
await stream.interrupt();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* already finished or not interruptible — nothing to clean up */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
15
140
|
export class ClaudeClient {
|
|
16
141
|
model;
|
|
17
142
|
maxTurns;
|
|
@@ -28,19 +153,16 @@ export class ClaudeClient {
|
|
|
28
153
|
this.mcpServers = opts.mcpServers ?? {};
|
|
29
154
|
}
|
|
30
155
|
/**
|
|
31
|
-
* Build env for the Claude Code subprocess.
|
|
32
|
-
*
|
|
33
|
-
* overriding the user's subscription-based auth.
|
|
156
|
+
* Build env for the Claude Code subprocess. Snapshots process.env (dropping
|
|
157
|
+
* nullish values) and applies auth precedence via {@link resolveAuthEnv}.
|
|
34
158
|
*/
|
|
35
159
|
buildEnv() {
|
|
36
160
|
const env = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
|
|
37
|
-
|
|
38
|
-
delete env.ANTHROPIC_API_KEY;
|
|
39
|
-
}
|
|
40
|
-
return env;
|
|
161
|
+
return resolveAuthEnv(env, { logger: this.logger });
|
|
41
162
|
}
|
|
42
163
|
async run(opts) {
|
|
43
|
-
const { instruction, context, tools, outputSchema, onProgress, maxTurns, disallowedTools } = opts;
|
|
164
|
+
const { instruction, context, tools, outputSchema, onProgress, maxTurns, disallowedTools, model, timeoutMs, signal, } = opts;
|
|
165
|
+
const effectiveModel = model ?? this.model;
|
|
44
166
|
// Tool-call accounting (Fix #1).
|
|
45
167
|
//
|
|
46
168
|
// Stream-driven recording, correlated by tool_use_id:
|
|
@@ -80,11 +202,16 @@ export class ClaudeClient {
|
|
|
80
202
|
.join("\n\n");
|
|
81
203
|
const env = this.buildEnv();
|
|
82
204
|
let response = "";
|
|
205
|
+
// Timeout / abort wiring (back-compat: only armed when requested).
|
|
206
|
+
// A single AbortController drives both an optional caller signal and an
|
|
207
|
+
// optional timeout timer. The SDK aborts the subprocess when this fires.
|
|
208
|
+
const abort = makeAbort(timeoutMs, signal);
|
|
209
|
+
let stream;
|
|
83
210
|
try {
|
|
84
211
|
const allMcpServers = { ...this.mcpServers };
|
|
85
212
|
if (sdkTools.length > 0)
|
|
86
213
|
allMcpServers["sweny-core"] = mcpServer;
|
|
87
|
-
|
|
214
|
+
stream = query({
|
|
88
215
|
prompt,
|
|
89
216
|
options: {
|
|
90
217
|
maxTurns: maxTurns ?? this.maxTurns,
|
|
@@ -94,7 +221,8 @@ export class ClaudeClient {
|
|
|
94
221
|
permissionMode: "bypassPermissions",
|
|
95
222
|
allowDangerouslySkipPermissions: true,
|
|
96
223
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
97
|
-
...(
|
|
224
|
+
...(abort ? { abortController: abort.controller } : {}),
|
|
225
|
+
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
98
226
|
...(Object.keys(allMcpServers).length > 0 ? { mcpServers: allMcpServers } : {}),
|
|
99
227
|
...(disallowedTools && disallowedTools.length > 0 ? { disallowedTools } : {}),
|
|
100
228
|
},
|
|
@@ -200,6 +328,11 @@ export class ClaudeClient {
|
|
|
200
328
|
}
|
|
201
329
|
}
|
|
202
330
|
catch (err) {
|
|
331
|
+
if (abort?.reason() === "timeout") {
|
|
332
|
+
const msg = `Claude query timed out after ${timeoutMs}ms`;
|
|
333
|
+
this.logger.error(msg);
|
|
334
|
+
return { status: "failed", data: { error: msg }, toolCalls };
|
|
335
|
+
}
|
|
203
336
|
this.logger.error(`Claude Code query failed: ${err.message}`);
|
|
204
337
|
return {
|
|
205
338
|
status: "failed",
|
|
@@ -207,19 +340,29 @@ export class ClaudeClient {
|
|
|
207
340
|
toolCalls,
|
|
208
341
|
};
|
|
209
342
|
}
|
|
343
|
+
finally {
|
|
344
|
+
// Interrupt the subprocess on any exit (early return, throw, or normal
|
|
345
|
+
// completion) so a wedged agent does not leak, and clear the timer so
|
|
346
|
+
// it cannot fire after we are done.
|
|
347
|
+
abort?.clear();
|
|
348
|
+
await interruptStream(stream);
|
|
349
|
+
}
|
|
210
350
|
return {
|
|
211
351
|
status: "success",
|
|
212
|
-
data: { summary: response, ...tryParseJSON(response) },
|
|
352
|
+
data: { summary: response, ...tryParseJSON(response, outputSchema, this.logger) },
|
|
213
353
|
toolCalls,
|
|
214
354
|
};
|
|
215
355
|
}
|
|
216
356
|
async evaluate(opts) {
|
|
217
|
-
const { question, context, choices } = opts;
|
|
357
|
+
const { question, context, choices, timeoutMs, signal } = opts;
|
|
218
358
|
const prompt = buildEvaluatePrompt(question, context, choices);
|
|
219
359
|
const env = this.buildEnv();
|
|
220
360
|
let response = "";
|
|
361
|
+
const abort = makeAbort(timeoutMs, signal);
|
|
362
|
+
let sdkFailed = false;
|
|
363
|
+
let stream;
|
|
221
364
|
try {
|
|
222
|
-
|
|
365
|
+
stream = query({
|
|
223
366
|
prompt,
|
|
224
367
|
options: {
|
|
225
368
|
maxTurns: 1,
|
|
@@ -228,6 +371,7 @@ export class ClaudeClient {
|
|
|
228
371
|
permissionMode: "bypassPermissions",
|
|
229
372
|
allowDangerouslySkipPermissions: true,
|
|
230
373
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
374
|
+
...(abort ? { abortController: abort.controller } : {}),
|
|
231
375
|
...(this.model ? { model: this.model } : {}),
|
|
232
376
|
},
|
|
233
377
|
});
|
|
@@ -237,13 +381,35 @@ export class ClaudeClient {
|
|
|
237
381
|
if (resultMsg.subtype === "success" && "result" in resultMsg) {
|
|
238
382
|
response = resultMsg.result;
|
|
239
383
|
}
|
|
384
|
+
else {
|
|
385
|
+
// Distinguish an SDK-level failure from a genuinely ambiguous
|
|
386
|
+
// model answer. Without this, both fall through to validIds[0]
|
|
387
|
+
// and log the same "Could not parse route choice" warning, so an
|
|
388
|
+
// operator cannot tell them apart. Short-circuit below so the
|
|
389
|
+
// ambiguous-answer warning never fires on top of this one.
|
|
390
|
+
sdkFailed = true;
|
|
391
|
+
this.logger.warn(`claude.evaluate: SDK returned non-success subtype "${resultMsg.subtype}" — falling back to first choice.`);
|
|
392
|
+
}
|
|
240
393
|
}
|
|
241
394
|
}
|
|
242
395
|
}
|
|
243
396
|
catch (err) {
|
|
244
|
-
|
|
397
|
+
if (abort?.reason() === "timeout") {
|
|
398
|
+
this.logger.warn(`Evaluate query timed out after ${timeoutMs}ms. Falling back to first choice.`);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
this.logger.warn(`Evaluate query failed: ${err.message}. Falling back to first choice.`);
|
|
402
|
+
}
|
|
245
403
|
return choices[0].id;
|
|
246
404
|
}
|
|
405
|
+
finally {
|
|
406
|
+
abort?.clear();
|
|
407
|
+
await interruptStream(stream);
|
|
408
|
+
}
|
|
409
|
+
// An SDK-level failure already logged a distinct message; do not also emit
|
|
410
|
+
// the ambiguous-answer warning, which would misattribute the cause.
|
|
411
|
+
if (sdkFailed)
|
|
412
|
+
return choices[0].id;
|
|
247
413
|
const text = response.trim().replace(/^["']|["']$/g, "");
|
|
248
414
|
const validIds = choices.map((c) => c.id);
|
|
249
415
|
// Exact match
|
|
@@ -258,7 +424,7 @@ export class ClaudeClient {
|
|
|
258
424
|
return validIds[0];
|
|
259
425
|
}
|
|
260
426
|
async ask(opts) {
|
|
261
|
-
const { instruction, context, model } = opts;
|
|
427
|
+
const { instruction, context, model, timeoutMs, signal } = opts;
|
|
262
428
|
const prompt = [
|
|
263
429
|
instruction,
|
|
264
430
|
Object.keys(context).length > 0 ? `\nContext:\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\`` : "",
|
|
@@ -268,8 +434,10 @@ export class ClaudeClient {
|
|
|
268
434
|
const env = this.buildEnv();
|
|
269
435
|
let response = "";
|
|
270
436
|
const effectiveModel = model ?? this.model;
|
|
437
|
+
const abort = makeAbort(timeoutMs, signal);
|
|
438
|
+
let stream;
|
|
271
439
|
try {
|
|
272
|
-
|
|
440
|
+
stream = query({
|
|
273
441
|
prompt,
|
|
274
442
|
options: {
|
|
275
443
|
maxTurns: 1,
|
|
@@ -278,6 +446,7 @@ export class ClaudeClient {
|
|
|
278
446
|
permissionMode: "bypassPermissions",
|
|
279
447
|
allowDangerouslySkipPermissions: true,
|
|
280
448
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
449
|
+
...(abort ? { abortController: abort.controller } : {}),
|
|
281
450
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
282
451
|
},
|
|
283
452
|
});
|
|
@@ -294,9 +463,18 @@ export class ClaudeClient {
|
|
|
294
463
|
}
|
|
295
464
|
}
|
|
296
465
|
catch (err) {
|
|
297
|
-
|
|
466
|
+
if (abort?.reason() === "timeout") {
|
|
467
|
+
this.logger.warn(`Ask query timed out after ${timeoutMs}ms — returning empty string`);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
this.logger.warn(`Ask query failed: ${err.message}`);
|
|
471
|
+
}
|
|
298
472
|
return "";
|
|
299
473
|
}
|
|
474
|
+
finally {
|
|
475
|
+
abort?.clear();
|
|
476
|
+
await interruptStream(stream);
|
|
477
|
+
}
|
|
300
478
|
return response.trim();
|
|
301
479
|
}
|
|
302
480
|
}
|
|
@@ -377,14 +555,32 @@ export function buildEvaluatePrompt(question, context, choices) {
|
|
|
377
555
|
].join("\n");
|
|
378
556
|
}
|
|
379
557
|
export function parseToolResultContent(content) {
|
|
558
|
+
// The Anthropic tool_result `content` field can be a block array
|
|
559
|
+
// (e.g. [{type:"text",text:"..."}, ...]) rather than a string. When it is,
|
|
560
|
+
// concatenate the text of every {type:"text"} block (recursing through any
|
|
561
|
+
// nested content), then run the same object/array JSON-parse logic on the
|
|
562
|
+
// joined string. This keeps `call.output` the actual payload instead of the
|
|
563
|
+
// raw wrapper array that eval/route logic would otherwise have to walk.
|
|
564
|
+
if (Array.isArray(content)) {
|
|
565
|
+
const text = collectBlockText(content);
|
|
566
|
+
return parseJsonObjectOrArray(text);
|
|
567
|
+
}
|
|
380
568
|
if (typeof content !== "string")
|
|
381
569
|
return content;
|
|
570
|
+
return parseJsonObjectOrArray(content);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Parse a string only when it is an unambiguous JSON object or array.
|
|
574
|
+
*
|
|
575
|
+
* Strings, numbers, booleans, and null would all corrupt or discard
|
|
576
|
+
* information (we cannot distinguish the literal text `"42"` from the number
|
|
577
|
+
* 42), so non-object/array inputs are preserved verbatim.
|
|
578
|
+
*/
|
|
579
|
+
function parseJsonObjectOrArray(content) {
|
|
382
580
|
const trimmed = content.trim();
|
|
383
581
|
if (trimmed.length === 0)
|
|
384
582
|
return content;
|
|
385
583
|
const first = trimmed[0];
|
|
386
|
-
// Only objects and arrays are unambiguous to parse. Strings, numbers,
|
|
387
|
-
// booleans, and null would all corrupt or discard information.
|
|
388
584
|
if (first !== "{" && first !== "[")
|
|
389
585
|
return content;
|
|
390
586
|
try {
|
|
@@ -394,6 +590,34 @@ export function parseToolResultContent(content) {
|
|
|
394
590
|
return content;
|
|
395
591
|
}
|
|
396
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Concatenate the text of an array of content blocks. Handles {type:"text"}
|
|
595
|
+
* blocks and recurses into any nested `content` array so deeply-wrapped tool
|
|
596
|
+
* results still flatten to their underlying text.
|
|
597
|
+
*/
|
|
598
|
+
function collectBlockText(blocks) {
|
|
599
|
+
const parts = [];
|
|
600
|
+
for (const block of blocks) {
|
|
601
|
+
if (typeof block === "string") {
|
|
602
|
+
parts.push(block);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (!block || typeof block !== "object")
|
|
606
|
+
continue;
|
|
607
|
+
const b = block;
|
|
608
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
609
|
+
parts.push(b.text);
|
|
610
|
+
}
|
|
611
|
+
else if (Array.isArray(b.content)) {
|
|
612
|
+
parts.push(collectBlockText(b.content));
|
|
613
|
+
}
|
|
614
|
+
else if (typeof b.text === "string") {
|
|
615
|
+
// Some producers omit an explicit type but still carry text.
|
|
616
|
+
parts.push(b.text);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return parts.join("");
|
|
620
|
+
}
|
|
397
621
|
/**
|
|
398
622
|
* Produce a short, single-line description of a tool-error payload suitable
|
|
399
623
|
* for the CI log stream. The full parsed value stays on the ToolCall for
|
|
@@ -494,24 +718,38 @@ function stripMcpPrefix(name) {
|
|
|
494
718
|
* Extract a JSON object from Claude's text response.
|
|
495
719
|
*
|
|
496
720
|
* Strategy (in order):
|
|
497
|
-
* 1. ```json code block``` —
|
|
721
|
+
* 1. LAST ```json code block``` — models put their real final answer last, so
|
|
722
|
+
* a prompt-injected fake fenced block placed earlier must not win.
|
|
498
723
|
* 2. Last brace-delimited `{...}` block — handles inline JSON at end of text
|
|
499
724
|
* 3. Full text parse — for responses that are pure JSON
|
|
500
725
|
* 4. Empty object — safe fallback
|
|
726
|
+
*
|
|
727
|
+
* When `outputSchema` is supplied, the parsed object is checked against it.
|
|
728
|
+
* A mismatch is logged (warning) rather than silently accepted, so a
|
|
729
|
+
* non-conforming model answer is at least attributable.
|
|
501
730
|
*/
|
|
502
|
-
function tryParseJSON(text) {
|
|
731
|
+
function tryParseJSON(text, outputSchema, logger) {
|
|
503
732
|
if (!text)
|
|
504
733
|
return {};
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
734
|
+
const finalize = (parsed) => {
|
|
735
|
+
if (outputSchema) {
|
|
736
|
+
const problems = schemaMismatches(parsed, outputSchema);
|
|
737
|
+
if (problems.length > 0) {
|
|
738
|
+
logger?.warn?.(`Claude output did not conform to outputSchema: ${problems.join("; ")}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return parsed;
|
|
742
|
+
};
|
|
743
|
+
// 1. Code block — scan ALL fenced blocks, prefer the LAST that parses.
|
|
744
|
+
const fences = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)];
|
|
745
|
+
for (let i = fences.length - 1; i >= 0; i--) {
|
|
508
746
|
try {
|
|
509
|
-
const parsed = JSON.parse(
|
|
747
|
+
const parsed = JSON.parse(fences[i][1].trim());
|
|
510
748
|
if (typeof parsed === "object" && parsed !== null)
|
|
511
|
-
return parsed;
|
|
749
|
+
return finalize(parsed);
|
|
512
750
|
}
|
|
513
751
|
catch {
|
|
514
|
-
/* try next
|
|
752
|
+
/* try the next-earlier fence, then fall through to brace scan */
|
|
515
753
|
}
|
|
516
754
|
}
|
|
517
755
|
// 2. Last brace-delimited block (scan backwards for matching braces)
|
|
@@ -527,7 +765,7 @@ function tryParseJSON(text) {
|
|
|
527
765
|
try {
|
|
528
766
|
const parsed = JSON.parse(text.slice(i, lastBrace + 1));
|
|
529
767
|
if (typeof parsed === "object" && parsed !== null)
|
|
530
|
-
return parsed;
|
|
768
|
+
return finalize(parsed);
|
|
531
769
|
}
|
|
532
770
|
catch {
|
|
533
771
|
/* try next strategy */
|
|
@@ -540,10 +778,77 @@ function tryParseJSON(text) {
|
|
|
540
778
|
try {
|
|
541
779
|
const parsed = JSON.parse(text.trim());
|
|
542
780
|
if (typeof parsed === "object" && parsed !== null)
|
|
543
|
-
return parsed;
|
|
781
|
+
return finalize(parsed);
|
|
544
782
|
}
|
|
545
783
|
catch {
|
|
546
784
|
/* fall through */
|
|
547
785
|
}
|
|
548
786
|
return {};
|
|
549
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Lightweight structural check of a parsed object against a JSON Schema.
|
|
790
|
+
*
|
|
791
|
+
* Deliberately shallow: it verifies `required` keys are present and that the
|
|
792
|
+
* top-level `type` and each declared property `type` match. It does NOT do
|
|
793
|
+
* full JSON Schema validation (no `$ref`, `oneOf`, formats, nested arrays).
|
|
794
|
+
* The contract here is "flag an obviously non-conforming object" so a mismatch
|
|
795
|
+
* is logged rather than silently accepted; it is not a validation gate.
|
|
796
|
+
*
|
|
797
|
+
* Returns a list of human-readable problems; empty means no detected mismatch.
|
|
798
|
+
*/
|
|
799
|
+
function schemaMismatches(value, schema) {
|
|
800
|
+
const problems = [];
|
|
801
|
+
const s = schema;
|
|
802
|
+
if (s.type === "object" || s.properties || s.required) {
|
|
803
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
804
|
+
return [`expected an object, got ${Array.isArray(value) ? "array" : typeof value}`];
|
|
805
|
+
}
|
|
806
|
+
const obj = value;
|
|
807
|
+
const required = Array.isArray(s.required) ? s.required : [];
|
|
808
|
+
for (const key of required) {
|
|
809
|
+
if (!(key in obj) || obj[key] === undefined) {
|
|
810
|
+
problems.push(`missing required property "${key}"`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const props = s.properties ?? {};
|
|
814
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
815
|
+
if (!(key in obj) || obj[key] === undefined || obj[key] === null)
|
|
816
|
+
continue;
|
|
817
|
+
const expected = propSchema?.type;
|
|
818
|
+
if (typeof expected === "string" && !jsonTypeMatches(obj[key], expected)) {
|
|
819
|
+
problems.push(`property "${key}" expected ${expected}, got ${jsonTypeOf(obj[key])}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else if (typeof s.type === "string" && !jsonTypeMatches(value, s.type)) {
|
|
824
|
+
problems.push(`expected ${s.type}, got ${jsonTypeOf(value)}`);
|
|
825
|
+
}
|
|
826
|
+
return problems;
|
|
827
|
+
}
|
|
828
|
+
function jsonTypeOf(value) {
|
|
829
|
+
if (value === null)
|
|
830
|
+
return "null";
|
|
831
|
+
if (Array.isArray(value))
|
|
832
|
+
return "array";
|
|
833
|
+
return typeof value;
|
|
834
|
+
}
|
|
835
|
+
function jsonTypeMatches(value, expected) {
|
|
836
|
+
switch (expected) {
|
|
837
|
+
case "object":
|
|
838
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
839
|
+
case "array":
|
|
840
|
+
return Array.isArray(value);
|
|
841
|
+
case "string":
|
|
842
|
+
return typeof value === "string";
|
|
843
|
+
case "number":
|
|
844
|
+
return typeof value === "number";
|
|
845
|
+
case "integer":
|
|
846
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
847
|
+
case "boolean":
|
|
848
|
+
return typeof value === "boolean";
|
|
849
|
+
case "null":
|
|
850
|
+
return value === null;
|
|
851
|
+
default:
|
|
852
|
+
return true; // unknown/unsupported type keyword — don't flag
|
|
853
|
+
}
|
|
854
|
+
}
|
package/dist/cli/check.d.ts
CHANGED
|
@@ -9,3 +9,10 @@ export interface CheckResult {
|
|
|
9
9
|
* Uses raw fetch — does NOT import provider packages.
|
|
10
10
|
*/
|
|
11
11
|
export declare function checkProviderConnectivity(config: CliConfig): Promise<CheckResult[]>;
|
|
12
|
+
/** Which credential `sweny check` should validate, mirroring resolveAuthEnv intent. */
|
|
13
|
+
export type CheckAuthMode = "oauth" | "api-key" | "auth-token" | "none";
|
|
14
|
+
export declare function resolveCheckAuthMode(config: Pick<CliConfig, "anthropicApiKey" | "anthropicAuthToken" | "claudeOauthToken" | "swenyAuth">): CheckAuthMode;
|
|
15
|
+
/** Redact a URL to scheme + host so userinfo / query (which can carry a key) never logs. */
|
|
16
|
+
export declare function redactUrl(raw: string): string;
|
|
17
|
+
export declare function checkAnthropicGateway(base: string, config: Pick<CliConfig, "anthropicApiKey" | "anthropicAuthToken" | "claudeOauthToken">, mode: CheckAuthMode): Promise<CheckResult>;
|
|
18
|
+
export declare function checkDatadog(creds: Record<string, string>): Promise<CheckResult>;
|