@pixelbyte-software/pixcode 1.42.5 → 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.
Files changed (45) hide show
  1. package/dist/assets/index-B-_FofJ_.css +32 -0
  2. package/dist/assets/{index-nefOyhzb.js → index-CDKI7Ucy.js} +140 -140
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/index.js +3 -0
  5. package/dist-server/server/index.js.map +1 -1
  6. package/dist-server/server/modules/orchestration/index.js +1 -0
  7. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  8. package/dist-server/server/modules/orchestration/workflows/approval-queue.js +72 -0
  9. package/dist-server/server/modules/orchestration/workflows/approval-queue.js.map +1 -0
  10. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +25 -0
  11. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  12. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +87 -0
  13. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
  14. package/dist-server/server/routes/public-api.js +7 -1
  15. package/dist-server/server/routes/public-api.js.map +1 -1
  16. package/dist-server/server/routes/remote.js +18 -0
  17. package/dist-server/server/routes/remote.js.map +1 -1
  18. package/dist-server/server/routes/webhooks.js +53 -0
  19. package/dist-server/server/routes/webhooks.js.map +1 -0
  20. package/dist-server/server/services/control-room.js +89 -0
  21. package/dist-server/server/services/control-room.js.map +1 -0
  22. package/dist-server/server/services/public-api-manifest.js +96 -0
  23. package/dist-server/server/services/public-api-manifest.js.map +1 -1
  24. package/dist-server/server/services/telegram/control-center.js +110 -0
  25. package/dist-server/server/services/telegram/control-center.js.map +1 -1
  26. package/dist-server/server/services/telegram/translations.js +24 -2
  27. package/dist-server/server/services/telegram/translations.js.map +1 -1
  28. package/dist-server/server/services/webhooks.js +198 -0
  29. package/dist-server/server/services/webhooks.js.map +1 -0
  30. package/package.json +1 -1
  31. package/scripts/smoke/v143-remote-control.mjs +76 -0
  32. package/server/index.js +4 -0
  33. package/server/modules/orchestration/index.ts +4 -0
  34. package/server/modules/orchestration/workflows/approval-queue.ts +106 -0
  35. package/server/modules/orchestration/workflows/workflow-runner.ts +25 -0
  36. package/server/modules/orchestration/workflows/workflow.routes.ts +95 -0
  37. package/server/routes/public-api.js +14 -1
  38. package/server/routes/remote.js +22 -0
  39. package/server/routes/webhooks.js +63 -0
  40. package/server/services/control-room.js +102 -0
  41. package/server/services/public-api-manifest.js +98 -0
  42. package/server/services/telegram/control-center.js +113 -0
  43. package/server/services/telegram/translations.js +24 -2
  44. package/server/services/webhooks.js +216 -0
  45. package/dist/assets/index-CHa1760s.css +0 -32
