@pixelbyte-software/pixcode 1.34.0 → 1.35.0

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 (155) hide show
  1. package/dist/api-docs.html +162 -9
  2. package/dist/assets/index-B8w57E1r.css +32 -0
  3. package/dist/assets/index-Djuh0wHV.js +854 -0
  4. package/dist/favicon.svg +8 -8
  5. package/dist/icons/icon-128x128.svg +9 -9
  6. package/dist/icons/icon-144x144.svg +9 -9
  7. package/dist/icons/icon-152x152.svg +9 -9
  8. package/dist/icons/icon-192x192.svg +9 -9
  9. package/dist/icons/icon-384x384.svg +9 -9
  10. package/dist/icons/icon-512x512.svg +9 -9
  11. package/dist/icons/icon-72x72.svg +9 -9
  12. package/dist/icons/icon-96x96.svg +9 -9
  13. package/dist/icons/icon-template.svg +9 -9
  14. package/dist/index.html +2 -2
  15. package/dist/logo.svg +12 -12
  16. package/dist/openapi.yaml +383 -1
  17. package/dist-server/server/claude-sdk.js +38 -7
  18. package/dist-server/server/claude-sdk.js.map +1 -1
  19. package/dist-server/server/cli.js +12 -17
  20. package/dist-server/server/cli.js.map +1 -1
  21. package/dist-server/server/daemon-manager.js +98 -51
  22. package/dist-server/server/daemon-manager.js.map +1 -1
  23. package/dist-server/server/database/json-store.js +8 -5
  24. package/dist-server/server/database/json-store.js.map +1 -1
  25. package/dist-server/server/index.js +31 -10
  26. package/dist-server/server/index.js.map +1 -1
  27. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js +45 -19
  28. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js.map +1 -1
  29. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -1
  30. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +1 -0
  31. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -1
  32. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js +202 -0
  33. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js.map +1 -0
  34. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js +205 -0
  35. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js.map +1 -0
  36. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js +205 -0
  37. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js.map +1 -0
  38. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js +205 -0
  39. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js.map +1 -0
  40. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js +205 -0
  41. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js.map +1 -0
  42. package/dist-server/server/modules/orchestration/a2a/routes.js +298 -34
  43. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
  44. package/dist-server/server/modules/orchestration/a2a/task-store.js +144 -0
  45. package/dist-server/server/modules/orchestration/a2a/task-store.js.map +1 -0
  46. package/dist-server/server/modules/orchestration/a2a/validator.js +16 -0
  47. package/dist-server/server/modules/orchestration/a2a/validator.js.map +1 -1
  48. package/dist-server/server/modules/orchestration/index.js +14 -0
  49. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  50. package/dist-server/server/modules/orchestration/preview/port-watcher.js +90 -0
  51. package/dist-server/server/modules/orchestration/preview/port-watcher.js.map +1 -0
  52. package/dist-server/server/modules/orchestration/preview/preview-proxy.js +58 -0
  53. package/dist-server/server/modules/orchestration/preview/preview-proxy.js.map +1 -0
  54. package/dist-server/server/modules/orchestration/preview/types.js +2 -0
  55. package/dist-server/server/modules/orchestration/preview/types.js.map +1 -0
  56. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js +37 -0
  57. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js.map +1 -0
  58. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +68 -0
  59. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -0
  60. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +128 -0
  61. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -0
  62. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js +2 -0
  63. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js.map +1 -0
  64. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js +126 -0
  65. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js.map +1 -0
  66. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +1047 -0
  67. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -0
  68. package/dist-server/server/modules/orchestration/workflows/workflow-store.js +76 -0
  69. package/dist-server/server/modules/orchestration/workflows/workflow-store.js.map +1 -0
  70. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +151 -0
  71. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -0
  72. package/dist-server/server/modules/orchestration/workflows/workflow.types.js +2 -0
  73. package/dist-server/server/modules/orchestration/workflows/workflow.types.js.map +1 -0
  74. package/dist-server/server/modules/orchestration/workflows/workspace-target.js +98 -0
  75. package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -0
  76. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +122 -0
  77. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -0
  78. package/dist-server/server/modules/orchestration/workspace/path-safety.js +48 -0
  79. package/dist-server/server/modules/orchestration/workspace/path-safety.js.map +1 -0
  80. package/dist-server/server/modules/orchestration/workspace/types.js +11 -0
  81. package/dist-server/server/modules/orchestration/workspace/types.js.map +1 -0
  82. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +80 -0
  83. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -0
  84. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +96 -0
  85. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -0
  86. package/dist-server/server/modules/providers/index.js +3 -0
  87. package/dist-server/server/modules/providers/index.js.map +1 -0
  88. package/dist-server/server/openai-codex.js +35 -4
  89. package/dist-server/server/openai-codex.js.map +1 -1
  90. package/dist-server/server/routes/taskmaster.js +106 -89
  91. package/dist-server/server/routes/taskmaster.js.map +1 -1
  92. package/package.json +3 -1
  93. package/scripts/smoke/a2a-roundtrip.mjs +167 -98
  94. package/scripts/smoke/orchestration-api.mjs +172 -0
  95. package/scripts/smoke/orchestration-live-run.mjs +176 -0
  96. package/server/claude-sdk.js +48 -7
  97. package/server/cli.js +12 -17
  98. package/server/daemon-manager.js +90 -51
  99. package/server/database/db.js +794 -794
  100. package/server/database/json-store.js +8 -5
  101. package/server/index.js +40 -9
  102. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -58
  103. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +55 -49
  104. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +284 -283
  105. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -0
  106. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -0
  107. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -0
  108. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -0
  109. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -0
  110. package/server/modules/orchestration/a2a/agent-card.ts +55 -55
  111. package/server/modules/orchestration/a2a/auth.middleware.ts +29 -29
  112. package/server/modules/orchestration/a2a/bus.ts +46 -46
  113. package/server/modules/orchestration/a2a/routes.ts +577 -264
  114. package/server/modules/orchestration/a2a/task-store.ts +178 -0
  115. package/server/modules/orchestration/a2a/types.ts +125 -111
  116. package/server/modules/orchestration/a2a/validator.ts +113 -90
  117. package/server/modules/orchestration/index.ts +66 -26
  118. package/server/modules/orchestration/preview/port-watcher.ts +112 -0
  119. package/server/modules/orchestration/preview/preview-proxy.ts +60 -0
  120. package/server/modules/orchestration/preview/types.ts +19 -0
  121. package/server/modules/orchestration/tasks/orchestration-task-store.ts +45 -0
  122. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +73 -0
  123. package/server/modules/orchestration/tasks/orchestration-task.service.ts +145 -0
  124. package/server/modules/orchestration/tasks/orchestration-task.types.ts +29 -0
  125. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -0
  126. package/server/modules/orchestration/workflows/workflow-runner.ts +1206 -0
  127. package/server/modules/orchestration/workflows/workflow-store.ts +97 -0
  128. package/server/modules/orchestration/workflows/workflow.routes.ts +169 -0
  129. package/server/modules/orchestration/workflows/workflow.types.ts +70 -0
  130. package/server/modules/orchestration/workflows/workspace-target.ts +120 -0
  131. package/server/modules/orchestration/workspace/docker-workspace.ts +135 -0
  132. package/server/modules/orchestration/workspace/path-safety.ts +55 -0
  133. package/server/modules/orchestration/workspace/types.ts +52 -0
  134. package/server/modules/orchestration/workspace/workspace-manager.ts +97 -0
  135. package/server/modules/orchestration/workspace/worktree-workspace.ts +125 -0
  136. package/server/modules/providers/index.ts +2 -0
  137. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +130 -130
  138. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  139. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  140. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -145
  141. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  142. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  143. package/server/modules/providers/shared/provider-configs.ts +142 -142
  144. package/server/openai-codex.js +40 -4
  145. package/server/qwen-code-cli.js +395 -395
  146. package/server/qwen-response-handler.js +73 -73
  147. package/server/routes/qwen.js +27 -27
  148. package/server/routes/taskmaster.js +116 -91
  149. package/server/services/external-access.js +171 -171
  150. package/server/services/provider-models.js +381 -381
  151. package/server/services/telegram/telegram-http-client.js +130 -130
  152. package/server/services/vapid-keys.js +36 -36
  153. package/server/utils/port-access.js +209 -209
  154. package/dist/assets/index-B1ghfb4w.css +0 -32
  155. package/dist/assets/index-BvClqlMf.js +0 -852
