@pixelbyte-software/pixcode 1.42.5 → 1.44.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-nefOyhzb.js → index-BsxRTx-l.js} +142 -140
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +6 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/index.js +1 -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.routes.js +87 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
- package/dist-server/server/routes/production-agent-loop.js +67 -0
- package/dist-server/server/routes/production-agent-loop.js.map +1 -0
- 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/production-agent-loop.js +233 -0
- package/dist-server/server/services/production-agent-loop.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/v144-production-loop.mjs +47 -0
- package/server/index.js +8 -0
- package/server/modules/orchestration/index.ts +4 -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.routes.ts +95 -0
- package/server/routes/production-agent-loop.js +90 -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/production-agent-loop.js +248 -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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { appConfigDb } from '../database/db.js';
|
|
4
|
+
|
|
5
|
+
const CONFIG_KEY = 'production_agent_loop';
|
|
6
|
+
|
|
7
|
+
export const DESKTOP_RELEASE_ASSET_TYPES = [
|
|
8
|
+
{ id: 'windows-x64', extension: '.exe', required: true },
|
|
9
|
+
{ id: 'linux-x64', extension: '.AppImage', required: true },
|
|
10
|
+
{ id: 'linux-deb', extension: '.deb', required: true },
|
|
11
|
+
{ id: 'macos-x64', extension: 'x64.dmg', required: true },
|
|
12
|
+
{ id: 'macos-arm64', extension: 'arm64.dmg', required: true },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function nowIso() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readStore() {
|
|
20
|
+
const raw = appConfigDb.get(CONFIG_KEY);
|
|
21
|
+
if (!raw) {
|
|
22
|
+
return {
|
|
23
|
+
issueRuns: [],
|
|
24
|
+
reviewQueue: [],
|
|
25
|
+
schedulerJobs: [],
|
|
26
|
+
checkpoints: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
return {
|
|
32
|
+
issueRuns: Array.isArray(parsed.issueRuns) ? parsed.issueRuns : [],
|
|
33
|
+
reviewQueue: Array.isArray(parsed.reviewQueue) ? parsed.reviewQueue : [],
|
|
34
|
+
schedulerJobs: Array.isArray(parsed.schedulerJobs) ? parsed.schedulerJobs : [],
|
|
35
|
+
checkpoints: Array.isArray(parsed.checkpoints) ? parsed.checkpoints : [],
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return {
|
|
39
|
+
issueRuns: [],
|
|
40
|
+
reviewQueue: [],
|
|
41
|
+
schedulerJobs: [],
|
|
42
|
+
checkpoints: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeStore(store) {
|
|
48
|
+
appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compact(text, max = 90) {
|
|
52
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
53
|
+
return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function slugify(value) {
|
|
57
|
+
const slug = compact(value, 64)
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
60
|
+
.replace(/^-+|-+$/g, '');
|
|
61
|
+
return slug || 'agent-task';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseGitHubIssueRef(input = {}) {
|
|
65
|
+
const url = typeof input.issueUrl === 'string' ? input.issueUrl.trim() : '';
|
|
66
|
+
const directNumber = Number.parseInt(String(input.issueNumber || ''), 10);
|
|
67
|
+
const urlMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
|
|
68
|
+
return {
|
|
69
|
+
owner: input.owner || urlMatch?.[1] || null,
|
|
70
|
+
repo: input.repo || urlMatch?.[2] || null,
|
|
71
|
+
issueNumber: Number.isFinite(directNumber) ? directNumber : Number.parseInt(urlMatch?.[3] || '0', 10) || null,
|
|
72
|
+
issueUrl: url || null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createIssueToPrRun(input = {}, userId = null) {
|
|
77
|
+
const issue = parseGitHubIssueRef(input);
|
|
78
|
+
if (!issue.issueNumber && !input.title) {
|
|
79
|
+
throw new Error('Issue-to-PR run requires an issue number, issue URL, or title.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const title = compact(input.title || `Issue ${issue.issueNumber}`);
|
|
83
|
+
const branchName = input.branchName || `pixcode/issue-${issue.issueNumber || 'manual'}-${slugify(title)}`;
|
|
84
|
+
const run = {
|
|
85
|
+
id: crypto.randomUUID(),
|
|
86
|
+
type: 'github_issue_to_pr',
|
|
87
|
+
status: 'queued',
|
|
88
|
+
createdAt: nowIso(),
|
|
89
|
+
updatedAt: nowIso(),
|
|
90
|
+
userId,
|
|
91
|
+
issue,
|
|
92
|
+
projectName: input.projectName || null,
|
|
93
|
+
projectPath: input.projectPath || null,
|
|
94
|
+
provider: input.provider || 'opencode',
|
|
95
|
+
model: input.model || null,
|
|
96
|
+
branchName,
|
|
97
|
+
baseBranch: input.baseBranch || 'main',
|
|
98
|
+
acceptanceCriteria: Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [],
|
|
99
|
+
agentRequest: {
|
|
100
|
+
projectPath: input.projectPath || undefined,
|
|
101
|
+
githubUrl: input.githubUrl || undefined,
|
|
102
|
+
message: [
|
|
103
|
+
`Resolve ${issue.issueUrl || `GitHub issue #${issue.issueNumber || ''}`}`.trim(),
|
|
104
|
+
input.body || title,
|
|
105
|
+
'Create a branch, run verification, and prepare a pull request summary.',
|
|
106
|
+
].filter(Boolean).join('\n\n'),
|
|
107
|
+
provider: input.provider || 'opencode',
|
|
108
|
+
model: input.model || undefined,
|
|
109
|
+
branchName,
|
|
110
|
+
createBranch: true,
|
|
111
|
+
createPR: true,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const store = readStore();
|
|
116
|
+
store.issueRuns.unshift(run);
|
|
117
|
+
writeStore(store);
|
|
118
|
+
return run;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseCiRepairSignals(logText = '') {
|
|
122
|
+
const text = String(logText || '');
|
|
123
|
+
const lines = text.split(/\r?\n/);
|
|
124
|
+
const failedCommands = [];
|
|
125
|
+
const files = new Set();
|
|
126
|
+
const errors = [];
|
|
127
|
+
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const trimmed = line.trim();
|
|
130
|
+
if (!trimmed) continue;
|
|
131
|
+
if (/npm ERR!|error TS\d+|FAIL|failed|exit code/i.test(trimmed)) {
|
|
132
|
+
errors.push(trimmed);
|
|
133
|
+
}
|
|
134
|
+
const command = trimmed.match(/(?:run|command|script)\s+[`'"]?([a-z0-9:_-]+)[`'"]?/i)?.[1];
|
|
135
|
+
if (command) failedCommands.push(command);
|
|
136
|
+
const file = trimmed.match(/((?:src|server|shared|scripts|desktop)\/[^\s:)]+)/)?.[1];
|
|
137
|
+
if (file) files.add(file);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
failedCommands: Array.from(new Set(failedCommands)),
|
|
142
|
+
files: Array.from(files),
|
|
143
|
+
errors: errors.slice(0, 25),
|
|
144
|
+
repairPrompt: [
|
|
145
|
+
'CI-aware repair loop:',
|
|
146
|
+
'1. Reproduce the failing command locally.',
|
|
147
|
+
'2. Fix only the failing behavior.',
|
|
148
|
+
'3. Re-run the failed command plus related smoke checks.',
|
|
149
|
+
'',
|
|
150
|
+
errors.slice(0, 8).join('\n'),
|
|
151
|
+
].join('\n').trim(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createReviewQueueItem(input = {}, userId = null) {
|
|
156
|
+
const item = {
|
|
157
|
+
id: crypto.randomUUID(),
|
|
158
|
+
status: input.status || 'review_requested',
|
|
159
|
+
createdAt: nowIso(),
|
|
160
|
+
updatedAt: nowIso(),
|
|
161
|
+
userId,
|
|
162
|
+
projectName: input.projectName || null,
|
|
163
|
+
projectPath: input.projectPath || null,
|
|
164
|
+
title: compact(input.title || 'Review requested'),
|
|
165
|
+
changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
|
|
166
|
+
notes: input.notes || '',
|
|
167
|
+
};
|
|
168
|
+
const store = readStore();
|
|
169
|
+
store.reviewQueue.unshift(item);
|
|
170
|
+
writeStore(store);
|
|
171
|
+
return item;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function updateReviewQueueItem(itemId, patch = {}) {
|
|
175
|
+
const store = readStore();
|
|
176
|
+
let updated = null;
|
|
177
|
+
store.reviewQueue = store.reviewQueue.map((item) => {
|
|
178
|
+
if (item.id !== itemId) return item;
|
|
179
|
+
updated = {
|
|
180
|
+
...item,
|
|
181
|
+
...patch,
|
|
182
|
+
id: item.id,
|
|
183
|
+
updatedAt: nowIso(),
|
|
184
|
+
};
|
|
185
|
+
return updated;
|
|
186
|
+
});
|
|
187
|
+
writeStore(store);
|
|
188
|
+
return updated;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function scheduleBackgroundAgentJob(input = {}, userId = null) {
|
|
192
|
+
const job = {
|
|
193
|
+
id: crypto.randomUUID(),
|
|
194
|
+
status: 'scheduled',
|
|
195
|
+
createdAt: nowIso(),
|
|
196
|
+
updatedAt: nowIso(),
|
|
197
|
+
userId,
|
|
198
|
+
name: compact(input.name || 'Background agent job'),
|
|
199
|
+
mode: input.mode || 'manual',
|
|
200
|
+
cron: input.cron || null,
|
|
201
|
+
watch: input.watch || null,
|
|
202
|
+
projectName: input.projectName || null,
|
|
203
|
+
provider: input.provider || 'opencode',
|
|
204
|
+
prompt: input.prompt || '',
|
|
205
|
+
nextRunAt: input.nextRunAt || null,
|
|
206
|
+
};
|
|
207
|
+
const store = readStore();
|
|
208
|
+
store.schedulerJobs.unshift(job);
|
|
209
|
+
writeStore(store);
|
|
210
|
+
return job;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function createWorkspaceCheckpoint(input = {}, userId = null) {
|
|
214
|
+
const checkpoint = {
|
|
215
|
+
id: crypto.randomUUID(),
|
|
216
|
+
protocol: 'pixcode.workspace-checkpoint.v1',
|
|
217
|
+
createdAt: nowIso(),
|
|
218
|
+
userId,
|
|
219
|
+
projectName: input.projectName || null,
|
|
220
|
+
projectPath: input.projectPath || null,
|
|
221
|
+
reason: compact(input.reason || 'manual checkpoint'),
|
|
222
|
+
gitHead: input.gitHead || null,
|
|
223
|
+
changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
|
|
224
|
+
metadata: input.metadata && typeof input.metadata === 'object' ? input.metadata : {},
|
|
225
|
+
};
|
|
226
|
+
const store = readStore();
|
|
227
|
+
store.checkpoints.unshift(checkpoint);
|
|
228
|
+
writeStore(store);
|
|
229
|
+
return checkpoint;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getProductionAgentLoopState() {
|
|
233
|
+
return readStore();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function evaluateDesktopReleaseAssetPolicy(assetNames = []) {
|
|
237
|
+
const names = Array.isArray(assetNames) ? assetNames.map(String) : [];
|
|
238
|
+
const required = DESKTOP_RELEASE_ASSET_TYPES.map((assetType) => ({
|
|
239
|
+
...assetType,
|
|
240
|
+
present: names.some((name) => name.endsWith(assetType.extension)),
|
|
241
|
+
}));
|
|
242
|
+
return {
|
|
243
|
+
protocol: 'pixcode.desktop-release-assets.v1',
|
|
244
|
+
required,
|
|
245
|
+
complete: required.every((asset) => asset.present),
|
|
246
|
+
rule: 'Every GitHub release must include Windows exe, Linux AppImage, Linux deb, macOS x64 dmg, and macOS arm64 dmg assets. Assets may be carried forward and renamed when the app updates internally.',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -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') {
|
|
@@ -18,7 +18,7 @@ const EN = {
|
|
|
18
18
|
'bridge.queued': '📨 Message forwarded to your latest session. I will reply when the agent responds.',
|
|
19
19
|
'bridge.disabled': 'Message bridge is disabled. Enable it in Pixcode → Settings → Telegram.',
|
|
20
20
|
'control.menu': 'Pixcode Telegram control center',
|
|
21
|
-
'control.help': 'Commands: /menu, /projects, /provider, /model, /workflows, /runs, /sessions, /newchat, /tasks, /task <id>, /chat <prompt>, /workflow <prompt>, /install, /auth, /settings, /progress final|steps|errors|all, /control on|off.',
|
|
21
|
+
'control.help': 'Commands: /menu, /projects, /provider, /model, /workflows, /runs, /approvals, /control-room, /webhooks, /sessions, /newchat, /tasks, /task <id>, /chat <prompt>, /workflow <prompt>, /install, /auth, /settings, /progress final|steps|errors|all, /control on|off.',
|
|
22
22
|
'control.examples': 'Examples:\n/chat summarize this project\n/chat fix the last error\n/workflow review the current changes\n/settings to change language and progress mode',
|
|
23
23
|
'control.unknownCommand': 'I do not know that command yet. Here are the available Pixcode commands:',
|
|
24
24
|
'control.onboarding': 'How to start:\n/projects to pick a project\n/provider to pick the CLI\n/model to choose the model\n/chat <prompt> to run the current agent\n/workflow <prompt> to run orchestration\n/help to see all commands',
|
|
@@ -32,6 +32,9 @@ const EN = {
|
|
|
32
32
|
'control.button.models': 'Models',
|
|
33
33
|
'control.button.workflows': 'Workflows',
|
|
34
34
|
'control.button.runs': 'Runs',
|
|
35
|
+
'control.button.approvals': 'Approvals',
|
|
36
|
+
'control.button.controlRoom': 'Control room',
|
|
37
|
+
'control.button.webhooks': 'Webhooks',
|
|
35
38
|
'control.button.sessions': 'Sessions',
|
|
36
39
|
'control.button.newChat': 'New chat',
|
|
37
40
|
'control.button.tasks': 'Tasks',
|
|
@@ -51,6 +54,8 @@ const EN = {
|
|
|
51
54
|
'control.button.mainMenu': 'Main menu',
|
|
52
55
|
'control.button.cancelRun': 'Cancel run',
|
|
53
56
|
'control.button.refresh': 'Refresh',
|
|
57
|
+
'control.button.approve': 'Approve',
|
|
58
|
+
'control.button.deny': 'Deny',
|
|
54
59
|
'control.noProjects': 'No Pixcode projects were found yet. Add/open a project in Pixcode first.',
|
|
55
60
|
'control.pickProject': 'Pick the project Telegram should control:',
|
|
56
61
|
'control.pickProvider': 'Pick the CLI provider Telegram should use:',
|
|
@@ -58,6 +63,12 @@ const EN = {
|
|
|
58
63
|
'control.pickWorkflow': 'Pick an orchestration workflow:',
|
|
59
64
|
'control.noRuns': 'No orchestration runs yet.',
|
|
60
65
|
'control.recentRuns': 'Recent orchestration runs:',
|
|
66
|
+
'control.approvalQueue': 'Pending approval queue:',
|
|
67
|
+
'control.noApprovals': 'No pending approvals.',
|
|
68
|
+
'control.approvalDecided': 'Approval {{approvalId}} {{status}}.\nRun: {{runId}}',
|
|
69
|
+
'control.controlRoomTitle': 'Multi-project control room',
|
|
70
|
+
'control.webhookTitle': 'Outbound webhooks',
|
|
71
|
+
'control.noWebhooks': 'No webhooks are configured yet.',
|
|
61
72
|
'control.noSessions': 'No sessions were found for the selected project.',
|
|
62
73
|
'control.recentSessions': 'Recent sessions:',
|
|
63
74
|
'control.newChatReady': 'New chat is ready. Send the prompt you want to run.',
|
|
@@ -115,7 +126,7 @@ const TR = {
|
|
|
115
126
|
'bridge.queued': '📨 Mesaj son oturumuna iletildi. Ajan cevap verince sana yazacağım.',
|
|
116
127
|
'bridge.disabled': 'Mesaj köprüsü kapalı. Pixcode → Ayarlar → Telegram\'dan açabilirsin.',
|
|
117
128
|
'control.menu': 'Pixcode Telegram kontrol merkezi',
|
|
118
|
-
'control.help': 'Komutlar: /menu, /projects, /provider, /model, /workflows, /runs, /sessions, /newchat, /tasks, /task <id>, /chat <prompt>, /workflow <prompt>, /install, /auth, /settings, /progress final|steps|errors|all, /control on|off.',
|
|
129
|
+
'control.help': 'Komutlar: /menu, /projects, /provider, /model, /workflows, /runs, /approvals, /control-room, /webhooks, /sessions, /newchat, /tasks, /task <id>, /chat <prompt>, /workflow <prompt>, /install, /auth, /settings, /progress final|steps|errors|all, /control on|off.',
|
|
119
130
|
'control.examples': 'Örnekler:\n/chat bu projeyi özetle\n/chat son hatayı düzelt\n/workflow mevcut değişiklikleri incele\n/settings ile dil ve ilerleme modunu değiştir',
|
|
120
131
|
'control.unknownCommand': 'Bu komutu henüz tanımıyorum. Kullanabileceğin Pixcode komutları:',
|
|
121
132
|
'control.onboarding': 'Nasıl başlarsın:\n/projects ile proje seç\n/provider ile CLI seç\n/model ile modeli seç\n/chat <prompt> ile ajanı çalıştır\n/workflow <prompt> ile orkestrasyon başlat\n/help ile tüm komutları gör',
|
|
@@ -129,6 +140,9 @@ const TR = {
|
|
|
129
140
|
'control.button.models': 'Modeller',
|
|
130
141
|
'control.button.workflows': 'İş akışları',
|
|
131
142
|
'control.button.runs': 'Çalışmalar',
|
|
143
|
+
'control.button.approvals': 'Onaylar',
|
|
144
|
+
'control.button.controlRoom': 'Kontrol odası',
|
|
145
|
+
'control.button.webhooks': 'Webhooklar',
|
|
132
146
|
'control.button.sessions': 'Oturumlar',
|
|
133
147
|
'control.button.newChat': 'Yeni sohbet',
|
|
134
148
|
'control.button.tasks': 'Görevler',
|
|
@@ -148,6 +162,8 @@ const TR = {
|
|
|
148
162
|
'control.button.mainMenu': 'Ana menü',
|
|
149
163
|
'control.button.cancelRun': 'Çalışmayı iptal et',
|
|
150
164
|
'control.button.refresh': 'Yenile',
|
|
165
|
+
'control.button.approve': 'Onayla',
|
|
166
|
+
'control.button.deny': 'Reddet',
|
|
151
167
|
'control.noProjects': 'Henüz Pixcode projesi bulunamadı. Önce Pixcode içinde proje ekle/aç.',
|
|
152
168
|
'control.pickProject': 'Telegramın kontrol edeceği projeyi seç:',
|
|
153
169
|
'control.pickProvider': 'Telegramın kullanacağı CLI sağlayıcısını seç:',
|
|
@@ -155,6 +171,12 @@ const TR = {
|
|
|
155
171
|
'control.pickWorkflow': 'Bir orkestrasyon iş akışı seç:',
|
|
156
172
|
'control.noRuns': 'Henüz orkestrasyon çalışması yok.',
|
|
157
173
|
'control.recentRuns': 'Son orkestrasyon çalışmaları:',
|
|
174
|
+
'control.approvalQueue': 'Bekleyen onay kuyruğu:',
|
|
175
|
+
'control.noApprovals': 'Bekleyen onay yok.',
|
|
176
|
+
'control.approvalDecided': '{{approvalId}} onayı {{status}}.\nÇalışma: {{runId}}',
|
|
177
|
+
'control.controlRoomTitle': 'Çok projeli kontrol odası',
|
|
178
|
+
'control.webhookTitle': 'Dış webhooklar',
|
|
179
|
+
'control.noWebhooks': 'Henüz webhook yapılandırılmadı.',
|
|
158
180
|
'control.noSessions': 'Seçili proje için oturum bulunamadı.',
|
|
159
181
|
'control.recentSessions': 'Son oturumlar:',
|
|
160
182
|
'control.newChatReady': 'Yeni sohbet hazır. Çalıştırmak istediğin promptu gönder.',
|