@pixelbyte-software/pixcode 1.33.11 → 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 (164) 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 +34 -9
  26. package/dist-server/server/index.js.map +1 -1
  27. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js +73 -0
  28. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js.map +1 -0
  29. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js +17 -0
  30. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -0
  31. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +234 -0
  32. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -0
  33. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js +202 -0
  34. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js.map +1 -0
  35. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js +205 -0
  36. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js.map +1 -0
  37. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js +205 -0
  38. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js.map +1 -0
  39. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js +205 -0
  40. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js.map +1 -0
  41. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js +205 -0
  42. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js.map +1 -0
  43. package/dist-server/server/modules/orchestration/a2a/agent-card.js +50 -0
  44. package/dist-server/server/modules/orchestration/a2a/agent-card.js.map +1 -0
  45. package/dist-server/server/modules/orchestration/a2a/auth.middleware.js +25 -0
  46. package/dist-server/server/modules/orchestration/a2a/auth.middleware.js.map +1 -0
  47. package/dist-server/server/modules/orchestration/a2a/bus.js +34 -0
  48. package/dist-server/server/modules/orchestration/a2a/bus.js.map +1 -0
  49. package/dist-server/server/modules/orchestration/a2a/routes.js +497 -0
  50. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -0
  51. package/dist-server/server/modules/orchestration/a2a/task-store.js +144 -0
  52. package/dist-server/server/modules/orchestration/a2a/task-store.js.map +1 -0
  53. package/dist-server/server/modules/orchestration/a2a/types.js +6 -0
  54. package/dist-server/server/modules/orchestration/a2a/types.js.map +1 -0
  55. package/dist-server/server/modules/orchestration/a2a/validator.js +101 -0
  56. package/dist-server/server/modules/orchestration/a2a/validator.js.map +1 -0
  57. package/dist-server/server/modules/orchestration/index.js +24 -0
  58. package/dist-server/server/modules/orchestration/index.js.map +1 -0
  59. package/dist-server/server/modules/orchestration/preview/port-watcher.js +90 -0
  60. package/dist-server/server/modules/orchestration/preview/port-watcher.js.map +1 -0
  61. package/dist-server/server/modules/orchestration/preview/preview-proxy.js +58 -0
  62. package/dist-server/server/modules/orchestration/preview/preview-proxy.js.map +1 -0
  63. package/dist-server/server/modules/orchestration/preview/types.js +2 -0
  64. package/dist-server/server/modules/orchestration/preview/types.js.map +1 -0
  65. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js +37 -0
  66. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js.map +1 -0
  67. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +68 -0
  68. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -0
  69. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +128 -0
  70. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -0
  71. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js +2 -0
  72. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js.map +1 -0
  73. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js +126 -0
  74. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js.map +1 -0
  75. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +1047 -0
  76. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -0
  77. package/dist-server/server/modules/orchestration/workflows/workflow-store.js +76 -0
  78. package/dist-server/server/modules/orchestration/workflows/workflow-store.js.map +1 -0
  79. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +151 -0
  80. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -0
  81. package/dist-server/server/modules/orchestration/workflows/workflow.types.js +2 -0
  82. package/dist-server/server/modules/orchestration/workflows/workflow.types.js.map +1 -0
  83. package/dist-server/server/modules/orchestration/workflows/workspace-target.js +98 -0
  84. package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -0
  85. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +122 -0
  86. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -0
  87. package/dist-server/server/modules/orchestration/workspace/path-safety.js +48 -0
  88. package/dist-server/server/modules/orchestration/workspace/path-safety.js.map +1 -0
  89. package/dist-server/server/modules/orchestration/workspace/types.js +11 -0
  90. package/dist-server/server/modules/orchestration/workspace/types.js.map +1 -0
  91. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +80 -0
  92. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -0
  93. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +96 -0
  94. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -0
  95. package/dist-server/server/modules/providers/index.js +3 -0
  96. package/dist-server/server/modules/providers/index.js.map +1 -0
  97. package/dist-server/server/openai-codex.js +35 -4
  98. package/dist-server/server/openai-codex.js.map +1 -1
  99. package/dist-server/server/routes/taskmaster.js +106 -89
  100. package/dist-server/server/routes/taskmaster.js.map +1 -1
  101. package/package.json +3 -1
  102. package/scripts/smoke/a2a-roundtrip.mjs +167 -0
  103. package/scripts/smoke/orchestration-api.mjs +172 -0
  104. package/scripts/smoke/orchestration-live-run.mjs +176 -0
  105. package/server/claude-sdk.js +48 -7
  106. package/server/cli.js +12 -17
  107. package/server/daemon-manager.js +90 -51
  108. package/server/database/db.js +794 -794
  109. package/server/database/json-store.js +8 -5
  110. package/server/index.js +49 -9
  111. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -0
  112. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +55 -0
  113. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +284 -0
  114. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -0
  115. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -0
  116. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -0
  117. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -0
  118. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -0
  119. package/server/modules/orchestration/a2a/agent-card.ts +55 -0
  120. package/server/modules/orchestration/a2a/auth.middleware.ts +29 -0
  121. package/server/modules/orchestration/a2a/bus.ts +46 -0
  122. package/server/modules/orchestration/a2a/routes.ts +577 -0
  123. package/server/modules/orchestration/a2a/task-store.ts +178 -0
  124. package/server/modules/orchestration/a2a/types.ts +125 -0
  125. package/server/modules/orchestration/a2a/validator.ts +113 -0
  126. package/server/modules/orchestration/index.ts +66 -0
  127. package/server/modules/orchestration/preview/port-watcher.ts +112 -0
  128. package/server/modules/orchestration/preview/preview-proxy.ts +60 -0
  129. package/server/modules/orchestration/preview/types.ts +19 -0
  130. package/server/modules/orchestration/tasks/orchestration-task-store.ts +45 -0
  131. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +73 -0
  132. package/server/modules/orchestration/tasks/orchestration-task.service.ts +145 -0
  133. package/server/modules/orchestration/tasks/orchestration-task.types.ts +29 -0
  134. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -0
  135. package/server/modules/orchestration/workflows/workflow-runner.ts +1206 -0
  136. package/server/modules/orchestration/workflows/workflow-store.ts +97 -0
  137. package/server/modules/orchestration/workflows/workflow.routes.ts +169 -0
  138. package/server/modules/orchestration/workflows/workflow.types.ts +70 -0
  139. package/server/modules/orchestration/workflows/workspace-target.ts +120 -0
  140. package/server/modules/orchestration/workspace/docker-workspace.ts +135 -0
  141. package/server/modules/orchestration/workspace/path-safety.ts +55 -0
  142. package/server/modules/orchestration/workspace/types.ts +52 -0
  143. package/server/modules/orchestration/workspace/workspace-manager.ts +97 -0
  144. package/server/modules/orchestration/workspace/worktree-workspace.ts +125 -0
  145. package/server/modules/providers/index.ts +2 -0
  146. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +130 -130
  147. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  148. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  149. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -145
  150. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  151. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  152. package/server/modules/providers/shared/provider-configs.ts +142 -142
  153. package/server/openai-codex.js +40 -4
  154. package/server/qwen-code-cli.js +395 -395
  155. package/server/qwen-response-handler.js +73 -73
  156. package/server/routes/qwen.js +27 -27
  157. package/server/routes/taskmaster.js +116 -91
  158. package/server/services/external-access.js +171 -171
  159. package/server/services/provider-models.js +381 -381
  160. package/server/services/telegram/telegram-http-client.js +130 -130
  161. package/server/services/vapid-keys.js +36 -36
  162. package/server/utils/port-access.js +209 -209
  163. package/dist/assets/index-B1ghfb4w.css +0 -32
  164. package/dist/assets/index-oLYHJ2X5.js +0 -852
