@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.
- package/dist/api-docs.html +162 -9
- package/dist/assets/index-B8w57E1r.css +32 -0
- package/dist/assets/index-Djuh0wHV.js +854 -0
- package/dist/favicon.svg +8 -8
- package/dist/icons/icon-128x128.svg +9 -9
- package/dist/icons/icon-144x144.svg +9 -9
- package/dist/icons/icon-152x152.svg +9 -9
- package/dist/icons/icon-192x192.svg +9 -9
- package/dist/icons/icon-384x384.svg +9 -9
- package/dist/icons/icon-512x512.svg +9 -9
- package/dist/icons/icon-72x72.svg +9 -9
- package/dist/icons/icon-96x96.svg +9 -9
- package/dist/icons/icon-template.svg +9 -9
- package/dist/index.html +2 -2
- package/dist/logo.svg +12 -12
- package/dist/openapi.yaml +383 -1
- package/dist-server/server/claude-sdk.js +38 -7
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +12 -17
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/daemon-manager.js +98 -51
- package/dist-server/server/daemon-manager.js.map +1 -1
- package/dist-server/server/database/json-store.js +8 -5
- package/dist-server/server/database/json-store.js.map +1 -1
- package/dist-server/server/index.js +34 -9
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/a2a/adapter-registry.js +73 -0
- package/dist-server/server/modules/orchestration/a2a/adapter-registry.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js +17 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +234 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js +202 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js +205 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js +205 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js +205 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js +205 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/agent-card.js +50 -0
- package/dist-server/server/modules/orchestration/a2a/agent-card.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/auth.middleware.js +25 -0
- package/dist-server/server/modules/orchestration/a2a/auth.middleware.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/bus.js +34 -0
- package/dist-server/server/modules/orchestration/a2a/bus.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/routes.js +497 -0
- package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/task-store.js +144 -0
- package/dist-server/server/modules/orchestration/a2a/task-store.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/types.js +6 -0
- package/dist-server/server/modules/orchestration/a2a/types.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/validator.js +101 -0
- package/dist-server/server/modules/orchestration/a2a/validator.js.map +1 -0
- package/dist-server/server/modules/orchestration/index.js +24 -0
- package/dist-server/server/modules/orchestration/index.js.map +1 -0
- package/dist-server/server/modules/orchestration/preview/port-watcher.js +90 -0
- package/dist-server/server/modules/orchestration/preview/port-watcher.js.map +1 -0
- package/dist-server/server/modules/orchestration/preview/preview-proxy.js +58 -0
- package/dist-server/server/modules/orchestration/preview/preview-proxy.js.map +1 -0
- package/dist-server/server/modules/orchestration/preview/types.js +2 -0
- package/dist-server/server/modules/orchestration/preview/types.js.map +1 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js +37 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js.map +1 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +68 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +128 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js +2 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js +126 -0
- package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +1047 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-store.js +76 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-store.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +151 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.types.js +2 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.types.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workspace-target.js +98 -0
- package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -0
- package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +122 -0
- package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -0
- package/dist-server/server/modules/orchestration/workspace/path-safety.js +48 -0
- package/dist-server/server/modules/orchestration/workspace/path-safety.js.map +1 -0
- package/dist-server/server/modules/orchestration/workspace/types.js +11 -0
- package/dist-server/server/modules/orchestration/workspace/types.js.map +1 -0
- package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +80 -0
- package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -0
- package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +96 -0
- package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -0
- package/dist-server/server/modules/providers/index.js +3 -0
- package/dist-server/server/modules/providers/index.js.map +1 -0
- package/dist-server/server/openai-codex.js +35 -4
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +106 -89
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/package.json +3 -1
- package/scripts/smoke/a2a-roundtrip.mjs +167 -0
- package/scripts/smoke/orchestration-api.mjs +172 -0
- package/scripts/smoke/orchestration-live-run.mjs +176 -0
- package/server/claude-sdk.js +48 -7
- package/server/cli.js +12 -17
- package/server/daemon-manager.js +90 -51
- package/server/database/db.js +794 -794
- package/server/database/json-store.js +8 -5
- package/server/index.js +49 -9
- package/server/modules/orchestration/a2a/adapter-registry.ts +108 -0
- package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +55 -0
- package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +284 -0
- package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -0
- package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -0
- package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -0
- package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -0
- package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -0
- package/server/modules/orchestration/a2a/agent-card.ts +55 -0
- package/server/modules/orchestration/a2a/auth.middleware.ts +29 -0
- package/server/modules/orchestration/a2a/bus.ts +46 -0
- package/server/modules/orchestration/a2a/routes.ts +577 -0
- package/server/modules/orchestration/a2a/task-store.ts +178 -0
- package/server/modules/orchestration/a2a/types.ts +125 -0
- package/server/modules/orchestration/a2a/validator.ts +113 -0
- package/server/modules/orchestration/index.ts +66 -0
- package/server/modules/orchestration/preview/port-watcher.ts +112 -0
- package/server/modules/orchestration/preview/preview-proxy.ts +60 -0
- package/server/modules/orchestration/preview/types.ts +19 -0
- package/server/modules/orchestration/tasks/orchestration-task-store.ts +45 -0
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +73 -0
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +145 -0
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +29 -0
- package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +1206 -0
- package/server/modules/orchestration/workflows/workflow-store.ts +97 -0
- package/server/modules/orchestration/workflows/workflow.routes.ts +169 -0
- package/server/modules/orchestration/workflows/workflow.types.ts +70 -0
- package/server/modules/orchestration/workflows/workspace-target.ts +120 -0
- package/server/modules/orchestration/workspace/docker-workspace.ts +135 -0
- package/server/modules/orchestration/workspace/path-safety.ts +55 -0
- package/server/modules/orchestration/workspace/types.ts +52 -0
- package/server/modules/orchestration/workspace/workspace-manager.ts +97 -0
- package/server/modules/orchestration/workspace/worktree-workspace.ts +125 -0
- package/server/modules/providers/index.ts +2 -0
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +130 -130
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
- package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
- package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -145
- package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
- package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
- package/server/modules/providers/shared/provider-configs.ts +142 -142
- package/server/openai-codex.js +40 -4
- package/server/qwen-code-cli.js +395 -395
- package/server/qwen-response-handler.js +73 -73
- package/server/routes/qwen.js +27 -27
- package/server/routes/taskmaster.js +116 -91
- package/server/services/external-access.js +171 -171
- package/server/services/provider-models.js +381 -381
- package/server/services/telegram/telegram-http-client.js +130 -130
- package/server/services/vapid-keys.js +36 -36
- package/server/utils/port-access.js +209 -209
- package/dist/assets/index-B1ghfb4w.css +0 -32
- 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
|
|
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]')}
|
|
2893
|
-
|
|
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 =
|
|
2914
|
-
const userArgs =
|
|
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),
|
|
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),
|
|
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),
|
|
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
|
-
|
|
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
|
+
}
|