@agentex/agent 0.0.24 → 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.
Files changed (164) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -0
  4. package/dist/derived.d.ts +5 -3
  5. package/dist/derived.d.ts.map +1 -1
  6. package/dist/derived.js +11 -7
  7. package/dist/derived.js.map +1 -1
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/acp/index.d.ts +1 -1
  13. package/dist/providers/acp/index.d.ts.map +1 -1
  14. package/dist/providers/acp/index.js +5 -97
  15. package/dist/providers/acp/index.js.map +1 -1
  16. package/dist/providers/acp/session.d.ts +8 -1
  17. package/dist/providers/acp/session.d.ts.map +1 -1
  18. package/dist/providers/acp/session.js +94 -0
  19. package/dist/providers/acp/session.js.map +1 -1
  20. package/dist/providers/claude/attach.d.ts +8 -0
  21. package/dist/providers/claude/attach.d.ts.map +1 -0
  22. package/dist/providers/claude/attach.js +113 -0
  23. package/dist/providers/claude/attach.js.map +1 -0
  24. package/dist/providers/claude/goal-capability.d.ts +15 -0
  25. package/dist/providers/claude/goal-capability.d.ts.map +1 -0
  26. package/dist/providers/claude/goal-capability.js +20 -0
  27. package/dist/providers/claude/goal-capability.js.map +1 -0
  28. package/dist/providers/claude/index.d.ts.map +1 -1
  29. package/dist/providers/claude/index.js +8 -4
  30. package/dist/providers/claude/index.js.map +1 -1
  31. package/dist/providers/claude/session.d.ts +11 -9
  32. package/dist/providers/claude/session.d.ts.map +1 -1
  33. package/dist/providers/claude/session.js +29 -14
  34. package/dist/providers/claude/session.js.map +1 -1
  35. package/dist/providers/codex/attach.d.ts +9 -0
  36. package/dist/providers/codex/attach.d.ts.map +1 -0
  37. package/dist/providers/codex/attach.js +93 -0
  38. package/dist/providers/codex/attach.js.map +1 -0
  39. package/dist/providers/codex/goal-capability.d.ts +13 -0
  40. package/dist/providers/codex/goal-capability.d.ts.map +1 -0
  41. package/dist/providers/codex/goal-capability.js +18 -0
  42. package/dist/providers/codex/goal-capability.js.map +1 -0
  43. package/dist/providers/codex/index.d.ts +1 -0
  44. package/dist/providers/codex/index.d.ts.map +1 -1
  45. package/dist/providers/codex/index.js +9 -6
  46. package/dist/providers/codex/index.js.map +1 -1
  47. package/dist/providers/codex/session.d.ts +11 -7
  48. package/dist/providers/codex/session.d.ts.map +1 -1
  49. package/dist/providers/codex/session.js +24 -12
  50. package/dist/providers/codex/session.js.map +1 -1
  51. package/dist/providers/codex/transcript-normalize.d.ts +28 -0
  52. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
  53. package/dist/providers/codex/transcript-normalize.js +191 -0
  54. package/dist/providers/codex/transcript-normalize.js.map +1 -0
  55. package/dist/providers/cursor/index.d.ts.map +1 -1
  56. package/dist/providers/cursor/index.js +2 -2
  57. package/dist/providers/cursor/index.js.map +1 -1
  58. package/dist/providers/openclaw/index.d.ts.map +1 -1
  59. package/dist/providers/openclaw/index.js +2 -2
  60. package/dist/providers/openclaw/index.js.map +1 -1
  61. package/dist/providers/opencode/index.d.ts.map +1 -1
  62. package/dist/providers/opencode/index.js +3 -5
  63. package/dist/providers/opencode/index.js.map +1 -1
  64. package/dist/providers/pi/index.d.ts.map +1 -1
  65. package/dist/providers/pi/index.js +3 -5
  66. package/dist/providers/pi/index.js.map +1 -1
  67. package/dist/providers/process/index.d.ts.map +1 -1
  68. package/dist/providers/process/index.js +2 -2
  69. package/dist/providers/process/index.js.map +1 -1
  70. package/dist/registry.d.ts +0 -1
  71. package/dist/registry.d.ts.map +1 -1
  72. package/dist/registry.js +0 -4
  73. package/dist/registry.js.map +1 -1
  74. package/dist/sessions/index.d.ts +3 -0
  75. package/dist/sessions/index.d.ts.map +1 -0
  76. package/dist/sessions/index.js +2 -0
  77. package/dist/sessions/index.js.map +1 -0
  78. package/dist/sessions/record.d.ts +43 -0
  79. package/dist/sessions/record.d.ts.map +1 -0
  80. package/dist/sessions/record.js +85 -0
  81. package/dist/sessions/record.js.map +1 -0
  82. package/dist/types.d.ts +119 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/uuid.d.ts +7 -1
  86. package/dist/utils/uuid.d.ts.map +1 -1
  87. package/dist/utils/uuid.js +21 -1
  88. package/dist/utils/uuid.js.map +1 -1
  89. package/package.json +64 -7
  90. package/src/derived.ts +311 -0
  91. package/src/goals/controller.ts +442 -0
  92. package/src/goals/index.ts +21 -0
  93. package/src/goals/normalize.ts +173 -0
  94. package/src/goals/sentinel.ts +90 -0
  95. package/src/index.ts +270 -0
  96. package/src/providers/_shared/http-agent.ts +304 -0
  97. package/src/providers/acp/index.ts +103 -0
  98. package/src/providers/acp/parse.ts +131 -0
  99. package/src/providers/acp/session.ts +744 -0
  100. package/src/providers/claude/attach.ts +147 -0
  101. package/src/providers/claude/codec.ts +43 -0
  102. package/src/providers/claude/execute.ts +300 -0
  103. package/src/providers/claude/goal-capability.ts +21 -0
  104. package/src/providers/claude/index.ts +72 -0
  105. package/src/providers/claude/mcp.ts +82 -0
  106. package/src/providers/claude/parse.ts +824 -0
  107. package/src/providers/claude/session.ts +1192 -0
  108. package/src/providers/claude/transcript.ts +555 -0
  109. package/src/providers/codex/attach.ts +123 -0
  110. package/src/providers/codex/codec.ts +50 -0
  111. package/src/providers/codex/execute.ts +337 -0
  112. package/src/providers/codex/goal-capability.ts +19 -0
  113. package/src/providers/codex/index.ts +57 -0
  114. package/src/providers/codex/modes.ts +159 -0
  115. package/src/providers/codex/parse.ts +691 -0
  116. package/src/providers/codex/plan-mode.ts +49 -0
  117. package/src/providers/codex/session.ts +1287 -0
  118. package/src/providers/codex/transcript-normalize.ts +197 -0
  119. package/src/providers/codex/transcript.ts +487 -0
  120. package/src/providers/codex/usage-scanner.ts +178 -0
  121. package/src/providers/copilot/index.ts +19 -0
  122. package/src/providers/cursor/codec.ts +44 -0
  123. package/src/providers/cursor/execute.ts +271 -0
  124. package/src/providers/cursor/index.ts +25 -0
  125. package/src/providers/cursor/parse.ts +288 -0
  126. package/src/providers/gemini/index.ts +21 -0
  127. package/src/providers/openclaw/codec.ts +40 -0
  128. package/src/providers/openclaw/execute.ts +19 -0
  129. package/src/providers/openclaw/index.ts +29 -0
  130. package/src/providers/opencode/codec.ts +50 -0
  131. package/src/providers/opencode/event-parse.ts +141 -0
  132. package/src/providers/opencode/execute.ts +251 -0
  133. package/src/providers/opencode/http-session.ts +427 -0
  134. package/src/providers/opencode/index.ts +30 -0
  135. package/src/providers/opencode/parse.ts +203 -0
  136. package/src/providers/opencode/server.ts +0 -0
  137. package/src/providers/pi/codec.ts +44 -0
  138. package/src/providers/pi/execute.ts +297 -0
  139. package/src/providers/pi/index.ts +30 -0
  140. package/src/providers/pi/parse.ts +231 -0
  141. package/src/providers/pi/session.ts +381 -0
  142. package/src/providers/process/execute.ts +148 -0
  143. package/src/providers/process/index.ts +52 -0
  144. package/src/registry.ts +40 -0
  145. package/src/sessions/index.ts +8 -0
  146. package/src/sessions/record.ts +108 -0
  147. package/src/types.ts +1638 -0
  148. package/src/utils/ask-user-question.ts +57 -0
  149. package/src/utils/auth.ts +661 -0
  150. package/src/utils/binary.ts +179 -0
  151. package/src/utils/endpoint.ts +172 -0
  152. package/src/utils/env.ts +63 -0
  153. package/src/utils/execute-all.ts +68 -0
  154. package/src/utils/exit-plan-mode.ts +40 -0
  155. package/src/utils/instructions.ts +427 -0
  156. package/src/utils/process.ts +223 -0
  157. package/src/utils/runtime-config.ts +100 -0
  158. package/src/utils/runtime-homes.ts +49 -0
  159. package/src/utils/skill-commands.ts +493 -0
  160. package/src/utils/skills.ts +500 -0
  161. package/src/utils/template.ts +16 -0
  162. package/src/utils/tool-names.ts +51 -0
  163. package/src/utils/uuid.ts +21 -0
  164. 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
+ }