@pixelbyte-software/pixcode 1.37.0 → 1.38.1

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 (53) hide show
  1. package/dist/assets/{index-D8uNxHf1.js → index-Br191izN.js} +139 -139
  2. package/dist/assets/index-BzL2G4Sw.css +32 -0
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/database/db.js +18 -3
  5. package/dist-server/server/database/db.js.map +1 -1
  6. package/dist-server/server/index.js +11 -0
  7. package/dist-server/server/index.js.map +1 -1
  8. package/dist-server/server/modules/providers/provider.routes.js +107 -0
  9. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  10. package/dist-server/server/routes/auth.js +15 -0
  11. package/dist-server/server/routes/auth.js.map +1 -1
  12. package/dist-server/server/routes/diagnostics.js +35 -0
  13. package/dist-server/server/routes/diagnostics.js.map +1 -0
  14. package/dist-server/server/routes/public-api.js +16 -0
  15. package/dist-server/server/routes/public-api.js.map +1 -0
  16. package/dist-server/server/routes/remote.js +26 -0
  17. package/dist-server/server/routes/remote.js.map +1 -0
  18. package/dist-server/server/routes/settings.js +23 -2
  19. package/dist-server/server/routes/settings.js.map +1 -1
  20. package/dist-server/server/routes/taskmaster.js +102 -2
  21. package/dist-server/server/routes/taskmaster.js.map +1 -1
  22. package/dist-server/server/services/diagnostics.js +142 -0
  23. package/dist-server/server/services/diagnostics.js.map +1 -0
  24. package/dist-server/server/services/public-api-manifest.js +83 -0
  25. package/dist-server/server/services/public-api-manifest.js.map +1 -0
  26. package/dist-server/server/services/remote-connection.js +120 -0
  27. package/dist-server/server/services/remote-connection.js.map +1 -0
  28. package/dist-server/server/services/telegram/control-center.js +62 -2
  29. package/dist-server/server/services/telegram/control-center.js.map +1 -1
  30. package/dist-server/server/services/telegram/translations.js +16 -4
  31. package/dist-server/server/services/telegram/translations.js.map +1 -1
  32. package/package.json +6 -1
  33. package/scripts/github/create-v1.38-issues.mjs +351 -0
  34. package/scripts/smoke/discord-release-workflow.mjs +24 -0
  35. package/scripts/smoke/v138-completion.mjs +132 -0
  36. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -0
  37. package/scripts/smoke/v138-diagnostics.mjs +63 -0
  38. package/scripts/smoke/v138-issue-planner.mjs +33 -0
  39. package/server/database/db.js +21 -3
  40. package/server/index.js +14 -0
  41. package/server/modules/providers/provider.routes.ts +134 -0
  42. package/server/routes/auth.js +20 -1
  43. package/server/routes/diagnostics.js +41 -0
  44. package/server/routes/public-api.js +21 -0
  45. package/server/routes/remote.js +33 -0
  46. package/server/routes/settings.js +25 -2
  47. package/server/routes/taskmaster.js +103 -2
  48. package/server/services/diagnostics.js +165 -0
  49. package/server/services/public-api-manifest.js +87 -0
  50. package/server/services/remote-connection.js +127 -0
  51. package/server/services/telegram/control-center.js +66 -2
  52. package/server/services/telegram/translations.js +16 -4
  53. package/dist/assets/index-CfHK8y_H.css +0 -32