@@ -1,283 +1,284 @@
1
- // server/modules/orchestration/a2a/adapters/claude-code.adapter.ts
2
- // Wraps the existing server/claude-sdk.js queryClaudeSDK() function.
3
- // claude-sdk.js was designed to stream SDK messages over a WebSocket
4
- // connection, so we feed it a "fake WS" that captures send() calls and
5
- // emits A2A bus events instead.
6
- //
7
- // IMPORTANT: claude-sdk.js calls ws.send(<NormalizedMessage object>) — it
8
- // does NOT JSON.stringify before send. Our shim therefore receives objects
9
- // (not strings) and dispatches on `frame.kind` (not `frame.type`). See
10
- // server/shared/types.ts for the MessageKind enum.
11
-
12
- import crypto from 'node:crypto';
13
-
14
- // eslint-disable-next-line boundaries/no-unknown -- claude-sdk.js is a top-level CLI runtime not yet classified by eslint.config.js; cleanup deferred (cascades into a server/services classification gap).
15
- import { abortClaudeSDKSession, queryClaudeSDK } from '@/claude-sdk.js';
16
- import { AbstractA2AAdapter } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
17
- import type {
18
- AdapterContext,
19
- TaskHandle,
20
- } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
21
- import type { AgentCard, Part, Task } from '@/modules/orchestration/a2a/types.js';
22
-
23
- interface FakeWS {
24
- send(data: unknown): void;
25
- readyState: number;
26
- }
27
-
28
- // WebSocket.OPEN per the ws library — claude-sdk.js gates send() on readyState === 1.
29
- const WS_OPEN = 1;
30
-
31
- function joinPartsToPrompt(parts: Part[]): string {
32
- return parts
33
- .map((p) => {
34
- if (p.kind === 'text') return p.text;
35
- if (p.kind === 'data') return JSON.stringify(p.data);
36
- // file parts: include name + uri/inline marker
37
- return `[file:${p.name}${p.uri ? ` uri=${p.uri}` : ''}]`;
38
- })
39
- .join('\n');
40
- }
41
-
42
- function newId(prefix: string): string {
43
- return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
44
- }
45
-
46
- export class ClaudeCodeA2AAdapter extends AbstractA2AAdapter {
47
- readonly id = 'claude-code';
48
-
49
- readonly agentCard: AgentCard = {
50
- name: 'pixcode-claude-code',
51
- description: 'Anthropic Claude Code, accessed via Pixcode',
52
- url: '/a2a/agents/claude-code',
53
- version: '1.0.0',
54
- capabilities: ['streaming', 'fileEdit', 'commandExec', 'mcp'],
55
- skills: [
56
- {
57
- id: 'architectural-review',
58
- description: 'Review code architecture and propose structural changes',
59
- },
60
- {
61
- id: 'typescript-edit',
62
- description: 'Edit TypeScript files with type-aware reasoning',
63
- },
64
- {
65
- id: 'multi-file-refactor',
66
- description: 'Coordinated edits across many files',
67
- },
68
- {
69
- id: 'test-run',
70
- description: 'Run test suites and react to results',
71
- },
72
- ],
73
- authentication: { type: 'bearer' },
74
- };
75
-
76
- private readonly active = new Map<string, { sessionId: string | null }>();
77
-
78
- async submitTask(task: Task, ctx: AdapterContext): Promise<TaskHandle> {
79
- // Foundation: only the last user message is fed in. Multi-turn resumption
80
- // (input-required tasks, workflow chaining) needs to pass options.sessionId
81
- // and append history; deferred to a follow-on plan.
82
- const promptText = joinPartsToPrompt(
83
- task.history[task.history.length - 1]?.parts ?? [],
84
- );
85
- const session = { sessionId: null as string | null };
86
- this.active.set(task.id, session);
87
-
88
- this.emitState(task.id, 'working');
89
-
90
- const fakeWS: FakeWS = {
91
- readyState: WS_OPEN,
92
- send: (data) => this.handleSdkFrame(task.id, data, session),
93
- };
94
-
95
- const finished = (async () => {
96
- try {
97
- await queryClaudeSDK(
98
- promptText,
99
- {
100
- cwd: ctx.cwd,
101
- permissionMode: ctx.permissionMode ?? 'default',
102
- },
103
- fakeWS,
104
- );
105
- // If cancelTask removed us from `active` first, suppress the spurious
106
- // 'completed' that would otherwise race the 'canceled' state.
107
- if (this.active.has(task.id)) {
108
- this.emitState(task.id, 'completed');
109
- }
110
- } catch (err) {
111
- if (this.active.has(task.id)) {
112
- this.emitState(task.id, 'failed', {
113
- code: 'ADAPTER_RUNTIME_ERROR',
114
- message: err instanceof Error ? err.message : String(err),
115
- });
116
- }
117
- } finally {
118
- this.active.delete(task.id);
119
- }
120
- })();
121
-
122
- return {
123
- cancel: () => this.cancelTask(task.id),
124
- finished,
125
- };
126
- }
127
-
128
- async cancelTask(taskId: string): Promise<void> {
129
- const session = this.active.get(taskId);
130
- if (!session) {
131
- this.emitState(taskId, 'canceled');
132
- return;
133
- }
134
- // Delete BEFORE awaiting so submitTask's IIFE guard (this.active.has)
135
- // suppresses the spurious 'completed' state when queryClaudeSDK's
136
- // for-await loop unwinds from the abort.
137
- this.active.delete(taskId);
138
- if (session.sessionId) {
139
- try {
140
- await abortClaudeSDKSession(session.sessionId);
141
- } catch {
142
- // swallow — adapter has already cleaned its own state
143
- }
144
- }
145
- this.emitState(taskId, 'canceled');
146
- }
147
-
148
- /**
149
- * claude-sdk.js calls `ws.send(<NormalizedMessage>)` with a JS OBJECT
150
- * (not a JSON string). We translate each frame into A2A bus events.
151
- * See server/shared/types.ts for the MessageKind union.
152
- */
153
- private handleSdkFrame(
154
- taskId: string,
155
- frame: unknown,
156
- session: { sessionId: string | null },
157
- ): void {
158
- if (!frame || typeof frame !== 'object') return;
159
- const f = frame as {
160
- kind?: string;
161
- sessionId?: unknown;
162
- newSessionId?: unknown;
163
- text?: unknown;
164
- content?: unknown;
165
- toolName?: unknown;
166
- toolInput?: unknown;
167
- toolResult?: unknown;
168
- };
169
-
170
- // session_created carries the new session id in `newSessionId`. Capture
171
- // it here so cancelTask can call abortClaudeSDKSession with the right id.
172
- if (
173
- f.kind === 'session_created' &&
174
- typeof f.newSessionId === 'string' &&
175
- !session.sessionId
176
- ) {
177
- session.sessionId = f.newSessionId;
178
- }
179
-
180
- switch (f.kind) {
181
- case 'session_created':
182
- case 'status':
183
- case 'stream_delta':
184
- case 'stream_end':
185
- // session_created and status are not user-facing.
186
- // stream_delta and stream_end CARRY user-visible delta text but are
187
- // not currently emitted by claude-sdk.js (it doesn't pass
188
- // includePartialMessages: true to query()). If that flag flips on
189
- // upstream, these cases must be re-routed to emit text Messages.
190
- return;
191
-
192
- case 'text':
193
- case 'thinking': {
194
- const text =
195
- typeof f.text === 'string'
196
- ? f.text
197
- : typeof f.content === 'string'
198
- ? f.content
199
- : null;
200
- if (text) {
201
- this.emitMessage(taskId, {
202
- messageId: newId('msg'),
203
- role: 'agent',
204
- parts: [{ kind: 'text', text }],
205
- taskId,
206
- });
207
- }
208
- return;
209
- }
210
-
211
- case 'tool_use': {
212
- this.emitArtifact(taskId, {
213
- artifactId: newId('art'),
214
- type: 'command-output',
215
- parts: [
216
- {
217
- kind: 'data',
218
- data: { toolName: f.toolName, toolInput: f.toolInput },
219
- },
220
- ],
221
- metadata: { source: 'claude-tool-use' },
222
- });
223
- return;
224
- }
225
-
226
- case 'tool_result': {
227
- this.emitArtifact(taskId, {
228
- artifactId: newId('art'),
229
- type: 'command-output',
230
- parts: [{ kind: 'data', data: { toolResult: f.toolResult } }],
231
- metadata: { source: 'claude-tool-result' },
232
- });
233
- return;
234
- }
235
-
236
- case 'permission_request':
237
- case 'permission_cancelled':
238
- case 'interactive_prompt':
239
- case 'task_notification':
240
- // Informational — surface as data artifact for visibility.
241
- this.emitArtifact(taskId, {
242
- artifactId: newId('art'),
243
- type: 'data',
244
- parts: [{ kind: 'data', data: f as Record<string, unknown> }],
245
- metadata: { source: `claude-${f.kind}` },
246
- });
247
- return;
248
-
249
- case 'error': {
250
- // claude-sdk.js catches internally and emits an error frame without
251
- // rethrowing, so the IIFE await would resolve cleanly. Force the
252
- // failed state here and remove from active so the IIFE's
253
- // 'completed' emit is suppressed by its active.has() guard.
254
- const message =
255
- typeof f.content === 'string'
256
- ? f.content
257
- : typeof f.text === 'string'
258
- ? f.text
259
- : 'Claude Code reported an error';
260
- this.emitState(taskId, 'failed', {
261
- code: 'CLAUDE_RUNTIME_ERROR',
262
- message,
263
- details: f as Record<string, unknown>,
264
- });
265
- this.active.delete(taskId);
266
- return;
267
- }
268
-
269
- case 'complete':
270
- // Lifecycle redundant with the IIFE's 'completed' emit; suppress to
271
- // avoid double-signaling. The IIFE owns terminal state transitions.
272
- return;
273
-
274
- default:
275
- // Unknown kind — surface for visibility
276
- this.emitArtifact(taskId, {
277
- artifactId: newId('art'),
278
- type: 'data',
279
- parts: [{ kind: 'data', data: f as Record<string, unknown> }],
280
- });
281
- }
282
- }
283
- }
1
+ // server/modules/orchestration/a2a/adapters/claude-code.adapter.ts
2
+ // Wraps the existing server/claude-sdk.js queryClaudeSDK() function.
3
+ // claude-sdk.js was designed to stream SDK messages over a WebSocket
4
+ // connection, so we feed it a "fake WS" that captures send() calls and
5
+ // emits A2A bus events instead.
6
+ //
7
+ // IMPORTANT: claude-sdk.js calls ws.send(<NormalizedMessage object>) — it
8
+ // does NOT JSON.stringify before send. Our shim therefore receives objects
9
+ // (not strings) and dispatches on `frame.kind` (not `frame.type`). See
10
+ // server/shared/types.ts for the MessageKind enum.
11
+
12
+ import crypto from 'node:crypto';
13
+
14
+ // eslint-disable-next-line boundaries/no-unknown -- claude-sdk.js is a top-level CLI runtime not yet classified by eslint.config.js; cleanup deferred (cascades into a server/services classification gap).
15
+ import { abortClaudeSDKSession, queryClaudeSDK } from '@/claude-sdk.js';
16
+ import { AbstractA2AAdapter } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
17
+ import type {
18
+ AdapterContext,
19
+ TaskHandle,
20
+ } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
21
+ import type { AgentCard, Part, Task } from '@/modules/orchestration/a2a/types.js';
22
+
23
+ interface FakeWS {
24
+ send(data: unknown): void;
25
+ readyState: number;
26
+ }
27
+
28
+ // WebSocket.OPEN per the ws library — claude-sdk.js gates send() on readyState === 1.
29
+ const WS_OPEN = 1;
30
+
31
+ function joinPartsToPrompt(parts: Part[]): string {
32
+ return parts
33
+ .map((p) => {
34
+ if (p.kind === 'text') return p.text;
35
+ if (p.kind === 'data') return JSON.stringify(p.data);
36
+ // file parts: include name + uri/inline marker
37
+ return `[file:${p.name}${p.uri ? ` uri=${p.uri}` : ''}]`;
38
+ })
39
+ .join('\n');
40
+ }
41
+
42
+ function newId(prefix: string): string {
43
+ return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
44
+ }
45
+
46
+ export class ClaudeCodeA2AAdapter extends AbstractA2AAdapter {
47
+ readonly id = 'claude-code';
48
+
49
+ readonly agentCard: AgentCard = {
50
+ name: 'pixcode-claude-code',
51
+ description: 'Anthropic Claude Code, accessed via Pixcode',
52
+ url: '/a2a/agents/claude-code',
53
+ version: '1.0.0',
54
+ capabilities: ['streaming', 'fileEdit', 'commandExec', 'mcp'],
55
+ skills: [
56
+ {
57
+ id: 'architectural-review',
58
+ description: 'Review code architecture and propose structural changes',
59
+ },
60
+ {
61
+ id: 'typescript-edit',
62
+ description: 'Edit TypeScript files with type-aware reasoning',
63
+ },
64
+ {
65
+ id: 'multi-file-refactor',
66
+ description: 'Coordinated edits across many files',
67
+ },
68
+ {
69
+ id: 'test-run',
70
+ description: 'Run test suites and react to results',
71
+ },
72
+ ],
73
+ authentication: { type: 'bearer' },
74
+ };
75
+
76
+ private readonly active = new Map<string, { sessionId: string | null }>();
77
+
78
+ async submitTask(task: Task, ctx: AdapterContext): Promise<TaskHandle> {
79
+ // Foundation: only the last user message is fed in. Multi-turn resumption
80
+ // (input-required tasks, workflow chaining) needs to pass options.sessionId
81
+ // and append history; deferred to a follow-on plan.
82
+ const promptText = joinPartsToPrompt(
83
+ task.history[task.history.length - 1]?.parts ?? [],
84
+ );
85
+ const session = { sessionId: null as string | null };
86
+ this.active.set(task.id, session);
87
+
88
+ this.emitState(task.id, 'working');
89
+
90
+ const fakeWS: FakeWS = {
91
+ readyState: WS_OPEN,
92
+ send: (data) => this.handleSdkFrame(task.id, data, session),
93
+ };
94
+
95
+ const finished = (async () => {
96
+ try {
97
+ await queryClaudeSDK(
98
+ promptText,
99
+ {
100
+ cwd: ctx.cwd,
101
+ permissionMode: ctx.permissionMode ?? 'default',
102
+ toolsSettings: ctx.toolsSettings,
103
+ },
104
+ fakeWS,
105
+ );
106
+ // If cancelTask removed us from `active` first, suppress the spurious
107
+ // 'completed' that would otherwise race the 'canceled' state.
108
+ if (this.active.has(task.id)) {
109
+ this.emitState(task.id, 'completed');
110
+ }
111
+ } catch (err) {
112
+ if (this.active.has(task.id)) {
113
+ this.emitState(task.id, 'failed', {
114
+ code: 'ADAPTER_RUNTIME_ERROR',
115
+ message: err instanceof Error ? err.message : String(err),
116
+ });
117
+ }
118
+ } finally {
119
+ this.active.delete(task.id);
120
+ }
121
+ })();
122
+
123
+ return {
124
+ cancel: () => this.cancelTask(task.id),
125
+ finished,
126
+ };
127
+ }
128
+
129
+ async cancelTask(taskId: string): Promise<void> {
130
+ const session = this.active.get(taskId);
131
+ if (!session) {
132
+ this.emitState(taskId, 'canceled');
133
+ return;
134
+ }
135
+ // Delete BEFORE awaiting so submitTask's IIFE guard (this.active.has)
136
+ // suppresses the spurious 'completed' state when queryClaudeSDK's
137
+ // for-await loop unwinds from the abort.
138
+ this.active.delete(taskId);
139
+ if (session.sessionId) {
140
+ try {
141
+ await abortClaudeSDKSession(session.sessionId);
142
+ } catch {
143
+ // swallow — adapter has already cleaned its own state
144
+ }
145
+ }
146
+ this.emitState(taskId, 'canceled');
147
+ }
148
+
149
+ /**
150
+ * claude-sdk.js calls `ws.send(<NormalizedMessage>)` with a JS OBJECT
151
+ * (not a JSON string). We translate each frame into A2A bus events.
152
+ * See server/shared/types.ts for the MessageKind union.
153
+ */
154
+ private handleSdkFrame(
155
+ taskId: string,
156
+ frame: unknown,
157
+ session: { sessionId: string | null },
158
+ ): void {
159
+ if (!frame || typeof frame !== 'object') return;
160
+ const f = frame as {
161
+ kind?: string;
162
+ sessionId?: unknown;
163
+ newSessionId?: unknown;
164
+ text?: unknown;
165
+ content?: unknown;
166
+ toolName?: unknown;
167
+ toolInput?: unknown;
168
+ toolResult?: unknown;
169
+ };
170
+
171
+ // session_created carries the new session id in `newSessionId`. Capture
172
+ // it here so cancelTask can call abortClaudeSDKSession with the right id.
173
+ if (
174
+ f.kind === 'session_created' &&
175
+ typeof f.newSessionId === 'string' &&
176
+ !session.sessionId
177
+ ) {
178
+ session.sessionId = f.newSessionId;
179
+ }
180
+
181
+ switch (f.kind) {
182
+ case 'session_created':
183
+ case 'status':
184
+ case 'stream_delta':
185
+ case 'stream_end':
186
+ // session_created and status are not user-facing.
187
+ // stream_delta and stream_end CARRY user-visible delta text but are
188
+ // not currently emitted by claude-sdk.js (it doesn't pass
189
+ // includePartialMessages: true to query()). If that flag flips on
190
+ // upstream, these cases must be re-routed to emit text Messages.
191
+ return;
192
+
193
+ case 'text':
194
+ case 'thinking': {
195
+ const text =
196
+ typeof f.text === 'string'
197
+ ? f.text
198
+ : typeof f.content === 'string'
199
+ ? f.content
200
+ : null;
201
+ if (text) {
202
+ this.emitMessage(taskId, {
203
+ messageId: newId('msg'),
204
+ role: 'agent',
205
+ parts: [{ kind: 'text', text }],
206
+ taskId,
207
+ });
208
+ }
209
+ return;
210
+ }
211
+
212
+ case 'tool_use': {
213
+ this.emitArtifact(taskId, {
214
+ artifactId: newId('art'),
215
+ type: 'command-output',
216
+ parts: [
217
+ {
218
+ kind: 'data',
219
+ data: { toolName: f.toolName, toolInput: f.toolInput },
220
+ },
221
+ ],
222
+ metadata: { source: 'claude-tool-use' },
223
+ });
224
+ return;
225
+ }
226
+
227
+ case 'tool_result': {
228
+ this.emitArtifact(taskId, {
229
+ artifactId: newId('art'),
230
+ type: 'command-output',
231
+ parts: [{ kind: 'data', data: { toolResult: f.toolResult } }],
232
+ metadata: { source: 'claude-tool-result' },
233
+ });
234
+ return;
235
+ }
236
+
237
+ case 'permission_request':
238
+ case 'permission_cancelled':
239
+ case 'interactive_prompt':
240
+ case 'task_notification':
241
+ // Informational — surface as data artifact for visibility.
242
+ this.emitArtifact(taskId, {
243
+ artifactId: newId('art'),
244
+ type: 'data',
245
+ parts: [{ kind: 'data', data: f as Record<string, unknown> }],
246
+ metadata: { source: `claude-${f.kind}` },
247
+ });
248
+ return;
249
+
250
+ case 'error': {
251
+ // claude-sdk.js catches internally and emits an error frame without
252
+ // rethrowing, so the IIFE await would resolve cleanly. Force the
253
+ // failed state here and remove from active so the IIFE's
254
+ // 'completed' emit is suppressed by its active.has() guard.
255
+ const message =
256
+ typeof f.content === 'string'
257
+ ? f.content
258
+ : typeof f.text === 'string'
259
+ ? f.text
260
+ : 'Claude Code reported an error';
261
+ this.emitState(taskId, 'failed', {
262
+ code: 'CLAUDE_RUNTIME_ERROR',
263
+ message,
264
+ details: f as Record<string, unknown>,
265
+ });
266
+ this.active.delete(taskId);
267
+ return;
268
+ }
269
+
270
+ case 'complete':
271
+ // Lifecycle redundant with the IIFE's 'completed' emit; suppress to
272
+ // avoid double-signaling. The IIFE owns terminal state transitions.
273
+ return;
274
+
275
+ default:
276
+ // Unknown kind — surface for visibility
277
+ this.emitArtifact(taskId, {
278
+ artifactId: newId('art'),
279
+ type: 'data',
280
+ parts: [{ kind: 'data', data: f as Record<string, unknown> }],
281
+ });
282
+ }
283
+ }
284
+ }