@@ -29,7 +29,7 @@ import path from 'node:path';
29
29
  const CURRENT_VERSION = 1;
30
30
 
31
31
  // Tables the store manages — empty arrays on a fresh file.
32
- const EMPTY_STORE = () => ({
32
+ const EMPTY_STORE = (extraTables = {}) => ({
33
33
  _version: CURRENT_VERSION,
34
34
  _sequences: {
35
35
  users: 0,
@@ -37,6 +37,7 @@ const EMPTY_STORE = () => ({
37
37
  user_credentials: 0,
38
38
  vapid_keys: 0,
39
39
  push_subscriptions: 0,
40
+ ...Object.fromEntries(Object.keys(extraTables).map((k) => [k, 0])),
40
41
  },
41
42
  users: [],
42
43
  api_keys: [],
@@ -48,12 +49,14 @@ const EMPTY_STORE = () => ({
48
49
  app_config: [], // each row: { key, value, created_at }
49
50
  telegram_config: [], // 0 or 1 row: { id=1, bot_token, bot_username, updated_at }
50
51
  telegram_links: [], // each row: { user_id, chat_id, ... }
52
+ ...Object.fromEntries(Object.entries(extraTables).map(([k, v]) => [k, v ?? []])),
51
53
  });
52
54
 
53
55
  export class JsonStore {
54
- constructor(filePath) {
56
+ constructor(filePath, extraTables = {}) {
55
57
  this.filePath = filePath;
56
58
  this.tmpPath = `${filePath}.tmp`;
59
+ this.extraTables = extraTables;
57
60
  this.data = null;
58
61
  this._ensureLoaded();
59
62
  }
@@ -69,7 +72,7 @@ export class JsonStore {
69
72
  }
70
73
 
71
74
  if (!fs.existsSync(this.filePath)) {
72
- this.data = EMPTY_STORE();
75
+ this.data = EMPTY_STORE(this.extraTables);
73
76
  this._flush();
74
77
  return;
75
78
  }
@@ -79,7 +82,7 @@ export class JsonStore {
79
82
  const parsed = JSON.parse(raw);
80
83
  // Fill missing keys from EMPTY_STORE so adding a new "table" in a
81
84
  // later schema doesn't crash a fresh deploy reading an old file.
82
- this.data = { ...EMPTY_STORE(), ...parsed };
85
+ this.data = { ...EMPTY_STORE(this.extraTables), ...parsed };
83
86
  // Ensure each well-known array key is actually an array — defends
84
87
  // against a hand-edited file that set one to null or an object.
85
88
  const empty = EMPTY_STORE();
@@ -96,7 +99,7 @@ export class JsonStore {
96
99
  const backup = `${this.filePath}.corrupt-${Date.now()}`;
97
100
  console.error(`[JsonStore] Failed to read ${this.filePath}: ${err.message}. Backing up to ${backup}.`);
98
101
  try { fs.renameSync(this.filePath, backup); } catch { /* noop */ }
99
- this.data = EMPTY_STORE();
102
+ this.data = EMPTY_STORE(this.extraTables);
100
103
  this._flush();
101
104
  }
102
105
  }
package/server/index.js CHANGED
@@ -71,6 +71,19 @@ import qwenRoutes from './routes/qwen.js';
71
71
  import pluginsRoutes from './routes/plugins.js';
72
72
  import messagesRoutes from './routes/messages.js';
73
73
  import providerRoutes from './modules/providers/provider.routes.js';
74
+ import {
75
+ createA2ARouter,
76
+ adapterRegistry,
77
+ ClaudeCodeA2AAdapter,
78
+ CodexA2AAdapter,
79
+ CursorA2AAdapter,
80
+ GeminiA2AAdapter,
81
+ OpenCodeA2AAdapter,
82
+ QwenA2AAdapter,
83
+ createPreviewProxyRouter,
84
+ createOrchestrationTaskRouter,
85
+ createWorkflowRouter,
86
+ } from './modules/orchestration/index.js';
74
87
  import networkRoutes from './routes/network.js';
75
88
  import telegramRoutes from './routes/telegram.js';
76
89
  import { restoreBotFromConfig } from './services/telegram/bot.js';
@@ -376,6 +389,18 @@ app.use('/api/sessions', authenticateToken, messagesRoutes);
376
389
  // Unified provider MCP routes (protected)
377
390
  app.use('/api/providers', authenticateToken, providerRoutes);
378
391
 
392
+ // A2A protocol router — has its own auth middleware, do NOT wrap with authenticateToken
393
+ adapterRegistry.register(new ClaudeCodeA2AAdapter());
394
+ adapterRegistry.register(new CodexA2AAdapter());
395
+ adapterRegistry.register(new CursorA2AAdapter());
396
+ adapterRegistry.register(new GeminiA2AAdapter());
397
+ adapterRegistry.register(new QwenA2AAdapter());
398
+ adapterRegistry.register(new OpenCodeA2AAdapter());
399
+ app.use('/a2a', createA2ARouter());
400
+ app.use('/preview', authenticateToken, createPreviewProxyRouter());
401
+ app.use('/api/orchestration', authenticateToken, createOrchestrationTaskRouter());
402
+ app.use('/api/orchestration', authenticateToken, createWorkflowRouter());
403
+
379
404
  // Network discovery / QR endpoints (protected)
380
405
  app.use('/api/network', authenticateToken, networkRoutes);
381
406
 
@@ -2822,6 +2847,7 @@ const SERVER_PORT = process.env.SERVER_PORT || 3001;
2822
2847
  const HOST = process.env.HOST || '0.0.0.0';
2823
2848
  const DISPLAY_HOST = getConnectableHost(HOST);
2824
2849
  const VITE_PORT = process.env.VITE_PORT || 5173;
2850
+ const SEPARATE_FRONTEND = process.env.PIXCODE_SEPARATE_FRONTEND === '1';
2825
2851
 
2826
2852
  async function isPortOpen(port, timeoutMs = 800) {
2827
2853
  return await new Promise((resolve) => {
@@ -2873,7 +2899,17 @@ function printSystemDaemonActiveNotice(port) {
2873
2899
  console.log(`${c.info('[INFO]')} Logs: ${c.bright(logsCommand)}`);
2874
2900
  }
2875
2901
 
2876
- function printUserDaemonActiveNotice(port, frontendPort) {
2902
+ function daemonFrontendArgs() {
2903
+ return SEPARATE_FRONTEND
2904
+ ? ['--frontend-port', String(VITE_PORT)]
2905
+ : ['--single-port'];
2906
+ }
2907
+
2908
+ function daemonInstallArgs(mode) {
2909
+ return ['install', '--mode', mode, '--port', String(SERVER_PORT), ...daemonFrontendArgs()];
2910
+ }
2911
+
2912
+ function printUserDaemonActiveNotice(port, frontendPort, frontendEnabled = SEPARATE_FRONTEND) {
2877
2913
  const effectivePort = Number(port) || 3001;
2878
2914
  const effectiveFrontendPort = Number(frontendPort) || 5173;
2879
2915
  const statusCommand = buildDaemonCliCommand(
@@ -2889,8 +2925,10 @@ function printUserDaemonActiveNotice(port, frontendPort) {
2889
2925
  DAEMON_COMMAND_CONTEXT
2890
2926
  );
2891
2927
  console.log(`${c.ok('[OK]')} User daemon is active for this account.`);
2892
- console.log(`${c.info('[INFO]')} Backend: ${c.bright(`http://localhost:${effectivePort}`)}`);
2893
- console.log(`${c.info('[INFO]')} Frontend: ${c.bright(`http://localhost:${effectiveFrontendPort}`)}`);
2928
+ console.log(`${c.info('[INFO]')} App URL: ${c.bright(`http://localhost:${effectivePort}`)}`);
2929
+ if (frontendEnabled) {
2930
+ console.log(`${c.info('[INFO]')} Frontend: ${c.bright(`http://localhost:${effectiveFrontendPort}`)}`);
2931
+ }
2894
2932
  console.log(`${c.info('[INFO]')} Status: ${c.bright(statusCommand)}`);
2895
2933
  console.log(`${c.info('[INFO]')} Stop: ${c.bright(stopCommand)}`);
2896
2934
  console.log(`${c.info('[INFO]')} Logs: ${c.bright(logsCommand)}`);
@@ -2910,8 +2948,8 @@ async function maybeAutoDaemonBootstrapFromIndex() {
2910
2948
 
2911
2949
  process.env.PIXCODE_DAEMON_ATTEMPTED = '1';
2912
2950
 
2913
- const systemArgs = ['install', '--mode=system', '--port', String(SERVER_PORT), '--frontend-port', String(VITE_PORT)];
2914
- const userArgs = ['install', '--mode=user', '--port', String(SERVER_PORT), '--frontend-port', String(VITE_PORT)];
2951
+ const systemArgs = daemonInstallArgs('system');
2952
+ const userArgs = daemonInstallArgs('user');
2915
2953
 
2916
2954
  try {
2917
2955
  console.log(`${c.info('[INFO]')} Linux detected. Enforcing system daemon mode for Pixcode...`);
@@ -2935,7 +2973,7 @@ async function maybeAutoDaemonBootstrapFromIndex() {
2935
2973
  {
2936
2974
  subcommand: 'install',
2937
2975
  mode: 'system',
2938
- extraArgs: ['--port', String(SERVER_PORT), '--frontend-port', String(VITE_PORT)],
2976
+ extraArgs: ['--port', String(SERVER_PORT), ...daemonFrontendArgs()],
2939
2977
  },
2940
2978
  DAEMON_COMMAND_CONTEXT
2941
2979
  );
@@ -2969,7 +3007,7 @@ async function maybeAutoDaemonBootstrapFromIndex() {
2969
3007
  {
2970
3008
  subcommand: 'install',
2971
3009
  mode: 'system',
2972
- extraArgs: ['--port', String(SERVER_PORT), '--frontend-port', String(VITE_PORT)],
3010
+ extraArgs: ['--port', String(SERVER_PORT), ...daemonFrontendArgs()],
2973
3011
  },
2974
3012
  DAEMON_COMMAND_CONTEXT
2975
3013
  );
@@ -2977,7 +3015,7 @@ async function maybeAutoDaemonBootstrapFromIndex() {
2977
3015
  {
2978
3016
  subcommand: 'install',
2979
3017
  mode: 'user',
2980
- extraArgs: ['--port', String(SERVER_PORT), '--frontend-port', String(VITE_PORT)],
3018
+ extraArgs: ['--port', String(SERVER_PORT), ...daemonFrontendArgs()],
2981
3019
  },
2982
3020
  DAEMON_COMMAND_CONTEXT
2983
3021
  );
@@ -3045,7 +3083,9 @@ async function startServer() {
3045
3083
  console.log(`${c.info('[INFO]')} To run in production mode, go to http://${DISPLAY_HOST}:${SERVER_PORT}`);
3046
3084
  }
3047
3085
 
3048
- console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`);
3086
+ if (SEPARATE_FRONTEND) {
3087
+ console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`);
3088
+ }
3049
3089
 
3050
3090
  server.listen(SERVER_PORT, HOST, async () => {
3051
3091
  const appInstallPath = APP_ROOT;
@@ -0,0 +1,108 @@
1
+ // server/modules/orchestration/a2a/adapter-registry.ts
2
+ // In-process registry mapping adapter ids to AbstractA2AAdapter
3
+ // instances. Resolution supports three id forms:
4
+ // - "claude-code" explicit
5
+ // - "skill:<skillId>" first REGISTERED adapter advertising that skill
6
+ // (Map iteration is insertion-ordered per ES spec).
7
+ // - "auto" first registered adapter (deterministic fallback
8
+ // until smarter routing arrives in a later plan)
9
+
10
+ import type { AbstractA2AAdapter } from '@/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js';
11
+ import type { AgentCard } from '@/modules/orchestration/a2a/types.js';
12
+
13
+ interface ResolveAdapterOptions {
14
+ preferredAdapterId?: string;
15
+ preferredProvider?: string;
16
+ preferredSkillId?: string;
17
+ }
18
+
19
+ class AdapterRegistry {
20
+ // Map iteration order is insertion-ordered (ES spec); auto and skill: resolution depend on this.
21
+ private readonly byId = new Map<string, AbstractA2AAdapter>();
22
+
23
+ register(adapter: AbstractA2AAdapter): void {
24
+ if (this.byId.has(adapter.id)) {
25
+ throw new Error(`A2A adapter already registered: ${adapter.id}`);
26
+ }
27
+ this.byId.set(adapter.id, adapter);
28
+ }
29
+
30
+ get(id: string): AbstractA2AAdapter | undefined {
31
+ return this.byId.get(id);
32
+ }
33
+
34
+ resolve(idOrSelector: string, options: ResolveAdapterOptions = {}): AbstractA2AAdapter | undefined {
35
+ const normalizedSelector = idOrSelector.trim();
36
+ if (!normalizedSelector) {
37
+ return undefined;
38
+ }
39
+
40
+ if (normalizedSelector === 'auto') {
41
+ return this.pickPreferred(this.list(), options);
42
+ }
43
+
44
+ if (normalizedSelector.startsWith('skill:')) {
45
+ const skill = normalizedSelector.slice('skill:'.length);
46
+ const matches = this.list().filter((adapter) =>
47
+ adapter.agentCard.skills.some((s) => s.id === skill),
48
+ );
49
+ if (matches.length === 0) {
50
+ return undefined;
51
+ }
52
+ return this.pickPreferred(matches, {
53
+ ...options,
54
+ preferredSkillId: options.preferredSkillId ?? skill,
55
+ });
56
+ }
57
+
58
+ return this.byId.get(normalizedSelector);
59
+ }
60
+
61
+ list(): AbstractA2AAdapter[] {
62
+ return [...this.byId.values()];
63
+ }
64
+
65
+ agentCards(): AgentCard[] {
66
+ return this.list().map((a) => a.agentCard);
67
+ }
68
+
69
+ private pickPreferred(
70
+ adapters: AbstractA2AAdapter[],
71
+ options: ResolveAdapterOptions,
72
+ ): AbstractA2AAdapter | undefined {
73
+ const {
74
+ preferredAdapterId,
75
+ preferredProvider,
76
+ preferredSkillId,
77
+ } = options;
78
+
79
+ if (preferredAdapterId) {
80
+ const byAdapterId = adapters.find((adapter) => adapter.id === preferredAdapterId);
81
+ if (byAdapterId) {
82
+ return byAdapterId;
83
+ }
84
+ }
85
+
86
+ if (preferredProvider) {
87
+ const normalizedProvider = preferredProvider.trim().toLowerCase();
88
+ const byProvider = adapters.find((adapter) => adapter.id === normalizedProvider);
89
+ if (byProvider) {
90
+ return byProvider;
91
+ }
92
+ }
93
+
94
+ if (preferredSkillId) {
95
+ const bySkill = adapters.find((adapter) =>
96
+ adapter.agentCard.skills.some((skill) => skill.id === preferredSkillId),
97
+ );
98
+ if (bySkill) {
99
+ return bySkill;
100
+ }
101
+ }
102
+
103
+ return adapters[0];
104
+ }
105
+ }
106
+
107
+ export const adapterRegistry = new AdapterRegistry();
108
+ export type { AdapterRegistry, ResolveAdapterOptions };
@@ -0,0 +1,55 @@
1
+ // server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts
2
+ // Base class every CLI adapter extends. Adapters wrap the
3
+ // existing per-CLI runtime files (claude-sdk.js, openai-codex.js, ...)
4
+ // and translate between A2A messages and the CLI's native I/O.
5
+
6
+ import { a2aBus } from '@/modules/orchestration/a2a/bus.js';
7
+ import type {
8
+ AgentCard,
9
+ Artifact,
10
+ Message,
11
+ Task,
12
+ TaskError,
13
+ TaskState,
14
+ } from '@/modules/orchestration/a2a/types.js';
15
+ import type { WorkspaceHandle } from '@/modules/orchestration/workspace/types.js';
16
+
17
+ export interface AdapterContext {
18
+ /** Isolated execution workspace for the task. */
19
+ workspace: WorkspaceHandle;
20
+ /** Compatibility alias while legacy adapters still accept cwd directly. */
21
+ cwd: string;
22
+ /** pixcode permission mode passed through to the underlying CLI. */
23
+ permissionMode?: string;
24
+ /** Provider model selected by the user in Pixcode. */
25
+ model?: string;
26
+ /** Provider-specific tool / permission settings from Pixcode Settings. */
27
+ toolsSettings?: Record<string, unknown>;
28
+ /** Optional parent task id when this adapter is invoked inside a workflow. */
29
+ parentTaskId?: string;
30
+ }
31
+
32
+ export interface TaskHandle {
33
+ cancel(): Promise<void>;
34
+ finished: Promise<void>;
35
+ }
36
+
37
+ export abstract class AbstractA2AAdapter {
38
+ abstract readonly id: string;
39
+ abstract readonly agentCard: AgentCard;
40
+
41
+ abstract submitTask(task: Task, ctx: AdapterContext): Promise<TaskHandle>;
42
+ abstract cancelTask(taskId: string): Promise<void>;
43
+
44
+ protected emitState(taskId: string, state: TaskState, error?: TaskError): void {
45
+ a2aBus.publish({ kind: 'task-state', taskId, state, error });
46
+ }
47
+
48
+ protected emitMessage(taskId: string, message: Message): void {
49
+ a2aBus.publish({ kind: 'message', taskId, message });
50
+ }
51
+
52
+ protected emitArtifact(taskId: string, artifact: Artifact): void {
53
+ a2aBus.publish({ kind: 'artifact', taskId, artifact });
54
+ }
55
+ }
@@ -0,0 +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
+ 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
+ }