@pixelbyte-software/pixcode 1.55.1 → 1.55.3

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 (172) hide show
  1. package/dist-server/server/modules/tasks/git-worktree.js +98 -0
  2. package/dist-server/server/modules/tasks/git-worktree.js.map +1 -0
  3. package/dist-server/server/modules/tasks/index.js +8 -0
  4. package/dist-server/server/modules/tasks/index.js.map +1 -0
  5. package/dist-server/server/modules/tasks/prompt-builder.js +85 -0
  6. package/dist-server/server/modules/tasks/prompt-builder.js.map +1 -0
  7. package/dist-server/server/modules/tasks/role-presets.js +94 -0
  8. package/dist-server/server/modules/tasks/role-presets.js.map +1 -0
  9. package/dist-server/server/modules/tasks/runners/claude-runner.js +109 -0
  10. package/dist-server/server/modules/tasks/runners/claude-runner.js.map +1 -0
  11. package/dist-server/server/modules/tasks/runners/codex-runner.js +67 -0
  12. package/dist-server/server/modules/tasks/runners/codex-runner.js.map +1 -0
  13. package/dist-server/server/modules/tasks/runners/spawn-runner.js +111 -0
  14. package/dist-server/server/modules/tasks/runners/spawn-runner.js.map +1 -0
  15. package/dist-server/server/modules/tasks/runners/types.js +5 -0
  16. package/dist-server/server/modules/tasks/runners/types.js.map +1 -0
  17. package/dist-server/server/modules/tasks/scheduler.js +69 -0
  18. package/dist-server/server/modules/tasks/scheduler.js.map +1 -0
  19. package/dist-server/server/modules/tasks/task-queue.js +81 -0
  20. package/dist-server/server/modules/tasks/task-queue.js.map +1 -0
  21. package/dist-server/server/modules/tasks/task-routes.js +195 -0
  22. package/dist-server/server/modules/tasks/task-routes.js.map +1 -0
  23. package/dist-server/server/modules/tasks/task-runner.js +187 -0
  24. package/dist-server/server/modules/tasks/task-runner.js.map +1 -0
  25. package/dist-server/server/modules/tasks/task-store.js +196 -0
  26. package/dist-server/server/modules/tasks/task-store.js.map +1 -0
  27. package/dist-server/server/modules/tasks/task-types.js +5 -0
  28. package/dist-server/server/modules/tasks/task-types.js.map +1 -0
  29. package/dist-server/server/modules/tasks/user-interaction.js +73 -0
  30. package/dist-server/server/modules/tasks/user-interaction.js.map +1 -0
  31. package/package.json +1 -1
  32. package/server/modules/tasks/git-worktree.ts +102 -0
  33. package/server/modules/tasks/index.ts +9 -0
  34. package/server/modules/tasks/prompt-builder.ts +98 -0
  35. package/server/modules/tasks/role-presets.ts +113 -0
  36. package/server/modules/tasks/runners/claude-runner.ts +120 -0
  37. package/server/modules/tasks/runners/codex-runner.ts +80 -0
  38. package/server/modules/tasks/runners/spawn-runner.ts +122 -0
  39. package/server/modules/tasks/runners/types.ts +25 -0
  40. package/server/modules/tasks/scheduler.ts +75 -0
  41. package/server/modules/tasks/task-queue.ts +91 -0
  42. package/server/modules/tasks/task-routes.ts +245 -0
  43. package/server/modules/tasks/task-runner.ts +219 -0
  44. package/server/modules/tasks/task-store.ts +223 -0
  45. package/server/modules/tasks/task-types.ts +100 -0
  46. package/server/modules/tasks/user-interaction.ts +93 -0
  47. package/server/routes/auth.js +13 -6
  48. package/dist/api-automation.html +0 -110
  49. package/dist/api-docs.html +0 -548
  50. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  51. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  52. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  53. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  54. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  55. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  56. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  57. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  58. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  59. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  60. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  61. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  62. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  63. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  64. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  65. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  66. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  67. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  68. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  69. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  70. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  71. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  72. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  73. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  74. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  75. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  76. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  77. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  78. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  79. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  80. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  81. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  82. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  83. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  84. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  85. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  86. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  87. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  88. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  89. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  90. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  91. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  92. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  93. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  94. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  95. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  96. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  97. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  98. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  99. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  100. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  101. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  102. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  103. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  104. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  105. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  106. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  107. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  108. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  109. package/dist/assets/index-B-ySsQqi.js +0 -877
  110. package/dist/assets/index-CWO38aJI.css +0 -32
  111. package/dist/assets/index-ak1p_4ew.js +0 -11
  112. package/dist/assets/localMonaco-DFIbtDNp.js +0 -1
  113. package/dist/assets/vendor-codemirror-CIYNS698.js +0 -41
  114. package/dist/assets/vendor-react-DB6V5Fl1.js +0 -59
  115. package/dist/assets/vendor-xterm-C7tpxJl7.js +0 -9
  116. package/dist/clear-cache.html +0 -85
  117. package/dist/convert-icons.md +0 -53
  118. package/dist/docs.html +0 -308
  119. package/dist/favicon.png +0 -0
  120. package/dist/favicon.svg +0 -8
  121. package/dist/features.html +0 -133
  122. package/dist/generate-icons.js +0 -49
  123. package/dist/humans.txt +0 -15
  124. package/dist/icons/claude-ai-icon.svg +0 -1
  125. package/dist/icons/codex-white.svg +0 -3
  126. package/dist/icons/codex.svg +0 -3
  127. package/dist/icons/cursor-white.svg +0 -12
  128. package/dist/icons/cursor.svg +0 -1
  129. package/dist/icons/gemini-ai-icon.svg +0 -1
  130. package/dist/icons/icon-128x128.png +0 -0
  131. package/dist/icons/icon-128x128.svg +0 -9
  132. package/dist/icons/icon-144x144.png +0 -0
  133. package/dist/icons/icon-144x144.svg +0 -9
  134. package/dist/icons/icon-152x152.png +0 -0
  135. package/dist/icons/icon-152x152.svg +0 -9
  136. package/dist/icons/icon-192x192.png +0 -0
  137. package/dist/icons/icon-192x192.svg +0 -9
  138. package/dist/icons/icon-384x384.png +0 -0
  139. package/dist/icons/icon-384x384.svg +0 -9
  140. package/dist/icons/icon-512x512.png +0 -0
  141. package/dist/icons/icon-512x512.svg +0 -9
  142. package/dist/icons/icon-72x72.png +0 -0
  143. package/dist/icons/icon-72x72.svg +0 -9
  144. package/dist/icons/icon-96x96.png +0 -0
  145. package/dist/icons/icon-96x96.svg +0 -9
  146. package/dist/icons/icon-template.svg +0 -9
  147. package/dist/icons/opencode-logo-dark.svg +0 -1
  148. package/dist/icons/opencode-logo-light.svg +0 -1
  149. package/dist/icons/qwen-ai-icon.png +0 -0
  150. package/dist/icons/qwen-logo.svg +0 -15
  151. package/dist/index.html +0 -62
  152. package/dist/landing.html +0 -268
  153. package/dist/llms-full.txt +0 -119
  154. package/dist/llms.txt +0 -53
  155. package/dist/logo-128.png +0 -0
  156. package/dist/logo-256.png +0 -0
  157. package/dist/logo-32.png +0 -0
  158. package/dist/logo-512.png +0 -0
  159. package/dist/logo-64.png +0 -0
  160. package/dist/logo.png +0 -0
  161. package/dist/logo.svg +0 -12
  162. package/dist/manifest.json +0 -61
  163. package/dist/openapi.yaml +0 -1696
  164. package/dist/orchestration.html +0 -125
  165. package/dist/robots.txt +0 -4
  166. package/dist/screenshots/cli-selection.png +0 -0
  167. package/dist/screenshots/desktop-main.png +0 -0
  168. package/dist/screenshots/mobile-chat.png +0 -0
  169. package/dist/screenshots/tools-modal.png +0 -0
  170. package/dist/site.css +0 -692
  171. package/dist/sitemap.xml +0 -51
  172. package/dist/sw.js +0 -132
