@openbuilder/cli 0.50.46 → 0.50.49
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/chunks/{main-tui-NPDVPKol.js → main-tui-DmZeCepg.js} +2 -2
- package/dist/chunks/{main-tui-NPDVPKol.js.map → main-tui-DmZeCepg.js.map} +1 -1
- package/dist/chunks/{run-Yh3YjeLl.js → run-BAh7Xc-y.js} +2 -2
- package/dist/chunks/{run-Yh3YjeLl.js.map → run-BAh7Xc-y.js.map} +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/index.js +904 -56
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -18,11 +18,11 @@ import { sql, eq, and, desc, isNull } from 'drizzle-orm';
|
|
|
18
18
|
import { randomUUID, createHash } from 'crypto';
|
|
19
19
|
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
20
20
|
import { z } from 'zod';
|
|
21
|
-
import os$1 from 'node:os';
|
|
22
|
-
import { randomUUID as randomUUID$1 } from 'node:crypto';
|
|
23
|
-
import express from 'express';
|
|
24
21
|
import { spawn } from 'node:child_process';
|
|
25
22
|
import { EventEmitter } from 'node:events';
|
|
23
|
+
import os$1, { homedir } from 'node:os';
|
|
24
|
+
import { randomUUID as randomUUID$1 } from 'node:crypto';
|
|
25
|
+
import express from 'express';
|
|
26
26
|
import { createServer, createConnection } from 'node:net';
|
|
27
27
|
import { readFile as readFile$1, rm, writeFile, readdir } from 'node:fs/promises';
|
|
28
28
|
import { simpleGit } from 'simple-git';
|
|
@@ -202,7 +202,8 @@ var init_tags$1 = __esm$1({
|
|
|
202
202
|
description: "Anthropic Claude - Balanced performance and speed",
|
|
203
203
|
logo: "/claude.png",
|
|
204
204
|
provider: "claude-code",
|
|
205
|
-
model: "claude-sonnet-4-5"
|
|
205
|
+
model: "claude-sonnet-4-5",
|
|
206
|
+
sdk: "agent"
|
|
206
207
|
},
|
|
207
208
|
{
|
|
208
209
|
value: "claude-opus-4-5",
|
|
@@ -210,7 +211,8 @@ var init_tags$1 = __esm$1({
|
|
|
210
211
|
description: "Anthropic Claude - Most capable for complex tasks",
|
|
211
212
|
logo: "/claude.png",
|
|
212
213
|
provider: "claude-code",
|
|
213
|
-
model: "claude-opus-4-5"
|
|
214
|
+
model: "claude-opus-4-5",
|
|
215
|
+
sdk: "agent"
|
|
214
216
|
},
|
|
215
217
|
{
|
|
216
218
|
value: "claude-haiku-4-5",
|
|
@@ -218,7 +220,8 @@ var init_tags$1 = __esm$1({
|
|
|
218
220
|
description: "Anthropic Claude - Fastest, good for iterations",
|
|
219
221
|
logo: "/claude.png",
|
|
220
222
|
provider: "claude-code",
|
|
221
|
-
model: "claude-haiku-4-5"
|
|
223
|
+
model: "claude-haiku-4-5",
|
|
224
|
+
sdk: "agent"
|
|
222
225
|
},
|
|
223
226
|
{
|
|
224
227
|
value: "gpt-5.2-codex",
|
|
@@ -226,7 +229,35 @@ var init_tags$1 = __esm$1({
|
|
|
226
229
|
description: "OpenAI Codex - Advanced code generation",
|
|
227
230
|
logo: "/openai.png",
|
|
228
231
|
provider: "openai-codex",
|
|
229
|
-
model: "openai/gpt-5.2-codex"
|
|
232
|
+
model: "openai/gpt-5.2-codex",
|
|
233
|
+
sdk: "agent"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
value: "factory-droid-opus",
|
|
237
|
+
label: "Factory Droid (Opus)",
|
|
238
|
+
description: "Factory Droid SDK with Claude Opus 4.5",
|
|
239
|
+
logo: "/factory.svg",
|
|
240
|
+
provider: "factory-droid",
|
|
241
|
+
model: "claude-opus-4-5-20251101",
|
|
242
|
+
sdk: "droid"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
value: "factory-droid-codex",
|
|
246
|
+
label: "Factory Droid (Codex)",
|
|
247
|
+
description: "Factory Droid SDK with GPT-5.2 Codex",
|
|
248
|
+
logo: "/factory.svg",
|
|
249
|
+
provider: "factory-droid",
|
|
250
|
+
model: "gpt-5.2-codex",
|
|
251
|
+
sdk: "droid"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
value: "factory-droid-glm",
|
|
255
|
+
label: "Factory Droid (GLM)",
|
|
256
|
+
description: "Factory Droid SDK with GLM 4.7",
|
|
257
|
+
logo: "/factory.svg",
|
|
258
|
+
provider: "factory-droid",
|
|
259
|
+
model: "glm-4.7",
|
|
260
|
+
sdk: "droid"
|
|
230
261
|
}
|
|
231
262
|
]
|
|
232
263
|
},
|
|
@@ -1269,6 +1300,94 @@ var init_codex_strategy$1 = __esm$1({
|
|
|
1269
1300
|
}
|
|
1270
1301
|
});
|
|
1271
1302
|
|
|
1303
|
+
// src/lib/agents/droid-strategy.ts
|
|
1304
|
+
var droid_strategy_exports$1 = {};
|
|
1305
|
+
__export$1(droid_strategy_exports$1, {
|
|
1306
|
+
default: () => droid_strategy_default$1
|
|
1307
|
+
});
|
|
1308
|
+
function buildDroidSections$1(context) {
|
|
1309
|
+
const sections = [];
|
|
1310
|
+
if (context.tags && context.tags.length > 0) {
|
|
1311
|
+
const resolved = resolveTags$1(context.tags);
|
|
1312
|
+
const tagPrompt = generatePromptFromTags$1(resolved, context.projectName, context.isNewProject);
|
|
1313
|
+
if (tagPrompt) {
|
|
1314
|
+
sections.push(tagPrompt);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (context.isNewProject) {
|
|
1318
|
+
sections.push(`## New Project Setup
|
|
1319
|
+
|
|
1320
|
+
- Project name: ${context.projectName}
|
|
1321
|
+
- Location: ${context.workingDirectory}
|
|
1322
|
+
- Operation type: ${context.operationType}
|
|
1323
|
+
|
|
1324
|
+
The template has already been downloaded. Install dependencies and customize the scaffold to satisfy the request.`);
|
|
1325
|
+
} else {
|
|
1326
|
+
let existingProjectSection = `## Existing Project Context
|
|
1327
|
+
|
|
1328
|
+
- Project location: ${context.workingDirectory}
|
|
1329
|
+
- Operation type: ${context.operationType}
|
|
1330
|
+
|
|
1331
|
+
Review the current codebase and apply the requested changes without re-scaffolding.`;
|
|
1332
|
+
if (context.conversationHistory && context.conversationHistory.length > 0) {
|
|
1333
|
+
existingProjectSection += `
|
|
1334
|
+
|
|
1335
|
+
**Recent Conversation History:**
|
|
1336
|
+
`;
|
|
1337
|
+
context.conversationHistory.forEach((msg, index2) => {
|
|
1338
|
+
const roleLabel = msg.role === "user" ? "User" : "Assistant";
|
|
1339
|
+
const content = msg.content.length > 500 ? msg.content.substring(0, 500) + "...[truncated]" : msg.content;
|
|
1340
|
+
existingProjectSection += `${index2 + 1}. ${roleLabel}:
|
|
1341
|
+
${content}
|
|
1342
|
+
|
|
1343
|
+
`;
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
sections.push(existingProjectSection);
|
|
1347
|
+
}
|
|
1348
|
+
sections.push(`## Workspace Rules
|
|
1349
|
+
- Use relative paths within the project.
|
|
1350
|
+
- Work inside the existing project structure.
|
|
1351
|
+
- Provide complete file contents without placeholders.`);
|
|
1352
|
+
if (context.fileTree) {
|
|
1353
|
+
sections.push(`## Project Structure
|
|
1354
|
+
${context.fileTree}`);
|
|
1355
|
+
}
|
|
1356
|
+
if (context.templateName) {
|
|
1357
|
+
sections.push(`## Template
|
|
1358
|
+
- Name: ${context.templateName}
|
|
1359
|
+
- Framework: ${context.templateFramework ?? "unknown"}`);
|
|
1360
|
+
}
|
|
1361
|
+
return sections;
|
|
1362
|
+
}
|
|
1363
|
+
function buildFullPrompt3$1(context, basePrompt) {
|
|
1364
|
+
if (!context.isNewProject) {
|
|
1365
|
+
return basePrompt;
|
|
1366
|
+
}
|
|
1367
|
+
return `${basePrompt}
|
|
1368
|
+
|
|
1369
|
+
CRITICAL: The template has already been prepared in ${context.workingDirectory}. Do not scaffold a new project.`;
|
|
1370
|
+
}
|
|
1371
|
+
var droidStrategy$1, droid_strategy_default$1;
|
|
1372
|
+
var init_droid_strategy$1 = __esm$1({
|
|
1373
|
+
"src/lib/agents/droid-strategy.ts"() {
|
|
1374
|
+
init_resolver$1();
|
|
1375
|
+
droidStrategy$1 = {
|
|
1376
|
+
buildSystemPromptSections: buildDroidSections$1,
|
|
1377
|
+
buildFullPrompt: buildFullPrompt3$1,
|
|
1378
|
+
shouldDownloadTemplate(context) {
|
|
1379
|
+
return context.isNewProject && !context.skipTemplates;
|
|
1380
|
+
},
|
|
1381
|
+
postTemplateSelected(context, template) {
|
|
1382
|
+
context.templateName = template.name;
|
|
1383
|
+
context.templateFramework = template.framework;
|
|
1384
|
+
context.fileTree = template.fileTree;
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
droid_strategy_default$1 = droidStrategy$1;
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1272
1391
|
// src/lib/prompts.ts
|
|
1273
1392
|
var CLAUDE_SYSTEM_PROMPT = `You are an elite coding assistant specialized in building visually stunning, production-ready JavaScript applications.
|
|
1274
1393
|
|
|
@@ -2099,12 +2218,18 @@ async function ensureRegistry$1() {
|
|
|
2099
2218
|
if (initialized$1) {
|
|
2100
2219
|
return;
|
|
2101
2220
|
}
|
|
2102
|
-
const [
|
|
2221
|
+
const [
|
|
2222
|
+
{ default: claudeStrategy2 },
|
|
2223
|
+
{ default: codexStrategy2 },
|
|
2224
|
+
{ default: droidStrategy2 }
|
|
2225
|
+
] = await Promise.all([
|
|
2103
2226
|
Promise.resolve().then(() => (init_claude_strategy$1(), claude_strategy_exports$1)),
|
|
2104
|
-
Promise.resolve().then(() => (init_codex_strategy$1(), codex_strategy_exports$1))
|
|
2227
|
+
Promise.resolve().then(() => (init_codex_strategy$1(), codex_strategy_exports$1)),
|
|
2228
|
+
Promise.resolve().then(() => (init_droid_strategy$1(), droid_strategy_exports$1))
|
|
2105
2229
|
]);
|
|
2106
2230
|
registerAgentStrategy$1("claude-code", claudeStrategy2);
|
|
2107
2231
|
registerAgentStrategy$1("openai-codex", codexStrategy2);
|
|
2232
|
+
registerAgentStrategy$1("factory-droid", droidStrategy2);
|
|
2108
2233
|
initialized$1 = true;
|
|
2109
2234
|
}
|
|
2110
2235
|
async function resolveAgentStrategy$1(agentId) {
|
|
@@ -4551,7 +4676,7 @@ var AgentCore = /*#__PURE__*/Object.freeze({
|
|
|
4551
4676
|
* Native Claude Agent SDK Integration
|
|
4552
4677
|
*
|
|
4553
4678
|
* This is the default SDK integration for AI-powered builds.
|
|
4554
|
-
* For multi-provider support, set
|
|
4679
|
+
* For multi-provider support, set ENABLE_OPENCODE_SDK=true to use opencode-sdk.ts instead.
|
|
4555
4680
|
*
|
|
4556
4681
|
* This module provides direct integration with the official @anthropic-ai/claude-agent-sdk
|
|
4557
4682
|
* without going through the AI SDK or community provider layers.
|
|
@@ -4563,9 +4688,9 @@ var AgentCore = /*#__PURE__*/Object.freeze({
|
|
|
4563
4688
|
* - Direct streaming without adaptation layer
|
|
4564
4689
|
*/
|
|
4565
4690
|
// Debug logging helper - suppressed in TUI mode (SILENT_MODE=1)
|
|
4566
|
-
const debugLog$
|
|
4691
|
+
const debugLog$4 = (message) => {
|
|
4567
4692
|
if (process.env.SILENT_MODE !== '1' && process.env.DEBUG_BUILD === '1') {
|
|
4568
|
-
debugLog$
|
|
4693
|
+
debugLog$4();
|
|
4569
4694
|
}
|
|
4570
4695
|
};
|
|
4571
4696
|
/**
|
|
@@ -4684,10 +4809,10 @@ function buildPromptWithImages(prompt, messageParts) {
|
|
|
4684
4809
|
*/
|
|
4685
4810
|
function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortController) {
|
|
4686
4811
|
return async function* nativeClaudeQuery(prompt, workingDirectory, systemPrompt, _agent, _codexThreadId, messageParts) {
|
|
4687
|
-
debugLog$
|
|
4688
|
-
debugLog$
|
|
4689
|
-
debugLog$
|
|
4690
|
-
debugLog$
|
|
4812
|
+
debugLog$4();
|
|
4813
|
+
debugLog$4();
|
|
4814
|
+
debugLog$4();
|
|
4815
|
+
debugLog$4(`[runner] [native-sdk] Prompt length: ${prompt.length}\n`);
|
|
4691
4816
|
// Build combined system prompt
|
|
4692
4817
|
const systemPromptSegments = [CLAUDE_SYSTEM_PROMPT.trim()];
|
|
4693
4818
|
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
@@ -4703,7 +4828,7 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4703
4828
|
const hasImages = messageParts?.some(p => p.type === 'image');
|
|
4704
4829
|
if (hasImages) {
|
|
4705
4830
|
messageParts?.filter(p => p.type === 'image').length || 0;
|
|
4706
|
-
debugLog$
|
|
4831
|
+
debugLog$4();
|
|
4707
4832
|
}
|
|
4708
4833
|
// Build the final prompt
|
|
4709
4834
|
const finalPrompt = buildPromptWithImages(prompt);
|
|
@@ -4737,7 +4862,7 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4737
4862
|
// See: https://github.com/anthropics/claude-agent-sdk-typescript/issues/46
|
|
4738
4863
|
abortController,
|
|
4739
4864
|
};
|
|
4740
|
-
debugLog$
|
|
4865
|
+
debugLog$4();
|
|
4741
4866
|
let messageCount = 0;
|
|
4742
4867
|
let toolCallCount = 0;
|
|
4743
4868
|
let textBlockCount = 0;
|
|
@@ -4754,7 +4879,7 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4754
4879
|
for (const block of transformed.message.content) {
|
|
4755
4880
|
if (block.type === 'tool_use') {
|
|
4756
4881
|
toolCallCount++;
|
|
4757
|
-
debugLog$
|
|
4882
|
+
debugLog$4(`[runner] [native-sdk] 🔧 Tool call: ${block.name}\n`);
|
|
4758
4883
|
}
|
|
4759
4884
|
else if (block.type === 'text') {
|
|
4760
4885
|
textBlockCount++;
|
|
@@ -4766,17 +4891,17 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4766
4891
|
// Log result messages
|
|
4767
4892
|
if (sdkMessage.type === 'result') {
|
|
4768
4893
|
if (sdkMessage.subtype === 'success') {
|
|
4769
|
-
debugLog$
|
|
4894
|
+
debugLog$4(`[runner] [native-sdk] ✅ Query complete - ${sdkMessage.num_turns} turns, $${sdkMessage.total_cost_usd?.toFixed(4)} USD\n`);
|
|
4770
4895
|
}
|
|
4771
4896
|
else {
|
|
4772
|
-
debugLog$
|
|
4897
|
+
debugLog$4(`[runner] [native-sdk] ⚠️ Query ended with: ${sdkMessage.subtype}\n`);
|
|
4773
4898
|
}
|
|
4774
4899
|
}
|
|
4775
4900
|
}
|
|
4776
|
-
debugLog$
|
|
4901
|
+
debugLog$4(`[runner] [native-sdk] 📊 Stream complete - ${messageCount} messages, ${toolCallCount} tool calls, ${textBlockCount} text blocks\n`);
|
|
4777
4902
|
}
|
|
4778
4903
|
catch (error) {
|
|
4779
|
-
debugLog$
|
|
4904
|
+
debugLog$4(`[runner] [native-sdk] ❌ Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
4780
4905
|
Sentry.captureException(error);
|
|
4781
4906
|
throw error;
|
|
4782
4907
|
}
|
|
@@ -4796,7 +4921,7 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4796
4921
|
* - Runner receives events and transforms them to SSE format for the frontend
|
|
4797
4922
|
*/
|
|
4798
4923
|
// Debug logging helper
|
|
4799
|
-
const debugLog$
|
|
4924
|
+
const debugLog$3 = (message) => {
|
|
4800
4925
|
if (process.env.SILENT_MODE !== '1' && process.env.DEBUG_BUILD === '1') {
|
|
4801
4926
|
console.error(message);
|
|
4802
4927
|
}
|
|
@@ -4961,10 +5086,10 @@ function parseSSEEvent(data) {
|
|
|
4961
5086
|
*/
|
|
4962
5087
|
function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
4963
5088
|
return async function* openCodeQuery(prompt, workingDirectory, systemPrompt, _agent, _codexThreadId, messageParts) {
|
|
4964
|
-
debugLog$
|
|
4965
|
-
debugLog$
|
|
4966
|
-
debugLog$
|
|
4967
|
-
debugLog$
|
|
5089
|
+
debugLog$3('[runner] [opencode-sdk] Starting OpenCode query');
|
|
5090
|
+
debugLog$3(`[runner] [opencode-sdk] Model: ${modelId}`);
|
|
5091
|
+
debugLog$3(`[runner] [opencode-sdk] Working dir: ${workingDirectory}`);
|
|
5092
|
+
debugLog$3(`[runner] [opencode-sdk] Prompt length: ${prompt.length}`);
|
|
4968
5093
|
// Ensure working directory exists
|
|
4969
5094
|
if (!existsSync(workingDirectory)) {
|
|
4970
5095
|
console.log(`[opencode-sdk] Creating working directory: ${workingDirectory}`);
|
|
@@ -5004,7 +5129,7 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5004
5129
|
});
|
|
5005
5130
|
try {
|
|
5006
5131
|
// Step 1: Create a session
|
|
5007
|
-
debugLog$
|
|
5132
|
+
debugLog$3('[runner] [opencode-sdk] Creating session...');
|
|
5008
5133
|
const sessionResponse = await fetch(`${baseUrl}/session`, {
|
|
5009
5134
|
method: 'POST',
|
|
5010
5135
|
headers,
|
|
@@ -5018,13 +5143,13 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5018
5143
|
}
|
|
5019
5144
|
const session = await sessionResponse.json();
|
|
5020
5145
|
sessionId = session.id;
|
|
5021
|
-
debugLog$
|
|
5146
|
+
debugLog$3(`[runner] [opencode-sdk] Session created: ${sessionId}`);
|
|
5022
5147
|
// Update span with session info
|
|
5023
5148
|
if (sessionId) {
|
|
5024
5149
|
aiSpan?.setAttribute('opencode.session_id', sessionId);
|
|
5025
5150
|
}
|
|
5026
5151
|
// Step 2: Subscribe to events
|
|
5027
|
-
debugLog$
|
|
5152
|
+
debugLog$3('[runner] [opencode-sdk] Subscribing to events...');
|
|
5028
5153
|
const eventResponse = await fetch(`${baseUrl}/event`, {
|
|
5029
5154
|
headers: {
|
|
5030
5155
|
...headers,
|
|
@@ -5045,7 +5170,7 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5045
5170
|
image: part.image,
|
|
5046
5171
|
mimeType: part.mimeType || 'image/png',
|
|
5047
5172
|
});
|
|
5048
|
-
debugLog$
|
|
5173
|
+
debugLog$3('[runner] [opencode-sdk] Added image part');
|
|
5049
5174
|
}
|
|
5050
5175
|
}
|
|
5051
5176
|
}
|
|
@@ -5055,7 +5180,7 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5055
5180
|
text: prompt,
|
|
5056
5181
|
});
|
|
5057
5182
|
// Step 4: Send the prompt (don't await - we'll stream the response)
|
|
5058
|
-
debugLog$
|
|
5183
|
+
debugLog$3('[runner] [opencode-sdk] Sending prompt...');
|
|
5059
5184
|
const promptPromise = fetch(`${baseUrl}/session/${sessionId}/message`, {
|
|
5060
5185
|
method: 'POST',
|
|
5061
5186
|
headers,
|
|
@@ -5079,7 +5204,7 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5079
5204
|
while (!completed) {
|
|
5080
5205
|
const { done, value } = await reader.read();
|
|
5081
5206
|
if (done) {
|
|
5082
|
-
debugLog$
|
|
5207
|
+
debugLog$3('[runner] [opencode-sdk] Event stream ended');
|
|
5083
5208
|
break;
|
|
5084
5209
|
}
|
|
5085
5210
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -5147,14 +5272,14 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5147
5272
|
subtype: 'success',
|
|
5148
5273
|
};
|
|
5149
5274
|
}
|
|
5150
|
-
debugLog$
|
|
5275
|
+
debugLog$3('[runner] [opencode-sdk] Query complete');
|
|
5151
5276
|
// Update span with final metrics
|
|
5152
5277
|
aiSpan?.setAttribute('opencode.tool_calls', toolCallCount);
|
|
5153
5278
|
aiSpan?.setAttribute('opencode.messages', messageCount);
|
|
5154
5279
|
aiSpan?.setStatus({ code: 1 }); // OK status
|
|
5155
5280
|
}
|
|
5156
5281
|
catch (error) {
|
|
5157
|
-
debugLog$
|
|
5282
|
+
debugLog$3(`[runner] [opencode-sdk] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
5158
5283
|
Sentry.captureException(error);
|
|
5159
5284
|
// Mark span as errored
|
|
5160
5285
|
aiSpan?.setStatus({ code: 2, message: error instanceof Error ? error.message : String(error) });
|
|
@@ -5177,10 +5302,518 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5177
5302
|
* Feature flag to control which implementation to use
|
|
5178
5303
|
*
|
|
5179
5304
|
* Default: Claude Agent SDK (native)
|
|
5180
|
-
* Set
|
|
5305
|
+
* Set ENABLE_OPENCODE_SDK=true to enable OpenCode multi-provider support
|
|
5181
5306
|
* Note: OPENCODE_URL must also be set to the OpenCode service URL
|
|
5182
5307
|
*/
|
|
5183
|
-
const
|
|
5308
|
+
const ENABLE_OPENCODE_SDK = process.env.ENABLE_OPENCODE_SDK === 'true' && !!process.env.OPENCODE_URL;
|
|
5309
|
+
|
|
5310
|
+
class JsonLineParser {
|
|
5311
|
+
buffer = "";
|
|
5312
|
+
parse(chunk) {
|
|
5313
|
+
this.buffer += chunk;
|
|
5314
|
+
const events = [];
|
|
5315
|
+
const lines = this.buffer.split("\n");
|
|
5316
|
+
// Keep the last incomplete line in buffer
|
|
5317
|
+
this.buffer = lines.pop() || "";
|
|
5318
|
+
for (const line of lines) {
|
|
5319
|
+
const trimmed = line.trim();
|
|
5320
|
+
if (!trimmed)
|
|
5321
|
+
continue;
|
|
5322
|
+
try {
|
|
5323
|
+
const event = JSON.parse(trimmed);
|
|
5324
|
+
events.push(event);
|
|
5325
|
+
}
|
|
5326
|
+
catch {
|
|
5327
|
+
// Skip malformed lines
|
|
5328
|
+
console.error("[Parser] Failed to parse line:", trimmed.slice(0, 100));
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
return events;
|
|
5332
|
+
}
|
|
5333
|
+
flush() {
|
|
5334
|
+
if (!this.buffer.trim())
|
|
5335
|
+
return [];
|
|
5336
|
+
try {
|
|
5337
|
+
const event = JSON.parse(this.buffer);
|
|
5338
|
+
this.buffer = "";
|
|
5339
|
+
return [event];
|
|
5340
|
+
}
|
|
5341
|
+
catch {
|
|
5342
|
+
this.buffer = "";
|
|
5343
|
+
return [];
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
reset() {
|
|
5347
|
+
this.buffer = "";
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
function findDroidPath() {
|
|
5352
|
+
// Check common installation locations
|
|
5353
|
+
const candidates = [
|
|
5354
|
+
join$1(homedir(), ".local", "bin", "droid"),
|
|
5355
|
+
join$1(homedir(), ".factory", "bin", "droid"),
|
|
5356
|
+
"/usr/local/bin/droid",
|
|
5357
|
+
"/opt/homebrew/bin/droid",
|
|
5358
|
+
"droid", // Fall back to PATH lookup
|
|
5359
|
+
];
|
|
5360
|
+
for (const candidate of candidates) {
|
|
5361
|
+
if (candidate === "droid" || existsSync(candidate)) {
|
|
5362
|
+
return candidate;
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
return "droid"; // Let it fail with a clear error if not found
|
|
5366
|
+
}
|
|
5367
|
+
class DroidSession extends EventEmitter {
|
|
5368
|
+
parser = new JsonLineParser();
|
|
5369
|
+
sessionId = null;
|
|
5370
|
+
startTime = null;
|
|
5371
|
+
options;
|
|
5372
|
+
droidPath;
|
|
5373
|
+
constructor(options = {}) {
|
|
5374
|
+
super();
|
|
5375
|
+
this.options = options;
|
|
5376
|
+
this.droidPath = findDroidPath();
|
|
5377
|
+
this.startTime = Date.now();
|
|
5378
|
+
}
|
|
5379
|
+
buildArgs(prompt, overrides) {
|
|
5380
|
+
const args = ["exec", "--output-format", "stream-json"];
|
|
5381
|
+
// Custom droid (must come before other flags)
|
|
5382
|
+
if (overrides?.droid) {
|
|
5383
|
+
args.push("--droid", overrides.droid);
|
|
5384
|
+
}
|
|
5385
|
+
if (this.options.autonomyLevel) {
|
|
5386
|
+
args.push("--auto", this.options.autonomyLevel);
|
|
5387
|
+
}
|
|
5388
|
+
else {
|
|
5389
|
+
args.push("--auto", "high");
|
|
5390
|
+
}
|
|
5391
|
+
// Model: override > fallback
|
|
5392
|
+
const model = overrides?.model || this.options.model;
|
|
5393
|
+
if (model) {
|
|
5394
|
+
args.push("-m", model);
|
|
5395
|
+
}
|
|
5396
|
+
// Reasoning: override > fallback
|
|
5397
|
+
const reasoning = overrides?.reasoningEffort || this.options.reasoningEffort;
|
|
5398
|
+
if (reasoning) {
|
|
5399
|
+
args.push("-r", reasoning);
|
|
5400
|
+
}
|
|
5401
|
+
// Spec mode
|
|
5402
|
+
if (overrides?.useSpec) {
|
|
5403
|
+
args.push("--use-spec");
|
|
5404
|
+
if (overrides.specModel) {
|
|
5405
|
+
args.push("--spec-model", overrides.specModel);
|
|
5406
|
+
}
|
|
5407
|
+
if (overrides.specReasoningEffort) {
|
|
5408
|
+
args.push("--spec-reasoning-effort", overrides.specReasoningEffort);
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
// Tool controls: override > session defaults
|
|
5412
|
+
const enabledTools = overrides?.enabledTools || this.options.enabledTools;
|
|
5413
|
+
if (enabledTools?.length) {
|
|
5414
|
+
args.push("--enabled-tools", enabledTools.join(","));
|
|
5415
|
+
}
|
|
5416
|
+
const disabledTools = overrides?.disabledTools || this.options.disabledTools;
|
|
5417
|
+
if (disabledTools?.length) {
|
|
5418
|
+
args.push("--disabled-tools", disabledTools.join(","));
|
|
5419
|
+
}
|
|
5420
|
+
// Use session-id for multi-turn conversation
|
|
5421
|
+
if (this.sessionId) {
|
|
5422
|
+
args.push("-s", this.sessionId);
|
|
5423
|
+
}
|
|
5424
|
+
// Prepend system prompt to the first message only (when no session exists yet)
|
|
5425
|
+
let finalPrompt = prompt;
|
|
5426
|
+
if (this.options.systemPrompt && !this.sessionId) {
|
|
5427
|
+
finalPrompt = `${this.options.systemPrompt}\n\n---\n\nTask: ${prompt}`;
|
|
5428
|
+
}
|
|
5429
|
+
args.push(finalPrompt);
|
|
5430
|
+
return args;
|
|
5431
|
+
}
|
|
5432
|
+
async *send(prompt, options) {
|
|
5433
|
+
const args = this.buildArgs(prompt, options);
|
|
5434
|
+
const cwd = this.options.cwd || process.cwd();
|
|
5435
|
+
const effectiveModel = options?.model || this.options.model || "default";
|
|
5436
|
+
const effectiveReasoning = options?.reasoningEffort || this.options.reasoningEffort || "default";
|
|
5437
|
+
const extras = [];
|
|
5438
|
+
if (options?.useSpec)
|
|
5439
|
+
extras.push("spec");
|
|
5440
|
+
if (options?.droid)
|
|
5441
|
+
extras.push(`droid=${options.droid}`);
|
|
5442
|
+
const extraInfo = extras.length ? ` [${extras.join(", ")}]` : "";
|
|
5443
|
+
console.log(`[DroidSession] Running (model=${effectiveModel}, reasoning=${effectiveReasoning})${extraInfo}:`, this.droidPath, args.slice(0, 5).join(" "), "...");
|
|
5444
|
+
const child = spawn(this.droidPath, args, {
|
|
5445
|
+
cwd,
|
|
5446
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5447
|
+
});
|
|
5448
|
+
this.parser.reset();
|
|
5449
|
+
let finalResult = null;
|
|
5450
|
+
let resolveNext = null;
|
|
5451
|
+
const eventQueue = [];
|
|
5452
|
+
const pushEvent = (event) => {
|
|
5453
|
+
if (resolveNext) {
|
|
5454
|
+
resolveNext(event);
|
|
5455
|
+
resolveNext = null;
|
|
5456
|
+
}
|
|
5457
|
+
else {
|
|
5458
|
+
eventQueue.push(event);
|
|
5459
|
+
}
|
|
5460
|
+
};
|
|
5461
|
+
child.stdout?.on("data", (data) => {
|
|
5462
|
+
const events = this.parser.parse(data.toString());
|
|
5463
|
+
for (const event of events) {
|
|
5464
|
+
// Capture session ID from init event
|
|
5465
|
+
if (event.type === "system" && event.subtype === "init") {
|
|
5466
|
+
this.sessionId = event.session_id;
|
|
5467
|
+
}
|
|
5468
|
+
this.emit("event", event);
|
|
5469
|
+
pushEvent(event);
|
|
5470
|
+
}
|
|
5471
|
+
});
|
|
5472
|
+
child.stderr?.on("data", (data) => {
|
|
5473
|
+
console.error("[DroidSession] stderr:", data.toString());
|
|
5474
|
+
});
|
|
5475
|
+
const processEnded = new Promise((resolve, reject) => {
|
|
5476
|
+
child.on("close", (code) => {
|
|
5477
|
+
// Flush any remaining data
|
|
5478
|
+
const remaining = this.parser.flush();
|
|
5479
|
+
for (const event of remaining) {
|
|
5480
|
+
pushEvent(event);
|
|
5481
|
+
}
|
|
5482
|
+
this.emit("close", code);
|
|
5483
|
+
resolve();
|
|
5484
|
+
});
|
|
5485
|
+
child.on("error", (err) => {
|
|
5486
|
+
console.error("[DroidSession] Process error:", err);
|
|
5487
|
+
this.emit("error", err);
|
|
5488
|
+
reject(err);
|
|
5489
|
+
});
|
|
5490
|
+
});
|
|
5491
|
+
const nextEvent = () => {
|
|
5492
|
+
if (eventQueue.length > 0) {
|
|
5493
|
+
return Promise.resolve(eventQueue.shift());
|
|
5494
|
+
}
|
|
5495
|
+
return new Promise((resolve) => {
|
|
5496
|
+
resolveNext = resolve;
|
|
5497
|
+
});
|
|
5498
|
+
};
|
|
5499
|
+
// Yield events until completion
|
|
5500
|
+
while (true) {
|
|
5501
|
+
const event = await Promise.race([
|
|
5502
|
+
nextEvent(),
|
|
5503
|
+
processEnded.then(() => null),
|
|
5504
|
+
]);
|
|
5505
|
+
if (!event) {
|
|
5506
|
+
// Process ended without completion event
|
|
5507
|
+
if (!finalResult) {
|
|
5508
|
+
throw new Error("Process ended without completion");
|
|
5509
|
+
}
|
|
5510
|
+
break;
|
|
5511
|
+
}
|
|
5512
|
+
yield event;
|
|
5513
|
+
if (event.type === "completion") {
|
|
5514
|
+
finalResult = {
|
|
5515
|
+
success: true,
|
|
5516
|
+
text: event.finalText,
|
|
5517
|
+
sessionId: event.session_id,
|
|
5518
|
+
numTurns: event.numTurns,
|
|
5519
|
+
durationMs: event.durationMs,
|
|
5520
|
+
};
|
|
5521
|
+
break;
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
return finalResult;
|
|
5525
|
+
}
|
|
5526
|
+
async close() {
|
|
5527
|
+
this.sessionId = null;
|
|
5528
|
+
this.startTime = null;
|
|
5529
|
+
this.parser.reset();
|
|
5530
|
+
}
|
|
5531
|
+
isAlive() {
|
|
5532
|
+
return this.sessionId !== null;
|
|
5533
|
+
}
|
|
5534
|
+
getSessionId() {
|
|
5535
|
+
return this.sessionId;
|
|
5536
|
+
}
|
|
5537
|
+
getUptime() {
|
|
5538
|
+
if (!this.startTime)
|
|
5539
|
+
return 0;
|
|
5540
|
+
return Date.now() - this.startTime;
|
|
5541
|
+
}
|
|
5542
|
+
getModel() {
|
|
5543
|
+
return this.options.model || "default";
|
|
5544
|
+
}
|
|
5545
|
+
getCwd() {
|
|
5546
|
+
return this.options.cwd || process.cwd();
|
|
5547
|
+
}
|
|
5548
|
+
updateOptions(options) {
|
|
5549
|
+
this.options = { ...this.options, ...options };
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
/**
|
|
5554
|
+
* Factory Droid SDK Integration
|
|
5555
|
+
*
|
|
5556
|
+
* This module provides integration with the Factory Droid SDK (droid-sdk)
|
|
5557
|
+
* for AI-powered builds. It wraps the DroidSession class and transforms
|
|
5558
|
+
* DroidEvents into the standard TransformedMessage format used by the runner.
|
|
5559
|
+
*
|
|
5560
|
+
* Enable by setting agent to 'factory-droid' in build requests.
|
|
5561
|
+
*/
|
|
5562
|
+
const debugLog$2 = (message) => {
|
|
5563
|
+
if (process.env.SILENT_MODE !== '1' && process.env.DEBUG_BUILD === '1') {
|
|
5564
|
+
console.log(message);
|
|
5565
|
+
}
|
|
5566
|
+
};
|
|
5567
|
+
/**
|
|
5568
|
+
* Transform a DroidEvent to our internal TransformedMessage format
|
|
5569
|
+
*/
|
|
5570
|
+
function transformDroidEvent(event) {
|
|
5571
|
+
switch (event.type) {
|
|
5572
|
+
case 'system': {
|
|
5573
|
+
// System init event - emit session info
|
|
5574
|
+
if (event.subtype === 'init') {
|
|
5575
|
+
return {
|
|
5576
|
+
type: 'system',
|
|
5577
|
+
subtype: 'init',
|
|
5578
|
+
session_id: event.session_id,
|
|
5579
|
+
};
|
|
5580
|
+
}
|
|
5581
|
+
return null;
|
|
5582
|
+
}
|
|
5583
|
+
case 'message': {
|
|
5584
|
+
// Message events from assistant
|
|
5585
|
+
if (event.role === 'assistant') {
|
|
5586
|
+
let text = event.text || '';
|
|
5587
|
+
// Filter out TodoWrite JSON that might be embedded in message text
|
|
5588
|
+
// The droid CLI sometimes echoes the todo list as JSON in its message
|
|
5589
|
+
if (text.includes('[{"activeForm"') || text.includes('[{"content"')) {
|
|
5590
|
+
// Try to remove JSON array that looks like todos
|
|
5591
|
+
text = text.replace(/\[(\s*\{[^[\]]*"(activeForm|content|status)"[^[\]]*\}\s*,?\s*)+\]/g, '').trim();
|
|
5592
|
+
}
|
|
5593
|
+
// Skip empty messages after filtering
|
|
5594
|
+
if (!text) {
|
|
5595
|
+
return null;
|
|
5596
|
+
}
|
|
5597
|
+
return {
|
|
5598
|
+
type: 'assistant',
|
|
5599
|
+
message: {
|
|
5600
|
+
id: event.id,
|
|
5601
|
+
content: [{ type: 'text', text }],
|
|
5602
|
+
},
|
|
5603
|
+
};
|
|
5604
|
+
}
|
|
5605
|
+
return null;
|
|
5606
|
+
}
|
|
5607
|
+
case 'tool_call': {
|
|
5608
|
+
// Tool call events - map to tool_use format
|
|
5609
|
+
let input = event.parameters;
|
|
5610
|
+
// Special handling for TodoWrite - droid CLI may send different formats,
|
|
5611
|
+
// but UI expects array format: { todos: [{ content, status, activeForm? }] }
|
|
5612
|
+
if (event.toolName === 'TodoWrite' && input) {
|
|
5613
|
+
const rawInput = input;
|
|
5614
|
+
// Case 1: todos is already an array (pass through with normalization)
|
|
5615
|
+
if (Array.isArray(rawInput.todos)) {
|
|
5616
|
+
const normalizedTodos = rawInput.todos.map((item) => ({
|
|
5617
|
+
content: item.content || item.activeForm || 'Untitled task',
|
|
5618
|
+
status: item.status || 'pending',
|
|
5619
|
+
activeForm: item.activeForm,
|
|
5620
|
+
}));
|
|
5621
|
+
input = { todos: normalizedTodos };
|
|
5622
|
+
process.stderr.write(`[runner] [droid-sdk] TodoWrite: ${normalizedTodos.length} todos (already array)\n`);
|
|
5623
|
+
}
|
|
5624
|
+
// Case 2: todos is a string (needs parsing)
|
|
5625
|
+
else if (typeof rawInput.todos === 'string') {
|
|
5626
|
+
const rawTodos = rawInput.todos;
|
|
5627
|
+
let parsedTodos = [];
|
|
5628
|
+
// Try to parse as JSON first (droid may send serialized array)
|
|
5629
|
+
const trimmed = rawTodos.trim();
|
|
5630
|
+
if (trimmed.startsWith('[')) {
|
|
5631
|
+
try {
|
|
5632
|
+
const jsonParsed = JSON.parse(trimmed);
|
|
5633
|
+
if (Array.isArray(jsonParsed)) {
|
|
5634
|
+
parsedTodos = jsonParsed.map((item) => ({
|
|
5635
|
+
content: item.content || item.activeForm || 'Untitled task',
|
|
5636
|
+
status: item.status || 'pending',
|
|
5637
|
+
activeForm: item.activeForm,
|
|
5638
|
+
}));
|
|
5639
|
+
process.stderr.write(`[runner] [droid-sdk] TodoWrite: ${parsedTodos.length} todos parsed from JSON string\n`);
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
catch {
|
|
5643
|
+
// Not valid JSON, fall through to line-by-line parsing
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
// If JSON parsing didn't work, try numbered format: "1. [completed] Task"
|
|
5647
|
+
if (parsedTodos.length === 0) {
|
|
5648
|
+
const lines = rawTodos.split('\n').filter((l) => l.trim());
|
|
5649
|
+
parsedTodos = lines.map((line) => {
|
|
5650
|
+
const match = line.match(/^\d+\.\s*\[(\w+)\]\s*(.+)$/);
|
|
5651
|
+
if (match) {
|
|
5652
|
+
return {
|
|
5653
|
+
content: match[2].trim(),
|
|
5654
|
+
status: match[1].toLowerCase(),
|
|
5655
|
+
};
|
|
5656
|
+
}
|
|
5657
|
+
return {
|
|
5658
|
+
content: line.replace(/^\d+\.\s*/, '').trim(),
|
|
5659
|
+
status: 'pending',
|
|
5660
|
+
};
|
|
5661
|
+
});
|
|
5662
|
+
process.stderr.write(`[runner] [droid-sdk] TodoWrite: ${parsedTodos.length} todos parsed from numbered format\n`);
|
|
5663
|
+
}
|
|
5664
|
+
input = { todos: parsedTodos };
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
return {
|
|
5668
|
+
type: 'assistant',
|
|
5669
|
+
message: {
|
|
5670
|
+
id: event.messageId,
|
|
5671
|
+
content: [{
|
|
5672
|
+
type: 'tool_use',
|
|
5673
|
+
id: event.toolId,
|
|
5674
|
+
name: event.toolName,
|
|
5675
|
+
input,
|
|
5676
|
+
}],
|
|
5677
|
+
},
|
|
5678
|
+
};
|
|
5679
|
+
}
|
|
5680
|
+
case 'tool_result': {
|
|
5681
|
+
// Tool result events - map to tool_result format
|
|
5682
|
+
return {
|
|
5683
|
+
type: 'user',
|
|
5684
|
+
message: {
|
|
5685
|
+
id: event.messageId,
|
|
5686
|
+
content: [{
|
|
5687
|
+
type: 'tool_result',
|
|
5688
|
+
tool_use_id: event.toolId,
|
|
5689
|
+
content: event.value,
|
|
5690
|
+
is_error: event.isError,
|
|
5691
|
+
}],
|
|
5692
|
+
},
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
case 'completion': {
|
|
5696
|
+
// Completion event - final result
|
|
5697
|
+
return {
|
|
5698
|
+
type: 'result',
|
|
5699
|
+
result: event.finalText,
|
|
5700
|
+
session_id: event.session_id,
|
|
5701
|
+
usage: {
|
|
5702
|
+
numTurns: event.numTurns,
|
|
5703
|
+
durationMs: event.durationMs,
|
|
5704
|
+
},
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
default:
|
|
5708
|
+
return null;
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
/**
|
|
5712
|
+
* Create a Factory Droid query function
|
|
5713
|
+
*
|
|
5714
|
+
* This implements the same BuildQueryFn interface as createNativeClaudeQuery
|
|
5715
|
+
* but uses the Factory Droid SDK instead.
|
|
5716
|
+
*/
|
|
5717
|
+
function createDroidQuery(modelId) {
|
|
5718
|
+
return async function* droidQuery(prompt, workingDirectory, systemPrompt, _agent, _codexThreadId, _messageParts) {
|
|
5719
|
+
debugLog$2('[runner] [droid-sdk] Starting Factory Droid query');
|
|
5720
|
+
debugLog$2(`[runner] [droid-sdk] Model: ${modelId || 'default'}`);
|
|
5721
|
+
debugLog$2(`[runner] [droid-sdk] Working dir: ${workingDirectory}`);
|
|
5722
|
+
debugLog$2(`[runner] [droid-sdk] Prompt length: ${prompt.length}`);
|
|
5723
|
+
// Build combined system prompt
|
|
5724
|
+
const systemPromptSegments = [CLAUDE_SYSTEM_PROMPT.trim()];
|
|
5725
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
5726
|
+
systemPromptSegments.push(systemPrompt.trim());
|
|
5727
|
+
}
|
|
5728
|
+
const combinedSystemPrompt = systemPromptSegments.join('\n\n');
|
|
5729
|
+
// Ensure working directory exists
|
|
5730
|
+
if (!existsSync(workingDirectory)) {
|
|
5731
|
+
console.log(`[droid-sdk] Creating working directory: ${workingDirectory}`);
|
|
5732
|
+
mkdirSync(workingDirectory, { recursive: true });
|
|
5733
|
+
}
|
|
5734
|
+
// Configure DroidSession options
|
|
5735
|
+
const sessionOptions = {
|
|
5736
|
+
model: modelId,
|
|
5737
|
+
cwd: workingDirectory,
|
|
5738
|
+
autonomyLevel: 'high',
|
|
5739
|
+
systemPrompt: combinedSystemPrompt,
|
|
5740
|
+
};
|
|
5741
|
+
// Force log to stderr so it shows in TUI mode
|
|
5742
|
+
process.stderr.write(`[runner] [droid-sdk] Creating DroidSession with model=${modelId || 'default'}, cwd=${workingDirectory}\n`);
|
|
5743
|
+
const session = new DroidSession(sessionOptions);
|
|
5744
|
+
// Listen for error events from the session (these bubble up from the child process)
|
|
5745
|
+
session.on('error', (err) => {
|
|
5746
|
+
process.stderr.write(`[runner] [droid-sdk] Session error event: ${err.message}\n`);
|
|
5747
|
+
if (err.stack) {
|
|
5748
|
+
process.stderr.write(`[runner] [droid-sdk] Stack: ${err.stack}\n`);
|
|
5749
|
+
}
|
|
5750
|
+
});
|
|
5751
|
+
session.on('close', (code) => {
|
|
5752
|
+
process.stderr.write(`[runner] [droid-sdk] Session closed with code: ${code}\n`);
|
|
5753
|
+
});
|
|
5754
|
+
let messageCount = 0;
|
|
5755
|
+
let toolCallCount = 0;
|
|
5756
|
+
let lastEventType = '';
|
|
5757
|
+
try {
|
|
5758
|
+
process.stderr.write(`[runner] [droid-sdk] Sending prompt to Droid (${prompt.length} chars)\n`);
|
|
5759
|
+
// Stream events from the Droid SDK
|
|
5760
|
+
for await (const event of session.send(prompt)) {
|
|
5761
|
+
messageCount++;
|
|
5762
|
+
lastEventType = event.type;
|
|
5763
|
+
// Log every event type to track progress
|
|
5764
|
+
if (event.type === 'tool_call') {
|
|
5765
|
+
toolCallCount++;
|
|
5766
|
+
process.stderr.write(`[runner] [droid-sdk] Event #${messageCount}: tool_call - ${event.toolName}\n`);
|
|
5767
|
+
}
|
|
5768
|
+
else if (event.type === 'tool_result') {
|
|
5769
|
+
const toolResult = event;
|
|
5770
|
+
const status = toolResult.isError ? 'ERROR' : 'OK';
|
|
5771
|
+
const preview = (toolResult.value || '').substring(0, 50);
|
|
5772
|
+
process.stderr.write(`[runner] [droid-sdk] Event #${messageCount}: tool_result [${status}] ${preview}...\n`);
|
|
5773
|
+
}
|
|
5774
|
+
else {
|
|
5775
|
+
process.stderr.write(`[runner] [droid-sdk] Event #${messageCount}: ${event.type}\n`);
|
|
5776
|
+
}
|
|
5777
|
+
// Transform Droid event to our internal format
|
|
5778
|
+
const transformed = transformDroidEvent(event);
|
|
5779
|
+
if (transformed) {
|
|
5780
|
+
yield transformed;
|
|
5781
|
+
}
|
|
5782
|
+
// Log completion
|
|
5783
|
+
if (event.type === 'completion') {
|
|
5784
|
+
process.stderr.write(`[runner] [droid-sdk] ✅ Query complete - ${event.numTurns} turns, ${event.durationMs}ms\n`);
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
process.stderr.write(`[runner] [droid-sdk] Stream finished - ${messageCount} events, ${toolCallCount} tool calls, last event: ${lastEventType}\n`);
|
|
5788
|
+
}
|
|
5789
|
+
catch (error) {
|
|
5790
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5791
|
+
const errorStack = error instanceof Error ? error.stack : '';
|
|
5792
|
+
// Force write to stderr to bypass TUI silent mode
|
|
5793
|
+
process.stderr.write(`\n[runner] [droid-sdk] ❌ Error: ${errorMessage}\n`);
|
|
5794
|
+
process.stderr.write(`[runner] [droid-sdk] Stack: ${errorStack}\n`);
|
|
5795
|
+
process.stderr.write(`[runner] [droid-sdk] Model: ${modelId || 'default'}\n`);
|
|
5796
|
+
process.stderr.write(`[runner] [droid-sdk] Working dir: ${workingDirectory}\n`);
|
|
5797
|
+
process.stderr.write(`[runner] [droid-sdk] Events received: ${messageCount}\n`);
|
|
5798
|
+
process.stderr.write(`[runner] [droid-sdk] Tool calls: ${toolCallCount}\n`);
|
|
5799
|
+
process.stderr.write(`[runner] [droid-sdk] Last event type: ${lastEventType}\n\n`);
|
|
5800
|
+
Sentry.captureException(error, {
|
|
5801
|
+
extra: {
|
|
5802
|
+
modelId,
|
|
5803
|
+
workingDirectory,
|
|
5804
|
+
promptLength: prompt.length,
|
|
5805
|
+
eventsReceived: messageCount,
|
|
5806
|
+
toolCalls: toolCallCount,
|
|
5807
|
+
}
|
|
5808
|
+
});
|
|
5809
|
+
throw error;
|
|
5810
|
+
}
|
|
5811
|
+
finally {
|
|
5812
|
+
// Cleanup session
|
|
5813
|
+
await session.close();
|
|
5814
|
+
}
|
|
5815
|
+
};
|
|
5816
|
+
}
|
|
5184
5817
|
|
|
5185
5818
|
// src/lib/logging/build-logger.ts
|
|
5186
5819
|
var BuildLogger = class {
|
|
@@ -5622,7 +6255,8 @@ var init_tags = __esm({
|
|
|
5622
6255
|
description: "Anthropic Claude - Balanced performance and speed",
|
|
5623
6256
|
logo: "/claude.png",
|
|
5624
6257
|
provider: "claude-code",
|
|
5625
|
-
model: "claude-sonnet-4-5"
|
|
6258
|
+
model: "claude-sonnet-4-5",
|
|
6259
|
+
sdk: "agent"
|
|
5626
6260
|
},
|
|
5627
6261
|
{
|
|
5628
6262
|
value: "claude-opus-4-5",
|
|
@@ -5630,7 +6264,8 @@ var init_tags = __esm({
|
|
|
5630
6264
|
description: "Anthropic Claude - Most capable for complex tasks",
|
|
5631
6265
|
logo: "/claude.png",
|
|
5632
6266
|
provider: "claude-code",
|
|
5633
|
-
model: "claude-opus-4-5"
|
|
6267
|
+
model: "claude-opus-4-5",
|
|
6268
|
+
sdk: "agent"
|
|
5634
6269
|
},
|
|
5635
6270
|
{
|
|
5636
6271
|
value: "claude-haiku-4-5",
|
|
@@ -5638,7 +6273,8 @@ var init_tags = __esm({
|
|
|
5638
6273
|
description: "Anthropic Claude - Fastest, good for iterations",
|
|
5639
6274
|
logo: "/claude.png",
|
|
5640
6275
|
provider: "claude-code",
|
|
5641
|
-
model: "claude-haiku-4-5"
|
|
6276
|
+
model: "claude-haiku-4-5",
|
|
6277
|
+
sdk: "agent"
|
|
5642
6278
|
},
|
|
5643
6279
|
{
|
|
5644
6280
|
value: "gpt-5.2-codex",
|
|
@@ -5646,7 +6282,35 @@ var init_tags = __esm({
|
|
|
5646
6282
|
description: "OpenAI Codex - Advanced code generation",
|
|
5647
6283
|
logo: "/openai.png",
|
|
5648
6284
|
provider: "openai-codex",
|
|
5649
|
-
model: "openai/gpt-5.2-codex"
|
|
6285
|
+
model: "openai/gpt-5.2-codex",
|
|
6286
|
+
sdk: "agent"
|
|
6287
|
+
},
|
|
6288
|
+
{
|
|
6289
|
+
value: "factory-droid-opus",
|
|
6290
|
+
label: "Factory Droid (Opus)",
|
|
6291
|
+
description: "Factory Droid SDK with Claude Opus 4.5",
|
|
6292
|
+
logo: "/factory.svg",
|
|
6293
|
+
provider: "factory-droid",
|
|
6294
|
+
model: "claude-opus-4-5-20251101",
|
|
6295
|
+
sdk: "droid"
|
|
6296
|
+
},
|
|
6297
|
+
{
|
|
6298
|
+
value: "factory-droid-codex",
|
|
6299
|
+
label: "Factory Droid (Codex)",
|
|
6300
|
+
description: "Factory Droid SDK with GPT-5.2 Codex",
|
|
6301
|
+
logo: "/factory.svg",
|
|
6302
|
+
provider: "factory-droid",
|
|
6303
|
+
model: "gpt-5.2-codex",
|
|
6304
|
+
sdk: "droid"
|
|
6305
|
+
},
|
|
6306
|
+
{
|
|
6307
|
+
value: "factory-droid-glm",
|
|
6308
|
+
label: "Factory Droid (GLM)",
|
|
6309
|
+
description: "Factory Droid SDK with GLM 4.7",
|
|
6310
|
+
logo: "/factory.svg",
|
|
6311
|
+
provider: "factory-droid",
|
|
6312
|
+
model: "glm-4.7",
|
|
6313
|
+
sdk: "droid"
|
|
5650
6314
|
}
|
|
5651
6315
|
]
|
|
5652
6316
|
},
|
|
@@ -6689,6 +7353,94 @@ var init_codex_strategy = __esm({
|
|
|
6689
7353
|
}
|
|
6690
7354
|
});
|
|
6691
7355
|
|
|
7356
|
+
// src/lib/agents/droid-strategy.ts
|
|
7357
|
+
var droid_strategy_exports = {};
|
|
7358
|
+
__export(droid_strategy_exports, {
|
|
7359
|
+
default: () => droid_strategy_default
|
|
7360
|
+
});
|
|
7361
|
+
function buildDroidSections(context) {
|
|
7362
|
+
const sections = [];
|
|
7363
|
+
if (context.tags && context.tags.length > 0) {
|
|
7364
|
+
const resolved = resolveTags(context.tags);
|
|
7365
|
+
const tagPrompt = generatePromptFromTags(resolved, context.projectName, context.isNewProject);
|
|
7366
|
+
if (tagPrompt) {
|
|
7367
|
+
sections.push(tagPrompt);
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
if (context.isNewProject) {
|
|
7371
|
+
sections.push(`## New Project Setup
|
|
7372
|
+
|
|
7373
|
+
- Project name: ${context.projectName}
|
|
7374
|
+
- Location: ${context.workingDirectory}
|
|
7375
|
+
- Operation type: ${context.operationType}
|
|
7376
|
+
|
|
7377
|
+
The template has already been downloaded. Install dependencies and customize the scaffold to satisfy the request.`);
|
|
7378
|
+
} else {
|
|
7379
|
+
let existingProjectSection = `## Existing Project Context
|
|
7380
|
+
|
|
7381
|
+
- Project location: ${context.workingDirectory}
|
|
7382
|
+
- Operation type: ${context.operationType}
|
|
7383
|
+
|
|
7384
|
+
Review the current codebase and apply the requested changes without re-scaffolding.`;
|
|
7385
|
+
if (context.conversationHistory && context.conversationHistory.length > 0) {
|
|
7386
|
+
existingProjectSection += `
|
|
7387
|
+
|
|
7388
|
+
**Recent Conversation History:**
|
|
7389
|
+
`;
|
|
7390
|
+
context.conversationHistory.forEach((msg, index) => {
|
|
7391
|
+
const roleLabel = msg.role === "user" ? "User" : "Assistant";
|
|
7392
|
+
const content = msg.content.length > 500 ? msg.content.substring(0, 500) + "...[truncated]" : msg.content;
|
|
7393
|
+
existingProjectSection += `${index + 1}. ${roleLabel}:
|
|
7394
|
+
${content}
|
|
7395
|
+
|
|
7396
|
+
`;
|
|
7397
|
+
});
|
|
7398
|
+
}
|
|
7399
|
+
sections.push(existingProjectSection);
|
|
7400
|
+
}
|
|
7401
|
+
sections.push(`## Workspace Rules
|
|
7402
|
+
- Use relative paths within the project.
|
|
7403
|
+
- Work inside the existing project structure.
|
|
7404
|
+
- Provide complete file contents without placeholders.`);
|
|
7405
|
+
if (context.fileTree) {
|
|
7406
|
+
sections.push(`## Project Structure
|
|
7407
|
+
${context.fileTree}`);
|
|
7408
|
+
}
|
|
7409
|
+
if (context.templateName) {
|
|
7410
|
+
sections.push(`## Template
|
|
7411
|
+
- Name: ${context.templateName}
|
|
7412
|
+
- Framework: ${context.templateFramework ?? "unknown"}`);
|
|
7413
|
+
}
|
|
7414
|
+
return sections;
|
|
7415
|
+
}
|
|
7416
|
+
function buildFullPrompt3(context, basePrompt) {
|
|
7417
|
+
if (!context.isNewProject) {
|
|
7418
|
+
return basePrompt;
|
|
7419
|
+
}
|
|
7420
|
+
return `${basePrompt}
|
|
7421
|
+
|
|
7422
|
+
CRITICAL: The template has already been prepared in ${context.workingDirectory}. Do not scaffold a new project.`;
|
|
7423
|
+
}
|
|
7424
|
+
var droidStrategy, droid_strategy_default;
|
|
7425
|
+
var init_droid_strategy = __esm({
|
|
7426
|
+
"src/lib/agents/droid-strategy.ts"() {
|
|
7427
|
+
init_resolver();
|
|
7428
|
+
droidStrategy = {
|
|
7429
|
+
buildSystemPromptSections: buildDroidSections,
|
|
7430
|
+
buildFullPrompt: buildFullPrompt3,
|
|
7431
|
+
shouldDownloadTemplate(context) {
|
|
7432
|
+
return context.isNewProject && !context.skipTemplates;
|
|
7433
|
+
},
|
|
7434
|
+
postTemplateSelected(context, template) {
|
|
7435
|
+
context.templateName = template.name;
|
|
7436
|
+
context.templateFramework = template.framework;
|
|
7437
|
+
context.fileTree = template.fileTree;
|
|
7438
|
+
}
|
|
7439
|
+
};
|
|
7440
|
+
droid_strategy_default = droidStrategy;
|
|
7441
|
+
}
|
|
7442
|
+
});
|
|
7443
|
+
|
|
6692
7444
|
// src/lib/agents/strategy.ts
|
|
6693
7445
|
var strategyRegistry = /* @__PURE__ */ new Map();
|
|
6694
7446
|
function registerAgentStrategy(agentId, strategy) {
|
|
@@ -6708,12 +7460,18 @@ async function ensureRegistry() {
|
|
|
6708
7460
|
if (initialized) {
|
|
6709
7461
|
return;
|
|
6710
7462
|
}
|
|
6711
|
-
const [
|
|
7463
|
+
const [
|
|
7464
|
+
{ default: claudeStrategy2 },
|
|
7465
|
+
{ default: codexStrategy2 },
|
|
7466
|
+
{ default: droidStrategy2 }
|
|
7467
|
+
] = await Promise.all([
|
|
6712
7468
|
Promise.resolve().then(() => (init_claude_strategy(), claude_strategy_exports)),
|
|
6713
|
-
Promise.resolve().then(() => (init_codex_strategy(), codex_strategy_exports))
|
|
7469
|
+
Promise.resolve().then(() => (init_codex_strategy(), codex_strategy_exports)),
|
|
7470
|
+
Promise.resolve().then(() => (init_droid_strategy(), droid_strategy_exports))
|
|
6714
7471
|
]);
|
|
6715
7472
|
registerAgentStrategy("claude-code", claudeStrategy2);
|
|
6716
7473
|
registerAgentStrategy("openai-codex", codexStrategy2);
|
|
7474
|
+
registerAgentStrategy("factory-droid", droidStrategy2);
|
|
6717
7475
|
initialized = true;
|
|
6718
7476
|
}
|
|
6719
7477
|
async function resolveAgentStrategy(agentId) {
|
|
@@ -9264,7 +10022,8 @@ var TAG_DEFINITIONS = [
|
|
|
9264
10022
|
description: "Anthropic Claude - Balanced performance and speed",
|
|
9265
10023
|
logo: "/claude.png",
|
|
9266
10024
|
provider: "claude-code",
|
|
9267
|
-
model: "claude-sonnet-4-5"
|
|
10025
|
+
model: "claude-sonnet-4-5",
|
|
10026
|
+
sdk: "agent"
|
|
9268
10027
|
},
|
|
9269
10028
|
{
|
|
9270
10029
|
value: "claude-opus-4-5",
|
|
@@ -9272,7 +10031,8 @@ var TAG_DEFINITIONS = [
|
|
|
9272
10031
|
description: "Anthropic Claude - Most capable for complex tasks",
|
|
9273
10032
|
logo: "/claude.png",
|
|
9274
10033
|
provider: "claude-code",
|
|
9275
|
-
model: "claude-opus-4-5"
|
|
10034
|
+
model: "claude-opus-4-5",
|
|
10035
|
+
sdk: "agent"
|
|
9276
10036
|
},
|
|
9277
10037
|
{
|
|
9278
10038
|
value: "claude-haiku-4-5",
|
|
@@ -9280,7 +10040,8 @@ var TAG_DEFINITIONS = [
|
|
|
9280
10040
|
description: "Anthropic Claude - Fastest, good for iterations",
|
|
9281
10041
|
logo: "/claude.png",
|
|
9282
10042
|
provider: "claude-code",
|
|
9283
|
-
model: "claude-haiku-4-5"
|
|
10043
|
+
model: "claude-haiku-4-5",
|
|
10044
|
+
sdk: "agent"
|
|
9284
10045
|
},
|
|
9285
10046
|
{
|
|
9286
10047
|
value: "gpt-5.2-codex",
|
|
@@ -9288,7 +10049,35 @@ var TAG_DEFINITIONS = [
|
|
|
9288
10049
|
description: "OpenAI Codex - Advanced code generation",
|
|
9289
10050
|
logo: "/openai.png",
|
|
9290
10051
|
provider: "openai-codex",
|
|
9291
|
-
model: "openai/gpt-5.2-codex"
|
|
10052
|
+
model: "openai/gpt-5.2-codex",
|
|
10053
|
+
sdk: "agent"
|
|
10054
|
+
},
|
|
10055
|
+
{
|
|
10056
|
+
value: "factory-droid-opus",
|
|
10057
|
+
label: "Factory Droid (Opus)",
|
|
10058
|
+
description: "Factory Droid SDK with Claude Opus 4.5",
|
|
10059
|
+
logo: "/factory.svg",
|
|
10060
|
+
provider: "factory-droid",
|
|
10061
|
+
model: "claude-opus-4-5-20251101",
|
|
10062
|
+
sdk: "droid"
|
|
10063
|
+
},
|
|
10064
|
+
{
|
|
10065
|
+
value: "factory-droid-codex",
|
|
10066
|
+
label: "Factory Droid (Codex)",
|
|
10067
|
+
description: "Factory Droid SDK with GPT-5.2 Codex",
|
|
10068
|
+
logo: "/factory.svg",
|
|
10069
|
+
provider: "factory-droid",
|
|
10070
|
+
model: "gpt-5.2-codex",
|
|
10071
|
+
sdk: "droid"
|
|
10072
|
+
},
|
|
10073
|
+
{
|
|
10074
|
+
value: "factory-droid-glm",
|
|
10075
|
+
label: "Factory Droid (GLM)",
|
|
10076
|
+
description: "Factory Droid SDK with GLM 4.7",
|
|
10077
|
+
logo: "/factory.svg",
|
|
10078
|
+
provider: "factory-droid",
|
|
10079
|
+
model: "glm-4.7",
|
|
10080
|
+
sdk: "droid"
|
|
9292
10081
|
}
|
|
9293
10082
|
]
|
|
9294
10083
|
},
|
|
@@ -10312,9 +11101,18 @@ function createCodexQuery() {
|
|
|
10312
11101
|
fileLog.info(`Total turns: ${turnCount}`);
|
|
10313
11102
|
};
|
|
10314
11103
|
}
|
|
11104
|
+
/**
|
|
11105
|
+
* SDK Feature Flags (environment variables)
|
|
11106
|
+
*
|
|
11107
|
+
* - ENABLE_OPENCODE_SDK: Imported from opencode-sdk.ts, enables OpenCode multi-provider routing
|
|
11108
|
+
* - ENABLE_FACTORY_SDK: Enables Factory Droid SDK for builds
|
|
11109
|
+
*
|
|
11110
|
+
* By default (no flags set), only Agent SDK (Claude + Codex) is available.
|
|
11111
|
+
*/
|
|
11112
|
+
const ENABLE_FACTORY_SDK = process.env.ENABLE_FACTORY_SDK === 'true';
|
|
10315
11113
|
function createBuildQuery(agent, modelId, abortController) {
|
|
10316
11114
|
// When OpenCode SDK is enabled, route ALL requests through it (including Codex)
|
|
10317
|
-
if (
|
|
11115
|
+
if (ENABLE_OPENCODE_SDK) {
|
|
10318
11116
|
// Map openai-codex agent to the correct OpenCode model
|
|
10319
11117
|
const normalizedModel = agent === "openai-codex"
|
|
10320
11118
|
? "openai/gpt-5.2-codex"
|
|
@@ -10327,6 +11125,16 @@ function createBuildQuery(agent, modelId, abortController) {
|
|
|
10327
11125
|
console.log('[runner] 🔄 Using direct Codex SDK (fallback mode)');
|
|
10328
11126
|
return createCodexQuery();
|
|
10329
11127
|
}
|
|
11128
|
+
// Factory Droid SDK - uses the droid CLI for builds (only if enabled)
|
|
11129
|
+
if (agent === "factory-droid") {
|
|
11130
|
+
if (!ENABLE_FACTORY_SDK) {
|
|
11131
|
+
console.warn('[runner] ⚠️ Factory Droid requested but ENABLE_FACTORY_SDK is not set');
|
|
11132
|
+
console.warn('[runner] ⚠️ Falling back to native Claude Agent SDK');
|
|
11133
|
+
return createNativeClaudeQuery(DEFAULT_CLAUDE_MODEL_ID, abortController);
|
|
11134
|
+
}
|
|
11135
|
+
console.log('[runner] 🔄 Using Factory Droid SDK');
|
|
11136
|
+
return createDroidQuery(modelId);
|
|
11137
|
+
}
|
|
10330
11138
|
// Default: Use native Claude Agent SDK (direct integration)
|
|
10331
11139
|
console.log('[runner] 🔄 Using NATIVE Claude Agent SDK');
|
|
10332
11140
|
return createNativeClaudeQuery(modelId ?? DEFAULT_CLAUDE_MODEL_ID, abortController);
|
|
@@ -10839,6 +11647,9 @@ async function startRunner(options = {}) {
|
|
|
10839
11647
|
}
|
|
10840
11648
|
else if (event.type === "build-failed") {
|
|
10841
11649
|
log(`❌ Build failed: ${event.error}`);
|
|
11650
|
+
// Debug: log stack trace to see where this was triggered from
|
|
11651
|
+
process.stderr.write(`[runner] BUILD-FAILED triggered. Error: ${event.error}\n`);
|
|
11652
|
+
process.stderr.write(`[runner] Stack trace at send time:\n${new Error().stack}\n`);
|
|
10842
11653
|
}
|
|
10843
11654
|
// Suppress: build-stream, runner-status, ack, etc.
|
|
10844
11655
|
if (socket && socket.readyState === WebSocket$1.OPEN) {
|
|
@@ -11617,7 +12428,7 @@ async function startRunner(options = {}) {
|
|
|
11617
12428
|
log("project name:", projectName);
|
|
11618
12429
|
// Determine agent to use for this build
|
|
11619
12430
|
const agent = command.payload.agent ?? DEFAULT_AGENT;
|
|
11620
|
-
const agentLabel = agent === "openai-codex" ? "Codex" : "Claude";
|
|
12431
|
+
const agentLabel = agent === "openai-codex" ? "Codex" : (agent === "factory-droid" ? "Droid" : "Claude");
|
|
11621
12432
|
log("selected agent:", agent);
|
|
11622
12433
|
const claudeModel = agent === "claude-code" &&
|
|
11623
12434
|
(command.payload.claudeModel === "claude-haiku-4-5" ||
|
|
@@ -11625,6 +12436,8 @@ async function startRunner(options = {}) {
|
|
|
11625
12436
|
command.payload.claudeModel === "claude-opus-4-5")
|
|
11626
12437
|
? command.payload.claudeModel
|
|
11627
12438
|
: DEFAULT_CLAUDE_MODEL_ID;
|
|
12439
|
+
// For factory-droid, use the droidModel from payload
|
|
12440
|
+
const droidModel = agent === "factory-droid" ? command.payload.droidModel : undefined;
|
|
11628
12441
|
// Create AbortController for cancellation support
|
|
11629
12442
|
const buildAbortController = new AbortController();
|
|
11630
12443
|
// Register build context for HTTP persistence
|
|
@@ -11646,7 +12459,12 @@ async function startRunner(options = {}) {
|
|
|
11646
12459
|
if (agent === "claude-code") {
|
|
11647
12460
|
log("claude model:", claudeModel);
|
|
11648
12461
|
}
|
|
11649
|
-
|
|
12462
|
+
else if (agent === "factory-droid") {
|
|
12463
|
+
log("droid model:", droidModel || "default");
|
|
12464
|
+
}
|
|
12465
|
+
// Select the appropriate model for the agent
|
|
12466
|
+
const modelId = agent === "factory-droid" ? droidModel : claudeModel;
|
|
12467
|
+
const agentQuery = createBuildQuery(agent, modelId, buildAbortController);
|
|
11650
12468
|
// Reset transformer state for new build
|
|
11651
12469
|
resetTransformerState();
|
|
11652
12470
|
setExpectedCwd(projectDirectory);
|
|
@@ -11898,18 +12716,48 @@ async function startRunner(options = {}) {
|
|
|
11898
12716
|
}
|
|
11899
12717
|
// Track completed todos for summary context
|
|
11900
12718
|
if (toolEvent.toolName === 'TodoWrite' && toolEvent.input) {
|
|
11901
|
-
const
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
12719
|
+
const rawInput = toolEvent.input;
|
|
12720
|
+
// Handle both formats:
|
|
12721
|
+
// 1. Droid CLI format: string with numbered list like "1. [completed] Task"
|
|
12722
|
+
// 2. Claude format: array of objects with content, status, etc.
|
|
12723
|
+
let todoItems = [];
|
|
12724
|
+
if (typeof rawInput.todos === 'string') {
|
|
12725
|
+
// Parse droid CLI string format: "1. [completed] First task\n2. [in_progress] Second task"
|
|
12726
|
+
const lines = rawInput.todos.split('\n').filter(l => l.trim());
|
|
12727
|
+
todoItems = lines.map((line, idx) => {
|
|
12728
|
+
// Match patterns like "1. [completed] Task content" or "1. [in_progress] Task"
|
|
12729
|
+
const match = line.match(/^\d+\.\s*\[(\w+)\]\s*(.+)$/);
|
|
12730
|
+
if (match) {
|
|
12731
|
+
const [, statusStr, content] = match;
|
|
12732
|
+
const status = statusStr;
|
|
12733
|
+
return {
|
|
12734
|
+
id: `todo-${Date.now()}-${idx}`,
|
|
12735
|
+
content: content.trim(),
|
|
12736
|
+
status,
|
|
12737
|
+
};
|
|
12738
|
+
}
|
|
12739
|
+
// Fallback: treat line as pending task
|
|
12740
|
+
return {
|
|
12741
|
+
id: `todo-${Date.now()}-${idx}`,
|
|
12742
|
+
content: line.replace(/^\d+\.\s*/, '').trim(),
|
|
12743
|
+
status: 'pending',
|
|
12744
|
+
};
|
|
12745
|
+
});
|
|
12746
|
+
}
|
|
12747
|
+
else if (Array.isArray(rawInput.todos)) {
|
|
12748
|
+
// Claude format: array of objects
|
|
12749
|
+
todoItems = rawInput.todos.map(t => ({
|
|
11905
12750
|
id: t.id || `todo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
11906
12751
|
content: t.content,
|
|
11907
12752
|
status: t.status,
|
|
11908
12753
|
priority: t.priority,
|
|
11909
12754
|
}));
|
|
12755
|
+
}
|
|
12756
|
+
if (todoItems.length > 0) {
|
|
12757
|
+
// Update the logger's todo list for the build panel
|
|
11910
12758
|
logger.updateTodos(todoItems);
|
|
11911
12759
|
// Get completed todos (excluding any "summarize" todos from old prompts)
|
|
11912
|
-
const completed =
|
|
12760
|
+
const completed = todoItems
|
|
11913
12761
|
.filter(t => t.status === 'completed' && !t.content.toLowerCase().includes('summarize'))
|
|
11914
12762
|
.map(t => t.content);
|
|
11915
12763
|
// Update our list with the latest completed todos
|