@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.
- package/dist/assets/{index-D8uNxHf1.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 +11 -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 +35 -0
- package/dist-server/server/routes/diagnostics.js.map +1 -0
- 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 +142 -0
- package/dist-server/server/services/diagnostics.js.map +1 -0
- 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 +6 -1
- package/scripts/github/create-v1.38-issues.mjs +351 -0
- package/scripts/smoke/discord-release-workflow.mjs +24 -0
- package/scripts/smoke/v138-completion.mjs +132 -0
- package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -0
- package/scripts/smoke/v138-diagnostics.mjs +63 -0
- package/scripts/smoke/v138-issue-planner.mjs +33 -0
- package/server/database/db.js +21 -3
- package/server/index.js +14 -0
- package/server/modules/providers/provider.routes.ts +134 -0
- package/server/routes/auth.js +20 -1
- package/server/routes/diagnostics.js +41 -0
- 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 +165 -0
- 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
package/server/database/db.js
CHANGED
|
@@ -379,17 +379,28 @@ const userDb = {
|
|
|
379
379
|
const apiKeysDb = {
|
|
380
380
|
generateApiKey: () => 'px_' + crypto.randomBytes(32).toString('hex'),
|
|
381
381
|
|
|
382
|
-
|
|
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;
|
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;
|
|
@@ -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) {
|