cyrus-claude-runner 0.2.49 → 0.2.50
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/ClaudeRunner.d.ts +20 -3
- package/dist/ClaudeRunner.d.ts.map +1 -1
- package/dist/ClaudeRunner.js +266 -89
- package/dist/ClaudeRunner.js.map +1 -1
- package/dist/HttpSessionStore.d.ts +94 -0
- package/dist/HttpSessionStore.d.ts.map +1 -0
- package/dist/HttpSessionStore.js +107 -0
- package/dist/HttpSessionStore.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -6
- package/dist/config.js.map +1 -1
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +32 -9
- package/dist/formatter.js.map +1 -1
- package/dist/home-directory-restrictions.d.ts +23 -0
- package/dist/home-directory-restrictions.d.ts.map +1 -0
- package/dist/home-directory-restrictions.js +86 -0
- package/dist/home-directory-restrictions.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/session-env.d.ts +47 -0
- package/dist/session-env.d.ts.map +1 -0
- package/dist/session-env.js +82 -0
- package/dist/session-env.js.map +1 -0
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/ClaudeRunner.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { type IAgentRunner } from "cyrus-core";
|
|
4
|
+
import { type IMessageFormatter } from "./formatter.js";
|
|
5
|
+
import type { ClaudeRunnerConfig, ClaudeRunnerEvents, ClaudeSessionInfo } from "./types.js";
|
|
4
6
|
export declare class AbortError extends Error {
|
|
5
7
|
constructor(message?: string);
|
|
6
8
|
}
|
|
7
|
-
import { type IMessageFormatter } from "./formatter.js";
|
|
8
|
-
import type { ClaudeRunnerConfig, ClaudeRunnerEvents, ClaudeSessionInfo } from "./types.js";
|
|
9
9
|
export declare interface ClaudeRunner {
|
|
10
10
|
on<K extends keyof ClaudeRunnerEvents>(event: K, listener: ClaudeRunnerEvents[K]): this;
|
|
11
11
|
emit<K extends keyof ClaudeRunnerEvents>(event: K, ...args: Parameters<ClaudeRunnerEvents[K]>): boolean;
|
|
@@ -26,12 +26,14 @@ export declare class ClaudeRunner extends EventEmitter implements IAgentRunner {
|
|
|
26
26
|
private readableLogStream;
|
|
27
27
|
private messages;
|
|
28
28
|
private streamingPrompt;
|
|
29
|
+
private activeQuery;
|
|
29
30
|
private cyrusHome;
|
|
30
31
|
private formatter;
|
|
31
32
|
private pendingResultMessage;
|
|
32
33
|
private canUseToolCallback;
|
|
33
34
|
private repositoryEnv;
|
|
34
|
-
|
|
35
|
+
private keepSessionWarm;
|
|
36
|
+
constructor(config: ClaudeRunnerConfig, keepSessionWarm?: boolean);
|
|
35
37
|
/**
|
|
36
38
|
* Create the canUseTool callback for intercepting AskUserQuestion tool calls.
|
|
37
39
|
*
|
|
@@ -71,6 +73,21 @@ export declare class ClaudeRunner extends EventEmitter implements IAgentRunner {
|
|
|
71
73
|
userPromptVersion?: string;
|
|
72
74
|
systemPromptVersion?: string;
|
|
73
75
|
}): void;
|
|
76
|
+
/**
|
|
77
|
+
* Interrupt the current turn without killing the session.
|
|
78
|
+
* The session stays warm and can accept new messages.
|
|
79
|
+
*
|
|
80
|
+
* Only safe to call on warm sessions (see {@link isWarm}). Calling
|
|
81
|
+
* `interrupt()` on a non-warm session aborts the underlying request and
|
|
82
|
+
* causes the SDK to emit a "Request was aborted" error. Callers should
|
|
83
|
+
* gate on `isWarm()` and prefer `stop()` for non-warm sessions.
|
|
84
|
+
*/
|
|
85
|
+
interrupt(): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Whether this runner keeps its SDK session warm between turns. Warm
|
|
88
|
+
* sessions can be safely interrupted; non-warm sessions cannot.
|
|
89
|
+
*/
|
|
90
|
+
isWarm(): boolean;
|
|
74
91
|
/**
|
|
75
92
|
* Stop the current Claude session
|
|
76
93
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClaudeRunner.d.ts","sourceRoot":"","sources":["../src/ClaudeRunner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"ClaudeRunner.d.ts","sourceRoot":"","sources":["../src/ClaudeRunner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAU3C,OAAO,EAKN,KAAK,UAAU,EAEf,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAEN,KAAK,YAAY,EAIjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAUhF,OAAO,KAAK,EACX,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,YAAY,CAAC;AAGpB,qBAAa,UAAW,SAAQ,KAAK;gBACxB,OAAO,CAAC,EAAE,MAAM;CAI5B;AA4KD,MAAM,CAAC,OAAO,WAAW,YAAY;IACpC,EAAE,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACpC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC7B,IAAI,CAAC;IACR,IAAI,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACtC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC;CACX;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,YAAa,YAAW,YAAY;IACrE;;OAEG;IACH,QAAQ,CAAC,sBAAsB,QAAQ;IAEvC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAoB;IACpC,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,oBAAoB,CAA2B;IACvD,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,eAAe,CAAU;gBAErB,MAAM,EAAE,kBAAkB,EAAE,eAAe,UAAQ;IAmB/D;;;;;;;;;;OAUG;IACH,OAAO,CAAC,wBAAwB;IA4GhC;;OAEG;IACG,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIvD;;OAEG;IACG,cAAc,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIxE;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAOvC;;OAEG;IACH,cAAc,IAAI,IAAI;IAMtB;;OAEG;YACW,eAAe;IAib7B;;OAEG;IACH,oBAAoB,CAAC,QAAQ,EAAE;QAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC7B,GAAG,IAAI;IAuCR;;;;;;;;OAQG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBhC;;;OAGG;IACH,MAAM,IAAI,OAAO;IAIjB;;OAEG;IACH,IAAI,IAAI,IAAI;IAsBZ;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,WAAW,IAAI,OAAO;IAQtB;;OAEG;IACH,cAAc,IAAI,iBAAiB,GAAG,IAAI;IAI1C;;OAEG;IACH,WAAW,IAAI,UAAU,EAAE;IAI3B;;OAEG;IACH,YAAY,IAAI,iBAAiB;IAIjC;;OAEG;IACH,OAAO,CAAC,cAAc;IA6CtB;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;OAEG;IACH,OAAO,CAAC,YAAY;IA0EpB;;OAEG;IACH,OAAO,CAAC,qBAAqB;CAsG7B"}
|
package/dist/ClaudeRunner.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { join } from "node:path";
|
|
5
4
|
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
-
import { createLogger, StreamingPrompt, } from "cyrus-core";
|
|
5
|
+
import { createLogger, LogLevel, StreamingPrompt, } from "cyrus-core";
|
|
7
6
|
import dotenv from "dotenv";
|
|
7
|
+
import { ClaudeMessageFormatter } from "./formatter.js";
|
|
8
|
+
import { buildHomeDirectoryDisallowedTools } from "./home-directory-restrictions.js";
|
|
9
|
+
import { checkLinuxSandboxRequirements, logSandboxRequirementFailures, } from "./sandbox-requirements.js";
|
|
10
|
+
import { buildBaseSessionEnv, normalizeMcpHttpTransport, } from "./session-env.js";
|
|
8
11
|
// AbortError is no longer exported in v1.0.95, so we define it locally
|
|
9
12
|
export class AbortError extends Error {
|
|
10
13
|
constructor(message) {
|
|
@@ -12,38 +15,138 @@ export class AbortError extends Error {
|
|
|
12
15
|
this.name = "AbortError";
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
|
-
// Create a require function for resolving module paths in ESM
|
|
16
|
-
const require = createRequire(import.meta.url);
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @returns The resolved path to cli.js, or undefined if resolution fails
|
|
19
|
+
* JSON.stringify replacer for Claude query options. The SDK's query options
|
|
20
|
+
* include non-serializable members (AbortController, async iterables,
|
|
21
|
+
* callbacks, pre-warmed sessions) — replace them with diagnostic placeholders
|
|
22
|
+
* so debug logs remain valid JSON.
|
|
23
23
|
*/
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const cliPath = join(sdkDir, "cli.js");
|
|
31
|
-
// Verify the cli.js file exists
|
|
32
|
-
if (existsSync(cliPath)) {
|
|
33
|
-
return cliPath;
|
|
34
|
-
}
|
|
35
|
-
console.warn(`[ClaudeRunner] Resolved SDK path but cli.js not found at: ${cliPath}`);
|
|
36
|
-
return undefined;
|
|
24
|
+
function serializeQueryOptionsReplacer(_key, value) {
|
|
25
|
+
if (typeof value === "function") {
|
|
26
|
+
return `[Function${value.name ? `: ${value.name}` : ""}]`;
|
|
27
|
+
}
|
|
28
|
+
if (value instanceof AbortController) {
|
|
29
|
+
return "[AbortController]";
|
|
37
30
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return undefined;
|
|
31
|
+
if (value !== null &&
|
|
32
|
+
typeof value === "object" &&
|
|
33
|
+
Symbol.asyncIterator in value) {
|
|
34
|
+
return "[AsyncIterable]";
|
|
43
35
|
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function buildSanitizedQueryOptions(queryOptions) {
|
|
39
|
+
const o = (queryOptions.options ?? {});
|
|
40
|
+
const out = {};
|
|
41
|
+
if (typeof o.model === "string")
|
|
42
|
+
out.model = o.model;
|
|
43
|
+
if (typeof o.fallbackModel === "string")
|
|
44
|
+
out.fallbackModel = o.fallbackModel;
|
|
45
|
+
if (typeof o.maxTurns === "number")
|
|
46
|
+
out.maxTurns = o.maxTurns;
|
|
47
|
+
if (typeof o.outputFormat === "string")
|
|
48
|
+
out.outputFormat = o.outputFormat;
|
|
49
|
+
if (typeof o.cwd === "string")
|
|
50
|
+
out.cwd = o.cwd;
|
|
51
|
+
if (Array.isArray(o.allowedDirectories)) {
|
|
52
|
+
out.allowedDirectoryCount = o.allowedDirectories.length;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(o.settingSources)) {
|
|
55
|
+
out.settingSources = o.settingSources;
|
|
56
|
+
}
|
|
57
|
+
if (typeof o.resume === "string") {
|
|
58
|
+
out.resumeSessionId = o.resume;
|
|
59
|
+
}
|
|
60
|
+
if (typeof o.permissionMode === "string") {
|
|
61
|
+
out.permissionMode = o.permissionMode;
|
|
62
|
+
}
|
|
63
|
+
// System prompt — keep the shape, not the prose. Append text routinely
|
|
64
|
+
// contains long form documentation that may include token/auth keywords.
|
|
65
|
+
if (o.systemPrompt && typeof o.systemPrompt === "object") {
|
|
66
|
+
const sp = o.systemPrompt;
|
|
67
|
+
out.systemPrompt = {
|
|
68
|
+
type: sp.type,
|
|
69
|
+
preset: sp.preset,
|
|
70
|
+
hasAppend: typeof sp.append === "string" && sp.append.length > 0,
|
|
71
|
+
appendLength: typeof sp.append === "string" ? sp.append.length : 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Tool allow/deny lists — bound the size so a 5000-entry list doesn't
|
|
75
|
+
// itself blow the attribute cap. Tool names like `Read(/abs/path/**)`
|
|
76
|
+
// are diagnostic gold and don't carry secrets.
|
|
77
|
+
const TOOL_LIST_PREVIEW = 50;
|
|
78
|
+
if (Array.isArray(o.allowedTools)) {
|
|
79
|
+
const arr = o.allowedTools;
|
|
80
|
+
out.allowedToolsCount = arr.length;
|
|
81
|
+
out.allowedToolsPreview = arr.slice(0, TOOL_LIST_PREVIEW);
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(o.disallowedTools)) {
|
|
84
|
+
const arr = o.disallowedTools;
|
|
85
|
+
out.disallowedToolsCount = arr.length;
|
|
86
|
+
out.disallowedToolsPreview = arr.slice(0, TOOL_LIST_PREVIEW);
|
|
87
|
+
}
|
|
88
|
+
// MCP servers — names only. Inner config carries auth headers, URLs with
|
|
89
|
+
// tokens in query strings, etc.
|
|
90
|
+
if (o.mcpServers && typeof o.mcpServers === "object") {
|
|
91
|
+
out.mcpServerNames = Object.keys(o.mcpServers);
|
|
92
|
+
}
|
|
93
|
+
// Env — key names only, no values. Spreads `process.env`, so values are
|
|
94
|
+
// inherently sensitive.
|
|
95
|
+
if (o.env && typeof o.env === "object") {
|
|
96
|
+
const envKeys = Object.keys(o.env);
|
|
97
|
+
out.envKeyCount = envKeys.length;
|
|
98
|
+
// First 100 names is plenty to confirm what flowed through.
|
|
99
|
+
out.envKeyNamesPreview = envKeys.slice(0, 100);
|
|
100
|
+
}
|
|
101
|
+
// Presence flags rather than payload for opaque/large fields.
|
|
102
|
+
out.hasHooks = !!o.hooks;
|
|
103
|
+
out.hasPlugins =
|
|
104
|
+
Array.isArray(o.plugins) && o.plugins.length > 0;
|
|
105
|
+
out.hasCanUseTool = typeof o.canUseTool === "function";
|
|
106
|
+
out.hasSandbox = !!o.sandbox;
|
|
107
|
+
out.hasExtraArgs = !!o.extraArgs;
|
|
108
|
+
out.hasPathToClaudeCodeExecutable =
|
|
109
|
+
typeof o.pathToClaudeCodeExecutable === "string";
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Flatten the sanitized query options into a set of primitive Sentry Logs
|
|
114
|
+
* attributes. Sentry attribute values must be primitives, so arrays and
|
|
115
|
+
* nested objects are joined into newline-separated strings (preview values
|
|
116
|
+
* are already bounded). Each top-level datum gets its own attribute key so a
|
|
117
|
+
* stray match in any one field can't filter the whole payload — and short
|
|
118
|
+
* scalar values rarely trip Sentry's pattern matchers in the first place.
|
|
119
|
+
*/
|
|
120
|
+
function flattenSanitizedQueryOptions(sanitized) {
|
|
121
|
+
const ATTR_PREFIX = "cqo.";
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const [key, value] of Object.entries(sanitized)) {
|
|
124
|
+
const attrKey = `${ATTR_PREFIX}${key}`;
|
|
125
|
+
if (value === null || value === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
if (typeof value === "string" ||
|
|
128
|
+
typeof value === "number" ||
|
|
129
|
+
typeof value === "boolean") {
|
|
130
|
+
out[attrKey] = value;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
out[attrKey] = value.map(String).join("\n");
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "object") {
|
|
138
|
+
// Nested object (e.g. systemPrompt summary). Stringify but keep it
|
|
139
|
+
// short — these summaries are intentionally tiny.
|
|
140
|
+
try {
|
|
141
|
+
out[attrKey] = JSON.stringify(value);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
out[attrKey] = "[unserialisable]";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
44
149
|
}
|
|
45
|
-
import { ClaudeMessageFormatter } from "./formatter.js";
|
|
46
|
-
import { checkLinuxSandboxRequirements, logSandboxRequirementFailures, } from "./sandbox-requirements.js";
|
|
47
150
|
/**
|
|
48
151
|
* Manages Claude SDK sessions and communication
|
|
49
152
|
*/
|
|
@@ -60,14 +163,17 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
60
163
|
readableLogStream = null;
|
|
61
164
|
messages = [];
|
|
62
165
|
streamingPrompt = null;
|
|
166
|
+
activeQuery = null;
|
|
63
167
|
cyrusHome;
|
|
64
168
|
formatter;
|
|
65
169
|
pendingResultMessage = null;
|
|
66
170
|
canUseToolCallback;
|
|
67
171
|
repositoryEnv = {};
|
|
68
|
-
|
|
172
|
+
keepSessionWarm;
|
|
173
|
+
constructor(config, keepSessionWarm = false) {
|
|
69
174
|
super();
|
|
70
175
|
this.config = config;
|
|
176
|
+
this.keepSessionWarm = keepSessionWarm;
|
|
71
177
|
this.logger = config.logger ?? createLogger({ component: "ClaudeRunner" });
|
|
72
178
|
this.cyrusHome = config.cyrusHome;
|
|
73
179
|
this.formatter = new ClaudeMessageFormatter();
|
|
@@ -213,7 +319,13 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
213
319
|
startedAt: new Date(),
|
|
214
320
|
isRunning: true,
|
|
215
321
|
};
|
|
216
|
-
this.
|
|
322
|
+
const isResumed = !!this.config.resumeSessionId;
|
|
323
|
+
this.logger.event(isResumed ? "session_resumed" : "session_started", {
|
|
324
|
+
resumeSessionId: this.config.resumeSessionId,
|
|
325
|
+
workingDirectory: this.config.workingDirectory,
|
|
326
|
+
model: this.config.model,
|
|
327
|
+
fallbackModel: this.config.fallbackModel,
|
|
328
|
+
});
|
|
217
329
|
this.logger.debug("Working directory:", this.config.workingDirectory);
|
|
218
330
|
// Ensure working directory exists
|
|
219
331
|
if (this.config.workingDirectory) {
|
|
@@ -265,13 +377,24 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
265
377
|
? [...processedAllowedTools, ...directoryTools]
|
|
266
378
|
: directoryTools;
|
|
267
379
|
}
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
380
|
+
// Build home directory restrictions: deny Read on everything in ~/
|
|
381
|
+
// that is not an ancestor of the working directory. This prevents
|
|
382
|
+
// Claude from reading SSH keys, credentials, etc. `Read(~/**)` does
|
|
383
|
+
// not work as a disallowedTools pattern — `~` is not expanded to the
|
|
384
|
+
// home directory path, so the pattern never matches.
|
|
385
|
+
const homeDisallowedTools = this.config.workingDirectory
|
|
386
|
+
? buildHomeDirectoryDisallowedTools(this.config.workingDirectory, this.config.allowedDirectories ?? [])
|
|
387
|
+
: [];
|
|
388
|
+
// Merge config-level denials with home directory denials, deduplicating in case
|
|
389
|
+
// any paths appear in both (e.g. an allowedDirectory that is also explicitly denied).
|
|
390
|
+
const processedDisallowedTools = [
|
|
391
|
+
...new Set([
|
|
392
|
+
...(this.config.disallowedTools ?? []),
|
|
393
|
+
...homeDisallowedTools,
|
|
394
|
+
]),
|
|
395
|
+
];
|
|
273
396
|
// Log disallowed tools if configured
|
|
274
|
-
if (processedDisallowedTools) {
|
|
397
|
+
if (processedDisallowedTools.length > 0) {
|
|
275
398
|
this.logger.debug("Disallowed tools configured:", processedDisallowedTools);
|
|
276
399
|
}
|
|
277
400
|
// Parse MCP config - merge file(s) and inline configs
|
|
@@ -308,14 +431,7 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
308
431
|
const mcpConfigContent = readFileSync(path, "utf8");
|
|
309
432
|
const mcpConfig = JSON.parse(mcpConfigContent);
|
|
310
433
|
const servers = mcpConfig.mcpServers || {};
|
|
311
|
-
|
|
312
|
-
// Config files (.mcp.json, mcp-*.json) often omit the `type` field,
|
|
313
|
-
// but the SDK requires an explicit discriminator for non-stdio transports.
|
|
314
|
-
for (const config of Object.values(servers)) {
|
|
315
|
-
if (!config.type && typeof config.url === "string") {
|
|
316
|
-
config.type = "http";
|
|
317
|
-
}
|
|
318
|
-
}
|
|
434
|
+
normalizeMcpHttpTransport(servers);
|
|
319
435
|
mcpServers = { ...mcpServers, ...servers };
|
|
320
436
|
this.logger.debug(`Loaded MCP servers from ${path}: ${Object.keys(servers).join(", ")}`);
|
|
321
437
|
}
|
|
@@ -332,10 +448,7 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
332
448
|
if (this.config.allowedDirectories) {
|
|
333
449
|
this.logger.debug("Allowed directories configured:", this.config.allowedDirectories);
|
|
334
450
|
}
|
|
335
|
-
|
|
336
|
-
// otherwise auto-resolve to fix issues in symlinked environments (CYPACK-762)
|
|
337
|
-
const pathToClaudeCodeExecutable = this.config.pathToClaudeCodeExecutable ||
|
|
338
|
-
resolveClaudeCodeExecutablePath();
|
|
451
|
+
const pathToClaudeCodeExecutable = this.config.pathToClaudeCodeExecutable;
|
|
339
452
|
// On Linux, setting CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 causes the SDK
|
|
340
453
|
// to run tool invocations under a bubblewrap-backed sandbox. If the
|
|
341
454
|
// host lacks `socat`, `bubblewrap`, or the kernel/AppArmor config
|
|
@@ -345,6 +458,7 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
345
458
|
// instead of failing opaquely mid-session.
|
|
346
459
|
const sandboxRequirements = checkLinuxSandboxRequirements();
|
|
347
460
|
logSandboxRequirementFailures(sandboxRequirements, this.logger);
|
|
461
|
+
const isDebugLogging = this.logger.getLevel() === LogLevel.DEBUG;
|
|
348
462
|
const queryOptions = {
|
|
349
463
|
prompt: promptForQuery,
|
|
350
464
|
options: {
|
|
@@ -365,21 +479,7 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
365
479
|
// see: https://docs.claude.com/en/docs/claude-code/sdk/migration-guide#settings-sources-no-longer-loaded-by-default
|
|
366
480
|
settingSources: ["user", "project", "local"],
|
|
367
481
|
env: {
|
|
368
|
-
|
|
369
|
-
...process.env,
|
|
370
|
-
...(process.env.PATH && { PATH: process.env.PATH }),
|
|
371
|
-
// Forward auth credentials from parent process — the SDK needs
|
|
372
|
-
// these for API calls.
|
|
373
|
-
// See: https://code.claude.com/docs/en/env-vars
|
|
374
|
-
...(process.env.ANTHROPIC_API_KEY && {
|
|
375
|
-
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
376
|
-
}),
|
|
377
|
-
...(process.env.CLAUDE_CODE_OAUTH_TOKEN && {
|
|
378
|
-
CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
379
|
-
}),
|
|
380
|
-
...(process.env.ANTHROPIC_AUTH_TOKEN && {
|
|
381
|
-
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
|
|
382
|
-
}),
|
|
482
|
+
...buildBaseSessionEnv(),
|
|
383
483
|
// CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is intentionally NOT set while
|
|
384
484
|
// the Linux bubblewrap sandbox side effects it triggers are being
|
|
385
485
|
// investigated. The sandbox requirements precheck is still run
|
|
@@ -387,9 +487,9 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
387
487
|
// See: CYPACK-1108.
|
|
388
488
|
...this.repositoryEnv,
|
|
389
489
|
...this.config.additionalEnv,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
490
|
+
// When logging at DEBUG level, enable the SDK's own debug output so
|
|
491
|
+
// --debug-to-stderr and DEBUG=1 propagate to the Claude subprocess.
|
|
492
|
+
...(isDebugLogging && { DEBUG_CLAUDE_AGENT_SDK: "1" }),
|
|
393
493
|
},
|
|
394
494
|
...(this.config.workingDirectory && {
|
|
395
495
|
cwd: this.config.workingDirectory,
|
|
@@ -398,7 +498,7 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
398
498
|
allowedDirectories: this.config.allowedDirectories,
|
|
399
499
|
}),
|
|
400
500
|
...(processedAllowedTools && { allowedTools: processedAllowedTools }),
|
|
401
|
-
...(processedDisallowedTools && {
|
|
501
|
+
...(processedDisallowedTools.length > 0 && {
|
|
402
502
|
disallowedTools: processedDisallowedTools,
|
|
403
503
|
}),
|
|
404
504
|
...(this.canUseToolCallback && {
|
|
@@ -407,6 +507,9 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
407
507
|
...(this.config.resumeSessionId && {
|
|
408
508
|
resume: this.config.resumeSessionId,
|
|
409
509
|
}),
|
|
510
|
+
...(this.config.sessionStore && {
|
|
511
|
+
sessionStore: this.config.sessionStore,
|
|
512
|
+
}),
|
|
410
513
|
...(Object.keys(mcpServers).length > 0 && { mcpServers }),
|
|
411
514
|
...(this.config.hooks && { hooks: this.config.hooks }),
|
|
412
515
|
...(this.config.plugins?.length && { plugins: this.config.plugins }),
|
|
@@ -420,8 +523,35 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
420
523
|
...(pathToClaudeCodeExecutable && { pathToClaudeCodeExecutable }),
|
|
421
524
|
},
|
|
422
525
|
};
|
|
526
|
+
// Local DEBUG console keeps the full untruncated payload — useful
|
|
527
|
+
// when troubleshooting on the host machine where secrets aren't an
|
|
528
|
+
// issue.
|
|
529
|
+
if (isDebugLogging) {
|
|
530
|
+
const serializedQueryOptions = JSON.stringify(queryOptions, serializeQueryOptionsReplacer, 2);
|
|
531
|
+
this.logger.debug(`Claude query options: ${serializedQueryOptions}`);
|
|
532
|
+
}
|
|
533
|
+
// What ships to Sentry is a flattened set of primitive attributes,
|
|
534
|
+
// not a single nested-JSON string. A long JSON value attached
|
|
535
|
+
// under a single key (we tried `options`) gets pattern-matched by
|
|
536
|
+
// Sentry's server-side scrubber and replaced with `[Filtered]`,
|
|
537
|
+
// wiping the entire diagnostic payload. Sending each datum as its
|
|
538
|
+
// own short, primitive attribute avoids that — short non-credential
|
|
539
|
+
// values don't trip the matcher, and a per-key filter (if it ever
|
|
540
|
+
// fires) only loses one attribute, not the whole payload.
|
|
541
|
+
const flat = flattenSanitizedQueryOptions(buildSanitizedQueryOptions(queryOptions));
|
|
542
|
+
this.logger.event("claude_query_options", flat);
|
|
423
543
|
// Process messages from the query
|
|
424
|
-
|
|
544
|
+
// Use pre-warmed session if available (eliminates cold-start subprocess spawn cost).
|
|
545
|
+
// warmSession.query() accepts both string and AsyncIterable<SDKUserMessage>,
|
|
546
|
+
// so promptForQuery works correctly for both start() and startStreaming().
|
|
547
|
+
if (this.config.warmSession) {
|
|
548
|
+
this.logger.debug("Using pre-warmed session for first turn");
|
|
549
|
+
this.activeQuery = this.config.warmSession.query(promptForQuery);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
this.activeQuery = query(queryOptions);
|
|
553
|
+
}
|
|
554
|
+
for await (const message of this.activeQuery) {
|
|
425
555
|
if (!this.sessionInfo?.isRunning) {
|
|
426
556
|
this.logger.info("Session was stopped, breaking from query loop");
|
|
427
557
|
break;
|
|
@@ -429,7 +559,9 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
429
559
|
// Extract session ID from first message if we don't have one yet
|
|
430
560
|
if (!this.sessionInfo.sessionId && message.session_id) {
|
|
431
561
|
this.sessionInfo.sessionId = message.session_id;
|
|
432
|
-
this.logger.
|
|
562
|
+
this.logger.event("claude_session_id_assigned", {
|
|
563
|
+
claudeSessionId: message.session_id,
|
|
564
|
+
});
|
|
433
565
|
// Update streaming prompt with session ID if it exists
|
|
434
566
|
if (this.streamingPrompt) {
|
|
435
567
|
this.streamingPrompt.updateSessionId(message.session_id);
|
|
@@ -451,25 +583,30 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
451
583
|
if (this.readableLogStream) {
|
|
452
584
|
this.writeReadableLogEntry(message);
|
|
453
585
|
}
|
|
454
|
-
// Emit
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
this.
|
|
467
|
-
this.
|
|
586
|
+
// Emit all messages (including result) immediately in-loop.
|
|
587
|
+
// When keepSessionWarm is true, the streamingPrompt stays open for
|
|
588
|
+
// follow-up messages so the SDK session can be reused. Otherwise we
|
|
589
|
+
// complete the streaming prompt on result so the for-await loop exits
|
|
590
|
+
// and the subprocess can shut down (pre-warm-sessions behavior).
|
|
591
|
+
this.logger.event("message_emitted", {
|
|
592
|
+
messageType: message.type,
|
|
593
|
+
claudeSessionId: this.sessionInfo?.sessionId,
|
|
594
|
+
});
|
|
595
|
+
this.emit("message", message);
|
|
596
|
+
this.processMessage(message);
|
|
597
|
+
if (message.type === "result" &&
|
|
598
|
+
!this.keepSessionWarm &&
|
|
599
|
+
this.streamingPrompt) {
|
|
600
|
+
this.streamingPrompt.complete();
|
|
468
601
|
}
|
|
469
602
|
}
|
|
603
|
+
this.activeQuery = null;
|
|
470
604
|
// Session completed successfully - mark as not running BEFORE emitting result
|
|
471
605
|
// This ensures any code checking isRunning() during result processing sees the correct state
|
|
472
|
-
this.logger.
|
|
606
|
+
this.logger.event("session_completed", {
|
|
607
|
+
messageCount: this.messages.length,
|
|
608
|
+
claudeSessionId: this.sessionInfo?.sessionId,
|
|
609
|
+
});
|
|
473
610
|
this.sessionInfo.isRunning = false;
|
|
474
611
|
// Emit deferred result message after marking isRunning = false
|
|
475
612
|
if (this.pendingResultMessage) {
|
|
@@ -495,10 +632,16 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
495
632
|
error.message.includes("Claude Code process exited with code 143");
|
|
496
633
|
if (isAbortError) {
|
|
497
634
|
// User-initiated stop - log at info level, not error
|
|
498
|
-
this.logger.
|
|
635
|
+
this.logger.event("session_stopped", {
|
|
636
|
+
reason: "user_abort",
|
|
637
|
+
claudeSessionId: this.sessionInfo?.sessionId,
|
|
638
|
+
});
|
|
499
639
|
}
|
|
500
640
|
else if (isSigterm) {
|
|
501
|
-
this.logger.
|
|
641
|
+
this.logger.event("session_stopped", {
|
|
642
|
+
reason: "sigterm",
|
|
643
|
+
claudeSessionId: this.sessionInfo?.sessionId,
|
|
644
|
+
});
|
|
502
645
|
}
|
|
503
646
|
else {
|
|
504
647
|
// Actual error - log and emit
|
|
@@ -509,6 +652,7 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
509
652
|
finally {
|
|
510
653
|
// Clean up
|
|
511
654
|
this.abortController = null;
|
|
655
|
+
this.activeQuery = null;
|
|
512
656
|
this.pendingResultMessage = null;
|
|
513
657
|
// Complete and clean up streaming prompt if it exists
|
|
514
658
|
if (this.streamingPrompt) {
|
|
@@ -563,12 +707,44 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
563
707
|
}
|
|
564
708
|
}
|
|
565
709
|
}
|
|
710
|
+
/**
|
|
711
|
+
* Interrupt the current turn without killing the session.
|
|
712
|
+
* The session stays warm and can accept new messages.
|
|
713
|
+
*
|
|
714
|
+
* Only safe to call on warm sessions (see {@link isWarm}). Calling
|
|
715
|
+
* `interrupt()` on a non-warm session aborts the underlying request and
|
|
716
|
+
* causes the SDK to emit a "Request was aborted" error. Callers should
|
|
717
|
+
* gate on `isWarm()` and prefer `stop()` for non-warm sessions.
|
|
718
|
+
*/
|
|
719
|
+
async interrupt() {
|
|
720
|
+
if (!this.keepSessionWarm) {
|
|
721
|
+
this.logger.debug("interrupt() called on non-warm session; falling back to stop()");
|
|
722
|
+
this.stop();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (this.activeQuery) {
|
|
726
|
+
this.logger.info("Interrupting current turn");
|
|
727
|
+
await this.activeQuery.interrupt();
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
this.logger.debug("interrupt() called but no active query");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Whether this runner keeps its SDK session warm between turns. Warm
|
|
735
|
+
* sessions can be safely interrupted; non-warm sessions cannot.
|
|
736
|
+
*/
|
|
737
|
+
isWarm() {
|
|
738
|
+
return this.keepSessionWarm;
|
|
739
|
+
}
|
|
566
740
|
/**
|
|
567
741
|
* Stop the current Claude session
|
|
568
742
|
*/
|
|
569
743
|
stop() {
|
|
570
744
|
if (this.abortController) {
|
|
571
|
-
this.logger.
|
|
745
|
+
this.logger.event("session_stop_requested", {
|
|
746
|
+
claudeSessionId: this.sessionInfo?.sessionId,
|
|
747
|
+
});
|
|
572
748
|
this.abortController.abort();
|
|
573
749
|
this.abortController = null;
|
|
574
750
|
}
|
|
@@ -577,6 +753,7 @@ export class ClaudeRunner extends EventEmitter {
|
|
|
577
753
|
this.streamingPrompt.complete();
|
|
578
754
|
this.streamingPrompt = null;
|
|
579
755
|
}
|
|
756
|
+
this.activeQuery = null;
|
|
580
757
|
if (this.sessionInfo) {
|
|
581
758
|
this.sessionInfo.isRunning = false;
|
|
582
759
|
}
|