@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/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 [{ default: claudeStrategy2 }, { default: codexStrategy2 }] = await Promise.all([
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 USE_OPENCODE_SDK=1 to use opencode-sdk.ts instead.
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$3 = (message) => {
4691
+ const debugLog$4 = (message) => {
4567
4692
  if (process.env.SILENT_MODE !== '1' && process.env.DEBUG_BUILD === '1') {
4568
- debugLog$3();
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$3();
4688
- debugLog$3();
4689
- debugLog$3();
4690
- debugLog$3(`[runner] [native-sdk] Prompt length: ${prompt.length}\n`);
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$3();
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$3();
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$3(`[runner] [native-sdk] 🔧 Tool call: ${block.name}\n`);
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$3(`[runner] [native-sdk] ✅ Query complete - ${sdkMessage.num_turns} turns, $${sdkMessage.total_cost_usd?.toFixed(4)} USD\n`);
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$3(`[runner] [native-sdk] ⚠️ Query ended with: ${sdkMessage.subtype}\n`);
4897
+ debugLog$4(`[runner] [native-sdk] ⚠️ Query ended with: ${sdkMessage.subtype}\n`);
4773
4898
  }
4774
4899
  }
4775
4900
  }
4776
- debugLog$3(`[runner] [native-sdk] 📊 Stream complete - ${messageCount} messages, ${toolCallCount} tool calls, ${textBlockCount} text blocks\n`);
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$3(`[runner] [native-sdk] ❌ Error: ${error instanceof Error ? error.message : String(error)}\n`);
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$2 = (message) => {
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$2('[runner] [opencode-sdk] Starting OpenCode query');
4965
- debugLog$2(`[runner] [opencode-sdk] Model: ${modelId}`);
4966
- debugLog$2(`[runner] [opencode-sdk] Working dir: ${workingDirectory}`);
4967
- debugLog$2(`[runner] [opencode-sdk] Prompt length: ${prompt.length}`);
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$2('[runner] [opencode-sdk] Creating session...');
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$2(`[runner] [opencode-sdk] Session created: ${sessionId}`);
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$2('[runner] [opencode-sdk] Subscribing to events...');
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$2('[runner] [opencode-sdk] Added image part');
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$2('[runner] [opencode-sdk] Sending prompt...');
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$2('[runner] [opencode-sdk] Event stream ended');
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$2('[runner] [opencode-sdk] Query complete');
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$2(`[runner] [opencode-sdk] Error: ${error instanceof Error ? error.message : String(error)}`);
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 USE_OPENCODE_SDK=1 to enable OpenCode multi-provider support
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 USE_OPENCODE_SDK = process.env.USE_OPENCODE_SDK === '1' && !!process.env.OPENCODE_URL;
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 [{ default: claudeStrategy2 }, { default: codexStrategy2 }] = await Promise.all([
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 (USE_OPENCODE_SDK) {
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
- const agentQuery = createBuildQuery(agent, claudeModel, buildAbortController);
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 input = toolEvent.input;
11902
- if (input.todos) {
11903
- // Update the logger's todo list for the build panel
11904
- const todoItems = input.todos.map(t => ({
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 = input.todos
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