@pixelbyte-software/pixcode 1.38.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 (48) hide show
  1. package/dist/assets/{index-C-gVa0Gf.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 +6 -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 +26 -3
  13. package/dist-server/server/routes/diagnostics.js.map +1 -1
  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 +52 -1
  23. package/dist-server/server/services/diagnostics.js.map +1 -1
  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 +2 -1
  33. package/scripts/smoke/v138-completion.mjs +132 -0
  34. package/server/database/db.js +21 -3
  35. package/server/index.js +8 -0
  36. package/server/modules/providers/provider.routes.ts +134 -0
  37. package/server/routes/auth.js +20 -1
  38. package/server/routes/diagnostics.js +29 -3
  39. package/server/routes/public-api.js +21 -0
  40. package/server/routes/remote.js +33 -0
  41. package/server/routes/settings.js +25 -2
  42. package/server/routes/taskmaster.js +103 -2
  43. package/server/services/diagnostics.js +61 -1
  44. package/server/services/public-api-manifest.js +87 -0
  45. package/server/services/remote-connection.js +127 -0
  46. package/server/services/telegram/control-center.js +66 -2
  47. package/server/services/telegram/translations.js +16 -4
  48. package/dist/assets/index-CfHK8y_H.css +0 -32
@@ -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;
@@ -4,12 +4,38 @@ import { collectDiagnostics } from '../services/diagnostics.js';
4
4
 
5
5
  const router = express.Router();
6
6
 
7
- router.get('/', (req, res) => {
8
- res.json(collectDiagnostics({
7
+ function buildDiagnostics(req) {
8
+ return collectDiagnostics({
9
9
  installMode: req.app.locals.installMode,
10
10
  serverVersion: req.app.locals.serverVersion,
11
11
  wss: req.app.locals.wss,
12
- }));
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
+ });
13
39
  });
14
40
 
15
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) {
@@ -48,6 +48,13 @@ function normalizeMemory(memory) {
48
48
  );
49
49
  }
50
50
 
51
+ function redactText(value) {
52
+ return String(value || '').replace(
53
+ /(ghp_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|npm_[A-Za-z0-9_]+|px_[A-Za-z0-9_]+|ck_[A-Za-z0-9_]+|[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,})/g,
54
+ '[redacted]',
55
+ );
56
+ }
57
+
51
58
  function resolveWebSocketClientCount(options) {
52
59
  if (Number.isInteger(options.wsClientCount)) {
53
60
  return options.wsClientCount;
@@ -55,14 +62,45 @@ function resolveWebSocketClientCount(options) {
55
62
  return options.wss?.clients?.size || 0;
56
63
  }
57
64
 
65
+ function normalizeProviderHealth(input = {}, env = process.env, now = new Date()) {
66
+ const defaults = {
67
+ claude: { configured: Boolean(env.ANTHROPIC_API_KEY || env.CLAUDE_API_KEY) },
68
+ codex: { configured: Boolean(env.OPENAI_API_KEY) },
69
+ cursor: { configured: false },
70
+ gemini: { configured: Boolean(env.GEMINI_API_KEY || env.GOOGLE_API_KEY) },
71
+ qwen: { configured: Boolean(env.DASHSCOPE_API_KEY || env.OPENAI_API_KEY) },
72
+ opencode: { configured: false },
73
+ };
74
+
75
+ return Object.fromEntries(
76
+ Object.entries({ ...defaults, ...input }).map(([provider, value]) => {
77
+ const raw = value && typeof value === 'object' ? value : {};
78
+ return [
79
+ provider,
80
+ redactDiagnostics({
81
+ status: raw.status || (raw.configured ? 'configured' : 'unknown'),
82
+ auth: raw.auth || (raw.configured ? 'configured' : 'not_configured'),
83
+ cli: raw.cli || raw.version || null,
84
+ checkedAt: raw.checkedAt || now.toISOString(),
85
+ details: raw.details || null,
86
+ }),
87
+ ];
88
+ }),
89
+ );
90
+ }
91
+
58
92
  export function collectDiagnostics(options = {}) {
59
93
  const now = options.now || new Date();
60
94
  const env = options.env || process.env;
61
95
  const versions = options.versions || process.versions;
62
96
  const memoryUsage = options.memoryUsage || process.memoryUsage;
63
97
  const uptime = options.uptime ?? process.uptime();
98
+ const activeRuns = Array.isArray(options.activeRuns) ? options.activeRuns : [];
99
+ const recentErrors = Array.isArray(options.recentErrors) ? options.recentErrors : [];
100
+ const providerHealth = normalizeProviderHealth(options.providerHealth, env, now);
101
+ const cache = options.cache && typeof options.cache === 'object' ? options.cache : {};
64
102
 
65
- return {
103
+ const diagnostics = {
66
104
  status: 'ok',
67
105
  timestamp: now.toISOString(),
68
106
  version: options.serverVersion || '0.0.0',
@@ -84,7 +122,29 @@ export function collectDiagnostics(options = {}) {
84
122
  telegramConfigured: Boolean(env.TELEGRAM_BOT_TOKEN),
85
123
  webPushConfigured: Boolean(env.VAPID_PUBLIC_KEY && env.VAPID_PRIVATE_KEY),
86
124
  },
125
+ providerHealth,
126
+ activeRuns: activeRuns.map((run) => redactDiagnostics(run)),
127
+ recentErrors: recentErrors.map((error) => redactDiagnostics({
128
+ ...error,
129
+ message: redactText(error?.message),
130
+ stack: error?.stack ? redactText(error.stack) : undefined,
131
+ })),
132
+ cache: redactDiagnostics({
133
+ providerHealthUpdatedAt: cache.providerHealthUpdatedAt || null,
134
+ diagnosticsUpdatedAt: now.toISOString(),
135
+ }),
136
+ manualRefresh: {
137
+ available: true,
138
+ endpoint: '/api/diagnostics/refresh',
139
+ },
140
+ bundle: {
141
+ copyable: true,
142
+ endpoint: '/api/diagnostics/bundle',
143
+ includes: ['runtime', 'websocket', 'notifications', 'providerHealth', 'activeRuns', 'recentErrors'],
144
+ },
87
145
  };
146
+
147
+ return redactDiagnostics(diagnostics);
88
148
  }
89
149
 
90
150
  export function redactDiagnostics(input) {
@@ -0,0 +1,87 @@
1
+ const API_GROUPS = [
2
+ { id: 'auth', title: 'Authentication', basePath: '/api/auth', scopes: ['auth:read', 'auth:write'] },
3
+ { id: 'projects', title: 'Projects', basePath: '/api/projects', scopes: ['projects:read', 'projects:write'] },
4
+ { id: 'sessions', title: 'Sessions and messages', basePath: '/api/sessions', scopes: ['sessions:read', 'sessions:write'] },
5
+ { id: 'providers', title: 'CLI providers', basePath: '/api/providers', scopes: ['providers:read', 'providers:write'] },
6
+ { id: 'orchestration', title: 'Orchestration runs', basePath: '/api/orchestration', scopes: ['orchestration:read', 'orchestration:write'] },
7
+ { id: 'taskmaster', title: 'Taskmaster queue', basePath: '/api/taskmaster', scopes: ['taskmaster:read', 'taskmaster:write'] },
8
+ { id: 'notifications', title: 'Notifications', basePath: '/api/settings/notifications', scopes: ['notifications:read', 'notifications:write'] },
9
+ { id: 'files', title: 'Files', basePath: '/api/projects/:projectName/files', scopes: ['files:read', 'files:write'] },
10
+ { id: 'git', title: 'Source control', basePath: '/api/git', scopes: ['git:read', 'git:write'] },
11
+ { id: 'settings', title: 'Settings and API keys', basePath: '/api/settings', scopes: ['settings:read', 'settings:write'] },
12
+ { id: 'updates', title: 'Update status', basePath: '/api/update', scopes: ['updates:read', 'updates:write'] },
13
+ { id: 'diagnostics', title: 'Diagnostics', basePath: '/api/diagnostics', scopes: ['diagnostics:read'] },
14
+ { id: 'remote', title: 'Remote connection', basePath: '/api/remote', scopes: ['remote:read', 'remote:write'] },
15
+ { id: 'telegram', title: 'Telegram control', basePath: '/api/telegram', scopes: ['telegram:read', 'telegram:write'] },
16
+ { id: 'plugins', title: 'Plugins and MCP tools', basePath: '/api/plugins', scopes: ['plugins:read', 'plugins:write'] },
17
+ ];
18
+
19
+ const API_SCOPES = Array.from(new Set(API_GROUPS.flatMap((group) => group.scopes))).sort();
20
+
21
+ export function buildPublicApiManifest({ baseUrl = '' } = {}) {
22
+ const origin = String(baseUrl || '').replace(/\/+$/, '');
23
+ return {
24
+ name: 'Pixcode Public API',
25
+ version: '1.38',
26
+ baseUrl: origin || null,
27
+ auth: {
28
+ transports: ['Authorization: Bearer <px_api_key>', 'X-API-Key: <px_api_key>', '?apiKey=<px_api_key>'],
29
+ websocket: 'Pass the same px_ API key as the token query parameter.',
30
+ },
31
+ apiKey: {
32
+ prefix: 'px_',
33
+ scopes: API_SCOPES,
34
+ revocable: true,
35
+ manageableAt: '/api/settings/api-keys',
36
+ },
37
+ groups: API_GROUPS,
38
+ examples: [
39
+ {
40
+ title: 'List projects',
41
+ curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/projects`,
42
+ },
43
+ {
44
+ title: 'Start a Taskmaster task with a model',
45
+ curl: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: px_your_key" -d '{"provider":"opencode","model":"minimax/minimax-m2"}' ${origin || 'http://127.0.0.1:3001'}/api/taskmaster/execute/my-project/1`,
46
+ },
47
+ {
48
+ title: 'Fetch diagnostics bundle',
49
+ curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/diagnostics/bundle`,
50
+ },
51
+ ],
52
+ };
53
+ }
54
+
55
+ export function buildOpenApiFragment(options = {}) {
56
+ const manifest = buildPublicApiManifest(options);
57
+ return {
58
+ openapi: '3.1.0',
59
+ info: {
60
+ title: manifest.name,
61
+ version: manifest.version,
62
+ },
63
+ security: [{ PixcodeApiKey: [] }],
64
+ components: {
65
+ securitySchemes: {
66
+ PixcodeApiKey: {
67
+ type: 'apiKey',
68
+ in: 'header',
69
+ name: 'X-API-Key',
70
+ description: 'Pixcode px_ API key. Keys are revocable and can carry scopes.',
71
+ },
72
+ },
73
+ },
74
+ paths: Object.fromEntries(
75
+ manifest.groups.map((group) => [
76
+ group.basePath,
77
+ {
78
+ get: {
79
+ summary: group.title,
80
+ 'x-pixcode-group': group.id,
81
+ 'x-pixcode-scopes': group.scopes,
82
+ },
83
+ },
84
+ ]),
85
+ ),
86
+ };
87
+ }