@pixelbyte-software/pixcode 1.42.4 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-B-_FofJ_.css +32 -0
- package/dist/assets/{index-cTGs3Dvx.js → index-CDKI7Ucy.js} +170 -170
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +3 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/index.js +2 -0
- package/dist-server/server/modules/orchestration/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/approval-queue.js +72 -0
- package/dist-server/server/modules/orchestration/workflows/approval-queue.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +25 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-templates.js +242 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-templates.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +21 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +121 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
- package/dist-server/server/routes/public-api.js +7 -1
- package/dist-server/server/routes/public-api.js.map +1 -1
- package/dist-server/server/routes/remote.js +18 -0
- package/dist-server/server/routes/remote.js.map +1 -1
- package/dist-server/server/routes/webhooks.js +53 -0
- package/dist-server/server/routes/webhooks.js.map +1 -0
- package/dist-server/server/services/control-room.js +89 -0
- package/dist-server/server/services/control-room.js.map +1 -0
- package/dist-server/server/services/public-api-manifest.js +96 -0
- package/dist-server/server/services/public-api-manifest.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +110 -0
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +24 -2
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/dist-server/server/services/webhooks.js +198 -0
- package/dist-server/server/services/webhooks.js.map +1 -0
- package/package.json +1 -1
- package/scripts/smoke/v143-remote-control.mjs +76 -0
- package/scripts/smoke/workflow-templates.mjs +43 -0
- package/server/index.js +4 -0
- package/server/modules/orchestration/index.ts +14 -0
- package/server/modules/orchestration/workflows/approval-queue.ts +106 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +25 -0
- package/server/modules/orchestration/workflows/workflow-templates.ts +272 -0
- package/server/modules/orchestration/workflows/workflow-trace.ts +22 -0
- package/server/modules/orchestration/workflows/workflow.routes.ts +139 -0
- package/server/routes/public-api.js +14 -1
- package/server/routes/remote.js +22 -0
- package/server/routes/webhooks.js +63 -0
- package/server/services/control-room.js +102 -0
- package/server/services/public-api-manifest.js +98 -0
- package/server/services/telegram/control-center.js +113 -0
- package/server/services/telegram/translations.js +24 -2
- package/server/services/webhooks.js +216 -0
- package/dist/assets/index-CHa1760s.css +0 -32
|
@@ -6,9 +6,19 @@ import {
|
|
|
6
6
|
type WorkflowReplayScope,
|
|
7
7
|
buildWorkflowReplayPlan,
|
|
8
8
|
} from '@/modules/orchestration/workflows/workflow-replay.js';
|
|
9
|
+
import {
|
|
10
|
+
applyWorkflowTemplateToMetadata,
|
|
11
|
+
builtInWorkflowTemplates,
|
|
12
|
+
getWorkflowTemplate,
|
|
13
|
+
} from '@/modules/orchestration/workflows/workflow-templates.js';
|
|
9
14
|
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
10
15
|
import { buildWorkflowTrace } from '@/modules/orchestration/workflows/workflow-trace.js';
|
|
11
16
|
import { findPixcodeAppRoot } from '@/modules/orchestration/workflows/workspace-target.js';
|
|
17
|
+
import {
|
|
18
|
+
listPendingApprovals,
|
|
19
|
+
resolvePermissionApproval,
|
|
20
|
+
type ApprovalDecisionSource,
|
|
21
|
+
} from '@/modules/orchestration/workflows/approval-queue.js';
|
|
12
22
|
import {
|
|
13
23
|
DEFAULT_PERMISSION_POLICY,
|
|
14
24
|
PERMISSION_CAPABILITIES,
|
|
@@ -17,6 +27,7 @@ import {
|
|
|
17
27
|
evaluatePermissionRequest,
|
|
18
28
|
normalizePermissionPolicy,
|
|
19
29
|
} from '@/modules/orchestration/security/permission-policy.js';
|
|
30
|
+
import { dispatchWebhookEvent } from '@/services/webhooks.js';
|
|
20
31
|
|
|
21
32
|
const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
|
|
22
33
|
|
|
@@ -130,6 +141,12 @@ export function createWorkflowRouter(): Router {
|
|
|
130
141
|
res.json({ workflows: workflowStore.listWorkflows() });
|
|
131
142
|
});
|
|
132
143
|
|
|
144
|
+
router.get('/workflows/templates', (_req, res) => {
|
|
145
|
+
res.json({
|
|
146
|
+
templates: builtInWorkflowTemplates,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
133
150
|
router.get('/workflows/context', (_req, res) => {
|
|
134
151
|
res.json({
|
|
135
152
|
appRoot: findPixcodeAppRoot(),
|
|
@@ -243,6 +260,55 @@ export function createWorkflowRouter(): Router {
|
|
|
243
260
|
});
|
|
244
261
|
});
|
|
245
262
|
|
|
263
|
+
router.get('/workflows/approvals', (req, res) => {
|
|
264
|
+
res.json({
|
|
265
|
+
pendingApprovals: listPendingApprovals({
|
|
266
|
+
projectId: readOptionalString(req.query.projectId),
|
|
267
|
+
includeResolved: readBooleanFlag(req.query.includeResolved),
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
router.post('/workflows/approvals/:approvalId', (req, res) => {
|
|
273
|
+
const allow = req.body?.allow === true;
|
|
274
|
+
const deny = req.body?.allow === false;
|
|
275
|
+
if (!allow && !deny) {
|
|
276
|
+
res.status(400).json({
|
|
277
|
+
error: {
|
|
278
|
+
code: 'PERMISSION_DECISION_REQUIRED',
|
|
279
|
+
message: 'Approval queue decisions require allow=true or allow=false.',
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const source = ['ui', 'telegram', 'api'].includes(req.body?.source)
|
|
286
|
+
? req.body.source as ApprovalDecisionSource
|
|
287
|
+
: 'api';
|
|
288
|
+
const result = resolvePermissionApproval({
|
|
289
|
+
approvalId: req.params.approvalId,
|
|
290
|
+
allow,
|
|
291
|
+
source,
|
|
292
|
+
resolvedBy: readRequestUserId(req),
|
|
293
|
+
message: readOptionalString(req.body?.message),
|
|
294
|
+
});
|
|
295
|
+
if (!result) {
|
|
296
|
+
res.status(404).json({ error: { code: 'APPROVAL_NOT_FOUND', message: req.params.approvalId } });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
dispatchWebhookEvent({
|
|
301
|
+
type: 'approval.resolved',
|
|
302
|
+
payload: {
|
|
303
|
+
approvalId: req.params.approvalId,
|
|
304
|
+
allow,
|
|
305
|
+
source,
|
|
306
|
+
runId: result.runId,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
res.json(result);
|
|
310
|
+
});
|
|
311
|
+
|
|
246
312
|
router.post('/workflows/runs/:runId/permission-approvals/:requestId', (req, res) => {
|
|
247
313
|
const run = workflowStore.getRun(req.params.runId);
|
|
248
314
|
if (!run) {
|
|
@@ -274,6 +340,15 @@ export function createWorkflowRouter(): Router {
|
|
|
274
340
|
}
|
|
275
341
|
|
|
276
342
|
workflowStore.setRun(run);
|
|
343
|
+
dispatchWebhookEvent({
|
|
344
|
+
type: 'approval.resolved',
|
|
345
|
+
payload: {
|
|
346
|
+
approvalId: req.params.requestId,
|
|
347
|
+
allow,
|
|
348
|
+
source: 'ui',
|
|
349
|
+
runId: run.id,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
277
352
|
res.json({
|
|
278
353
|
runId: run.id,
|
|
279
354
|
pendingApprovals: readRunArray(run, 'pendingPermissionApprovals')
|
|
@@ -348,6 +423,14 @@ export function createWorkflowRouter(): Router {
|
|
|
348
423
|
},
|
|
349
424
|
},
|
|
350
425
|
);
|
|
426
|
+
dispatchWebhookEvent({
|
|
427
|
+
type: 'run.started',
|
|
428
|
+
payload: {
|
|
429
|
+
runId: replayRun.id,
|
|
430
|
+
workflowId: replayRun.workflowId,
|
|
431
|
+
replayOf: run.id,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
351
434
|
res.status(202).json({
|
|
352
435
|
run: replayRun,
|
|
353
436
|
replayPlan,
|
|
@@ -413,6 +496,13 @@ export function createWorkflowRouter(): Router {
|
|
|
413
496
|
res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
|
|
414
497
|
return;
|
|
415
498
|
}
|
|
499
|
+
dispatchWebhookEvent({
|
|
500
|
+
type: 'run.canceled',
|
|
501
|
+
payload: {
|
|
502
|
+
runId: run.id,
|
|
503
|
+
workflowId: run.workflowId,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
416
506
|
res.json(run);
|
|
417
507
|
});
|
|
418
508
|
|
|
@@ -432,6 +522,14 @@ export function createWorkflowRouter(): Router {
|
|
|
432
522
|
workflowName: workflow.name,
|
|
433
523
|
},
|
|
434
524
|
);
|
|
525
|
+
dispatchWebhookEvent({
|
|
526
|
+
type: 'run.started',
|
|
527
|
+
payload: {
|
|
528
|
+
runId: run.id,
|
|
529
|
+
workflowId: run.workflowId,
|
|
530
|
+
workflowName: workflow.name,
|
|
531
|
+
},
|
|
532
|
+
});
|
|
435
533
|
res.status(202).json(run);
|
|
436
534
|
} catch (error) {
|
|
437
535
|
res.status(400).json({
|
|
@@ -443,5 +541,46 @@ export function createWorkflowRouter(): Router {
|
|
|
443
541
|
}
|
|
444
542
|
});
|
|
445
543
|
|
|
544
|
+
router.post('/workflows/templates/:templateId/runs', (req, res) => {
|
|
545
|
+
const template = getWorkflowTemplate(req.params.templateId);
|
|
546
|
+
if (!template) {
|
|
547
|
+
res.status(404).json({ error: { code: 'WORKFLOW_TEMPLATE_NOT_FOUND', message: req.params.templateId } });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const workflow = workflowStore.getWorkflow(template.workflowId);
|
|
551
|
+
if (!workflow) {
|
|
552
|
+
res.status(404).json({ error: { code: 'WORKFLOW_NOT_FOUND', message: template.workflowId } });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const run = workflowRunner.start(
|
|
558
|
+
workflow,
|
|
559
|
+
typeof req.body?.input === 'string' ? req.body.input : '',
|
|
560
|
+
{
|
|
561
|
+
...applyWorkflowTemplateToMetadata(template, readMetadata(req.body)),
|
|
562
|
+
userId: readRequestUserId(req),
|
|
563
|
+
workflowName: workflow.name,
|
|
564
|
+
},
|
|
565
|
+
);
|
|
566
|
+
dispatchWebhookEvent({
|
|
567
|
+
type: 'run.started',
|
|
568
|
+
payload: {
|
|
569
|
+
runId: run.id,
|
|
570
|
+
workflowId: run.workflowId,
|
|
571
|
+
templateId: template.id,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
res.status(202).json(run);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
res.status(400).json({
|
|
577
|
+
error: {
|
|
578
|
+
code: 'WORKFLOW_TEMPLATE_START_FAILED',
|
|
579
|
+
message: error instanceof Error ? error.message : String(error),
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
446
585
|
return router;
|
|
447
586
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildCurlCookbook,
|
|
5
|
+
buildOpenApiFragment,
|
|
6
|
+
buildPublicApiManifest,
|
|
7
|
+
buildTypeScriptSdkStarter,
|
|
8
|
+
} from '../services/public-api-manifest.js';
|
|
4
9
|
|
|
5
10
|
const router = express.Router();
|
|
6
11
|
|
|
@@ -18,4 +23,12 @@ router.get('/openapi', (req, res) => {
|
|
|
18
23
|
res.json(buildOpenApiFragment({ baseUrl: requestBaseUrl(req) }));
|
|
19
24
|
});
|
|
20
25
|
|
|
26
|
+
router.get('/sdk/typescript', (req, res) => {
|
|
27
|
+
res.type('text/typescript').send(buildTypeScriptSdkStarter({ baseUrl: requestBaseUrl(req) }));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
router.get('/cookbook', (req, res) => {
|
|
31
|
+
res.json(buildCurlCookbook({ baseUrl: requestBaseUrl(req) }));
|
|
32
|
+
});
|
|
33
|
+
|
|
21
34
|
export default router;
|
package/server/routes/remote.js
CHANGED
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
getPublicRemoteConnectionConfig,
|
|
6
6
|
saveRemoteConnectionConfig,
|
|
7
7
|
} from '../services/remote-connection.js';
|
|
8
|
+
import {
|
|
9
|
+
buildControlRoomSnapshot,
|
|
10
|
+
buildMobileConsoleLayout,
|
|
11
|
+
} from '../services/control-room.js';
|
|
8
12
|
|
|
9
13
|
const router = express.Router();
|
|
10
14
|
|
|
@@ -30,4 +34,22 @@ router.post('/check', async (req, res) => {
|
|
|
30
34
|
}
|
|
31
35
|
});
|
|
32
36
|
|
|
37
|
+
router.get('/control-room', async (_req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
res.json({
|
|
40
|
+
success: true,
|
|
41
|
+
controlRoom: await buildControlRoomSnapshot(),
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
res.status(500).json({ success: false, error: error.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
router.get('/console-layout', (_req, res) => {
|
|
49
|
+
res.json({
|
|
50
|
+
success: true,
|
|
51
|
+
layout: buildMobileConsoleLayout(),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
33
55
|
export default router;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PIXCODE_WEBHOOK_EVENT_TYPES,
|
|
5
|
+
deleteWebhook,
|
|
6
|
+
deliverWebhookEvent,
|
|
7
|
+
listWebhooks,
|
|
8
|
+
upsertWebhook,
|
|
9
|
+
} from '../services/webhooks.js';
|
|
10
|
+
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
|
|
13
|
+
router.get('/', (_req, res) => {
|
|
14
|
+
res.json({
|
|
15
|
+
success: true,
|
|
16
|
+
eventTypes: PIXCODE_WEBHOOK_EVENT_TYPES,
|
|
17
|
+
webhooks: listWebhooks(),
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.post('/', (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const webhook = upsertWebhook(req.body || {});
|
|
24
|
+
res.status(201).json({ success: true, webhook });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
res.status(400).json({ success: false, error: error.message });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
router.patch('/:id', (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const webhook = upsertWebhook({ ...(req.body || {}), id: req.params.id });
|
|
33
|
+
res.json({ success: true, webhook });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
res.status(400).json({ success: false, error: error.message });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.delete('/:id', (req, res) => {
|
|
40
|
+
if (!deleteWebhook(req.params.id)) {
|
|
41
|
+
res.status(404).json({ success: false, error: 'Webhook not found.' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
res.json({ success: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
router.post('/test', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await deliverWebhookEvent({
|
|
50
|
+
type: req.body?.type || 'run.completed',
|
|
51
|
+
payload: {
|
|
52
|
+
test: true,
|
|
53
|
+
message: 'Pixcode webhook test delivery',
|
|
54
|
+
sentBy: req.user?.id ?? null,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
res.json({ success: true, result });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
res.status(500).json({ success: false, error: error.message });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default router;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { listPendingApprovals, workflowStore } from '../modules/orchestration/index.js';
|
|
2
|
+
import { getProjects } from '../projects.js';
|
|
3
|
+
|
|
4
|
+
import { listWebhooks } from './webhooks.js';
|
|
5
|
+
|
|
6
|
+
const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
|
|
7
|
+
|
|
8
|
+
function projectPath(project) {
|
|
9
|
+
return project.fullPath || project.path || '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runBelongsToProject(run, project) {
|
|
13
|
+
const projectId = run.metadata?.projectId;
|
|
14
|
+
const path = run.metadata?.projectPath;
|
|
15
|
+
return projectId === project.name || path === projectPath(project);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function countSessions(project) {
|
|
19
|
+
return [
|
|
20
|
+
project.sessions,
|
|
21
|
+
project.codexSessions,
|
|
22
|
+
project.cursorSessions,
|
|
23
|
+
project.geminiSessions,
|
|
24
|
+
project.qwenSessions,
|
|
25
|
+
project.opencodeSessions,
|
|
26
|
+
].reduce((total, sessions) => total + (Array.isArray(sessions) ? sessions.length : 0), 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function summarizeProject(project, runs, approvals) {
|
|
30
|
+
const projectRuns = runs.filter((run) => runBelongsToProject(run, project));
|
|
31
|
+
const runningRuns = projectRuns.filter((run) => !TERMINAL_RUN_STATES.has(run.status));
|
|
32
|
+
const failedRuns = projectRuns.filter((run) => run.status === 'failed');
|
|
33
|
+
const projectApprovals = approvals.filter((approval) => (
|
|
34
|
+
approval.runId && projectRuns.some((run) => run.id === approval.runId)
|
|
35
|
+
));
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: project.name,
|
|
39
|
+
name: project.displayName || project.name,
|
|
40
|
+
path: projectPath(project),
|
|
41
|
+
sessionCount: countSessions(project),
|
|
42
|
+
activeRunCount: runningRuns.length,
|
|
43
|
+
failedRunCount: failedRuns.length,
|
|
44
|
+
pendingApprovalCount: projectApprovals.length,
|
|
45
|
+
latestRuns: projectRuns.slice(0, 4).map((run) => ({
|
|
46
|
+
id: run.id,
|
|
47
|
+
workflowId: run.workflowId,
|
|
48
|
+
status: run.status,
|
|
49
|
+
startedAt: run.startedAt,
|
|
50
|
+
finishedAt: run.finishedAt,
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function buildControlRoomSnapshot({ maxProjects = 4 } = {}) {
|
|
56
|
+
const projects = await getProjects();
|
|
57
|
+
const runs = workflowStore.listRuns();
|
|
58
|
+
const pendingApprovals = listPendingApprovals();
|
|
59
|
+
const webhooks = listWebhooks();
|
|
60
|
+
const projectCards = projects
|
|
61
|
+
.map((project) => summarizeProject(project, runs, pendingApprovals))
|
|
62
|
+
.sort((a, b) => (
|
|
63
|
+
b.pendingApprovalCount - a.pendingApprovalCount ||
|
|
64
|
+
b.activeRunCount - a.activeRunCount ||
|
|
65
|
+
b.failedRunCount - a.failedRunCount ||
|
|
66
|
+
a.name.localeCompare(b.name)
|
|
67
|
+
))
|
|
68
|
+
.slice(0, maxProjects);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
protocol: 'pixcode.control-room.v1',
|
|
72
|
+
generatedAt: new Date().toISOString(),
|
|
73
|
+
maxProjects,
|
|
74
|
+
mobileFirst: true,
|
|
75
|
+
totals: {
|
|
76
|
+
projects: projects.length,
|
|
77
|
+
activeRuns: runs.filter((run) => !TERMINAL_RUN_STATES.has(run.status)).length,
|
|
78
|
+
failedRuns: runs.filter((run) => run.status === 'failed').length,
|
|
79
|
+
pendingApprovals: pendingApprovals.length,
|
|
80
|
+
webhooks: webhooks.length,
|
|
81
|
+
enabledWebhooks: webhooks.filter((webhook) => webhook.enabled).length,
|
|
82
|
+
},
|
|
83
|
+
projects: projectCards,
|
|
84
|
+
approvals: pendingApprovals.slice(0, 20),
|
|
85
|
+
webhooks,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildMobileConsoleLayout() {
|
|
90
|
+
return {
|
|
91
|
+
protocol: 'pixcode.remote-console-layout.v1',
|
|
92
|
+
mobileFirst: true,
|
|
93
|
+
sections: [
|
|
94
|
+
{ id: 'projects', title: 'Projects', priority: 1 },
|
|
95
|
+
{ id: 'approvals', title: 'Approval queue', priority: 2 },
|
|
96
|
+
{ id: 'runs', title: 'Runs', priority: 3 },
|
|
97
|
+
{ id: 'webhooks', title: 'Webhooks', priority: 4 },
|
|
98
|
+
{ id: 'api', title: 'API SDK', priority: 5 },
|
|
99
|
+
],
|
|
100
|
+
maxVisibleProjects: 4,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -13,6 +13,7 @@ const API_GROUPS = [
|
|
|
13
13
|
{ id: 'diagnostics', title: 'Diagnostics', basePath: '/api/diagnostics', scopes: ['diagnostics:read'] },
|
|
14
14
|
{ id: 'remote', title: 'Remote connection', basePath: '/api/remote', scopes: ['remote:read', 'remote:write'] },
|
|
15
15
|
{ id: 'telegram', title: 'Telegram control', basePath: '/api/telegram', scopes: ['telegram:read', 'telegram:write'] },
|
|
16
|
+
{ id: 'webhooks', title: 'Outbound webhooks', basePath: '/api/webhooks', scopes: ['webhooks:read', 'webhooks:write'] },
|
|
16
17
|
{ id: 'plugins', title: 'Plugins and MCP tools', basePath: '/api/plugins', scopes: ['plugins:read', 'plugins:write'] },
|
|
17
18
|
];
|
|
18
19
|
|
|
@@ -48,6 +49,103 @@ export function buildPublicApiManifest({ baseUrl = '' } = {}) {
|
|
|
48
49
|
title: 'Fetch diagnostics bundle',
|
|
49
50
|
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/diagnostics/bundle`,
|
|
50
51
|
},
|
|
52
|
+
{
|
|
53
|
+
title: 'Read the mobile remote control room',
|
|
54
|
+
curl: `curl -H "X-API-Key: px_your_key" ${origin || 'http://127.0.0.1:3001'}/api/remote/control-room`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: 'Register an outbound webhook',
|
|
58
|
+
curl: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: px_your_key" -d '{"name":"CI listener","url":"https://example.com/pixcode","events":["run.completed","approval.needed"]}' ${origin || 'http://127.0.0.1:3001'}/api/webhooks`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildTypeScriptSdkStarter({ baseUrl = '' } = {}) {
|
|
65
|
+
const origin = String(baseUrl || 'http://127.0.0.1:3001').replace(/\/+$/, '');
|
|
66
|
+
return `export type PixcodeRun = {
|
|
67
|
+
id: string;
|
|
68
|
+
workflowId: string;
|
|
69
|
+
status: 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export class PixcodeClient {
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly apiKey: string,
|
|
75
|
+
private readonly baseUrl = '${origin}',
|
|
76
|
+
) {}
|
|
77
|
+
|
|
78
|
+
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
79
|
+
const response = await fetch(new URL(path, this.baseUrl), {
|
|
80
|
+
...init,
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
'X-API-Key': this.apiKey,
|
|
84
|
+
...(init.headers || {}),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) throw new Error(\`Pixcode API \${response.status}: \${await response.text()}\`);
|
|
88
|
+
return response.json() as Promise<T>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
projects() {
|
|
92
|
+
return this.request<{ projects: unknown[] }>('/api/projects');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
controlRoom() {
|
|
96
|
+
return this.request<{ success: true; controlRoom: unknown }>('/api/remote/control-room');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
approvals() {
|
|
100
|
+
return this.request<{ pendingApprovals: unknown[] }>('/api/orchestration/workflows/approvals');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
decideApproval(approvalId: string, allow: boolean) {
|
|
104
|
+
return this.request(\`/api/orchestration/workflows/approvals/\${encodeURIComponent(approvalId)}\`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body: JSON.stringify({ allow, source: 'api' }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
startWorkflow(workflowId: string, input: string, metadata: Record<string, unknown> = {}) {
|
|
111
|
+
return this.request<PixcodeRun>(\`/api/orchestration/workflows/\${encodeURIComponent(workflowId)}/runs\`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: JSON.stringify({ input, metadata }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildCurlCookbook({ baseUrl = '' } = {}) {
|
|
121
|
+
const origin = String(baseUrl || 'http://127.0.0.1:3001').replace(/\/+$/, '');
|
|
122
|
+
return {
|
|
123
|
+
title: 'Pixcode Public API Cookbook',
|
|
124
|
+
variables: {
|
|
125
|
+
PIXCODE_URL: origin,
|
|
126
|
+
PIXCODE_API_KEY: 'px_your_key',
|
|
127
|
+
},
|
|
128
|
+
examples: [
|
|
129
|
+
{
|
|
130
|
+
title: 'List projects',
|
|
131
|
+
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/projects"`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
title: 'Read the mobile control room',
|
|
135
|
+
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/remote/control-room"`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
title: 'List pending approvals',
|
|
139
|
+
command: `curl -H "X-API-Key: $PIXCODE_API_KEY" "$PIXCODE_URL/api/orchestration/workflows/approvals"`,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
title: 'Approve a pending action',
|
|
143
|
+
command: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: $PIXCODE_API_KEY" -d '{"allow":true,"source":"api"}' "$PIXCODE_URL/api/orchestration/workflows/approvals/approval_id"`,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
title: 'Create a webhook',
|
|
147
|
+
command: `curl -X POST -H "Content-Type: application/json" -H "X-API-Key: $PIXCODE_API_KEY" -d '{"name":"CI listener","url":"https://example.com/pixcode","events":["run.completed","run.failed","approval.needed"]}' "$PIXCODE_URL/api/webhooks"`,
|
|
148
|
+
},
|
|
51
149
|
],
|
|
52
150
|
};
|
|
53
151
|
}
|
|
@@ -37,6 +37,10 @@ const CONTROL_COMMANDS = new Set([
|
|
|
37
37
|
'/workflows',
|
|
38
38
|
'/orchestration',
|
|
39
39
|
'/runs',
|
|
40
|
+
'/approvals',
|
|
41
|
+
'/controlroom',
|
|
42
|
+
'/control-room',
|
|
43
|
+
'/webhooks',
|
|
40
44
|
'/sessions',
|
|
41
45
|
'/newchat',
|
|
42
46
|
'/tasks',
|
|
@@ -203,6 +207,8 @@ function mainMenuKeyboard(lang) {
|
|
|
203
207
|
[button(t(lang, 'control.button.projects'), 'projects'), button(t(lang, 'control.button.provider'), 'providers')],
|
|
204
208
|
[button(t(lang, 'control.button.models'), 'models'), button(t(lang, 'control.button.workflows'), 'workflows')],
|
|
205
209
|
[button(t(lang, 'control.button.tasks'), 'tasks'), button(t(lang, 'control.button.runs'), 'runs')],
|
|
210
|
+
[button(t(lang, 'control.button.approvals'), 'approvals'), button(t(lang, 'control.button.controlRoom'), 'control_room')],
|
|
211
|
+
[button(t(lang, 'control.button.webhooks'), 'webhooks')],
|
|
206
212
|
[button(t(lang, 'control.button.sessions'), 'sessions'), button(t(lang, 'control.button.newChat'), 'new_chat')],
|
|
207
213
|
[button(t(lang, 'control.button.install'), 'install_menu'), button(t(lang, 'control.button.auth'), 'auth_menu')],
|
|
208
214
|
[button(t(lang, 'control.button.settings'), 'settings')],
|
|
@@ -338,6 +344,82 @@ async function showRuns({ bot, chatId, link, editMessageId }) {
|
|
|
338
344
|
});
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
async function showApprovalQueue({ bot, chatId, link, editMessageId }) {
|
|
348
|
+
const lang = languageFor(link);
|
|
349
|
+
const data = await localApi(link.user_id, '/api/orchestration/workflows/approvals');
|
|
350
|
+
const approvals = Array.isArray(data?.pendingApprovals) ? data.pendingApprovals : [];
|
|
351
|
+
if (approvals.length === 0) {
|
|
352
|
+
await send(bot, chatId, t(lang, 'control.noApprovals'), { editMessageId });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const keyboard = [];
|
|
357
|
+
const lines = approvals.slice(0, 8).map((approval, index) => {
|
|
358
|
+
const label = compact(approval.summary || approval.reason || approval.id, 70);
|
|
359
|
+
keyboard.push([
|
|
360
|
+
button(t(lang, 'control.button.approve'), 'approval_decide', { approvalId: approval.id, allow: true }),
|
|
361
|
+
button(t(lang, 'control.button.deny'), 'approval_decide', { approvalId: approval.id, allow: false }),
|
|
362
|
+
]);
|
|
363
|
+
return `${index + 1}. ${label}\nRun: ${approval.runId}`;
|
|
364
|
+
});
|
|
365
|
+
keyboard.push([button(t(lang, 'control.button.refresh'), 'approvals'), button(t(lang, 'control.button.mainMenu'), 'menu')]);
|
|
366
|
+
await send(bot, chatId, `${t(lang, 'control.approvalQueue')}\n\n${lines.join('\n\n')}`, {
|
|
367
|
+
editMessageId,
|
|
368
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function showControlRoom({ bot, chatId, link, editMessageId }) {
|
|
373
|
+
const lang = languageFor(link);
|
|
374
|
+
const data = await localApi(link.user_id, '/api/remote/control-room');
|
|
375
|
+
const snapshot = data?.controlRoom || data;
|
|
376
|
+
const projects = Array.isArray(snapshot?.projects) ? snapshot.projects : [];
|
|
377
|
+
const totals = snapshot?.totals || {};
|
|
378
|
+
const lines = projects.map((project, index) => [
|
|
379
|
+
`${index + 1}. ${compact(project.name || project.id, 44)}`,
|
|
380
|
+
`Runs: ${project.activeRunCount || 0} active / ${project.failedRunCount || 0} failed`,
|
|
381
|
+
`Approvals: ${project.pendingApprovalCount || 0}`,
|
|
382
|
+
].join('\n'));
|
|
383
|
+
await send(bot, chatId, [
|
|
384
|
+
t(lang, 'control.controlRoomTitle'),
|
|
385
|
+
'',
|
|
386
|
+
`Projects: ${totals.projects || 0}`,
|
|
387
|
+
`Active runs: ${totals.activeRuns || 0}`,
|
|
388
|
+
`Pending approvals: ${totals.pendingApprovals || 0}`,
|
|
389
|
+
'',
|
|
390
|
+
lines.join('\n\n') || t(lang, 'control.noProjects'),
|
|
391
|
+
].join('\n'), {
|
|
392
|
+
editMessageId,
|
|
393
|
+
reply_markup: {
|
|
394
|
+
inline_keyboard: [
|
|
395
|
+
[button(t(lang, 'control.button.approvals'), 'approvals'), button(t(lang, 'control.button.runs'), 'runs')],
|
|
396
|
+
[button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function showWebhookMenu({ bot, chatId, link, editMessageId }) {
|
|
403
|
+
const lang = languageFor(link);
|
|
404
|
+
const data = await localApi(link.user_id, '/api/webhooks');
|
|
405
|
+
const webhooks = Array.isArray(data?.webhooks) ? data.webhooks : [];
|
|
406
|
+
const lines = webhooks.slice(0, 10).map((webhook, index) => (
|
|
407
|
+
`${index + 1}. ${webhook.enabled ? 'on' : 'off'} ${compact(webhook.name || webhook.url, 50)}\n${compact(webhook.events?.join(', ') || webhook.url, 90)}`
|
|
408
|
+
));
|
|
409
|
+
await send(bot, chatId, [
|
|
410
|
+
t(lang, 'control.webhookTitle'),
|
|
411
|
+
'',
|
|
412
|
+
lines.join('\n\n') || t(lang, 'control.noWebhooks'),
|
|
413
|
+
].join('\n'), {
|
|
414
|
+
editMessageId,
|
|
415
|
+
reply_markup: {
|
|
416
|
+
inline_keyboard: [
|
|
417
|
+
[button(t(lang, 'control.button.refresh'), 'webhooks'), button(t(lang, 'control.button.mainMenu'), 'menu')],
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
341
423
|
async function showSessions({ bot, chatId, link, editMessageId }) {
|
|
342
424
|
const lang = languageFor(link);
|
|
343
425
|
const state = getState(link.user_id);
|
|
@@ -783,6 +865,18 @@ async function handleCommand({ bot, chatId, link, text }) {
|
|
|
783
865
|
await showRuns({ bot, chatId, link });
|
|
784
866
|
return true;
|
|
785
867
|
}
|
|
868
|
+
if (command === '/approvals') {
|
|
869
|
+
await showApprovalQueue({ bot, chatId, link });
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
if (command === '/controlroom' || command === '/control-room') {
|
|
873
|
+
await showControlRoom({ bot, chatId, link });
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
if (command === '/webhooks') {
|
|
877
|
+
await showWebhookMenu({ bot, chatId, link });
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
786
880
|
if (command === '/sessions') {
|
|
787
881
|
await showSessions({ bot, chatId, link });
|
|
788
882
|
return true;
|
|
@@ -927,6 +1021,9 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
|
|
|
927
1021
|
if (action === 'models_refresh') return showModelMenu({ bot, chatId, link, refresh: true, editMessageId });
|
|
928
1022
|
if (action === 'workflows') return showWorkflowMenu({ bot, chatId, link, editMessageId });
|
|
929
1023
|
if (action === 'runs') return showRuns({ bot, chatId, link, editMessageId });
|
|
1024
|
+
if (action === 'approvals') return showApprovalQueue({ bot, chatId, link, editMessageId });
|
|
1025
|
+
if (action === 'control_room') return showControlRoom({ bot, chatId, link, editMessageId });
|
|
1026
|
+
if (action === 'webhooks') return showWebhookMenu({ bot, chatId, link, editMessageId });
|
|
930
1027
|
if (action === 'sessions') return showSessions({ bot, chatId, link, editMessageId });
|
|
931
1028
|
if (action === 'new_chat') return startNewChat({ bot, chatId, link, editMessageId });
|
|
932
1029
|
if (action === 'tasks') return showTaskMasterTasks({ bot, chatId, link, editMessageId });
|
|
@@ -989,6 +1086,22 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
|
|
|
989
1086
|
await send(bot, chatId, t(languageFor(link), 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
|
|
990
1087
|
return;
|
|
991
1088
|
}
|
|
1089
|
+
if (action === 'approval_decide') {
|
|
1090
|
+
const result = await localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
|
|
1091
|
+
method: 'POST',
|
|
1092
|
+
body: {
|
|
1093
|
+
allow: payload.allow === true,
|
|
1094
|
+
source: 'telegram',
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
const lang = languageFor(link);
|
|
1098
|
+
await send(bot, chatId, t(lang, 'control.approvalDecided', {
|
|
1099
|
+
approvalId: payload.approvalId,
|
|
1100
|
+
status: payload.allow === true ? 'approved' : 'denied',
|
|
1101
|
+
runId: result?.runId || '',
|
|
1102
|
+
}), { editMessageId });
|
|
1103
|
+
return showApprovalQueue({ bot, chatId, link });
|
|
1104
|
+
}
|
|
992
1105
|
if (action === 'task_run') return runTaskMasterTask({ bot, chatId, link, taskId: payload.taskId });
|
|
993
1106
|
if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider });
|
|
994
1107
|
if (action === 'auth_provider') {
|