@cleocode/adapters 2026.4.46 → 2026.4.48

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 (54) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +19562 -372
  4. package/dist/index.js.map +4 -4
  5. package/dist/providers/claude-code/adapter.d.ts +12 -6
  6. package/dist/providers/claude-code/adapter.d.ts.map +1 -1
  7. package/dist/providers/claude-sdk/index.d.ts +18 -0
  8. package/dist/providers/claude-sdk/index.d.ts.map +1 -0
  9. package/dist/providers/claude-sdk/mcp-registry.d.ts +40 -0
  10. package/dist/providers/claude-sdk/mcp-registry.d.ts.map +1 -0
  11. package/dist/providers/claude-sdk/session-store.d.ts +78 -0
  12. package/dist/providers/claude-sdk/session-store.d.ts.map +1 -0
  13. package/dist/providers/claude-sdk/spawn.d.ts +79 -0
  14. package/dist/providers/claude-sdk/spawn.d.ts.map +1 -0
  15. package/dist/providers/claude-sdk/tool-bridge.d.ts +38 -0
  16. package/dist/providers/claude-sdk/tool-bridge.d.ts.map +1 -0
  17. package/dist/providers/openai-sdk/adapter.d.ts +77 -0
  18. package/dist/providers/openai-sdk/adapter.d.ts.map +1 -0
  19. package/dist/providers/openai-sdk/guardrails.d.ts +67 -0
  20. package/dist/providers/openai-sdk/guardrails.d.ts.map +1 -0
  21. package/dist/providers/openai-sdk/handoff.d.ts +94 -0
  22. package/dist/providers/openai-sdk/handoff.d.ts.map +1 -0
  23. package/dist/providers/openai-sdk/index.d.ts +39 -0
  24. package/dist/providers/openai-sdk/index.d.ts.map +1 -0
  25. package/dist/providers/openai-sdk/install.d.ts +61 -0
  26. package/dist/providers/openai-sdk/install.d.ts.map +1 -0
  27. package/dist/providers/openai-sdk/spawn.d.ts +146 -0
  28. package/dist/providers/openai-sdk/spawn.d.ts.map +1 -0
  29. package/dist/providers/openai-sdk/tracing.d.ts +89 -0
  30. package/dist/providers/openai-sdk/tracing.d.ts.map +1 -0
  31. package/dist/providers/shared/conduit-trace-writer.d.ts +72 -0
  32. package/dist/providers/shared/conduit-trace-writer.d.ts.map +1 -0
  33. package/dist/providers/shared/sdk-result-mapper.d.ts +51 -0
  34. package/dist/providers/shared/sdk-result-mapper.d.ts.map +1 -0
  35. package/package.json +5 -3
  36. package/src/index.ts +8 -0
  37. package/src/providers/claude-code/adapter.ts +41 -4
  38. package/src/providers/claude-sdk/__tests__/spawn.test.ts +448 -0
  39. package/src/providers/claude-sdk/index.ts +18 -0
  40. package/src/providers/claude-sdk/mcp-registry.ts +96 -0
  41. package/src/providers/claude-sdk/session-store.ts +103 -0
  42. package/src/providers/claude-sdk/spawn.ts +242 -0
  43. package/src/providers/claude-sdk/tool-bridge.ts +51 -0
  44. package/src/providers/openai-sdk/__tests__/openai-sdk-spawn.test.ts +716 -0
  45. package/src/providers/openai-sdk/adapter.ts +138 -0
  46. package/src/providers/openai-sdk/guardrails.ts +158 -0
  47. package/src/providers/openai-sdk/handoff.ts +187 -0
  48. package/src/providers/openai-sdk/index.ts +55 -0
  49. package/src/providers/openai-sdk/install.ts +135 -0
  50. package/src/providers/openai-sdk/manifest.json +45 -0
  51. package/src/providers/openai-sdk/spawn.ts +300 -0
  52. package/src/providers/openai-sdk/tracing.ts +175 -0
  53. package/src/providers/shared/conduit-trace-writer.ts +101 -0
  54. package/src/providers/shared/sdk-result-mapper.ts +83 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Claude SDK Spawn Provider