@@ -379,17 +379,28 @@ const userDb = {
379
379
  const apiKeysDb = {
380
380
  generateApiKey: () => 'px_' + crypto.randomBytes(32).toString('hex'),
381
381
 
382
- createApiKey: (userId, keyName) => {
382
+ normalizeScopes: (scopes) => {
383
+ if (!Array.isArray(scopes)) return [];
384
+ return Array.from(new Set(scopes
385
+ .filter((scope) => typeof scope === 'string')
386
+ .map((scope) => scope.trim())
387
+ .filter(Boolean)
388
+ )).sort();
389
+ },
390
+
391
+ createApiKey: (userId, keyName, scopes = []) => {
383
392
  const apiKey = apiKeysDb.generateApiKey();
393
+ const normalizedScopes = apiKeysDb.normalizeScopes(scopes);
384
394
  const row = store.insert('api_keys', {
385
395
  user_id: userId,
386
396
  key_name: keyName,
387
397
  api_key: apiKey,
398
+ scopes: normalizedScopes,
388
399
  created_at: nowIso(),
389
400
  last_used: null,
390
401
  is_active: true,
391
402
  });
392
- return { id: row.id, keyName, apiKey };
403
+ return { id: row.id, keyName, apiKey, scopes: normalizedScopes };
393
404
  },
394
405
 
395
406
  getApiKeys: (userId) => {
@@ -402,6 +413,7 @@ const apiKeysDb = {
402
413
  id: r.id,
403
414
  key_name: r.key_name,
404
415
  api_key: r.api_key,
416
+ scopes: apiKeysDb.normalizeScopes(r.scopes),
405
417
  created_at: r.created_at,
406
418
  last_used: r.last_used,
407
419
  is_active: r.is_active ? 1 : 0,
@@ -420,6 +432,7 @@ const apiKeysDb = {
420
432
  id: user.id,
421
433
  username: user.username,
422
434
  api_key_id: key.id,
435
+ api_key_scopes: apiKeysDb.normalizeScopes(key.scopes),
423
436
  };
424
437
  },
425
438
 
@@ -428,6 +441,11 @@ const apiKeysDb = {
428
441
 
429
442
  toggleApiKey: (userId, apiKeyId, isActive) =>
430
443
  store.updateWhere('api_keys', (r) => r.id === apiKeyId && r.user_id === userId, { is_active: Boolean(isActive) }) > 0,
444
+
445
+ updateApiKeyScopes: (userId, apiKeyId, scopes = []) =>
446
+ store.updateWhere('api_keys', (r) => r.id === apiKeyId && r.user_id === userId, {
447
+ scopes: apiKeysDb.normalizeScopes(scopes),
448
+ }) > 0,
431
449
  };
432
450
 
433
451
  // ---------------------------------------------------------------------------
@@ -691,7 +709,7 @@ function normalizeTelegramControlState(value = {}) {
691
709
  const selectedProvider = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'].includes(raw.selectedProvider)
692
710
  ? raw.selectedProvider
693
711
  : DEFAULT_TELEGRAM_CONTROL_STATE.selectedProvider;
694
- const progressMode = ['final', 'steps', 'all'].includes(raw.progressMode)
712
+ const progressMode = ['final', 'steps', 'all', 'errors'].includes(raw.progressMode)
695
713
  ? raw.progressMode
696
714
  : DEFAULT_TELEGRAM_CONTROL_STATE.progressMode;
697
715
 
package/server/index.js CHANGED
@@ -75,6 +75,9 @@ import geminiRoutes from './routes/gemini.js';
75
75
  import qwenRoutes from './routes/qwen.js';
76
76
  import pluginsRoutes from './routes/plugins.js';
77
77
  import messagesRoutes from './routes/messages.js';
78
+ import diagnosticsRoutes from './routes/diagnostics.js';
79
+ import remoteRoutes from './routes/remote.js';
80
+ import publicApiRoutes from './routes/public-api.js';
78
81
  import providerRoutes from './modules/providers/provider.routes.js';
79
82
  import {
80
83
  createA2ARouter,
@@ -320,6 +323,8 @@ const wss = new WebSocketServer({
320
323
 
321
324
  // Make WebSocket server available to routes
322
325
  app.locals.wss = wss;
326
+ app.locals.installMode = installMode;
327
+ app.locals.serverVersion = SERVER_VERSION;
323
328
  setNotificationWebSocketServer(wss);
324
329
 
325
330
  app.use(cors({ exposedHeaders: ['X-Refreshed-Token'] }));
@@ -391,6 +396,15 @@ app.use('/api/plugins', authenticateToken, pluginsRoutes);
391
396
  // Unified session messages route (protected)
392
397
  app.use('/api/sessions', authenticateToken, messagesRoutes);
393
398
 
399
+ // Diagnostics API Routes (protected)
400
+ app.use('/api/diagnostics', authenticateToken, diagnosticsRoutes);
401
+
402
+ // Remote connection API Routes (protected)
403
+ app.use('/api/remote', authenticateToken, remoteRoutes);
404
+
405
+ // Public automation manifest (protected so private host details only go to signed-in clients)
406
+ app.use('/api/public', authenticateToken, publicApiRoutes);
407
+
394
408
  // Unified provider MCP routes (protected)
395
409
  app.use('/api/providers', authenticateToken, providerRoutes);
396
410
 
@@ -670,6 +670,102 @@ const resolveConfigFile = (provider: string, fileId: string): { descriptor: Prov
670
670
  return { descriptor, absolutePath };
671
671
  };
672
672
 
673
+ const SENSITIVE_CONFIG_PATTERN = /(api[_-]?key|token|secret|password|authorization|bearer)\s*[:=]\s*["']?([^"'\n\r]+)/ig;
674
+
675
+ function redactProviderConfigPreview(contents: string): string {
676
+ return contents.replace(SENSITIVE_CONFIG_PATTERN, (_match, key) => `${key}: [redacted]`);
677
+ }
678
+
679
+ async function validateProviderConfigContents(descriptor: ProviderConfigFile, contents: string) {
680
+ if (Buffer.byteLength(contents, 'utf8') > MAX_CONFIG_FILE_SIZE_BYTES) {
681
+ throw new AppError(
682
+ `Config contents exceed ${MAX_CONFIG_FILE_SIZE_BYTES} bytes`,
683
+ { code: 'PROVIDER_CONFIG_TOO_LARGE', statusCode: 413 },
684
+ );
685
+ }
686
+
687
+ if (descriptor.format === 'json') {
688
+ try {
689
+ JSON.parse(contents || '{}');
690
+ } catch (err) {
691
+ throw new AppError(`Invalid JSON: ${(err as Error).message}`, {
692
+ code: 'PROVIDER_CONFIG_INVALID_JSON',
693
+ statusCode: 400,
694
+ });
695
+ }
696
+ }
697
+
698
+ return {
699
+ valid: true,
700
+ format: descriptor.format,
701
+ readonly: Boolean(descriptor.readonly),
702
+ preview: redactProviderConfigPreview(contents).slice(0, 4000),
703
+ };
704
+ }
705
+
706
+ async function buildProviderPluginState(provider: string) {
707
+ const files = PROVIDER_CONFIG_FILES[provider] || [];
708
+ const configs = await Promise.all(files.map(async (entry) => {
709
+ const absolutePath = path.resolve(os.homedir(), entry.relativePath);
710
+ let exists = false;
711
+ let size: number | null = null;
712
+ let updatedAt: string | null = null;
713
+ let preview = '';
714
+ try {
715
+ const stat = await fs.stat(absolutePath);
716
+ exists = stat.isFile();
717
+ size = stat.size;
718
+ updatedAt = stat.mtime.toISOString();
719
+ if (exists && stat.size <= MAX_CONFIG_FILE_SIZE_BYTES) {
720
+ preview = redactProviderConfigPreview(await fs.readFile(absolutePath, 'utf8')).slice(0, 1200);
721
+ }
722
+ } catch {
723
+ // Missing config files are normal for CLIs that have not been used yet.
724
+ }
725
+
726
+ return {
727
+ id: entry.id,
728
+ label: entry.label,
729
+ format: entry.format,
730
+ readonly: Boolean(entry.readonly),
731
+ relativePath: entry.relativePath,
732
+ absolutePath,
733
+ exists,
734
+ size,
735
+ updatedAt,
736
+ preview,
737
+ canBackup: exists,
738
+ canValidate: entry.format === 'json' || entry.format === 'env' || entry.format === 'toml' || entry.format === 'text',
739
+ };
740
+ }));
741
+
742
+ return {
743
+ provider,
744
+ supported: files.length > 0,
745
+ configCount: files.length,
746
+ installedCount: configs.filter((config) => config.exists).length,
747
+ configs,
748
+ };
749
+ }
750
+
751
+ router.get(
752
+ '/plugin-state',
753
+ asyncHandler(async (_req: Request, res: Response) => {
754
+ const providers = await Promise.all(
755
+ Object.keys(PROVIDER_CONFIG_FILES).map((provider) => buildProviderPluginState(provider)),
756
+ );
757
+ res.json(createApiSuccessResponse({ providers }));
758
+ }),
759
+ );
760
+
761
+ router.get(
762
+ '/plugin-state/:provider',
763
+ asyncHandler(async (req: Request, res: Response) => {
764
+ const provider = parseProvider(req.params.provider);
765
+ res.json(createApiSuccessResponse(await buildProviderPluginState(provider)));
766
+ }),
767
+ );
768
+
673
769
  router.get(
674
770
  '/:provider/config-files',
675
771
  asyncHandler(async (req: Request, res: Response) => {
@@ -826,4 +922,42 @@ router.put(
826
922
  }),
827
923
  );
828
924
 
925
+ router.post(
926
+ '/:provider/config-files/:fileId/validate',
927
+ asyncHandler(async (req: Request, res: Response) => {
928
+ const provider = String(req.params.provider);
929
+ const fileId = String(req.params.fileId);
930
+ const { descriptor, absolutePath } = resolveConfigFile(provider, fileId);
931
+ const contents = typeof req.body?.contents === 'string'
932
+ ? req.body.contents
933
+ : await fs.readFile(absolutePath, 'utf8').catch(() => '');
934
+ res.json(createApiSuccessResponse(await validateProviderConfigContents(descriptor, contents)));
935
+ }),
936
+ );
937
+
938
+ router.post(
939
+ '/:provider/config-files/:fileId/backup',
940
+ asyncHandler(async (req: Request, res: Response) => {
941
+ const provider = String(req.params.provider);
942
+ const fileId = String(req.params.fileId);
943
+ const { absolutePath } = resolveConfigFile(provider, fileId);
944
+ const stat = await fs.stat(absolutePath);
945
+ if (!stat.isFile()) {
946
+ throw new AppError(`${absolutePath} is not a regular file`, {
947
+ code: 'PROVIDER_CONFIG_NOT_FILE',
948
+ statusCode: 409,
949
+ });
950
+ }
951
+ const backupPath = `${absolutePath}.pixcode-backup-${Date.now()}`;
952
+ await fs.copyFile(absolutePath, backupPath);
953
+ res.json(createApiSuccessResponse({
954
+ provider,
955
+ fileId,
956
+ absolutePath,
957
+ backupPath,
958
+ size: stat.size,
959
+ }));
960
+ }),
961
+ );
962
+
829
963
  export default router;
@@ -7,6 +7,10 @@ import bcrypt from 'bcryptjs';
7
7
 
8
8
  import { userDb, db } from '../database/db.js';
9
9
  import { generateToken, authenticateToken } from '../middleware/auth.js';
10
+ import {
11
+ getPublicRemoteConnectionConfig,
12
+ saveRemoteConnectionConfig,
13
+ } from '../services/remote-connection.js';
10
14
 
11
15
  const router = express.Router();
12
16
 
@@ -24,6 +28,21 @@ router.get('/status', async (req, res) => {
24
28
  }
25
29
  });
26
30
 
31
+ // First-run connection mode is intentionally public: it is needed before
32
+ // account creation so a fresh desktop install can decide whether it controls
33
+ // this machine or a remote Pixcode server.
34
+ router.get('/connection-mode', (req, res) => {
35
+ res.json({ success: true, connection: getPublicRemoteConnectionConfig() });
36
+ });
37
+
38
+ router.put('/connection-mode', (req, res) => {
39
+ try {
40
+ res.json({ success: true, connection: saveRemoteConnectionConfig(req.body || {}) });
41
+ } catch (error) {
42
+ res.status(400).json({ success: false, error: error.message });
43
+ }
44
+ });
45
+
27
46
  // User registration (setup) - only allowed if no users exist
28
47
  router.post('/register', async (req, res) => {
29
48
  try {
@@ -137,4 +156,4 @@ router.post('/logout', authenticateToken, (req, res) => {
137
156
  res.json({ success: true, message: 'Logged out successfully' });
138
157
  });
139
158
 
140
- export default router;
159
+ export default router;
@@ -0,0 +1,41 @@
1
+ import express from 'express';
2
+
3
+ import { collectDiagnostics } from '../services/diagnostics.js';
4
+
5
+ const router = express.Router();
6
+
7
+ function buildDiagnostics(req) {
8
+ return collectDiagnostics({
9
+ installMode: req.app.locals.installMode,
10
+ serverVersion: req.app.locals.serverVersion,
11
+ wss: req.app.locals.wss,
12
+ activeRuns: req.app.locals.activeRuns || [],
13
+ recentErrors: req.app.locals.recentErrors || [],
14
+ providerHealth: req.app.locals.providerHealth || {},
15
+ cache: req.app.locals.diagnosticsCache || {},
16
+ });
17
+ }
18
+
19
+ router.get('/', (req, res) => {
20
+ res.json(buildDiagnostics(req));
21
+ });
22
+
23
+ router.post('/refresh', (req, res) => {
24
+ req.app.locals.diagnosticsCache = {
25
+ ...(req.app.locals.diagnosticsCache || {}),
26
+ diagnosticsUpdatedAt: new Date().toISOString(),
27
+ manualRefresh: true,
28
+ };
29
+ res.json(buildDiagnostics(req));
30
+ });
31
+
32
+ router.get('/bundle', (req, res) => {
33
+ const diagnostics = buildDiagnostics(req);
34
+ res.json({
35
+ generatedAt: diagnostics.timestamp,
36
+ copyable: true,
37
+ diagnostics,
38
+ });
39
+ });
40
+
41
+ export default router;
@@ -0,0 +1,21 @@
1
+ import express from 'express';
2
+
3
+ import { buildOpenApiFragment, buildPublicApiManifest } from '../services/public-api-manifest.js';
4
+
5
+ const router = express.Router();
6
+
7
+ function requestBaseUrl(req) {
8
+ const proto = req.headers['x-forwarded-proto'] || req.protocol;
9
+ const host = req.headers['x-forwarded-host'] || req.headers.host;
10
+ return host ? `${proto}://${host}` : '';
11
+ }
12
+
13
+ router.get('/manifest', (req, res) => {
14
+ res.json(buildPublicApiManifest({ baseUrl: requestBaseUrl(req) }));
15
+ });
16
+
17
+ router.get('/openapi', (req, res) => {
18
+ res.json(buildOpenApiFragment({ baseUrl: requestBaseUrl(req) }));
19
+ });
20
+
21
+ export default router;
@@ -0,0 +1,33 @@
1
+ import express from 'express';
2
+
3
+ import {
4
+ checkRemoteConnection,
5
+ getPublicRemoteConnectionConfig,
6
+ saveRemoteConnectionConfig,
7
+ } from '../services/remote-connection.js';
8
+
9
+ const router = express.Router();
10
+
11
+ router.get('/config', (req, res) => {
12
+ res.json({ success: true, connection: getPublicRemoteConnectionConfig() });
13
+ });
14
+
15
+ router.put('/config', (req, res) => {
16
+ try {
17
+ const connection = saveRemoteConnectionConfig(req.body || {});
18
+ res.json({ success: true, connection });
19
+ } catch (error) {
20
+ res.status(400).json({ success: false, error: error.message });
21
+ }
22
+ });
23
+
24
+ router.post('/check', async (req, res) => {
25
+ try {
26
+ const health = await checkRemoteConnection(req.body && Object.keys(req.body).length ? req.body : undefined);
27
+ res.json({ success: true, health, connection: getPublicRemoteConnectionConfig() });
28
+ } catch (error) {
29
+ res.status(400).json({ success: false, error: error.message });
30
+ }
31
+ });
32
+
33
+ export default router;
@@ -29,13 +29,13 @@ router.get('/api-keys', async (req, res) => {
29
29
  // Create a new API key
30
30
  router.post('/api-keys', async (req, res) => {
31
31
  try {
32
- const { keyName } = req.body;
32
+ const { keyName, scopes } = req.body;
33
33
 
34
34
  if (!keyName || !keyName.trim()) {
35
35
  return res.status(400).json({ error: 'Key name is required' });
36
36
  }
37
37
 
38
- const result = apiKeysDb.createApiKey(req.user.id, keyName.trim());
38
+ const result = apiKeysDb.createApiKey(req.user.id, keyName.trim(), scopes);
39
39
  res.json({
40
40
  success: true,
41
41
  apiKey: result
@@ -86,6 +86,29 @@ router.patch('/api-keys/:keyId/toggle', async (req, res) => {
86
86
  }
87
87
  });
88
88
 
89
+ // Update API key scopes
90
+ router.patch('/api-keys/:keyId/scopes', async (req, res) => {
91
+ try {
92
+ const { keyId } = req.params;
93
+ const { scopes } = req.body;
94
+
95
+ if (!Array.isArray(scopes)) {
96
+ return res.status(400).json({ error: 'scopes must be an array' });
97
+ }
98
+
99
+ const success = apiKeysDb.updateApiKeyScopes(req.user.id, parseInt(keyId), scopes);
100
+
101
+ if (success) {
102
+ res.json({ success: true });
103
+ } else {
104
+ res.status(404).json({ error: 'API key not found' });
105
+ }
106
+ } catch (error) {
107
+ console.error('Error updating API key scopes:', error);
108
+ res.status(500).json({ error: 'Failed to update API key scopes' });
109
+ }
110
+ });
111
+
89
112
  // ===============================
90
113
  // Generic Credentials Management
91
114
  // ===============================
@@ -161,6 +161,29 @@ function taskMasterExecutionDescription(task) {
161
161
  ].filter(Boolean).join('\n\n');
162
162
  }
163
163
 
164
+ function buildTaskMasterQueueSummary(projectName, projectPath, tasks) {
165
+ const normalized = tasks.map((task) => ({
166
+ ...task,
167
+ queueState: ['done', 'completed', 'cancelled', 'canceled'].includes(String(task.status || '').toLowerCase())
168
+ ? 'finished'
169
+ : String(task.status || 'pending') === 'in-progress'
170
+ ? 'running'
171
+ : 'queued',
172
+ }));
173
+ return {
174
+ projectName,
175
+ projectPath,
176
+ queue: normalized,
177
+ totals: {
178
+ all: normalized.length,
179
+ queued: normalized.filter((task) => task.queueState === 'queued').length,
180
+ running: normalized.filter((task) => task.queueState === 'running').length,
181
+ finished: normalized.filter((task) => task.queueState === 'finished').length,
182
+ },
183
+ timestamp: new Date().toISOString(),
184
+ };
185
+ }
186
+
164
187
  // API Routes
165
188
 
166
189
  /**
@@ -362,6 +385,66 @@ router.get('/tasks/:projectName', async (req, res) => {
362
385
  }
363
386
  });
364
387
 
388
+ /**
389
+ * GET /api/taskmaster/queue/:projectName
390
+ * Stable automation endpoint for remote UI, Telegram, and external clients.
391
+ */
392
+ router.get('/queue/:projectName', async (req, res) => {
393
+ try {
394
+ const { projectName } = req.params;
395
+ const { projectPath, transformedTasks } = await readTaskMasterTasks(projectName);
396
+ res.json(buildTaskMasterQueueSummary(projectName, projectPath, transformedTasks));
397
+ } catch (error) {
398
+ if (error?.code === 'ENOENT') {
399
+ return res.json(buildTaskMasterQueueSummary(req.params.projectName, null, []));
400
+ }
401
+ console.error('TaskMaster queue loading error:', error);
402
+ res.status(500).json({
403
+ error: 'Failed to load TaskMaster queue',
404
+ message: error.message
405
+ });
406
+ }
407
+ });
408
+
409
+ /**
410
+ * GET /api/taskmaster/task/:projectName/:taskId
411
+ * Load a single TaskMaster item with queue metadata.
412
+ */
413
+ router.get('/task/:projectName/:taskId', async (req, res) => {
414
+ try {
415
+ const { projectName, taskId } = req.params;
416
+ const { projectPath, transformedTasks } = await readTaskMasterTasks(projectName);
417
+ const task = transformedTasks.find((candidate) => String(candidate.id) === String(taskId));
418
+ if (!task) {
419
+ return res.status(404).json({
420
+ success: false,
421
+ error: 'TaskMaster task not found',
422
+ message: `Task "${taskId}" was not found in project "${projectName}"`
423
+ });
424
+ }
425
+ res.json({
426
+ success: true,
427
+ projectName,
428
+ projectPath,
429
+ task,
430
+ execution: {
431
+ supportsProvider: true,
432
+ supportsModel: true,
433
+ supportsFallbackProvider: true,
434
+ supportsPermissionMode: true,
435
+ supportsWorkerSlot: true,
436
+ },
437
+ });
438
+ } catch (error) {
439
+ console.error('TaskMaster task detail error:', error);
440
+ res.status(500).json({
441
+ success: false,
442
+ error: 'Failed to load TaskMaster task',
443
+ message: error.message
444
+ });
445
+ }
446
+ });
447
+
365
448
  /**
366
449
  * POST /api/taskmaster/execute/:projectName/:taskId
367
450
  * Import a TaskMaster task into orchestration and dispatch it to a CLI agent.
@@ -376,6 +459,8 @@ router.post('/execute/:projectName/:taskId', async (req, res) => {
376
459
  : '';
377
460
  const model = typeof req.body?.model === 'string' ? req.body.model : undefined;
378
461
  const permissionMode = typeof req.body?.permissionMode === 'string' ? req.body.permissionMode : undefined;
462
+ const fallbackProvider = typeof req.body?.fallbackProvider === 'string' ? req.body.fallbackProvider : undefined;
463
+ const workerSlot = Number.isInteger(req.body?.workerSlot) ? req.body.workerSlot : undefined;
379
464
  const isolation = ['host', 'worktree', 'docker'].includes(req.body?.isolation)
380
465
  ? req.body.isolation
381
466
  : 'worktree';
@@ -403,7 +488,14 @@ router.post('/execute/:projectName/:taskId', async (req, res) => {
403
488
  projectId,
404
489
  taskmasterId: String(task.id),
405
490
  title: `TaskMaster #${task.id}: ${task.title}`,
406
- description: taskMasterExecutionDescription(task)
491
+ description: taskMasterExecutionDescription(task),
492
+ metadata: {
493
+ provider: adapterId,
494
+ model,
495
+ permissionMode,
496
+ fallbackProvider,
497
+ workerSlot,
498
+ },
407
499
  });
408
500
 
409
501
  const dispatchedTask = await orchestrationTaskService.dispatch(orchestrationTask.id, {
@@ -411,7 +503,9 @@ router.post('/execute/:projectName/:taskId', async (req, res) => {
411
503
  isolation,
412
504
  projectPath,
413
505
  model,
414
- permissionMode
506
+ permissionMode,
507
+ fallbackProvider,
508
+ workerSlot,
415
509
  });
416
510
 
417
511
  res.json({
@@ -419,6 +513,13 @@ router.post('/execute/:projectName/:taskId', async (req, res) => {
419
513
  projectName,
420
514
  projectPath,
421
515
  taskmasterTask: task,
516
+ execution: {
517
+ provider: adapterId,
518
+ model,
519
+ permissionMode,
520
+ fallbackProvider,
521
+ workerSlot,
522
+ },
422
523
  task: dispatchedTask
423
524
  });
424
525
  } catch (error) {