@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.
- package/dist/assets/{index-C-gVa0Gf.js → index-Br191izN.js} +139 -139
- package/dist/assets/index-BzL2G4Sw.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/database/db.js +18 -3
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +6 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +107 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/routes/auth.js +15 -0
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/diagnostics.js +26 -3
- package/dist-server/server/routes/diagnostics.js.map +1 -1
- package/dist-server/server/routes/public-api.js +16 -0
- package/dist-server/server/routes/public-api.js.map +1 -0
- package/dist-server/server/routes/remote.js +26 -0
- package/dist-server/server/routes/remote.js.map +1 -0
- package/dist-server/server/routes/settings.js +23 -2
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +102 -2
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/services/diagnostics.js +52 -1
- package/dist-server/server/services/diagnostics.js.map +1 -1
- package/dist-server/server/services/public-api-manifest.js +83 -0
- package/dist-server/server/services/public-api-manifest.js.map +1 -0
- package/dist-server/server/services/remote-connection.js +120 -0
- package/dist-server/server/services/remote-connection.js.map +1 -0
- package/dist-server/server/services/telegram/control-center.js +62 -2
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +16 -4
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +2 -1
- package/scripts/smoke/v138-completion.mjs +132 -0
- package/server/database/db.js +21 -3
- package/server/index.js +8 -0
- package/server/modules/providers/provider.routes.ts +134 -0
- package/server/routes/auth.js +20 -1
- package/server/routes/diagnostics.js +29 -3
- package/server/routes/public-api.js +21 -0
- package/server/routes/remote.js +33 -0
- package/server/routes/settings.js +25 -2
- package/server/routes/taskmaster.js +103 -2
- package/server/services/diagnostics.js +61 -1
- package/server/services/public-api-manifest.js +87 -0
- package/server/services/remote-connection.js +127 -0
- package/server/services/telegram/control-center.js +66 -2
- package/server/services/telegram/translations.js +16 -4
- 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;
|
package/server/routes/auth.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
+
}
|