3
+ *
4
+ * Implements `AdapterSpawnProvider` using the `@anthropic-ai/claude-agent-sdk`
5
+ * programmatic API instead of shelling out to the `claude` CLI.
6
+ *
7
+ * Differences from `ClaudeCodeSpawnProvider`:
8
+ * - Uses SDK `query()` instead of a detached child process
9
+ * - Awaits full completion before returning (synchronous output capture)
10
+ * - Session IDs from the SDK enable future multi-turn resumption
11
+ * - No temp files, no OS PIDs — tracking is purely in-memory session IDs
12
+ * - `canSpawn()` checks for `ANTHROPIC_API_KEY` rather than CLI availability
13
+ *
14
+ * CANT enrichment is identical to the CLI provider: `buildCantEnrichedPrompt()`
15
+ * is called before `query()` and the result is passed as the SDK prompt string.
16
+ *
17
+ * @task T581
18
+ */
19
+
20
+ import type { AdapterSpawnProvider, SpawnContext, SpawnResult } from '@cleocode/contracts';
21
+ import { getErrorMessage } from '@cleocode/contracts';
22
+ import { getServers } from './mcp-registry.js';
23
+ import { SessionStore } from './session-store.js';
24
+ import { resolveTools } from './tool-bridge.js';
25
+
26
+ /** Model used when no model is specified in spawn options. */
27
+ const DEFAULT_MODEL = 'claude-sonnet-4-5';
28
+
29
+ /**
30
+ * Spawn provider that uses the Anthropic Claude Agent SDK for programmatic
31
+ * subagent execution.
32
+ *
33
+ * Each call to `spawn()` runs a full SDK `query()` to completion and
34
+ * captures the output. Sessions are tracked in `SessionStore` so callers
35
+ * can inspect active sessions via `listRunning()` and cancel them via
36
+ * `terminate()`.
37
+ *
38
+ * @remarks
39
+ * The `permissionMode: 'bypassPermissions'` + `allowDangerouslySkipPermissions: true`
40
+ * combination mirrors the `--dangerously-skip-permissions` flag used by
41
+ * the CLI provider. Both are required by the SDK when bypassing all tool
42
+ * permission prompts.
43
+ */
44
+ export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
45
+ /** In-memory session registry. */
46
+ private readonly sessions = new SessionStore();
47
+
48
+ /**
49
+ * Check whether the SDK can be used in the current environment.
50
+ *
51
+ * Returns `true` if `ANTHROPIC_API_KEY` is set. No binary check is needed
52
+ * because the SDK manages the Claude Code subprocess internally.
53
+ *
54
+ * @returns `true` when an API key is present
55
+ */
56
+ async canSpawn(): Promise<boolean> {
57
+ return !!process.env.ANTHROPIC_API_KEY;
58
+ }
59
+
60
+ /**
61
+ * Spawn a subagent using the Claude Agent SDK.
62
+ *
63
+ * Enriches the prompt via CANT context, runs the SDK `query()` to
64
+ * completion, captures all assistant text output, and returns a
65
+ * `SpawnResult` with the final output and exit code.
66
+ *
67
+ * @param context - Spawn context with taskId, prompt, options
68
+ * @returns Resolved spawn result (status: 'completed' or 'failed')
69
+ */
70
+ async spawn(context: SpawnContext): Promise<SpawnResult> {
71
+ const instanceId = `sdk-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
72
+ const startTime = new Date().toISOString();
73
+
74
+ // Register in session store immediately so listRunning() reflects it
75
+ // before the async query starts.
76
+ this.sessions.add({
77
+ instanceId,
78
+ sessionId: undefined,
79
+ taskId: context.taskId,
80
+ startTime,
81
+ });
82
+
83
+ try {
84
+ // CANT enrichment — best-effort, identical to ClaudeCodeSpawnProvider.
85
+ let enrichedPrompt = context.prompt;
86
+ try {
87
+ const { buildCantEnrichedPrompt } = await import('../../cant-context.js');
88
+ enrichedPrompt = await buildCantEnrichedPrompt({
89
+ projectDir: context.workingDirectory ?? process.cwd(),
90
+ basePrompt: context.prompt,
91
+ agentName: (context.options?.agentName as string) ?? undefined,
92
+ });
93
+ } catch {
94
+ // CANT enrichment unavailable — use raw prompt
95
+ }
96
+
97
+ // Lazy-import the SDK to avoid hard failures when ANTHROPIC_API_KEY
98
+ // is absent (canSpawn() guards the normal path, but tests may import
99
+ // this module without the key).
100
+ const { query } = await import('@anthropic-ai/claude-agent-sdk');
101
+
102
+ // Build allowedTools from the spawn context or fall back to CLEO defaults.
103
+ const toolAllowlist = context.options?.toolAllowlist as string[] | undefined;
104
+ const allowedTools = resolveTools(toolAllowlist);
105
+
106
+ // Resolve available MCP servers for the working directory.
107
+ const workDir = context.workingDirectory ?? process.cwd();
108
+ const mcpServers = getServers(workDir);
109
+
110
+ // Resume support: pass a prior session ID if provided.
111
+ const resumeSessionId = context.options?.resumeSessionId as string | undefined;
112
+
113
+ const sdkQuery = query({
114
+ prompt: enrichedPrompt,
115
+ options: {
116
+ cwd: workDir,
117
+ model: (context.options?.model as string) ?? DEFAULT_MODEL,
118
+ allowedTools,
119
+ permissionMode: 'bypassPermissions',
120
+ allowDangerouslySkipPermissions: true,
121
+ ...(Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),
122
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
123
+ },
124
+ });
125
+
126
+ // Stream messages from the SDK, collecting text output and session ID.
127
+ const textParts: string[] = [];
128
+ let exitCode = 0;
129
+ let finalError: string | undefined;
130
+
131
+ for await (const message of sdkQuery) {
132
+ // Capture the session ID from the first message that carries it.
133
+ if ('session_id' in message && typeof message.session_id === 'string') {
134
+ this.sessions.setSessionId(instanceId, message.session_id);
135
+ }
136
+
137
+ if (message.type === 'assistant') {
138
+ // Aggregate text blocks from assistant messages.
139
+ for (const block of message.message.content) {
140
+ if (block.type === 'text') {
141
+ textParts.push(block.text);
142
+ }
143
+ }
144
+ } else if (message.type === 'result') {
145
+ if (message.subtype === 'success') {
146
+ // The result field on success contains the final summary text.
147
+ if (message.result) {
148
+ textParts.push(message.result);
149
+ }
150
+ exitCode = message.is_error ? 1 : 0;
151
+ } else {
152
+ // Error subtypes: error_max_turns, error_during_execution, etc.
153
+ exitCode = 1;
154
+ if ('errors' in message && Array.isArray(message.errors) && message.errors.length > 0) {
155
+ finalError = (message.errors as string[]).join('; ');
156
+ } else {
157
+ // Fallback: use the subtype string as the error description so
158
+ // `finalError` is always truthy for non-success result messages.
159
+ finalError = String(message.subtype);
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ const endTime = new Date().toISOString();
166
+ this.sessions.remove(instanceId);
167
+
168
+ const output = textParts.join('\n').trim();
169
+
170
+ if (finalError) {
171
+ return {
172
+ instanceId,
173
+ taskId: context.taskId,
174
+ providerId: 'claude-sdk',
175
+ status: 'failed',
176
+ output,
177
+ exitCode,
178
+ startTime,
179
+ endTime,
180
+ error: finalError,
181
+ };
182
+ }
183
+
184
+ return {
185
+ instanceId,
186
+ taskId: context.taskId,
187
+ providerId: 'claude-sdk',
188
+ status: 'completed',
189
+ output,
190
+ exitCode,
191
+ startTime,
192
+ endTime,
193
+ };
194
+ } catch (error) {
195
+ const endTime = new Date().toISOString();
196
+ this.sessions.remove(instanceId);
197
+
198
+ return {
199
+ instanceId,
200
+ taskId: context.taskId,
201
+ providerId: 'claude-sdk',
202
+ status: 'failed',
203
+ startTime,
204
+ endTime,
205
+ exitCode: 1,
206
+ error: getErrorMessage(error),
207
+ };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * List sessions currently tracked as active (spawned but not yet completed).
213
+ *
214
+ * Because SDK sessions run to completion inside `spawn()`, this list is
215
+ * typically empty unless concurrent spawns are in flight.
216
+ *
217
+ * @returns Array of in-flight spawn results
218
+ */
219
+ async listRunning(): Promise<SpawnResult[]> {
220
+ return this.sessions.listActive().map((entry) => ({
221
+ instanceId: entry.instanceId,
222
+ taskId: entry.taskId,
223
+ providerId: 'claude-sdk',
224
+ status: 'running' as const,
225
+ startTime: entry.startTime,
226
+ }));
227
+ }
228
+
229
+ /**
230
+ * Remove a session from tracking.
231
+ *
232
+ * The underlying SDK query runs inside `spawn()` and cannot be cancelled
233
+ * externally once the async iterator is in flight. Removing the entry from
234
+ * the store prevents it from appearing in `listRunning()` but does not
235
+ * interrupt the in-progress HTTP request.
236
+ *
237
+ * @param instanceId - ID of the spawn instance to terminate
238
+ */
239
+ async terminate(instanceId: string): Promise<void> {
240
+ this.sessions.remove(instanceId);
241
+ }
242
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tool Bridge for Claude SDK Spawn Provider
3
+ *
4
+ * Maps CLEO's tool allowlist (string names) to the SDK's `allowedTools`
5
+ * option format. The SDK accepts plain tool name strings such as
6
+ * `"Read"`, `"Bash"`, `"Edit"` for built-in Claude Code tools and
7
+ * `"mcp__<server>__<tool>"` for MCP-backed tools.
8
+ *
9
+ * @task T581
10
+ */
11
+
12
+ /**
13
+ * Default CLEO tool set passed to the SDK when no explicit allowlist
14
+ * is provided in `SpawnContext.options.toolAllowlist`.
15
+ *
16
+ * Mirrors the standard agent tool surface used by the Claude Code CLI
17
+ * `--dangerously-skip-permissions` mode.
18
+ */
19
+ export const DEFAULT_TOOLS: readonly string[] = [
20
+ 'Read',
21
+ 'Write',
22
+ 'Edit',
23
+ 'Bash',
24
+ 'Glob',
25
+ 'Grep',
26
+ ] as const;
27
+
28
+ /**
29
+ * Resolves a CLEO tool allowlist to the SDK `allowedTools` array.
30
+ *
31
+ * When `allowlist` is undefined or empty, the default CLEO tool set is
32
+ * returned. When an explicit list is provided, it is returned as-is so
33
+ * callers can pass MCP tool strings (`mcp__server__tool`) alongside
34
+ * built-in names without transformation.
35
+ *
36
+ * @param allowlist - Optional array of tool names from `SpawnContext.options`
37
+ * @returns Array of SDK-compatible tool name strings
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * resolveTools(); // ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep']
42
+ * resolveTools(['Read', 'Bash']); // ['Read', 'Bash']
43
+ * resolveTools(['mcp__brain__search']); // ['mcp__brain__search']
44
+ * ```
45
+ */
46
+ export function resolveTools(allowlist?: string[]): string[] {
47
+ if (!allowlist || allowlist.length === 0) {
48
+ return [...DEFAULT_TOOLS];
49
+ }
50
+ return [...allowlist];
51
+ }