@agentex/agent 0.0.23 → 0.0.26
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/CHANGELOG.md +338 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/derived.d.ts +5 -3
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +11 -7
- package/dist/derived.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/acp/index.d.ts +1 -1
- package/dist/providers/acp/index.d.ts.map +1 -1
- package/dist/providers/acp/index.js +5 -97
- package/dist/providers/acp/index.js.map +1 -1
- package/dist/providers/acp/session.d.ts +8 -1
- package/dist/providers/acp/session.d.ts.map +1 -1
- package/dist/providers/acp/session.js +94 -0
- package/dist/providers/acp/session.js.map +1 -1
- package/dist/providers/claude/attach.d.ts +8 -0
- package/dist/providers/claude/attach.d.ts.map +1 -0
- package/dist/providers/claude/attach.js +113 -0
- package/dist/providers/claude/attach.js.map +1 -0
- package/dist/providers/claude/execute.d.ts.map +1 -1
- package/dist/providers/claude/execute.js +17 -2
- package/dist/providers/claude/execute.js.map +1 -1
- package/dist/providers/claude/goal-capability.d.ts +15 -0
- package/dist/providers/claude/goal-capability.d.ts.map +1 -0
- package/dist/providers/claude/goal-capability.js +20 -0
- package/dist/providers/claude/goal-capability.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -4
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +11 -9
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +36 -14
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +9 -0
- package/dist/providers/codex/attach.d.ts.map +1 -0
- package/dist/providers/codex/attach.js +93 -0
- package/dist/providers/codex/attach.js.map +1 -0
- package/dist/providers/codex/execute.d.ts.map +1 -1
- package/dist/providers/codex/execute.js +17 -3
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/goal-capability.d.ts +13 -0
- package/dist/providers/codex/goal-capability.d.ts.map +1 -0
- package/dist/providers/codex/goal-capability.js +18 -0
- package/dist/providers/codex/goal-capability.js.map +1 -0
- package/dist/providers/codex/index.d.ts +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +9 -6
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts +11 -7
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +37 -12
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +28 -0
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
- package/dist/providers/codex/transcript-normalize.js +191 -0
- package/dist/providers/codex/transcript-normalize.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +2 -2
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +2 -2
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +3 -5
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +3 -5
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +2 -2
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +0 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -4
- package/dist/registry.js.map +1 -1
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/record.d.ts +43 -0
- package/dist/sessions/record.d.ts.map +1 -0
- package/dist/sessions/record.js +85 -0
- package/dist/sessions/record.js.map +1 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/endpoint.d.ts +38 -0
- package/dist/utils/endpoint.d.ts.map +1 -0
- package/dist/utils/endpoint.js +151 -0
- package/dist/utils/endpoint.js.map +1 -0
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +5 -1
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/uuid.d.ts +7 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +21 -1
- package/dist/utils/uuid.js.map +1 -1
- package/package.json +64 -7
- package/src/derived.ts +311 -0
- package/src/goals/controller.ts +442 -0
- package/src/goals/index.ts +21 -0
- package/src/goals/normalize.ts +173 -0
- package/src/goals/sentinel.ts +90 -0
- package/src/index.ts +270 -0
- package/src/providers/_shared/http-agent.ts +304 -0
- package/src/providers/acp/index.ts +103 -0
- package/src/providers/acp/parse.ts +131 -0
- package/src/providers/acp/session.ts +744 -0
- package/src/providers/claude/attach.ts +147 -0
- package/src/providers/claude/codec.ts +43 -0
- package/src/providers/claude/execute.ts +300 -0
- package/src/providers/claude/goal-capability.ts +21 -0
- package/src/providers/claude/index.ts +72 -0
- package/src/providers/claude/mcp.ts +82 -0
- package/src/providers/claude/parse.ts +824 -0
- package/src/providers/claude/session.ts +1192 -0
- package/src/providers/claude/transcript.ts +555 -0
- package/src/providers/codex/attach.ts +123 -0
- package/src/providers/codex/codec.ts +50 -0
- package/src/providers/codex/execute.ts +337 -0
- package/src/providers/codex/goal-capability.ts +19 -0
- package/src/providers/codex/index.ts +57 -0
- package/src/providers/codex/modes.ts +159 -0
- package/src/providers/codex/parse.ts +691 -0
- package/src/providers/codex/plan-mode.ts +49 -0
- package/src/providers/codex/session.ts +1287 -0
- package/src/providers/codex/transcript-normalize.ts +197 -0
- package/src/providers/codex/transcript.ts +487 -0
- package/src/providers/codex/usage-scanner.ts +178 -0
- package/src/providers/copilot/index.ts +19 -0
- package/src/providers/cursor/codec.ts +44 -0
- package/src/providers/cursor/execute.ts +271 -0
- package/src/providers/cursor/index.ts +25 -0
- package/src/providers/cursor/parse.ts +288 -0
- package/src/providers/gemini/index.ts +21 -0
- package/src/providers/openclaw/codec.ts +40 -0
- package/src/providers/openclaw/execute.ts +19 -0
- package/src/providers/openclaw/index.ts +29 -0
- package/src/providers/opencode/codec.ts +50 -0
- package/src/providers/opencode/event-parse.ts +141 -0
- package/src/providers/opencode/execute.ts +251 -0
- package/src/providers/opencode/http-session.ts +427 -0
- package/src/providers/opencode/index.ts +30 -0
- package/src/providers/opencode/parse.ts +203 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/pi/codec.ts +44 -0
- package/src/providers/pi/execute.ts +297 -0
- package/src/providers/pi/index.ts +30 -0
- package/src/providers/pi/parse.ts +231 -0
- package/src/providers/pi/session.ts +381 -0
- package/src/providers/process/execute.ts +148 -0
- package/src/providers/process/index.ts +52 -0
- package/src/registry.ts +40 -0
- package/src/sessions/index.ts +8 -0
- package/src/sessions/record.ts +108 -0
- package/src/types.ts +1638 -0
- package/src/utils/ask-user-question.ts +57 -0
- package/src/utils/auth.ts +661 -0
- package/src/utils/binary.ts +179 -0
- package/src/utils/endpoint.ts +172 -0
- package/src/utils/env.ts +63 -0
- package/src/utils/execute-all.ts +68 -0
- package/src/utils/exit-plan-mode.ts +40 -0
- package/src/utils/instructions.ts +427 -0
- package/src/utils/process.ts +223 -0
- package/src/utils/runtime-config.ts +100 -0
- package/src/utils/runtime-homes.ts +49 -0
- package/src/utils/skill-commands.ts +493 -0
- package/src/utils/skills.ts +500 -0
- package/src/utils/template.ts +16 -0
- package/src/utils/tool-names.ts +51 -0
- package/src/utils/uuid.ts +21 -0
- package/src/utils/workspace.ts +156 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import type {
|
|
3
|
+
AttachOptions,
|
|
4
|
+
CatchUpOptions,
|
|
5
|
+
CatchUpYield,
|
|
6
|
+
FoundTranscript,
|
|
7
|
+
LastTurnStatus,
|
|
8
|
+
SessionAttachment,
|
|
9
|
+
SessionContext,
|
|
10
|
+
SessionRecord,
|
|
11
|
+
} from "../../types.js";
|
|
12
|
+
import {
|
|
13
|
+
assertSessionRecord,
|
|
14
|
+
createSessionRecord,
|
|
15
|
+
MalformedSessionRecordError,
|
|
16
|
+
} from "../../sessions/record.js";
|
|
17
|
+
import { getRuntimeHomeEnvVar } from "../../utils/runtime-homes.js";
|
|
18
|
+
import { claudeSessionCodec } from "./codec.js";
|
|
19
|
+
import {
|
|
20
|
+
findClaudeTranscriptBySessionId,
|
|
21
|
+
getClaudeTranscriptPath,
|
|
22
|
+
peekClaudeTranscript,
|
|
23
|
+
readClaudeTranscript,
|
|
24
|
+
} from "./transcript.js";
|
|
25
|
+
// Heavy-on-heavy is fine: this whole module loads lazily via
|
|
26
|
+
// `claudeProvider.attachSession`, behind the same dynamic-import boundary as
|
|
27
|
+
// `session.ts` itself (spec §5.3 / §9.6).
|
|
28
|
+
import { createClaudeSession } from "./session.js";
|
|
29
|
+
|
|
30
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
await stat(filePath);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Home-dir override derived from `opts.env` (same var the transcript helpers honor). */
|
|
40
|
+
function homeOverride(opts?: AttachOptions): string | undefined {
|
|
41
|
+
const key = getRuntimeHomeEnvVar("claude");
|
|
42
|
+
return key ? opts?.env?.[key] : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Locate a Claude transcript with an optional home override — mirrors
|
|
47
|
+
* `claudeTranscriptOps.find` (cwd fast-path, then session-id scan) but threads
|
|
48
|
+
* `claudeHome` through so `AttachOptions.env` is honored without mutating
|
|
49
|
+
* `process.env`.
|
|
50
|
+
*/
|
|
51
|
+
async function locate(
|
|
52
|
+
sessionId: string,
|
|
53
|
+
cwd: string | null,
|
|
54
|
+
claudeHome: string | undefined,
|
|
55
|
+
): Promise<FoundTranscript | null> {
|
|
56
|
+
if (cwd) {
|
|
57
|
+
const loc = await getClaudeTranscriptPath({
|
|
58
|
+
sessionId,
|
|
59
|
+
cwd,
|
|
60
|
+
...(claudeHome ? { claudeHome } : {}),
|
|
61
|
+
});
|
|
62
|
+
if (await pathExists(loc.filePath)) {
|
|
63
|
+
return { filePath: loc.filePath, cwd: loc.canonicalCwd };
|
|
64
|
+
}
|
|
65
|
+
// cwd was wrong (session launched in a different worktree) → fall through.
|
|
66
|
+
}
|
|
67
|
+
const found = await findClaudeTranscriptBySessionId({
|
|
68
|
+
sessionId,
|
|
69
|
+
...(claudeHome ? { claudeHome } : {}),
|
|
70
|
+
});
|
|
71
|
+
return found ? { filePath: found.filePath, cwd: found.cwd } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const EMPTY: AsyncIterable<CatchUpYield> = {
|
|
75
|
+
async *[Symbol.asyncIterator]() {
|
|
76
|
+
/* no transcript → nothing to replay */
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read-only reattachment to a durable Claude session. Composition of existing
|
|
82
|
+
* primitives (codec + transcript ops + `createClaudeSession` resume) — spawns
|
|
83
|
+
* nothing; `resume` is the one and only live-continuation path.
|
|
84
|
+
*/
|
|
85
|
+
export async function attachClaudeSession(
|
|
86
|
+
record: SessionRecord,
|
|
87
|
+
opts?: AttachOptions,
|
|
88
|
+
): Promise<SessionAttachment> {
|
|
89
|
+
assertSessionRecord(record);
|
|
90
|
+
|
|
91
|
+
// 1. Normalize params through the codec.
|
|
92
|
+
const params = claudeSessionCodec.deserialize(record.params);
|
|
93
|
+
if (!params) {
|
|
94
|
+
throw new MalformedSessionRecordError(
|
|
95
|
+
"claude session record params carry no usable sessionId",
|
|
96
|
+
"params",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const sessionId = params["sessionId"] as string;
|
|
100
|
+
const cwd =
|
|
101
|
+
(typeof params["cwd"] === "string" ? (params["cwd"] as string) : null) ?? record.cwd ?? null;
|
|
102
|
+
|
|
103
|
+
const normalized = createSessionRecord({
|
|
104
|
+
providerType: "claude",
|
|
105
|
+
params,
|
|
106
|
+
cwd,
|
|
107
|
+
displayId: claudeSessionCodec.getDisplayId?.(params) ?? null,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 2. Locate the transcript (honoring opts.env home override).
|
|
111
|
+
const transcript = await locate(sessionId, cwd, homeOverride(opts));
|
|
112
|
+
|
|
113
|
+
// 3. Classify how the last persisted turn ended.
|
|
114
|
+
let lastTurn: LastTurnStatus = "unknown";
|
|
115
|
+
if (transcript) {
|
|
116
|
+
const { lastEvent } = await peekClaudeTranscript(transcript.filePath);
|
|
117
|
+
if (lastEvent === null) lastTurn = "unknown";
|
|
118
|
+
else if (lastEvent.type === "result") lastTurn = "completed";
|
|
119
|
+
else lastTurn = "interrupted";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
record: normalized,
|
|
124
|
+
transcript,
|
|
125
|
+
lastTurn,
|
|
126
|
+
// 4. Replay normalized events with checkpointable offsets.
|
|
127
|
+
catchUp(catchOpts?: CatchUpOptions): AsyncIterable<CatchUpYield> {
|
|
128
|
+
if (!transcript) return EMPTY;
|
|
129
|
+
const filePath = transcript.filePath;
|
|
130
|
+
return {
|
|
131
|
+
async *[Symbol.asyncIterator]() {
|
|
132
|
+
for await (const { event, offset } of readClaudeTranscript({
|
|
133
|
+
filePath,
|
|
134
|
+
...(catchOpts?.fromOffset !== undefined ? { fromOffset: catchOpts.fromOffset } : {}),
|
|
135
|
+
...(catchOpts?.sinceEventId !== undefined ? { sinceEventId: catchOpts.sinceEventId } : {}),
|
|
136
|
+
})) {
|
|
137
|
+
yield { event, offset, eventId: event.eventId };
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
// 5. Continue live — exactly `createSession` with the record's params.
|
|
143
|
+
resume(ctx?: SessionContext) {
|
|
144
|
+
return createClaudeSession({ ...ctx, sessionParams: params });
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SessionCodec } from "../../types.js";
|
|
2
|
+
|
|
3
|
+
function readNonEmptyString(value: unknown): string | null {
|
|
4
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseAsObject(raw: unknown): Record<string, unknown> | null {
|
|
8
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
9
|
+
return raw as Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const claudeSessionCodec: SessionCodec = {
|
|
13
|
+
deserialize(raw: unknown): Record<string, unknown> | null {
|
|
14
|
+
const obj = parseAsObject(raw);
|
|
15
|
+
if (!obj) return null;
|
|
16
|
+
const sessionId = readNonEmptyString(obj["sessionId"]) ?? readNonEmptyString(obj["session_id"]);
|
|
17
|
+
if (!sessionId) return null;
|
|
18
|
+
const cwd =
|
|
19
|
+
readNonEmptyString(obj["cwd"]) ??
|
|
20
|
+
readNonEmptyString(obj["workdir"]) ??
|
|
21
|
+
readNonEmptyString(obj["folder"]);
|
|
22
|
+
return {
|
|
23
|
+
sessionId,
|
|
24
|
+
...(cwd ? { cwd } : {}),
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
serialize(params: Record<string, unknown> | null): Record<string, unknown> | null {
|
|
29
|
+
if (!params) return null;
|
|
30
|
+
const sessionId = readNonEmptyString(params["sessionId"]) ?? readNonEmptyString(params["session_id"]);
|
|
31
|
+
if (!sessionId) return null;
|
|
32
|
+
const cwd = readNonEmptyString(params["cwd"]);
|
|
33
|
+
return {
|
|
34
|
+
sessionId,
|
|
35
|
+
...(cwd ? { cwd } : {}),
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
getDisplayId(params: Record<string, unknown> | null): string | null {
|
|
40
|
+
if (!params) return null;
|
|
41
|
+
return readNonEmptyString(params["sessionId"]) ?? readNonEmptyString(params["session_id"]);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { ExecutionContext, ExecutionResult } from "../../types.js";
|
|
3
|
+
import { findBinary } from "../../utils/binary.js";
|
|
4
|
+
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
5
|
+
import { translateEndpoint } from "../../utils/endpoint.js";
|
|
6
|
+
import { runChildProcess, deriveErrorCode } from "../../utils/process.js";
|
|
7
|
+
import { detectAuth } from "../../utils/auth.js";
|
|
8
|
+
import { buildSkillsDir, cleanupSkillsDir } from "../../utils/skills.js";
|
|
9
|
+
import { claudeFeatureArgs, cleanupMcpConfig, stageMcpConfig } from "./mcp.js";
|
|
10
|
+
import { createToolNameTracker } from "../../utils/tool-names.js";
|
|
11
|
+
import { uuidv7 } from "../../utils/uuid.js";
|
|
12
|
+
import { prepareWorkspace } from "../../utils/workspace.js";
|
|
13
|
+
import type { PreparedWorkspace } from "../../utils/workspace.js";
|
|
14
|
+
import { parseClaudeStreamJson, parseStreamLine, isClaudeUnknownSessionError, isClaudeAuthRequired, CLAUDE_LOGIN_COMMAND, type PartialStreamContext } from "./parse.js";
|
|
15
|
+
|
|
16
|
+
export async function executeClaudeProvider(ctx: ExecutionContext): Promise<ExecutionResult> {
|
|
17
|
+
const runId = ctx.runId ?? uuidv7();
|
|
18
|
+
let cwd = ctx.cwd ?? process.cwd();
|
|
19
|
+
const config = ctx.config ?? {};
|
|
20
|
+
const rawModel = ctx.model ?? config.model;
|
|
21
|
+
const startedAt = new Date().toISOString();
|
|
22
|
+
|
|
23
|
+
// 1. Resolve binary
|
|
24
|
+
ctx.onLifecycle?.({ phase: "preparing", step: "binary" });
|
|
25
|
+
let resolvedBinary;
|
|
26
|
+
try {
|
|
27
|
+
resolvedBinary = await findBinary("claude", config.command);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
const errorMessage = err instanceof Error ? err.message : "Binary not found";
|
|
30
|
+
ctx.onLifecycle?.({ phase: "error", message: errorMessage });
|
|
31
|
+
return {
|
|
32
|
+
runId,
|
|
33
|
+
exitCode: null,
|
|
34
|
+
signal: null,
|
|
35
|
+
status: "failed" as const,
|
|
36
|
+
startedAt,
|
|
37
|
+
completedAt: new Date().toISOString(),
|
|
38
|
+
durationMs: Date.now() - new Date(startedAt).getTime(),
|
|
39
|
+
errorMessage,
|
|
40
|
+
errorCode: "binary_not_found",
|
|
41
|
+
costUsd: null,
|
|
42
|
+
model: null,
|
|
43
|
+
summary: null,
|
|
44
|
+
sessionParams: null,
|
|
45
|
+
sessionDisplayId: null,
|
|
46
|
+
clearSession: false,
|
|
47
|
+
billingType: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Workspace isolation
|
|
52
|
+
let workspace: PreparedWorkspace | undefined;
|
|
53
|
+
if (config.workspace) {
|
|
54
|
+
ctx.onLifecycle?.({ phase: "preparing", step: "workspace" });
|
|
55
|
+
workspace = await prepareWorkspace(cwd, config.workspace);
|
|
56
|
+
cwd = workspace.cwd;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Build env & detect auth/billing
|
|
60
|
+
ctx.onLifecycle?.({ phase: "preparing", step: "auth" });
|
|
61
|
+
const env = buildEnv(ctx.env);
|
|
62
|
+
ensurePathInEnv(env);
|
|
63
|
+
// Custom endpoint (BYOK / gateway / alt model) — env-only for claude. `unset`
|
|
64
|
+
// clears ambient Anthropic creds that would otherwise leak to a custom baseUrl.
|
|
65
|
+
const endpointTx = translateEndpoint("claude", config.endpoint);
|
|
66
|
+
Object.assign(env, endpointTx.env);
|
|
67
|
+
for (const key of endpointTx.unset) delete env[key];
|
|
68
|
+
const auth = detectAuth("claude", env);
|
|
69
|
+
// Any explicit endpoint auth or a custom base URL is external/BYOK billing,
|
|
70
|
+
// not the local subscription (detectAuth only recognizes ANTHROPIC_API_KEY).
|
|
71
|
+
const usesCustomEndpoint = !!(
|
|
72
|
+
config.endpoint?.baseUrl || config.endpoint?.authToken || config.endpoint?.apiKey
|
|
73
|
+
);
|
|
74
|
+
const billingType = usesCustomEndpoint ? "api" : auth.billingType;
|
|
75
|
+
// Skip Bedrock model-id remapping for a custom endpoint: ambient AWS creds
|
|
76
|
+
// make detectAuth return a Bedrock resolveModelId that would otherwise rewrite
|
|
77
|
+
// `--model` into a Bedrock id and send it to the wrong place.
|
|
78
|
+
const model = !usesCustomEndpoint && rawModel && auth.resolveModelId
|
|
79
|
+
? auth.resolveModelId(rawModel)
|
|
80
|
+
: rawModel;
|
|
81
|
+
|
|
82
|
+
// 3. Build skills dir
|
|
83
|
+
ctx.onLifecycle?.({ phase: "preparing", step: "skills" });
|
|
84
|
+
let skillsDir: string | null = null;
|
|
85
|
+
if (config.skillDirs && config.skillDirs.length > 0) {
|
|
86
|
+
try {
|
|
87
|
+
skillsDir = await buildSkillsDir(config.skillDirs, "claude");
|
|
88
|
+
} catch {
|
|
89
|
+
// Skill injection failure is non-fatal
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3.5 Stage MCP config — attached via `--mcp-config <file>` (mode 0600),
|
|
94
|
+
// never argv: http server headers can carry bearer tokens and argv is
|
|
95
|
+
// world-readable via `ps`.
|
|
96
|
+
let mcpConfigPath: string | null = null;
|
|
97
|
+
if (config.mcpServers && config.mcpServers.length > 0) {
|
|
98
|
+
mcpConfigPath = await stageMcpConfig(config.mcpServers);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 4. Build args
|
|
102
|
+
const buildArgs = (resumeSessionId: string | null): string[] => {
|
|
103
|
+
const args = [...resolvedBinary.prefixArgs, "--print", "-", "--output-format", "stream-json", "--verbose"];
|
|
104
|
+
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
|
105
|
+
// planMode and skipPermissions are mutually exclusive — planMode wins.
|
|
106
|
+
if (config.planMode) {
|
|
107
|
+
args.push("--permission-mode", "plan");
|
|
108
|
+
} else if (config.skipPermissions) {
|
|
109
|
+
args.push("--dangerously-skip-permissions");
|
|
110
|
+
}
|
|
111
|
+
if (model) args.push("--model", model);
|
|
112
|
+
if (config.effort) args.push("--effort", config.effort);
|
|
113
|
+
if (config.maxTurns && config.maxTurns > 0) args.push("--max-turns", String(config.maxTurns));
|
|
114
|
+
if (config.instructionsFile) args.push("--append-system-prompt-file", config.instructionsFile);
|
|
115
|
+
if (skillsDir) args.push("--add-dir", skillsDir);
|
|
116
|
+
args.push(...claudeFeatureArgs(config, mcpConfigPath));
|
|
117
|
+
// extraArgs stay LAST so hosts can override any generated flag.
|
|
118
|
+
if (config.extraArgs) args.push(...config.extraArgs);
|
|
119
|
+
return args;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// 5. Determine session resume
|
|
123
|
+
const sessionParams = ctx.sessionParams ?? null;
|
|
124
|
+
const sessionId = (() => {
|
|
125
|
+
if (!sessionParams) return null;
|
|
126
|
+
const id = sessionParams["sessionId"] as string | undefined ?? sessionParams["session_id"] as string | undefined;
|
|
127
|
+
if (!id || typeof id !== "string") return null;
|
|
128
|
+
const sessionCwd = sessionParams["cwd"] as string | undefined;
|
|
129
|
+
if (sessionCwd && path.resolve(sessionCwd) !== path.resolve(cwd)) return null;
|
|
130
|
+
return id;
|
|
131
|
+
})();
|
|
132
|
+
|
|
133
|
+
// 6. Run attempt
|
|
134
|
+
const runAttempt = async (resumeSessionId: string | null) => {
|
|
135
|
+
const args = buildArgs(resumeSessionId);
|
|
136
|
+
|
|
137
|
+
// stdout line buffer for real-time event parsing
|
|
138
|
+
let lineBuffer = "";
|
|
139
|
+
// Correlates tool_call → tool_result so emitted tool_result events carry
|
|
140
|
+
// toolName. One tracker per attempt (a retry restarts the stream).
|
|
141
|
+
const trackToolName = createToolNameTracker();
|
|
142
|
+
// Tracks the owning message id across --include-partial-messages stream
|
|
143
|
+
// lines so deltas reconcile with their consolidated assistant event.
|
|
144
|
+
const partialCtx: PartialStreamContext = { messageId: null };
|
|
145
|
+
|
|
146
|
+
ctx.onLifecycle?.({ phase: "spawning" });
|
|
147
|
+
const proc = await runChildProcess({
|
|
148
|
+
runId,
|
|
149
|
+
command: resolvedBinary.bin,
|
|
150
|
+
args,
|
|
151
|
+
cwd,
|
|
152
|
+
env,
|
|
153
|
+
stdin: ctx.prompt,
|
|
154
|
+
timeoutSec: config.timeoutSec,
|
|
155
|
+
graceSec: config.graceSec,
|
|
156
|
+
onStart: (pid) => {
|
|
157
|
+
ctx.onLifecycle?.({ phase: "running", pid });
|
|
158
|
+
ctx.onStart?.(pid);
|
|
159
|
+
},
|
|
160
|
+
signal: ctx.signal,
|
|
161
|
+
onOutput: async (stream, chunk) => {
|
|
162
|
+
// Forward raw output
|
|
163
|
+
if (ctx.onOutput) {
|
|
164
|
+
try { await ctx.onOutput(stream, chunk); } catch { /* swallow */ }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse stdout lines for stream events
|
|
168
|
+
if (stream === "stdout" && ctx.onEvent) {
|
|
169
|
+
lineBuffer += chunk;
|
|
170
|
+
const lines = lineBuffer.split("\n");
|
|
171
|
+
// Keep the last (possibly incomplete) line in the buffer
|
|
172
|
+
lineBuffer = lines.pop() ?? "";
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
const trimmed = line.trim();
|
|
175
|
+
if (!trimmed) continue;
|
|
176
|
+
for (const event of parseStreamLine(trimmed, partialCtx)) {
|
|
177
|
+
try { await ctx.onEvent(trackToolName(event)); } catch { /* swallow */ }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Parse remaining buffer
|
|
185
|
+
if (lineBuffer.trim() && ctx.onEvent) {
|
|
186
|
+
for (const event of parseStreamLine(lineBuffer.trim())) {
|
|
187
|
+
try { await ctx.onEvent(trackToolName(event)); } catch { /* swallow */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return proc;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// 7. Initial attempt
|
|
196
|
+
let proc = await runAttempt(sessionId);
|
|
197
|
+
let clearSession = false;
|
|
198
|
+
|
|
199
|
+
// 8. Check for unknown session — retry once
|
|
200
|
+
if (
|
|
201
|
+
sessionId &&
|
|
202
|
+
!proc.timedOut &&
|
|
203
|
+
(proc.exitCode ?? 0) !== 0 &&
|
|
204
|
+
isClaudeUnknownSessionError(proc.stdout, proc.stderr)
|
|
205
|
+
) {
|
|
206
|
+
proc = await runAttempt(null);
|
|
207
|
+
clearSession = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 9. Parse result
|
|
211
|
+
const parsed = parseClaudeStreamJson(proc.stdout);
|
|
212
|
+
const processErrorCode = deriveErrorCode(proc);
|
|
213
|
+
|
|
214
|
+
// Determine error code: process-level errors take precedence, then provider-specific.
|
|
215
|
+
// parsed.errorCode already covers `auth_required` when the structured
|
|
216
|
+
// signal (api_error_status 401/403 or documented auth text) fired in
|
|
217
|
+
// parseClaudeStreamJson. The regex fallback below catches edge cases
|
|
218
|
+
// where the CLI bailed before emitting a result event at all (binary
|
|
219
|
+
// failure, stderr-only error from a wrapper).
|
|
220
|
+
let errorCode = processErrorCode;
|
|
221
|
+
if (!errorCode && parsed.errorCode) {
|
|
222
|
+
errorCode = parsed.errorCode;
|
|
223
|
+
}
|
|
224
|
+
if (!errorCode && isClaudeAuthRequired(proc.stdout, proc.stderr)) {
|
|
225
|
+
errorCode = "auth_required";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const errorMessage = (() => {
|
|
229
|
+
if (proc.timedOut) return `Timed out after ${config.timeoutSec ?? 0}s`;
|
|
230
|
+
if (errorCode === "auth_required") {
|
|
231
|
+
// Prefer the provider's user-facing string when we captured it,
|
|
232
|
+
// so callers can show "OAuth token has expired" rather than a
|
|
233
|
+
// generic banner. Fall back to a recovery hint otherwise.
|
|
234
|
+
return parsed.summary
|
|
235
|
+
? `${parsed.summary} (run \`${CLAUDE_LOGIN_COMMAND}\`)`
|
|
236
|
+
: `Claude requires authentication. Run \`${CLAUDE_LOGIN_COMMAND}\`.`;
|
|
237
|
+
}
|
|
238
|
+
if ((proc.exitCode ?? 0) !== 0 && !parsed.summary) {
|
|
239
|
+
const stderrLine = proc.stderr.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
240
|
+
return stderrLine ?? `Claude exited with code ${proc.exitCode ?? -1}`;
|
|
241
|
+
}
|
|
242
|
+
if (parsed.isError) return parsed.summary;
|
|
243
|
+
return null;
|
|
244
|
+
})();
|
|
245
|
+
|
|
246
|
+
const resolvedSessionId = parsed.sessionId;
|
|
247
|
+
const resultSessionParams = resolvedSessionId
|
|
248
|
+
? { sessionId: resolvedSessionId, cwd }
|
|
249
|
+
: null;
|
|
250
|
+
|
|
251
|
+
const completedAt = new Date().toISOString();
|
|
252
|
+
const resolvedModel = parsed.model ?? model ?? null;
|
|
253
|
+
const status = proc.aborted ? "aborted" as const
|
|
254
|
+
: proc.timedOut ? "timeout" as const
|
|
255
|
+
: (errorCode || errorMessage) ? "failed" as const
|
|
256
|
+
: "completed" as const;
|
|
257
|
+
|
|
258
|
+
if (status === "completed") {
|
|
259
|
+
ctx.onLifecycle?.({ phase: "completed" });
|
|
260
|
+
} else if (status === "aborted") {
|
|
261
|
+
ctx.onLifecycle?.({ phase: "cancelled" });
|
|
262
|
+
} else {
|
|
263
|
+
ctx.onLifecycle?.({ phase: "error", message: errorMessage ?? "Unknown error" });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
runId,
|
|
268
|
+
exitCode: proc.exitCode,
|
|
269
|
+
signal: proc.signal ?? null,
|
|
270
|
+
status,
|
|
271
|
+
startedAt,
|
|
272
|
+
completedAt,
|
|
273
|
+
durationMs: new Date(completedAt).getTime() - new Date(startedAt).getTime(),
|
|
274
|
+
errorMessage,
|
|
275
|
+
errorCode,
|
|
276
|
+
usage: parsed.modelUsage ?? undefined,
|
|
277
|
+
costUsd: parsed.costUsd,
|
|
278
|
+
model: resolvedModel,
|
|
279
|
+
summary: parsed.summary,
|
|
280
|
+
sessionParams: resultSessionParams,
|
|
281
|
+
sessionDisplayId: resolvedSessionId,
|
|
282
|
+
clearSession,
|
|
283
|
+
billingType,
|
|
284
|
+
stopReason: parsed.stopReason,
|
|
285
|
+
terminalReason: parsed.terminalReason,
|
|
286
|
+
numTurns: parsed.numTurns,
|
|
287
|
+
durationApiMs: parsed.durationApiMs,
|
|
288
|
+
permissionDenials: parsed.permissionDenials ?? undefined,
|
|
289
|
+
rateLimits: parsed.rateLimits.length > 0 ? parsed.rateLimits : undefined,
|
|
290
|
+
raw: parsed.finalEvent,
|
|
291
|
+
workspace,
|
|
292
|
+
};
|
|
293
|
+
} finally {
|
|
294
|
+
// 10. Clean up staged dirs/files
|
|
295
|
+
if (skillsDir) {
|
|
296
|
+
await cleanupSkillsDir(skillsDir);
|
|
297
|
+
}
|
|
298
|
+
await cleanupMcpConfig(mcpConfigPath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GoalCapability } from "../../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude enforces goals natively via a Stop-hook + fast-model sentinel (the
|
|
5
|
+
* `/goal` command). Binary met/not-met, self-clearing on completion.
|
|
6
|
+
*
|
|
7
|
+
* `statuses` describes the NATIVE producible set. A Claude session that falls to
|
|
8
|
+
* the emulation engine (a custom `sentinel`, or `enforce:"emulate"`) can also
|
|
9
|
+
* produce `blocked` (`blockedReason:"max_iterations"`) — see GoalController.
|
|
10
|
+
*
|
|
11
|
+
* Lives in its own leaf module (not `session.ts`) so `index.ts` can read the
|
|
12
|
+
* static `capabilities.goals` field without pulling the heavy session machinery
|
|
13
|
+
* into the module graph (spec §5.1).
|
|
14
|
+
*/
|
|
15
|
+
export const claudeGoalCapability: GoalCapability = {
|
|
16
|
+
mechanism: "sentinel",
|
|
17
|
+
enforced: true,
|
|
18
|
+
statuses: ["active", "met", "cleared"],
|
|
19
|
+
clears: "both",
|
|
20
|
+
telemetry: false,
|
|
21
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ProviderModule, SessionContext, AgentSession, QuotaContext, QuotaStatus } from "../../types.js";
|
|
2
|
+
import { detectAuth, resolveAuthForProvider } from "../../utils/auth.js";
|
|
3
|
+
import { claudeSessionCodec } from "./codec.js";
|
|
4
|
+
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
5
|
+
import { claudeTranscriptOps } from "./transcript.js";
|
|
6
|
+
import { claudeGoalCapability } from "./goal-capability.js";
|
|
7
|
+
|
|
8
|
+
async function checkQuota(ctx: QuotaContext): Promise<QuotaStatus> {
|
|
9
|
+
const env = buildEnv(ctx.env);
|
|
10
|
+
ensurePathInEnv(env);
|
|
11
|
+
const auth = detectAuth("claude", env);
|
|
12
|
+
return {
|
|
13
|
+
available: auth.method !== "subscription" || !!env["ANTHROPIC_API_KEY"],
|
|
14
|
+
billingType: auth.billingType,
|
|
15
|
+
detail: { method: auth.method, region: auth.region },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const claudeProvider: ProviderModule = {
|
|
20
|
+
type: "claude",
|
|
21
|
+
capabilities: {
|
|
22
|
+
sessions: true,
|
|
23
|
+
modelDiscovery: false,
|
|
24
|
+
quotaProbing: true,
|
|
25
|
+
mcp: true,
|
|
26
|
+
skills: true,
|
|
27
|
+
skillInventory: "provider-init",
|
|
28
|
+
skillInvocation: "native-slash",
|
|
29
|
+
instructions: true,
|
|
30
|
+
workspace: true,
|
|
31
|
+
planMode: true,
|
|
32
|
+
concurrentSend: true,
|
|
33
|
+
cancelQueuedMessage: true,
|
|
34
|
+
stopTask: true,
|
|
35
|
+
modes: false,
|
|
36
|
+
goals: claudeGoalCapability,
|
|
37
|
+
durableSessions: true,
|
|
38
|
+
},
|
|
39
|
+
// Heavy machinery (execute.ts, session.ts, attach.ts) loads lazily on first
|
|
40
|
+
// use — every ProviderModule method is already async, so this is invisible to
|
|
41
|
+
// callers.
|
|
42
|
+
execute: async (ctx) => (await import("./execute.js")).executeClaudeProvider(ctx),
|
|
43
|
+
createSession: async (ctx: SessionContext): Promise<AgentSession> =>
|
|
44
|
+
(await import("./session.js")).createClaudeSession(ctx),
|
|
45
|
+
resolveAuth: (ctx) => resolveAuthForProvider("claude", ctx),
|
|
46
|
+
sessionCodec: claudeSessionCodec,
|
|
47
|
+
checkQuota,
|
|
48
|
+
transcript: claudeTranscriptOps,
|
|
49
|
+
attachSession: async (record, opts) =>
|
|
50
|
+
(await import("./attach.js")).attachClaudeSession(record, opts),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
getClaudeTranscriptPath,
|
|
55
|
+
findClaudeTranscriptBySessionId,
|
|
56
|
+
readClaudeTranscript,
|
|
57
|
+
peekClaudeTranscript,
|
|
58
|
+
claudeTranscriptOps,
|
|
59
|
+
sanitizeProjectPath,
|
|
60
|
+
resolveClaudeHome,
|
|
61
|
+
canonicalizeCwd,
|
|
62
|
+
MAX_SANITIZED_LENGTH,
|
|
63
|
+
} from "./transcript.js";
|
|
64
|
+
export type {
|
|
65
|
+
GetClaudeTranscriptPathOptions,
|
|
66
|
+
ClaudeTranscriptLocation,
|
|
67
|
+
FindClaudeTranscriptOptions,
|
|
68
|
+
FoundClaudeTranscript,
|
|
69
|
+
ReadClaudeTranscriptOptions,
|
|
70
|
+
ClaudeTranscriptYield,
|
|
71
|
+
ClaudePeekResult,
|
|
72
|
+
} from "./transcript.js";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { McpServerConfig, ProviderConfig } from "../../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map agentex `McpServerConfig[]` into Claude's `--mcp-config` JSON shape:
|
|
8
|
+
* `{ "mcpServers": { "<name>": { … } } }`.
|
|
9
|
+
*
|
|
10
|
+
* stdio servers (the default arm) keep `command`/`args`/`env`; http/sse servers
|
|
11
|
+
* carry `url` (+ optional `headers`).
|
|
12
|
+
*/
|
|
13
|
+
export function buildMcpConfigJson(servers: McpServerConfig[]): {
|
|
14
|
+
mcpServers: Record<string, Record<string, unknown>>;
|
|
15
|
+
} {
|
|
16
|
+
const mcpServers: Record<string, Record<string, unknown>> = {};
|
|
17
|
+
for (const s of servers) {
|
|
18
|
+
if ("url" in s) {
|
|
19
|
+
mcpServers[s.name] = {
|
|
20
|
+
type: s.type,
|
|
21
|
+
url: s.url,
|
|
22
|
+
...(s.headers ? { headers: s.headers } : {}),
|
|
23
|
+
};
|
|
24
|
+
} else {
|
|
25
|
+
mcpServers[s.name] = {
|
|
26
|
+
type: "stdio",
|
|
27
|
+
command: s.command,
|
|
28
|
+
...(s.args ? { args: s.args } : {}),
|
|
29
|
+
...(s.env ? { env: s.env } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { mcpServers };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stage the MCP config as a JSON file (mode 0600) in a fresh `agentex-mcp-*`
|
|
38
|
+
* temp dir and return the file path. HTTP server `headers` can carry bearer
|
|
39
|
+
* tokens, so the config never goes through argv — argv is world-readable via
|
|
40
|
+
* `ps`. Clean up with {@link cleanupMcpConfig} when the run/session ends.
|
|
41
|
+
*/
|
|
42
|
+
export async function stageMcpConfig(servers: McpServerConfig[]): Promise<string> {
|
|
43
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "agentex-mcp-"));
|
|
44
|
+
const file = path.join(dir, "mcp-config.json");
|
|
45
|
+
await fs.writeFile(file, JSON.stringify(buildMcpConfigJson(servers), null, 2), {
|
|
46
|
+
mode: 0o600,
|
|
47
|
+
});
|
|
48
|
+
return file;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Remove a staged MCP config (and its temp dir). Idempotent; null-safe. */
|
|
52
|
+
export async function cleanupMcpConfig(filePath: string | null): Promise<void> {
|
|
53
|
+
if (!filePath) return;
|
|
54
|
+
try {
|
|
55
|
+
await fs.rm(path.dirname(filePath), { recursive: true, force: true });
|
|
56
|
+
} catch {
|
|
57
|
+
/* already gone */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The argv tail for Claude's MCP + tool-filtering + partial-message features.
|
|
63
|
+
* Shared by the execute and session arg builders so both stay in lockstep, and
|
|
64
|
+
* exported so tests can snapshot the exact argv. Callers append
|
|
65
|
+
* `config.extraArgs` AFTER this (the host-override invariant).
|
|
66
|
+
*/
|
|
67
|
+
export function claudeFeatureArgs(config: ProviderConfig, mcpConfigPath: string | null): string[] {
|
|
68
|
+
const args: string[] = [];
|
|
69
|
+
if (mcpConfigPath) args.push("--mcp-config", mcpConfigPath);
|
|
70
|
+
// Strict works with or without an attached config: strict + no config means
|
|
71
|
+
// "no MCP at all", which embedding hosts use to block ambient .mcp.json /
|
|
72
|
+
// user-scope servers from leaking into a product-controlled session.
|
|
73
|
+
if (config.strictMcpConfig) args.push("--strict-mcp-config");
|
|
74
|
+
if (config.allowedTools && config.allowedTools.length > 0) {
|
|
75
|
+
args.push("--allowed-tools", config.allowedTools.join(","));
|
|
76
|
+
}
|
|
77
|
+
if (config.disallowedTools && config.disallowedTools.length > 0) {
|
|
78
|
+
args.push("--disallowed-tools", config.disallowedTools.join(","));
|
|
79
|
+
}
|
|
80
|
+
if (config.includePartialMessages) args.push("--include-partial-messages");
|
|
81
|
+
return args;
|
|
82
|
+
}
|