@@ -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.',
@@ -0,0 +1,216 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ import { appConfigDb } from '../database/db.js';
4
+
5
+ const CONFIG_KEY = 'webhooks';
6
+ const MAX_ATTEMPTS = 2;
7
+
8
+ export const PIXCODE_WEBHOOK_EVENT_TYPES = [
9
+ 'run.started',
10
+ 'run.completed',
11
+ 'run.failed',
12
+ 'run.canceled',
13
+ 'file.changed',
14
+ 'approval.needed',
15
+ 'approval.resolved',
16
+ 'live_view.started',
17
+ 'live_view.failed',
18
+ ];
19
+
20
+ function nowIso() {
21
+ return new Date().toISOString();
22
+ }
23
+
24
+ function readStore() {
25
+ const raw = appConfigDb.get(CONFIG_KEY);
26
+ if (!raw) return { version: 1, webhooks: [] };
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ return {
30
+ version: 1,
31
+ webhooks: Array.isArray(parsed?.webhooks) ? parsed.webhooks.map(normalizeWebhook).filter(Boolean) : [],
32
+ };
33
+ } catch {
34
+ return { version: 1, webhooks: [] };
35
+ }
36
+ }
37
+
38
+ function writeStore(store) {
39
+ appConfigDb.set(CONFIG_KEY, JSON.stringify({
40
+ version: 1,
41
+ webhooks: store.webhooks.map(normalizeWebhook).filter(Boolean),
42
+ }));
43
+ }
44
+
45
+ function normalizeUrl(value) {
46
+ const raw = typeof value === 'string' ? value.trim() : '';
47
+ if (!raw) throw new Error('Webhook URL is required.');
48
+ const parsed = new URL(raw);
49
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
50
+ throw new Error('Webhook URL must use http or https.');
51
+ }
52
+ parsed.hash = '';
53
+ return parsed.toString();
54
+ }
55
+
56
+ function normalizeEvents(events) {
57
+ if (!Array.isArray(events) || events.length === 0) return ['run.completed', 'run.failed', 'approval.needed'];
58
+ return Array.from(new Set(events
59
+ .filter((event) => PIXCODE_WEBHOOK_EVENT_TYPES.includes(event))
60
+ )).sort();
61
+ }
62
+
63
+ function normalizeWebhook(input) {
64
+ if (!input || typeof input !== 'object') return null;
65
+ const id = typeof input.id === 'string' && input.id ? input.id : crypto.randomUUID();
66
+ const name = typeof input.name === 'string' && input.name.trim() ? input.name.trim() : 'Pixcode webhook';
67
+ const url = normalizeUrl(input.url);
68
+ const secret = typeof input.secret === 'string' && input.secret.trim()
69
+ ? input.secret.trim()
70
+ : crypto.randomBytes(24).toString('hex');
71
+ return {
72
+ id,
73
+ name,
74
+ url,
75
+ secret,
76
+ enabled: input.enabled !== false,
77
+ events: normalizeEvents(input.events),
78
+ createdAt: typeof input.createdAt === 'string' ? input.createdAt : nowIso(),
79
+ updatedAt: typeof input.updatedAt === 'string' ? input.updatedAt : nowIso(),
80
+ lastDelivery: input.lastDelivery && typeof input.lastDelivery === 'object' ? input.lastDelivery : null,
81
+ };
82
+ }
83
+
84
+ function publicWebhook(webhook) {
85
+ return {
86
+ id: webhook.id,
87
+ name: webhook.name,
88
+ url: webhook.url,
89
+ enabled: webhook.enabled,
90
+ events: webhook.events,
91
+ createdAt: webhook.createdAt,
92
+ updatedAt: webhook.updatedAt,
93
+ secretPresent: Boolean(webhook.secret),
94
+ lastDelivery: webhook.lastDelivery,
95
+ };
96
+ }
97
+
98
+ export function listWebhooks() {
99
+ return readStore().webhooks.map(publicWebhook);
100
+ }
101
+
102
+ export function upsertWebhook(input = {}) {
103
+ const store = readStore();
104
+ const existing = typeof input.id === 'string'
105
+ ? store.webhooks.find((webhook) => webhook.id === input.id)
106
+ : null;
107
+ const webhook = normalizeWebhook({
108
+ ...existing,
109
+ ...input,
110
+ id: existing?.id ?? input.id,
111
+ secret: input.secret === undefined ? existing?.secret : input.secret,
112
+ createdAt: existing?.createdAt,
113
+ updatedAt: nowIso(),
114
+ });
115
+ if (!webhook) throw new Error('Invalid webhook payload.');
116
+ const next = store.webhooks.filter((candidate) => candidate.id !== webhook.id);
117
+ next.push(webhook);
118
+ writeStore({ ...store, webhooks: next });
119
+ return publicWebhook(webhook);
120
+ }
121
+
122
+ export function deleteWebhook(webhookId) {
123
+ const store = readStore();
124
+ const next = store.webhooks.filter((webhook) => webhook.id !== webhookId);
125
+ if (next.length === store.webhooks.length) return false;
126
+ writeStore({ ...store, webhooks: next });
127
+ return true;
128
+ }
129
+
130
+ function signPayload(secret, body) {
131
+ return crypto.createHmac('sha256', secret).update(body).digest('hex');
132
+ }
133
+
134
+ function deliveryPayload(event) {
135
+ return {
136
+ id: crypto.randomUUID(),
137
+ protocol: 'pixcode.webhook.v1',
138
+ emittedAt: nowIso(),
139
+ event,
140
+ };
141
+ }
142
+
143
+ async function deliverToWebhook(webhook, event) {
144
+ const payload = deliveryPayload(event);
145
+ const body = JSON.stringify(payload);
146
+ const signature = signPayload(webhook.secret, body);
147
+ let lastError = null;
148
+
149
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
150
+ try {
151
+ const response = await fetch(webhook.url, {
152
+ method: 'POST',
153
+ headers: {
154
+ 'Content-Type': 'application/json',
155
+ 'X-Pixcode-Event': event.type,
156
+ 'X-Pixcode-Delivery': payload.id,
157
+ 'X-Pixcode-Signature-256': `sha256=${signature}`,
158
+ },
159
+ body,
160
+ });
161
+ const result = {
162
+ ok: response.ok,
163
+ status: response.status,
164
+ attempt,
165
+ eventType: event.type,
166
+ deliveredAt: nowIso(),
167
+ };
168
+ return result;
169
+ } catch (error) {
170
+ lastError = {
171
+ ok: false,
172
+ attempt,
173
+ eventType: event.type,
174
+ deliveredAt: nowIso(),
175
+ error: error?.message || String(error),
176
+ };
177
+ }
178
+ }
179
+
180
+ return lastError;
181
+ }
182
+
183
+ function recordDelivery(webhookId, delivery) {
184
+ const store = readStore();
185
+ const next = store.webhooks.map((webhook) => (
186
+ webhook.id === webhookId ? { ...webhook, lastDelivery: delivery, updatedAt: nowIso() } : webhook
187
+ ));
188
+ writeStore({ ...store, webhooks: next });
189
+ }
190
+
191
+ export async function deliverWebhookEvent(event) {
192
+ const normalized = {
193
+ type: event?.type,
194
+ payload: event?.payload && typeof event.payload === 'object' ? event.payload : {},
195
+ };
196
+ if (!PIXCODE_WEBHOOK_EVENT_TYPES.includes(normalized.type)) {
197
+ return { delivered: 0, skipped: true, reason: 'unsupported_event' };
198
+ }
199
+
200
+ const webhooks = readStore().webhooks.filter((webhook) =>
201
+ webhook.enabled && webhook.events.includes(normalized.type)
202
+ );
203
+ const deliveries = [];
204
+ for (const webhook of webhooks) {
205
+ const delivery = await deliverToWebhook(webhook, normalized);
206
+ recordDelivery(webhook.id, delivery);
207
+ deliveries.push({ webhookId: webhook.id, ...delivery });
208
+ }
209
+ return { delivered: deliveries.length, deliveries };
210
+ }
211
+
212
+ export function dispatchWebhookEvent(event) {
213
+ deliverWebhookEvent(event).catch((error) => {
214
+ console.warn('[webhooks] delivery failed:', error?.message || error);
215
+ });
216
+ }