@@ -0,0 +1,120 @@
1
+ // ============================================================
2
+ // Claude Runner — Uses @anthropic-ai/claude-agent-sdk
3
+ // ============================================================
4
+
5
+ import { query } from '@anthropic-ai/claude-agent-sdk';
6
+
7
+ import { buildPrompt, buildContinuePrompt } from '../prompt-builder.js';
8
+ import type { AgentRunner, RunContext, RunResult } from './types.js';
9
+
10
+ export const claudeRunner: AgentRunner = {
11
+ async run(ctx: RunContext): Promise<RunResult> {
12
+ const { task, projectPath, abortController } = ctx;
13
+ const workdir = task.worktreePath || projectPath;
14
+
15
+ // Build prompt — continue or fresh
16
+ const prompt = task.continueSession && task.predecessorTaskId
17
+ ? buildContinuePrompt(task)
18
+ : buildPrompt(task, workdir);
19
+
20
+ // Build clean env
21
+ const cleanEnv: Record<string, string | undefined> = { ...process.env };
22
+ delete cleanEnv.CLAUDECODE;
23
+
24
+ // Permission mode
25
+ const permissionMode = task.permissionMode || 'auto_edit';
26
+
27
+ await ctx.logTask('info', `Starting Claude Code agent. Model: ${task.model || 'default'}, Permission: ${permissionMode}`);
28
+
29
+ let resultText = '';
30
+ let costUsd = 0;
31
+ let tokenInput = 0;
32
+ let tokenOutput = 0;
33
+ let sessionId: string | undefined;
34
+
35
+ try {
36
+ const response = query({
37
+ prompt,
38
+ options: {
39
+ cwd: workdir,
40
+ model: task.model,
41
+ permissionMode: permissionMode as any,
42
+ maxTurns: 150,
43
+ env: cleanEnv,
44
+ abortController,
45
+ ...(task.continueSession && task.sessionId
46
+ ? { resume: task.sessionId }
47
+ : {}),
48
+ },
49
+ });
50
+
51
+ for await (const message of response) {
52
+ if (abortController.signal.aborted) break;
53
+
54
+ switch (message.type) {
55
+ case 'assistant': {
56
+ if (message.message?.content) {
57
+ for (const block of message.message.content) {
58
+ if (block.type === 'text') {
59
+ resultText += block.text + '\n';
60
+ await ctx.logTask('info', `[Claude] ${block.text.slice(0, 200)}`);
61
+ }
62
+ }
63
+ }
64
+ // Extract token usage
65
+ if (message.message?.usage) {
66
+ tokenInput += message.message.usage.input_tokens || 0;
67
+ tokenOutput += message.message.usage.output_tokens || 0;
68
+ costUsd += ((message.message.usage.input_tokens || 0) * 0.000003) +
69
+ ((message.message.usage.output_tokens || 0) * 0.000015);
70
+ }
71
+ break;
72
+ }
73
+
74
+ case 'result': {
75
+ // SDKResultMessage is a union — check subtype before accessing fields
76
+ if (message.subtype === 'success') {
77
+ const success = message as any;
78
+ if (success.result) resultText = success.result;
79
+ if (success.session_id) sessionId = success.session_id;
80
+ if (success.total_cost_usd) costUsd = success.total_cost_usd;
81
+ if (success.usage) {
82
+ tokenInput = success.usage.input_tokens || tokenInput;
83
+ tokenOutput = success.usage.output_tokens || tokenOutput;
84
+ }
85
+ }
86
+ break;
87
+ }
88
+
89
+ case 'system': {
90
+ if (message.subtype === 'init' && message.session_id) {
91
+ sessionId = message.session_id;
92
+ await ctx.logTask('info', `Claude session: ${sessionId}`);
93
+ }
94
+ break;
95
+ }
96
+ }
97
+ }
98
+
99
+ // Check budget
100
+ if (task.maxBudgetUsd && costUsd > task.maxBudgetUsd) {
101
+ await ctx.logTask('warn', `Budget exceeded: $${costUsd.toFixed(4)} > $${task.maxBudgetUsd}`);
102
+ }
103
+
104
+ await ctx.logTask('info', `Claude completed. Tokens: ${tokenInput} in / ${tokenOutput} out. Cost: $${costUsd.toFixed(4)}`);
105
+
106
+ return {
107
+ result: resultText.slice(0, 10000),
108
+ costUsd,
109
+ tokenCount: { input: tokenInput, output: tokenOutput },
110
+ sessionId,
111
+ };
112
+ } catch (err: any) {
113
+ if (err?.name === 'AbortError' || abortController.signal.aborted) {
114
+ await ctx.logTask('info', 'Claude task was cancelled');
115
+ return { result: 'Task cancelled by user', sessionId };
116
+ }
117
+ throw err;
118
+ }
119
+ },
120
+ };
@@ -0,0 +1,80 @@
1
+ // ============================================================
2
+ // Codex Runner — Uses @openai/codex-sdk
3
+ // ============================================================
4
+
5
+ import { Codex } from '@openai/codex-sdk';
6
+
7
+ import { buildPrompt, buildContinuePrompt } from '../prompt-builder.js';
8
+ import type { AgentRunner, RunContext, RunResult } from './types.js';
9
+
10
+ export const codexRunner: AgentRunner = {
11
+ async run(ctx: RunContext): Promise<RunResult> {
12
+ const { task, projectPath, abortController } = ctx;
13
+ const workdir = task.worktreePath || projectPath;
14
+
15
+ const prompt = task.continueSession && task.predecessorTaskId
16
+ ? buildContinuePrompt(task)
17
+ : buildPrompt(task, workdir);
18
+
19
+ // Clean env
20
+ const cleanEnv: Record<string, string> = {};
21
+ for (const [k, v] of Object.entries(process.env)) {
22
+ if (v !== undefined && k !== 'CLAUDECODE') {
23
+ cleanEnv[k] = v;
24
+ }
25
+ }
26
+
27
+ await ctx.logTask('info', `Starting Codex agent. Model: ${task.model || 'o4-mini'}, Thinking: ${task.thinkingEnabled}`);
28
+
29
+ const codex = new Codex({
30
+ env: cleanEnv,
31
+ config: {
32
+ model: task.model || 'o4-mini',
33
+ approval_policy: 'never',
34
+ ...(task.thinkingEnabled ? { reasoning_effort: 'high' as const } : {}),
35
+ },
36
+ });
37
+
38
+ let resultText = '';
39
+ let costUsd = 0;
40
+ let tokenInput = 0;
41
+ let tokenOutput = 0;
42
+ let sessionId: string | undefined;
43
+
44
+ try {
45
+ // Use startThread() — the correct Codex SDK API
46
+ const thread = codex.startThread();
47
+
48
+ // Use run() for non-streaming — returns a Turn with finalResponse and usage
49
+ const turn = await thread.run(prompt, {
50
+ signal: abortController.signal,
51
+ });
52
+
53
+ // Extract results from the completed turn
54
+ resultText = turn.finalResponse || '';
55
+ sessionId = thread.id || undefined;
56
+
57
+ if (turn.usage) {
58
+ tokenInput = turn.usage.input_tokens || 0;
59
+ tokenOutput = turn.usage.output_tokens || 0;
60
+ costUsd = (tokenInput * 0.0000025) + (tokenOutput * 0.00001);
61
+ }
62
+
63
+ await ctx.logTask('info', `[Codex] ${resultText.slice(0, 200)}`);
64
+ await ctx.logTask('info', `Codex completed. Tokens: ${tokenInput} in / ${tokenOutput} out. Cost: $${costUsd.toFixed(4)}`);
65
+
66
+ return {
67
+ result: resultText.slice(0, 10000),
68
+ costUsd,
69
+ tokenCount: { input: tokenInput, output: tokenOutput },
70
+ sessionId,
71
+ };
72
+ } catch (err: any) {
73
+ if (err?.name === 'AbortError' || abortController.signal.aborted) {
74
+ await ctx.logTask('info', 'Codex task was cancelled');
75
+ return { result: 'Task cancelled by user', sessionId };
76
+ }
77
+ throw err;
78
+ }
79
+ },
80
+ };
@@ -0,0 +1,122 @@
1
+ // ============================================================
2
+ // Spawn Runner — Generic CLI spawn runner for Gemini, Qwen, OpenCode
3
+ // Uses child_process spawn with real-time output streaming
4
+ // ============================================================
5
+
6
+ import { spawn } from 'node:child_process';
7
+
8
+ import { buildPrompt, buildContinuePrompt } from '../prompt-builder.js';
9
+ import type { AgentRunner, RunContext, RunResult } from './types.js';
10
+
11
+ // CLI command mapping
12
+ const CLI_COMMANDS: Record<string, { bin: string; args: string[]; promptFlag: string }> = {
13
+ gemini: {
14
+ bin: 'gemini',
15
+ args: ['--yolo'],
16
+ promptFlag: '-p',
17
+ },
18
+ qwen: {
19
+ bin: 'qwen',
20
+ args: [],
21
+ promptFlag: '-p',
22
+ },
23
+ opencode: {
24
+ bin: 'opencode',
25
+ args: ['run'],
26
+ promptFlag: '',
27
+ },
28
+ };
29
+
30
+ function getCliCommand(agentType: string, model?: string): { bin: string; args: string[]; promptFlag: string } {
31
+ const config = CLI_COMMANDS[agentType];
32
+ if (!config) throw new Error(`Unknown CLI agent: ${agentType}`);
33
+ const args = [...config.args];
34
+ if (model && agentType === 'gemini') args.push('--model', model);
35
+ if (model && agentType === 'qwen') args.push('--model', model);
36
+ return { ...config, args };
37
+ }
38
+
39
+ export function createSpawnRunner(agentType: string): AgentRunner {
40
+ return {
41
+ async run(ctx: RunContext): Promise<RunResult> {
42
+ const { task, projectPath, abortController } = ctx;
43
+ const workdir = task.worktreePath || projectPath;
44
+
45
+ const prompt = task.continueSession && task.predecessorTaskId
46
+ ? buildContinuePrompt(task)
47
+ : buildPrompt(task, workdir);
48
+
49
+ const { bin, args, promptFlag } = getCliCommand(agentType, task.model);
50
+
51
+ const fullArgs = promptFlag
52
+ ? [...args, promptFlag, prompt]
53
+ : [...args, prompt];
54
+
55
+ await ctx.logTask('info', `Starting ${agentType} agent. Bin: ${bin}, Model: ${task.model || 'default'}`);
56
+
57
+ return new Promise<RunResult>((resolve, reject) => {
58
+ const child = spawn(bin, fullArgs, {
59
+ cwd: workdir,
60
+ env: { ...process.env },
61
+ stdio: ['pipe', 'pipe', 'pipe'],
62
+ shell: false,
63
+ });
64
+
65
+ let stdout = '';
66
+ let stderr = '';
67
+
68
+ child.stdout?.on('data', (data: Buffer) => {
69
+ const text = data.toString('utf-8');
70
+ stdout += text;
71
+ // Log chunks
72
+ const lines = text.split('\n').filter(Boolean);
73
+ for (const line of lines) {
74
+ ctx.logTask('info', `[${agentType}] ${line.slice(0, 200)}`);
75
+ }
76
+ });
77
+
78
+ child.stderr?.on('data', (data: Buffer) => {
79
+ const text = data.toString('utf-8');
80
+ stderr += text;
81
+ const lines = text.split('\n').filter(Boolean);
82
+ for (const line of lines) {
83
+ ctx.logTask('warn', `[${agentType}] ${line.slice(0, 200)}`);
84
+ }
85
+ });
86
+
87
+ // Handle abort
88
+ const abortHandler = () => {
89
+ try {
90
+ child.kill('SIGTERM');
91
+ setTimeout(() => {
92
+ if (!child.killed) child.kill('SIGKILL');
93
+ }, 5000);
94
+ } catch {}
95
+ };
96
+ abortController.signal.addEventListener('abort', abortHandler, { once: true });
97
+
98
+ child.on('close', (code: number) => {
99
+ abortController.signal.removeEventListener('abort', abortHandler);
100
+
101
+ if (code === 0 || abortController.signal.aborted) {
102
+ ctx.logTask('info', `${agentType} completed. Exit code: ${code}`);
103
+ resolve({
104
+ result: stdout.slice(0, 10000),
105
+ costUsd: 0, // CLI doesn't report cost directly
106
+ tokenCount: { input: 0, output: 0 },
107
+ });
108
+ } else {
109
+ ctx.logTask('error', `${agentType} failed. Exit code: ${code}. stderr: ${stderr.slice(0, 500)}`);
110
+ reject(new Error(`${agentType} exited with code ${code}: ${stderr.slice(0, 500)}`));
111
+ }
112
+ });
113
+
114
+ child.on('error', (err: Error) => {
115
+ abortController.signal.removeEventListener('abort', abortHandler);
116
+ ctx.logTask('error', `${agentType} spawn error: ${err.message}`);
117
+ reject(err);
118
+ });
119
+ });
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,25 @@
1
+ // ============================================================
2
+ // Runner Types — Interface for agent runners
3
+ // ============================================================
4
+
5
+ import type { Task } from '../task-types.js';
6
+
7
+ export interface RunContext {
8
+ task: Task;
9
+ projectPath: string;
10
+ abortController: AbortController;
11
+ logTask: (level: 'info' | 'warn' | 'error' | 'debug', message: string) => void;
12
+ broadcastStatus: (status: string) => void;
13
+ askUser: (question: string, options?: string[]) => Promise<string>;
14
+ }
15
+
16
+ export interface RunResult {
17
+ result?: string;
18
+ costUsd?: number;
19
+ tokenCount?: { input: number; output: number };
20
+ sessionId?: string;
21
+ }
22
+
23
+ export interface AgentRunner {
24
+ run(ctx: RunContext): Promise<RunResult>;
25
+ }
@@ -0,0 +1,75 @@
1
+ // ============================================================
2
+ // Scheduler — Polls the queue every 2 seconds and runs ready tasks
3
+ // ============================================================
4
+
5
+ import { taskEvents } from './task-runner.js';
6
+ import { getTasksToRun, onTaskTerminated, enqueue } from './task-queue.js';
7
+ import { getTask, updateTask } from './task-store.js';
8
+ import { runTask } from './task-runner.js';
9
+ import { resolveProjectPath } from './task-runner.js';
10
+
11
+ export class TaskScheduler {
12
+ private intervalId: NodeJS.Timeout | null = null;
13
+ private running = false;
14
+
15
+ start(): void {
16
+ if (this.intervalId) return;
17
+
18
+ // Poll every 2 seconds
19
+ this.intervalId = setInterval(() => this.processQueue(), 2000);
20
+
21
+ // Listen for events to trigger immediate processing
22
+ taskEvents.on('task:completed', (taskId: string) => this.onTaskTerminated(taskId));
23
+ taskEvents.on('task:failed', (taskId: string) => this.onTaskTerminated(taskId));
24
+ taskEvents.on('task:cancelled', (taskId: string) => this.onTaskTerminated(taskId));
25
+
26
+ // Listen for enqueue events
27
+ taskEvents.on('task:enqueued', () => this.processQueue());
28
+
29
+ console.log('[scheduler] Task scheduler started (poll interval: 2s)');
30
+ }
31
+
32
+ stop(): void {
33
+ if (this.intervalId) {
34
+ clearInterval(this.intervalId);
35
+ this.intervalId = null;
36
+ }
37
+ taskEvents.removeAllListeners('task:completed');
38
+ taskEvents.removeAllListeners('task:failed');
39
+ taskEvents.removeAllListeners('task:cancelled');
40
+ taskEvents.removeAllListeners('task:enqueued');
41
+ console.log('[scheduler] Task scheduler stopped');
42
+ }
43
+
44
+ private async processQueue(): Promise<void> {
45
+ if (this.running) return; // prevent re-entrant
46
+ this.running = true;
47
+ try {
48
+ const tasksToRun = getTasksToRun();
49
+ for (const task of tasksToRun) {
50
+ // Fire and forget — don't await runTask
51
+ const projectPath = await resolveProjectPath(task.projectId);
52
+ runTask(task, projectPath).catch((err) => {
53
+ console.error(`[scheduler] Task ${task.id} failed:`, err);
54
+ });
55
+ }
56
+ } catch (err) {
57
+ console.error('[scheduler] Error processing queue:', err);
58
+ } finally {
59
+ this.running = false;
60
+ }
61
+ }
62
+
63
+ private async onTaskTerminated(taskId: string): Promise<void> {
64
+ try {
65
+ await onTaskTerminated(taskId);
66
+ // Immediately check if dependent tasks can run
67
+ this.processQueue();
68
+ } catch (err) {
69
+ console.error('[scheduler] Error handling task termination:', err);
70
+ }
71
+ }
72
+ }
73
+
74
+ // Singleton
75
+ export const taskScheduler = new TaskScheduler();
@@ -0,0 +1,91 @@
1
+ // ============================================================
2
+ // Task Queue — Manages task dependencies and concurrency
3
+ // ============================================================
4
+
5
+ import { getTask, updateTask, getTasksByStatus, getPredecessor } from './task-store.js';
6
+ import { taskEvents } from './task-runner.js';
7
+ import type { Task, TaskStatus } from './task-types.js';
8
+
9
+ const TERMINAL_STATUSES: TaskStatus[] = ['COMPLETED', 'FAILED', 'CANCELLED'];
10
+
11
+ // Default concurrency: 3 concurrent tasks
12
+ let MAX_CONCURRENT = parseInt(process.env.PIXCODE_MAX_CONCURRENT_TASKS || '3', 10);
13
+
14
+ export function setMaxConcurrent(n: number): void {
15
+ MAX_CONCURRENT = Math.max(1, n);
16
+ }
17
+
18
+ export function getMaxConcurrent(): number {
19
+ return MAX_CONCURRENT;
20
+ }
21
+
22
+ /**
23
+ * Enqueue a task — checks predecessor, transitions to QUEUED if ready.
24
+ */
25
+ export async function enqueue(taskId: string): Promise<void> {
26
+ const task = getTask(taskId);
27
+ if (!task) throw new Error(`Task ${taskId} not found`);
28
+
29
+ // Check predecessor
30
+ if (task.predecessorTaskId) {
31
+ const predecessor = getPredecessor(task);
32
+ if (predecessor && !TERMINAL_STATUSES.includes(predecessor.status)) {
33
+ // Predecessor still running — task stays PENDING
34
+ return;
35
+ }
36
+ if (predecessor && predecessor.status !== 'COMPLETED') {
37
+ // Predecessor failed or cancelled — cancel this task too
38
+ updateTask(taskId, {
39
+ status: 'CANCELLED',
40
+ result: `Predecessor task "${predecessor.title}" is ${predecessor.status}`,
41
+ });
42
+ taskEvents.emit('task:status', { taskId, status: 'CANCELLED' });
43
+ return;
44
+ }
45
+ // Predecessor completed — continue session if requested
46
+ if (predecessor && task.continueSession && predecessor.sessionId) {
47
+ updateTask(taskId, { sessionId: predecessor.sessionId });
48
+ }
49
+ }
50
+
51
+ // Ready to queue
52
+ updateTask(taskId, { status: 'QUEUED' });
53
+ taskEvents.emit('task:status', { taskId, status: 'QUEUED' });
54
+ }
55
+
56
+ /**
57
+ * Get tasks that are ready to run (QUEUED + within concurrency limit).
58
+ */
59
+ export function getTasksToRun(): Task[] {
60
+ const runningCount = getTasksByStatus('RUNNING').length +
61
+ getTasksByStatus('AWAITING_INPUT').length;
62
+
63
+ if (runningCount >= MAX_CONCURRENT) return [];
64
+
65
+ const queued = getTasksByStatus('QUEUED');
66
+ const slotsAvailable = MAX_CONCURRENT - runningCount;
67
+
68
+ // Sort by priority then creation time
69
+ const priorityOrder: Record<string, number> = { urgent: 0, high: 1, normal: 2, low: 3 };
70
+ return queued
71
+ .sort((a, b) => {
72
+ const pDiff = (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2);
73
+ if (pDiff !== 0) return pDiff;
74
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
75
+ })
76
+ .slice(0, slotsAvailable);
77
+ }
78
+
79
+ /**
80
+ * Called when a task terminates — check if dependent tasks can proceed.
81
+ */
82
+ export async function onTaskTerminated(taskId: string): Promise<void> {
83
+ // Find tasks that have this task as predecessor
84
+ // We need to scan all PENDING tasks
85
+ const pending = getTasksByStatus('PENDING');
86
+ for (const p of pending) {
87
+ if (p.predecessorTaskId === taskId) {
88
+ await enqueue(p.id);
89
+ }
90
+ }
91
+ }