@joshski/dust 0.1.79 → 0.1.81

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/dust.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // lib/cli/run.ts
6
- import { existsSync, statSync as statSync3 } from "node:fs";
6
+ import { existsSync as existsSync2, statSync as statSync3, writeSync } from "node:fs";
7
7
  import {
8
8
  chmod as chmod3,
9
9
  mkdir as mkdir3,
@@ -13,6 +13,18 @@ import {
13
13
  writeFile as writeFile3
14
14
  } from "node:fs/promises";
15
15
 
16
+ // lib/command-events.ts
17
+ function createEventEmitter(writeEvent) {
18
+ let sequence = 0;
19
+ return (event) => {
20
+ writeEvent({
21
+ sequence: sequence++,
22
+ timestamp: new Date().toISOString(),
23
+ event
24
+ });
25
+ };
26
+ }
27
+
16
28
  // lib/git/file-sorter.ts
17
29
  function createGitDirectoryFileSorter(gitRunner) {
18
30
  return async (dir, files) => {
@@ -306,7 +318,7 @@ async function loadSettings(cwd, fileSystem) {
306
318
  }
307
319
 
308
320
  // lib/version.ts
309
- var DUST_VERSION = "0.1.79";
321
+ var DUST_VERSION = "0.1.81";
310
322
 
311
323
  // lib/session.ts
312
324
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -1664,7 +1676,7 @@ async function audit(dependencies) {
1664
1676
  }
1665
1677
 
1666
1678
  // lib/cli/commands/bucket.ts
1667
- import { spawn as nodeSpawn4 } from "node:child_process";
1679
+ import { spawn as nodeSpawn5 } from "node:child_process";
1668
1680
  import { accessSync, statSync } from "node:fs";
1669
1681
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1670
1682
  import { homedir } from "node:os";
@@ -1710,9 +1722,9 @@ async function clearToken(fileSystem, homeDir) {
1710
1722
  throw error;
1711
1723
  }
1712
1724
  }
1713
- async function defaultExchangeCode(code) {
1725
+ async function defaultExchangeCode(code, fetchFn = fetch) {
1714
1726
  const host = getDustbucketHost();
1715
- const response = await fetch(`${host}/auth/cli/exchange`, {
1727
+ const response = await fetchFn(`${host}/auth/cli/exchange`, {
1716
1728
  method: "POST",
1717
1729
  headers: { "Content-Type": "application/json" },
1718
1730
  body: JSON.stringify({ code })
@@ -2021,6 +2033,31 @@ var defaultDependencies = {
2021
2033
  spawn: nodeSpawn2,
2022
2034
  createInterface: nodeCreateInterface
2023
2035
  };
2036
+ function buildDockerRunArguments(docker, claudeArguments, env) {
2037
+ const dockerArguments = [
2038
+ "run",
2039
+ "--rm",
2040
+ "-i",
2041
+ "-v",
2042
+ `${docker.repoPath}:/workspace`,
2043
+ "-w",
2044
+ "/workspace",
2045
+ "-v",
2046
+ `${docker.homeDir}/.claude:/root/.claude:ro`,
2047
+ "-v",
2048
+ `${docker.homeDir}/.ssh:/root/.ssh:ro`
2049
+ ];
2050
+ if (docker.hasGitconfig) {
2051
+ dockerArguments.push("-v", `${docker.homeDir}/.gitconfig:/root/.gitconfig:ro`);
2052
+ }
2053
+ for (const [key, value] of Object.entries(env)) {
2054
+ dockerArguments.push("-e", `${key}=${value}`);
2055
+ }
2056
+ dockerArguments.push(docker.imageTag);
2057
+ dockerArguments.push("claude");
2058
+ dockerArguments.push(...claudeArguments);
2059
+ return dockerArguments;
2060
+ }
2024
2061
  async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDependencies) {
2025
2062
  const {
2026
2063
  cwd,
@@ -2031,7 +2068,8 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
2031
2068
  sessionId,
2032
2069
  dangerouslySkipPermissions,
2033
2070
  env,
2034
- signal
2071
+ signal,
2072
+ docker
2035
2073
  } = options;
2036
2074
  const claudeArguments = [
2037
2075
  "-p",
@@ -2059,10 +2097,15 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
2059
2097
  if (dangerouslySkipPermissions) {
2060
2098
  claudeArguments.push("--dangerously-skip-permissions");
2061
2099
  }
2062
- const proc = dependencies.spawn("claude", claudeArguments, {
2100
+ const mergedEnv = { ...process.env, ...env };
2101
+ const proc = docker ? dependencies.spawn("docker", buildDockerRunArguments(docker, claudeArguments, env ?? {}), {
2063
2102
  cwd,
2064
2103
  stdio: ["ignore", "pipe", "pipe"],
2065
- env: { ...process.env, ...env }
2104
+ env: mergedEnv
2105
+ }) : dependencies.spawn("claude", claudeArguments, {
2106
+ cwd,
2107
+ stdio: ["ignore", "pipe", "pipe"],
2108
+ env: mergedEnv
2066
2109
  });
2067
2110
  if (!proc.stdout) {
2068
2111
  throw new Error("Failed to get stdout from claude process");
@@ -2438,30 +2481,45 @@ function getRepoPath(repoName, reposDir) {
2438
2481
  const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
2439
2482
  return join7(reposDir, safeName);
2440
2483
  }
2441
- async function cloneRepository(repository, targetPath, spawn, context) {
2484
+ function cloneWithUrl(url, targetPath, spawn) {
2442
2485
  return new Promise((resolve) => {
2443
- const proc = spawn("git", ["clone", repository.gitUrl, targetPath], {
2486
+ const proc = spawn("git", ["clone", url, targetPath], {
2444
2487
  stdio: ["ignore", "pipe", "pipe"],
2445
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
2488
+ env: {
2489
+ ...process.env,
2490
+ GIT_TERMINAL_PROMPT: "0",
2491
+ GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=accept-new"
2492
+ }
2446
2493
  });
2447
2494
  let stderr = "";
2448
2495
  proc.stderr?.on("data", (data) => {
2449
2496
  stderr += data.toString();
2450
2497
  });
2451
2498
  proc.on("close", (code) => {
2452
- if (code === 0) {
2453
- resolve(true);
2454
- } else {
2455
- context.stderr(`Failed to clone ${repository.name}: ${stderr.trim()}`);
2456
- resolve(false);
2457
- }
2499
+ resolve({ success: code === 0, stderr: stderr.trim() });
2458
2500
  });
2459
2501
  proc.on("error", (error) => {
2460
- context.stderr(`Failed to clone ${repository.name}: ${error.message}`);
2461
- resolve(false);
2502
+ resolve({ success: false, stderr: error.message });
2462
2503
  });
2463
2504
  });
2464
2505
  }
2506
+ async function cloneRepository(repository, targetPath, spawn, context) {
2507
+ const httpsResult = await cloneWithUrl(repository.gitUrl, targetPath, spawn);
2508
+ if (httpsResult.success) {
2509
+ return true;
2510
+ }
2511
+ if (repository.gitSshUrl) {
2512
+ context.stderr(`HTTPS clone failed for ${repository.name}, trying SSH: ${httpsResult.stderr}`);
2513
+ const sshResult = await cloneWithUrl(repository.gitSshUrl, targetPath, spawn);
2514
+ if (sshResult.success) {
2515
+ return true;
2516
+ }
2517
+ context.stderr(`Failed to clone ${repository.name} via SSH: ${sshResult.stderr}`);
2518
+ return false;
2519
+ }
2520
+ context.stderr(`Failed to clone ${repository.name}: ${httpsResult.stderr}`);
2521
+ return false;
2522
+ }
2465
2523
  async function removeRepository(path, spawn, context) {
2466
2524
  return new Promise((resolve) => {
2467
2525
  const proc = spawn("rm", ["-rf", path], {
@@ -2522,7 +2580,66 @@ function formatAgentEvent(event) {
2522
2580
 
2523
2581
  // lib/cli/commands/loop.ts
2524
2582
  import { spawn as nodeSpawn3 } from "node:child_process";
2583
+ import { existsSync } from "node:fs";
2525
2584
  import os from "node:os";
2585
+ import path2 from "node:path";
2586
+
2587
+ // lib/docker/docker-agent.ts
2588
+ import path from "node:path";
2589
+ var log = createLogger("dust:docker:agent");
2590
+ async function isDockerAvailable(dependencies) {
2591
+ return new Promise((resolve) => {
2592
+ const proc = dependencies.spawn("docker", ["--version"], {
2593
+ stdio: ["ignore", "pipe", "pipe"]
2594
+ });
2595
+ proc.on("close", (code) => {
2596
+ resolve(code === 0);
2597
+ });
2598
+ proc.on("error", () => {
2599
+ resolve(false);
2600
+ });
2601
+ });
2602
+ }
2603
+ function generateImageTag(repoPath) {
2604
+ const repoName = path.basename(repoPath);
2605
+ const sanitized = repoName.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
2606
+ return `dust-agent-${sanitized}`;
2607
+ }
2608
+ async function buildDockerImage(config, dependencies) {
2609
+ const dockerfilePath = path.join(config.repoPath, ".dust", "Dockerfile");
2610
+ log(`building Docker image ${config.imageTag} from ${dockerfilePath}`);
2611
+ return new Promise((resolve) => {
2612
+ const proc = dependencies.spawn("docker", ["build", "-t", config.imageTag, "-f", dockerfilePath, config.repoPath], {
2613
+ stdio: ["ignore", "pipe", "pipe"]
2614
+ });
2615
+ let stderr = "";
2616
+ proc.stderr?.on("data", (data) => {
2617
+ stderr += data.toString();
2618
+ });
2619
+ proc.on("close", (code) => {
2620
+ if (code === 0) {
2621
+ log(`Docker image ${config.imageTag} built successfully`);
2622
+ resolve({ success: true });
2623
+ } else {
2624
+ log(`Docker build failed: ${stderr}`);
2625
+ resolve({
2626
+ success: false,
2627
+ error: `Docker build failed with exit code ${code}: ${stderr.trim()}`
2628
+ });
2629
+ }
2630
+ });
2631
+ proc.on("error", (error) => {
2632
+ resolve({
2633
+ success: false,
2634
+ error: `Docker build failed: ${error.message}`
2635
+ });
2636
+ });
2637
+ });
2638
+ }
2639
+ function hasDockerfile(repoPath, dependencies) {
2640
+ const dockerfilePath = path.join(repoPath, ".dust", "Dockerfile");
2641
+ return dependencies.existsSync(dockerfilePath);
2642
+ }
2526
2643
 
2527
2644
  // lib/artifacts/workflow-tasks.ts
2528
2645
  var IDEA_TRANSITION_PREFIXES = [
@@ -2530,10 +2647,79 @@ var IDEA_TRANSITION_PREFIXES = [
2530
2647
  "Decompose Idea: ",
2531
2648
  "Shelve Idea: "
2532
2649
  ];
2650
+ var CAPTURE_IDEA_PREFIX = "Add Idea: ";
2533
2651
  var EXPEDITE_IDEA_PREFIX = "Expedite Idea: ";
2534
2652
  function titleToFilename(title) {
2535
2653
  return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
2536
2654
  }
2655
+ var WORKFLOW_SECTION_HEADINGS = [
2656
+ { type: "refine", heading: "Refines Idea" },
2657
+ { type: "decompose-idea", heading: "Decomposes Idea" },
2658
+ { type: "shelve", heading: "Shelves Idea" }
2659
+ ];
2660
+ function extractIdeaSlugFromSection(content, sectionHeading) {
2661
+ const lines = content.split(`
2662
+ `);
2663
+ let inSection = false;
2664
+ for (const line of lines) {
2665
+ if (line.startsWith("## ")) {
2666
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
2667
+ continue;
2668
+ }
2669
+ if (!inSection)
2670
+ continue;
2671
+ if (line.startsWith("# "))
2672
+ break;
2673
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
2674
+ if (linkMatch) {
2675
+ const target = linkMatch[2];
2676
+ const slugMatch = target.match(/([^/]+)\.md$/);
2677
+ if (slugMatch) {
2678
+ return slugMatch[1];
2679
+ }
2680
+ }
2681
+ }
2682
+ return null;
2683
+ }
2684
+ async function findAllWorkflowTasks(fileSystem, dustPath) {
2685
+ const tasksPath = `${dustPath}/tasks`;
2686
+ const captureIdeaTasks = [];
2687
+ const workflowTasksByIdeaSlug = new Map;
2688
+ if (!fileSystem.exists(tasksPath)) {
2689
+ return { captureIdeaTasks, workflowTasksByIdeaSlug };
2690
+ }
2691
+ const files = await fileSystem.readdir(tasksPath);
2692
+ for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
2693
+ const content = await fileSystem.readFile(`${tasksPath}/${file}`);
2694
+ const titleMatch = content.match(/^#\s+(.+)$/m);
2695
+ if (!titleMatch)
2696
+ continue;
2697
+ const title = titleMatch[1].trim();
2698
+ const taskSlug = file.replace(/\.md$/, "");
2699
+ if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
2700
+ captureIdeaTasks.push({
2701
+ taskSlug,
2702
+ ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
2703
+ });
2704
+ } else if (title.startsWith(EXPEDITE_IDEA_PREFIX)) {
2705
+ captureIdeaTasks.push({
2706
+ taskSlug,
2707
+ ideaTitle: title.slice(EXPEDITE_IDEA_PREFIX.length)
2708
+ });
2709
+ }
2710
+ for (const { type, heading } of WORKFLOW_SECTION_HEADINGS) {
2711
+ const linkedSlug = extractIdeaSlugFromSection(content, heading);
2712
+ if (linkedSlug) {
2713
+ workflowTasksByIdeaSlug.set(linkedSlug, {
2714
+ type,
2715
+ ideaSlug: linkedSlug,
2716
+ taskSlug
2717
+ });
2718
+ }
2719
+ }
2720
+ }
2721
+ return { captureIdeaTasks, workflowTasksByIdeaSlug };
2722
+ }
2537
2723
 
2538
2724
  // lib/cli/commands/focus.ts
2539
2725
  function buildImplementationInstructions(bin, hooksInstalled, taskTitle, taskPath, installCommand) {
@@ -2669,9 +2855,25 @@ async function next(dependencies) {
2669
2855
  return { exitCode: 1 };
2670
2856
  }
2671
2857
  if (result.tasks.length === 0) {
2858
+ context.emitEvent?.({
2859
+ type: "tasks-listed",
2860
+ tasks: []
2861
+ });
2672
2862
  return { exitCode: 0 };
2673
2863
  }
2674
2864
  printTaskList(context, result.tasks);
2865
+ context.emitEvent?.({
2866
+ type: "tasks-listed",
2867
+ tasks: result.tasks.map((task) => {
2868
+ const parts = task.path.split("/");
2869
+ const filename = parts[parts.length - 1];
2870
+ return {
2871
+ path: task.path,
2872
+ title: task.title ?? filename.replace(".md", ""),
2873
+ blockedBy: []
2874
+ };
2875
+ })
2876
+ });
2675
2877
  return { exitCode: 0 };
2676
2878
  }
2677
2879
 
@@ -2708,6 +2910,14 @@ function formatLoopEvent(event) {
2708
2910
  return `Completed iteration ${event.iteration}/${event.maxIterations}`;
2709
2911
  case "loop.ended":
2710
2912
  return `Reached max iterations (${event.maxIterations}). Exiting.`;
2913
+ case "loop.docker_detected":
2914
+ return `Docker mode: found .dust/Dockerfile (image: ${event.imageTag})`;
2915
+ case "loop.docker_building":
2916
+ return `Building Docker image ${event.imageTag}...`;
2917
+ case "loop.docker_built":
2918
+ return `Docker image ${event.imageTag} ready`;
2919
+ case "loop.docker_error":
2920
+ return `Docker error: ${event.error}`;
2711
2921
  }
2712
2922
  }
2713
2923
  function createPostEvent(fetchFn) {
@@ -2747,7 +2957,7 @@ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgen
2747
2957
  postEvent(eventsUrl, payload).catch(onError);
2748
2958
  };
2749
2959
  }
2750
- var log = createLogger("dust:cli:commands:loop");
2960
+ var log2 = createLogger("dust:cli:commands:loop");
2751
2961
  var SLEEP_INTERVAL_MS = 30000;
2752
2962
  var SLEEP_STEP_MS = 1000;
2753
2963
  var DEFAULT_MAX_ITERATIONS = 10;
@@ -2797,15 +3007,16 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
2797
3007
  onRawEvent,
2798
3008
  hooksInstalled = false,
2799
3009
  signal,
2800
- logger = log,
2801
- repositoryId
3010
+ logger = log2,
3011
+ repositoryId,
3012
+ docker
2802
3013
  } = options;
2803
3014
  const baseEnv = buildUnattendedEnv({ repositoryId });
2804
- log("syncing with remote");
3015
+ log2("syncing with remote");
2805
3016
  onLoopEvent({ type: "loop.syncing" });
2806
3017
  const pullResult = await gitPull(context.cwd, spawn);
2807
3018
  if (!pullResult.success) {
2808
- log(`git pull failed: ${pullResult.message}`);
3019
+ log2(`git pull failed: ${pullResult.message}`);
2809
3020
  onLoopEvent({
2810
3021
  type: "loop.sync_skipped",
2811
3022
  reason: pullResult.message
@@ -2836,7 +3047,8 @@ Make sure the repository is in a clean state and synced with remote before finis
2836
3047
  cwd: context.cwd,
2837
3048
  dangerouslySkipPermissions: true,
2838
3049
  env: baseEnv,
2839
- signal
3050
+ signal,
3051
+ docker
2840
3052
  },
2841
3053
  onRawEvent
2842
3054
  });
@@ -2855,12 +3067,12 @@ Make sure the repository is in a clean state and synced with remote before finis
2855
3067
  onLoopEvent({ type: "loop.checking_tasks" });
2856
3068
  const tasks = await findAvailableTasks(dependencies);
2857
3069
  if (tasks.length === 0) {
2858
- log("no tasks available");
3070
+ log2("no tasks available");
2859
3071
  onLoopEvent({ type: "loop.no_tasks" });
2860
3072
  return "no_tasks";
2861
3073
  }
2862
3074
  const task = tasks[0];
2863
- log(`found ${tasks.length} task(s), picking: ${task.title ?? task.path}`);
3075
+ log2(`found ${tasks.length} task(s), picking: ${task.title ?? task.path}`);
2864
3076
  onLoopEvent({ type: "loop.tasks_found" });
2865
3077
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
2866
3078
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
@@ -2888,11 +3100,12 @@ ${instructions}`;
2888
3100
  cwd: context.cwd,
2889
3101
  dangerouslySkipPermissions: true,
2890
3102
  env: baseEnv,
2891
- signal
3103
+ signal,
3104
+ docker
2892
3105
  },
2893
3106
  onRawEvent
2894
3107
  });
2895
- log(`${agentName} completed task: ${task.title ?? task.path}`);
3108
+ log2(`${agentName} completed task: ${task.title ?? task.path}`);
2896
3109
  onAgentEvent?.({ type: "agent-session-ended", success: true });
2897
3110
  return "ran_claude";
2898
3111
  } catch (error) {
@@ -2947,7 +3160,36 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2947
3160
  sendWireEvent(event);
2948
3161
  };
2949
3162
  const hooksInstalled = await manageGitHooks(dependencies);
2950
- log(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
3163
+ let dockerConfig;
3164
+ const dockerDeps = {
3165
+ spawn: loopDependencies.dockerDeps?.spawn ?? loopDependencies.spawn,
3166
+ homedir: loopDependencies.dockerDeps?.homedir ?? os.homedir,
3167
+ existsSync: loopDependencies.dockerDeps?.existsSync ?? existsSync
3168
+ };
3169
+ if (hasDockerfile(context.cwd, dockerDeps)) {
3170
+ const imageTag = generateImageTag(context.cwd);
3171
+ onLoopEvent({ type: "loop.docker_detected", imageTag });
3172
+ if (!await isDockerAvailable(dockerDeps)) {
3173
+ context.stderr("Docker not available. Install Docker or remove .dust/Dockerfile to run without Docker.");
3174
+ return { exitCode: 1 };
3175
+ }
3176
+ onLoopEvent({ type: "loop.docker_building", imageTag });
3177
+ const buildResult = await buildDockerImage({ repoPath: context.cwd, imageTag }, dockerDeps);
3178
+ if (!buildResult.success) {
3179
+ onLoopEvent({ type: "loop.docker_error", error: buildResult.error });
3180
+ context.stderr(buildResult.error);
3181
+ return { exitCode: 1 };
3182
+ }
3183
+ onLoopEvent({ type: "loop.docker_built", imageTag });
3184
+ const homeDir = os.homedir();
3185
+ dockerConfig = {
3186
+ imageTag,
3187
+ repoPath: context.cwd,
3188
+ homeDir,
3189
+ hasGitconfig: existsSync(path2.join(homeDir, ".gitconfig"))
3190
+ };
3191
+ }
3192
+ log2(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
2951
3193
  onLoopEvent({ type: "loop.warning" });
2952
3194
  onLoopEvent({
2953
3195
  type: "loop.started",
@@ -2957,7 +3199,10 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2957
3199
  context.stdout(" Press Ctrl+C to stop");
2958
3200
  context.stdout("");
2959
3201
  let completedIterations = 0;
2960
- const iterationOptions = { hooksInstalled };
3202
+ const iterationOptions = {
3203
+ hooksInstalled,
3204
+ docker: dockerConfig
3205
+ };
2961
3206
  if (eventsUrl) {
2962
3207
  iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent);
2963
3208
  }
@@ -2965,12 +3210,12 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2965
3210
  agentSessionId = crypto.randomUUID();
2966
3211
  const result = await runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, iterationOptions);
2967
3212
  if (result === "no_tasks") {
2968
- log("sleeping, no tasks");
3213
+ log2("sleeping, no tasks");
2969
3214
  const writeInline = context.stdoutInline ?? context.stdout;
2970
3215
  await sleepWithProgress(loopDependencies.sleep, SLEEP_INTERVAL_MS, writeInline, context.stdout);
2971
3216
  } else {
2972
3217
  completedIterations++;
2973
- log(`iteration ${completedIterations}/${maxIterations} complete, result=${result}`);
3218
+ log2(`iteration ${completedIterations}/${maxIterations} complete, result=${result}`);
2974
3219
  onLoopEvent({
2975
3220
  type: "loop.iteration_complete",
2976
3221
  iteration: completedIterations,
@@ -2978,13 +3223,135 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2978
3223
  });
2979
3224
  }
2980
3225
  }
2981
- log(`loop ended after ${completedIterations} iterations`);
3226
+ log2(`loop ended after ${completedIterations} iterations`);
2982
3227
  onLoopEvent({ type: "loop.ended", maxIterations });
2983
3228
  return { exitCode: 0 };
2984
3229
  }
2985
3230
 
3231
+ // lib/codex/spawn-codex.ts
3232
+ import { spawn as nodeSpawn4 } from "node:child_process";
3233
+ import { createInterface as nodeCreateInterface2 } from "node:readline";
3234
+ var debug2 = createLogger("dust.codex.spawn-codex");
3235
+ var defaultDependencies2 = {
3236
+ spawn: nodeSpawn4,
3237
+ createInterface: nodeCreateInterface2
3238
+ };
3239
+ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependencies2) {
3240
+ const { cwd, env, signal } = options;
3241
+ const codexArguments = ["exec", prompt, "--json", "--yolo"];
3242
+ if (cwd) {
3243
+ codexArguments.push("--cd", cwd);
3244
+ }
3245
+ const proc = dependencies.spawn("codex", codexArguments, {
3246
+ stdio: ["ignore", "pipe", "pipe"],
3247
+ env: { ...process.env, ...env }
3248
+ });
3249
+ if (!proc.stdout) {
3250
+ throw new Error("Failed to get stdout from codex process");
3251
+ }
3252
+ let stderrOutput = "";
3253
+ proc.stderr?.on("data", (data) => {
3254
+ stderrOutput += data.toString();
3255
+ });
3256
+ const closePromise = new Promise((resolve, reject) => {
3257
+ proc.on("close", (code) => {
3258
+ if (code === 0 || code === null)
3259
+ resolve();
3260
+ else {
3261
+ const errMsg = stderrOutput.trim() ? `codex exited with code ${code}: ${stderrOutput.trim()}` : `codex exited with code ${code}`;
3262
+ reject(new Error(errMsg));
3263
+ }
3264
+ });
3265
+ proc.on("error", reject);
3266
+ });
3267
+ const abortHandler = () => {
3268
+ if (!proc.killed) {
3269
+ proc.kill();
3270
+ }
3271
+ };
3272
+ if (signal?.aborted) {
3273
+ abortHandler();
3274
+ } else if (signal) {
3275
+ signal.addEventListener("abort", abortHandler, { once: true });
3276
+ }
3277
+ const rl = dependencies.createInterface({ input: proc.stdout });
3278
+ try {
3279
+ for await (const line of rl) {
3280
+ if (!line.trim())
3281
+ continue;
3282
+ try {
3283
+ yield JSON.parse(line);
3284
+ } catch {
3285
+ debug2("Skipping malformed JSON line: %s", line.slice(0, 200));
3286
+ }
3287
+ }
3288
+ await closePromise;
3289
+ } finally {
3290
+ signal?.removeEventListener("abort", abortHandler);
3291
+ rl.close?.();
3292
+ }
3293
+ }
3294
+
3295
+ // lib/codex/event-parser.ts
3296
+ function* parseCodexRawEvent(raw) {
3297
+ if (raw.type !== "item.completed")
3298
+ return;
3299
+ const item = raw.item;
3300
+ if (!item)
3301
+ return;
3302
+ if (item.type === "agent_message" && typeof item.text === "string") {
3303
+ yield { type: "text_delta", text: `${item.text}
3304
+ ` };
3305
+ } else if (item.type === "command_execution" && typeof item.command === "string") {
3306
+ yield {
3307
+ type: "tool_use",
3308
+ id: typeof item.id === "string" ? item.id : "",
3309
+ name: "command_execution",
3310
+ input: { command: item.command }
3311
+ };
3312
+ if (typeof item.aggregated_output === "string") {
3313
+ yield {
3314
+ type: "tool_result",
3315
+ toolUseId: typeof item.id === "string" ? item.id : "",
3316
+ content: item.aggregated_output || `(exit code: ${item.exit_code ?? "unknown"})`
3317
+ };
3318
+ }
3319
+ }
3320
+ }
3321
+
3322
+ // lib/codex/streamer.ts
3323
+ async function streamCodexEvents(events, sink, onRawEvent) {
3324
+ let hadTextOutput = false;
3325
+ for await (const raw of events) {
3326
+ onRawEvent?.(raw);
3327
+ for (const event of parseCodexRawEvent(raw)) {
3328
+ processEvent(event, sink, { hadTextOutput });
3329
+ if (event.type === "text_delta") {
3330
+ hadTextOutput = true;
3331
+ } else if (event.type === "tool_use") {
3332
+ hadTextOutput = false;
3333
+ }
3334
+ }
3335
+ }
3336
+ }
3337
+
3338
+ // lib/codex/run.ts
3339
+ var defaultRunnerDependencies2 = {
3340
+ spawnCodex,
3341
+ createStdoutSink,
3342
+ streamCodexEvents
3343
+ };
3344
+ async function run2(prompt, options = {}, dependencies = defaultRunnerDependencies2) {
3345
+ const isRunOptions = (opt) => ("spawnOptions" in opt) || ("onRawEvent" in opt);
3346
+ const spawnOptions = isRunOptions(options) ? options.spawnOptions ?? {} : options;
3347
+ const onRawEvent = isRunOptions(options) ? options.onRawEvent : undefined;
3348
+ const events = dependencies.spawnCodex(prompt, spawnOptions);
3349
+ const sink = dependencies.createStdoutSink();
3350
+ await dependencies.streamCodexEvents(events, sink, onRawEvent);
3351
+ }
3352
+
2986
3353
  // lib/bucket/repository-loop.ts
2987
- var log2 = createLogger("dust:bucket:repository-loop");
3354
+ var log3 = createLogger("dust:bucket:repository-loop");
2988
3355
  var FALLBACK_TIMEOUT_MS = 300000;
2989
3356
  function createLogCallbacks(logBuffer) {
2990
3357
  return {
@@ -3030,59 +3397,40 @@ function createWakeUpHandler(repoState, resolve) {
3030
3397
  }
3031
3398
  function createNoOpGlobScanner() {
3032
3399
  return {
3033
- scan: async function* () {}
3400
+ scan: async function* noOpScan() {}
3034
3401
  };
3035
3402
  }
3036
- async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3037
- const { spawn, run: run2, fileSystem, sleep } = repoDeps;
3038
- const repoName = repoState.repository.name;
3039
- const settings = await loadSettings(repoState.path, fileSystem);
3040
- const logCallbacks = createLogCallbacks(repoState.logBuffer);
3041
- const commandDeps = {
3042
- arguments: [],
3043
- context: {
3044
- cwd: repoState.path,
3045
- stdout: (msg) => logCallbacks.stdout(msg),
3046
- stderr: (msg) => logCallbacks.stderr(msg)
3047
- },
3048
- fileSystem,
3049
- globScanner: createNoOpGlobScanner(),
3050
- settings
3051
- };
3052
- let partialLine = "";
3053
- const bufferSinkDeps = {
3054
- ...defaultRunnerDependencies,
3055
- createStdoutSink: () => ({
3056
- write: (text) => {
3057
- partialLine += text;
3058
- const lines = partialLine.split(`
3403
+ function createBufferStdoutSink(loopState, logBuffer) {
3404
+ return {
3405
+ write(text) {
3406
+ loopState.partialLine += text;
3407
+ const lines = loopState.partialLine.split(`
3059
3408
  `);
3060
- for (let i = 0;i < lines.length - 1; i++) {
3061
- appendLogLine(repoState.logBuffer, createLogLine(lines[i], "stdout"));
3062
- }
3063
- partialLine = lines[lines.length - 1];
3064
- },
3065
- line: (text) => {
3066
- partialLine = flushAndLogMultiLine(partialLine, text, repoState.logBuffer);
3409
+ for (let i = 0;i < lines.length - 1; i++) {
3410
+ appendLogLine(logBuffer, createLogLine(lines[i], "stdout"));
3067
3411
  }
3068
- })
3069
- };
3070
- const bufferRun = (prompt, options) => run2(prompt, options, bufferSinkDeps);
3071
- const loopDeps = {
3072
- spawn,
3073
- run: bufferRun,
3074
- sleep,
3075
- postEvent: async () => {}
3412
+ loopState.partialLine = lines[lines.length - 1];
3413
+ },
3414
+ line(text) {
3415
+ loopState.partialLine = flushAndLogMultiLine(loopState.partialLine, text, logBuffer);
3416
+ }
3076
3417
  };
3077
- let agentSessionId;
3078
- let sequence = 0;
3079
- const onLoopEvent = (event) => {
3418
+ }
3419
+ function createBufferRun(run3, bufferSinkDeps) {
3420
+ return (prompt, options) => run3(prompt, options, bufferSinkDeps);
3421
+ }
3422
+ async function noOpPostEvent() {}
3423
+ function createLoopEventHandler(logBuffer) {
3424
+ return function onLoopEvent(event) {
3080
3425
  const formatted = formatLoopEvent(event);
3081
3426
  if (formatted !== null) {
3082
- appendLogLine(repoState.logBuffer, createLogLine(formatted, "stdout"));
3427
+ appendLogLine(logBuffer, createLogLine(formatted, "stdout"));
3083
3428
  }
3084
3429
  };
3085
- const onAgentEvent = (event) => {
3430
+ }
3431
+ function createAgentEventHandler(parameters) {
3432
+ const { repoState, sendEvent, sessionId, repoName, loopState } = parameters;
3433
+ return function onAgentEvent(event) {
3086
3434
  if (event.type === "agent-session-started") {
3087
3435
  repoState.agentStatus = "busy";
3088
3436
  } else if (event.type === "agent-session-ended") {
@@ -3093,26 +3441,88 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3093
3441
  appendLogLine(repoState.logBuffer, createLogLine(formatted, "stdout"));
3094
3442
  }
3095
3443
  if (sendEvent && sessionId) {
3096
- sequence++;
3444
+ loopState.sequence++;
3097
3445
  sendEvent(buildEventMessage({
3098
- sequence,
3446
+ sequence: loopState.sequence,
3099
3447
  sessionId,
3100
3448
  repository: repoName,
3101
3449
  repoId: repoState.repository.id,
3102
3450
  event,
3103
- agentSessionId
3451
+ agentSessionId: loopState.agentSessionId
3104
3452
  }));
3105
3453
  }
3106
3454
  };
3455
+ }
3456
+ function createCancelHandler(abortController) {
3457
+ return abortController.abort.bind(abortController);
3458
+ }
3459
+ function setupFallbackTimeout(repoState, sleep, resolve, wakeUpForThisWait) {
3460
+ sleep(FALLBACK_TIMEOUT_MS).then(function onFallbackTimeout() {
3461
+ if (repoState.wakeUp === wakeUpForThisWait) {
3462
+ repoState.wakeUp = undefined;
3463
+ resolve();
3464
+ }
3465
+ });
3466
+ }
3467
+ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3468
+ const { spawn, run: run3, fileSystem, sleep } = repoDeps;
3469
+ const repoName = repoState.repository.name;
3470
+ const settings = await loadSettings(repoState.path, fileSystem);
3471
+ const logCallbacks = createLogCallbacks(repoState.logBuffer);
3472
+ const commandDeps = {
3473
+ arguments: [],
3474
+ context: {
3475
+ cwd: repoState.path,
3476
+ stdout: logCallbacks.stdout,
3477
+ stderr: logCallbacks.stderr
3478
+ },
3479
+ fileSystem,
3480
+ globScanner: createNoOpGlobScanner(),
3481
+ settings
3482
+ };
3483
+ const loopState = {
3484
+ partialLine: "",
3485
+ sequence: 0,
3486
+ agentSessionId: undefined
3487
+ };
3488
+ const isCodex = repoState.repository.agentProvider === "codex";
3489
+ const agentType = isCodex ? "codex" : "claude";
3490
+ const createStdoutSink2 = () => createBufferStdoutSink(loopState, repoState.logBuffer);
3491
+ let bufferRun;
3492
+ if (isCodex) {
3493
+ const codexBufferSinkDeps = {
3494
+ ...defaultRunnerDependencies2,
3495
+ createStdoutSink: createStdoutSink2
3496
+ };
3497
+ bufferRun = (prompt, options) => run2(prompt, options, codexBufferSinkDeps);
3498
+ } else {
3499
+ const bufferSinkDeps = {
3500
+ ...defaultRunnerDependencies,
3501
+ createStdoutSink: createStdoutSink2
3502
+ };
3503
+ bufferRun = createBufferRun(run3, bufferSinkDeps);
3504
+ }
3505
+ const loopDeps = {
3506
+ spawn,
3507
+ run: bufferRun,
3508
+ sleep,
3509
+ postEvent: noOpPostEvent,
3510
+ agentType
3511
+ };
3512
+ const onLoopEvent = createLoopEventHandler(repoState.logBuffer);
3513
+ const onAgentEvent = createAgentEventHandler({
3514
+ repoState,
3515
+ sendEvent,
3516
+ sessionId,
3517
+ repoName,
3518
+ loopState
3519
+ });
3107
3520
  const hooksInstalled = await manageGitHooks(commandDeps);
3108
- const logLine = (msg) => appendLogLine(repoState.logBuffer, createLogLine(msg, "stdout"));
3109
- log2(`loop started for ${repoName} at ${repoState.path}`);
3521
+ log3(`loop started for ${repoName} at ${repoState.path}`);
3110
3522
  while (!repoState.stopRequested) {
3111
- agentSessionId = crypto.randomUUID();
3523
+ loopState.agentSessionId = crypto.randomUUID();
3112
3524
  const abortController = new AbortController;
3113
- const cancelCurrentIteration = () => {
3114
- abortController.abort();
3115
- };
3525
+ const cancelCurrentIteration = createCancelHandler(abortController);
3116
3526
  repoState.cancelCurrentIteration = cancelCurrentIteration;
3117
3527
  let result;
3118
3528
  try {
@@ -3124,7 +3534,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3124
3534
  });
3125
3535
  } catch (error) {
3126
3536
  const msg = error instanceof Error ? error.message : String(error);
3127
- log2(`iteration error for ${repoName}: ${msg}`);
3537
+ log3(`iteration error for ${repoName}: ${msg}`);
3128
3538
  appendLogLine(repoState.logBuffer, createLogLine(`Loop error: ${msg}`, "stderr"));
3129
3539
  await sleep(1e4);
3130
3540
  continue;
@@ -3136,39 +3546,34 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3136
3546
  if (result === "no_tasks") {
3137
3547
  if (repoState.taskAvailablePending) {
3138
3548
  repoState.taskAvailablePending = false;
3139
- log2(`${repoName}: task signal received during iteration, rechecking`);
3140
- logLine("Task signal received during iteration, rechecking...");
3549
+ log3(`${repoName}: task signal received during iteration, rechecking`);
3550
+ appendLogLine(repoState.logBuffer, createLogLine("Task signal received during iteration, rechecking...", "stdout"));
3141
3551
  continue;
3142
3552
  }
3143
- log2(`${repoName}: no tasks available, waiting`);
3144
- logLine("Waiting for tasks...");
3145
- await new Promise((resolve) => {
3553
+ log3(`${repoName}: no tasks available, waiting`);
3554
+ appendLogLine(repoState.logBuffer, createLogLine("Waiting for tasks...", "stdout"));
3555
+ await new Promise(function waitForTasks(resolve) {
3146
3556
  const wakeUpForThisWait = createWakeUpHandler(repoState, resolve);
3147
3557
  repoState.wakeUp = wakeUpForThisWait;
3148
- sleep(FALLBACK_TIMEOUT_MS).then(() => {
3149
- if (repoState.wakeUp === wakeUpForThisWait) {
3150
- repoState.wakeUp = undefined;
3151
- resolve();
3152
- }
3153
- });
3558
+ setupFallbackTimeout(repoState, sleep, resolve, wakeUpForThisWait);
3154
3559
  });
3155
3560
  }
3156
3561
  }
3157
- log2(`loop stopped for ${repoName}`);
3562
+ log3(`loop stopped for ${repoName}`);
3158
3563
  appendLogLine(repoState.logBuffer, createLogLine(`Stopped loop for ${repoName}`, "stdout"));
3159
3564
  }
3160
3565
 
3161
3566
  // lib/bucket/repository.ts
3162
- var log3 = createLogger("dust:bucket:repository");
3567
+ var log4 = createLogger("dust:bucket:repository");
3163
3568
  function startRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3164
- log3(`starting loop for ${repoState.repository.name}`);
3569
+ log4(`starting loop for ${repoState.repository.name}`);
3165
3570
  repoState.stopRequested = false;
3166
3571
  repoState.loopPromise = runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId).catch((error) => {
3167
3572
  const message = error instanceof Error ? error.message : String(error);
3168
- log3(`loop crashed for ${repoState.repository.name}: ${message}`);
3573
+ log4(`loop crashed for ${repoState.repository.name}: ${message}`);
3169
3574
  appendLogLine(repoState.logBuffer, createLogLine(`Repository loop crashed: ${message}`, "stderr"));
3170
3575
  }).finally(() => {
3171
- log3(`loop finished for ${repoState.repository.name}`);
3576
+ log4(`loop finished for ${repoState.repository.name}`);
3172
3577
  repoState.loopPromise = null;
3173
3578
  repoState.agentStatus = "idle";
3174
3579
  repoState.wakeUp = undefined;
@@ -3191,10 +3596,10 @@ function parseRepository(data) {
3191
3596
  }
3192
3597
  async function addRepository(repository, manager, repoDeps, context) {
3193
3598
  if (manager.repositories.has(repository.name)) {
3194
- log3(`repository ${repository.name} already exists, skipping add`);
3599
+ log4(`repository ${repository.name} already exists, skipping add`);
3195
3600
  return;
3196
3601
  }
3197
- log3(`adding repository ${repository.name}`);
3602
+ log4(`adding repository ${repository.name}`);
3198
3603
  const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
3199
3604
  await repoDeps.fileSystem.mkdir(dirname2(repoPath), { recursive: true });
3200
3605
  if (repoDeps.fileSystem.exists(repoPath)) {
@@ -3234,7 +3639,7 @@ async function removeRepositoryFromManager(repoName, manager, repoDeps, context)
3234
3639
  if (!repoState) {
3235
3640
  return;
3236
3641
  }
3237
- log3(`removing repository ${repoName}`);
3642
+ log4(`removing repository ${repoName}`);
3238
3643
  repoState.stopRequested = true;
3239
3644
  repoState.cancelCurrentIteration?.();
3240
3645
  repoState.wakeUp?.();
@@ -3292,13 +3697,20 @@ function parseServerMessage(data) {
3292
3697
  if (typeof repo.id !== "number" || typeof repo.url !== "string" || typeof repo.hasTask !== "boolean") {
3293
3698
  return null;
3294
3699
  }
3295
- repositories.push({
3700
+ const item = {
3296
3701
  id: repo.id,
3297
3702
  name: repo.name,
3298
3703
  gitUrl: repo.gitUrl,
3299
3704
  url: repo.url,
3300
3705
  hasTask: repo.hasTask
3301
- });
3706
+ };
3707
+ if (typeof repo.gitSshUrl === "string") {
3708
+ item.gitSshUrl = repo.gitSshUrl;
3709
+ }
3710
+ if (typeof repo.agentProvider === "string") {
3711
+ item.agentProvider = repo.agentProvider;
3712
+ }
3713
+ repositories.push(item);
3302
3714
  }
3303
3715
  return { type: "repository-list", repositories };
3304
3716
  }
@@ -3739,7 +4151,7 @@ function handleKeyInput(state, key, options) {
3739
4151
  }
3740
4152
 
3741
4153
  // lib/cli/commands/bucket.ts
3742
- var log4 = createLogger("dust:cli:commands:bucket");
4154
+ var log5 = createLogger("dust:cli:commands:bucket");
3743
4155
  var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
3744
4156
  var INITIAL_RECONNECT_DELAY_MS = 1000;
3745
4157
  var MAX_RECONNECT_DELAY_MS = 30000;
@@ -3799,27 +4211,27 @@ function defaultWriteStdout(data) {
3799
4211
  }
3800
4212
  function createAuthFileSystem(dependencies) {
3801
4213
  return {
3802
- exists: (path) => {
4214
+ exists: (path3) => {
3803
4215
  try {
3804
- dependencies.accessSync(path);
4216
+ dependencies.accessSync(path3);
3805
4217
  return true;
3806
4218
  } catch {
3807
4219
  return false;
3808
4220
  }
3809
4221
  },
3810
- isDirectory: (path) => {
4222
+ isDirectory: (path3) => {
3811
4223
  try {
3812
- return dependencies.statSync(path).isDirectory();
4224
+ return dependencies.statSync(path3).isDirectory();
3813
4225
  } catch {
3814
4226
  return false;
3815
4227
  }
3816
4228
  },
3817
- getFileCreationTime: (path) => dependencies.statSync(path).birthtimeMs,
3818
- readFile: (path) => dependencies.readFile(path, "utf8"),
3819
- writeFile: (path, content) => dependencies.writeFile(path, content, "utf8"),
3820
- mkdir: (path, options) => dependencies.mkdir(path, options).then(() => {}),
3821
- readdir: (path) => dependencies.readdir(path),
3822
- chmod: (path, mode) => dependencies.chmod(path, mode),
4229
+ getFileCreationTime: (path3) => dependencies.statSync(path3).birthtimeMs,
4230
+ readFile: (path3) => dependencies.readFile(path3, "utf8"),
4231
+ writeFile: (path3, content) => dependencies.writeFile(path3, content, "utf8"),
4232
+ mkdir: (path3, options) => dependencies.mkdir(path3, options).then(() => {}),
4233
+ readdir: (path3) => dependencies.readdir(path3),
4234
+ chmod: (path3, mode) => dependencies.chmod(path3, mode),
3823
4235
  rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
3824
4236
  };
3825
4237
  }
@@ -3835,7 +4247,7 @@ function createDefaultBucketDependencies() {
3835
4247
  rename: (oldPath, newPath) => import("node:fs/promises").then((mod) => mod.rename(oldPath, newPath))
3836
4248
  });
3837
4249
  return {
3838
- spawn: nodeSpawn4,
4250
+ spawn: nodeSpawn5,
3839
4251
  createWebSocket: defaultCreateWebSocket,
3840
4252
  setupKeypress: defaultSetupKeypress,
3841
4253
  setupSignals: defaultSetupSignals,
@@ -3887,20 +4299,20 @@ function toRepositoryDependencies(bucketDeps, fileSystem) {
3887
4299
  }
3888
4300
  function ensureRepositoryLoopRunning(repoState, state, repoDeps, context, useTUI) {
3889
4301
  if (repoState.loopPromise || repoState.wakeUp || repoState.stopRequested) {
3890
- log4(`loop already running/waiting for ${repoState.repository.name}`);
4302
+ log5(`loop already running/waiting for ${repoState.repository.name}`);
3891
4303
  return;
3892
4304
  }
3893
4305
  logMessage(state, context, useTUI, `Repository loop not running for ${repoState.repository.name}; restarting`);
3894
4306
  startRepositoryLoop(repoState, repoDeps, state.sendEvent, state.sessionId);
3895
4307
  }
3896
4308
  function signalTaskAvailable(repoState, state, repoDeps, context, useTUI) {
3897
- log4(`task-available signal for ${repoState.repository.name}`);
4309
+ log5(`task-available signal for ${repoState.repository.name}`);
3898
4310
  ensureRepositoryLoopRunning(repoState, state, repoDeps, context, useTUI);
3899
4311
  if (repoState.wakeUp) {
3900
- log4(`waking loop for ${repoState.repository.name}`);
4312
+ log5(`waking loop for ${repoState.repository.name}`);
3901
4313
  repoState.wakeUp();
3902
4314
  } else {
3903
- log4(`marking task pending for ${repoState.repository.name} (loop busy)`);
4315
+ log5(`marking task pending for ${repoState.repository.name} (loop busy)`);
3904
4316
  repoState.taskAvailablePending = true;
3905
4317
  }
3906
4318
  }
@@ -4034,7 +4446,7 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4034
4446
  logMessage(state, context, useTUI, `Invalid WebSocket message format: ${event.data}`, "stderr");
4035
4447
  return;
4036
4448
  }
4037
- log4(`ws message: ${message.type}`);
4449
+ log5(`ws message: ${message.type}`);
4038
4450
  if (message.type === "repository-list") {
4039
4451
  const repos = message.repositories;
4040
4452
  logMessage(state, context, useTUI, `Received repository list (${repos.length} repositories):`);
@@ -4084,7 +4496,7 @@ async function shutdown(state, bucketDeps, context) {
4084
4496
  if (state.shuttingDown)
4085
4497
  return;
4086
4498
  state.shuttingDown = true;
4087
- log4("shutdown initiated");
4499
+ log5("shutdown initiated");
4088
4500
  context.stdout("Shutting down...");
4089
4501
  if (state.reconnectTimer) {
4090
4502
  clearTimeout(state.reconnectTimer);
@@ -4287,16 +4699,16 @@ function createDefaultUploadDependencies() {
4287
4699
  getHomeDir: () => homedir2(),
4288
4700
  fileSystem: authFileSystem
4289
4701
  },
4290
- readFileBytes: async (path) => {
4291
- const buffer = await Bun.file(path).arrayBuffer();
4702
+ readFileBytes: async (path3) => {
4703
+ const buffer = await Bun.file(path3).arrayBuffer();
4292
4704
  return new Uint8Array(buffer);
4293
4705
  },
4294
- getFileSize: async (path) => {
4295
- const file = Bun.file(path);
4706
+ getFileSize: async (path3) => {
4707
+ const file = Bun.file(path3);
4296
4708
  return file.size;
4297
4709
  },
4298
- fileExists: async (path) => {
4299
- const file = Bun.file(path);
4710
+ fileExists: async (path3) => {
4711
+ const file = Bun.file(path3);
4300
4712
  return file.exists();
4301
4713
  },
4302
4714
  uploadFile: async (url, token, fileBytes, contentType, fileName) => {
@@ -5090,12 +5502,12 @@ function validateNoCycles(allPrincipleRelationships) {
5090
5502
  }
5091
5503
  for (const rel of allPrincipleRelationships) {
5092
5504
  const visited = new Set;
5093
- const path = [];
5505
+ const path3 = [];
5094
5506
  let current = rel.filePath;
5095
5507
  while (current) {
5096
5508
  if (visited.has(current)) {
5097
- const cycleStart = path.indexOf(current);
5098
- const cyclePath = path.slice(cycleStart).concat(current);
5509
+ const cycleStart = path3.indexOf(current);
5510
+ const cyclePath = path3.slice(cycleStart).concat(current);
5099
5511
  violations.push({
5100
5512
  file: rel.filePath,
5101
5513
  message: `Cycle detected in principle hierarchy: ${cyclePath.join(" -> ")}`
@@ -5103,7 +5515,7 @@ function validateNoCycles(allPrincipleRelationships) {
5103
5515
  break;
5104
5516
  }
5105
5517
  visited.add(current);
5106
- path.push(current);
5518
+ path3.push(current);
5107
5519
  const currentRel = relationshipMap.get(current);
5108
5520
  if (currentRel && currentRel.parentPrinciples.length > 0) {
5109
5521
  current = currentRel.parentPrinciples[0];
@@ -5305,7 +5717,7 @@ async function lintMarkdown(dependencies) {
5305
5717
  }
5306
5718
 
5307
5719
  // lib/cli/commands/check.ts
5308
- var log5 = createLogger("dust:cli:commands:check");
5720
+ var log6 = createLogger("dust:cli:commands:check");
5309
5721
  var DEFAULT_CHECK_TIMEOUT_MS = 13000;
5310
5722
  var MAX_OUTPUT_LINES = 500;
5311
5723
  var KEEP_LINES = 250;
@@ -5325,14 +5737,27 @@ function truncateOutput(output) {
5325
5737
  ].join(`
5326
5738
  `);
5327
5739
  }
5328
- async function runSingleCheck(check, cwd, runner) {
5740
+ async function runSingleCheck(check, cwd, runner, emitEvent) {
5329
5741
  const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
5330
- log5(`running check ${check.name}: ${check.command}`);
5742
+ log6(`running check ${check.name}: ${check.command}`);
5743
+ emitEvent?.({ type: "check-started", name: check.name });
5331
5744
  const startTime = Date.now();
5332
5745
  const result = await runner.run(check.command, cwd, timeoutMs);
5333
5746
  const durationMs = Date.now() - startTime;
5334
5747
  const status = result.timedOut ? "timed out" : result.exitCode === 0 ? "passed" : "failed";
5335
- log5(`check ${check.name} ${status} (${durationMs}ms)`);
5748
+ log6(`check ${check.name} ${status} (${durationMs}ms)`);
5749
+ if (result.exitCode === 0) {
5750
+ emitEvent?.({ type: "check-passed", name: check.name, durationMs });
5751
+ } else {
5752
+ const failedEvent = {
5753
+ type: "check-failed",
5754
+ name: check.name,
5755
+ durationMs
5756
+ };
5757
+ if (result.output)
5758
+ failedEvent.output = result.output;
5759
+ emitEvent?.(failedEvent);
5760
+ }
5336
5761
  return {
5337
5762
  name: check.name,
5338
5763
  command: check.command,
@@ -5344,25 +5769,26 @@ async function runSingleCheck(check, cwd, runner) {
5344
5769
  timeoutSeconds: timeoutMs / 1000
5345
5770
  };
5346
5771
  }
5347
- async function runConfiguredChecks(checks, cwd, runner) {
5348
- const promises = checks.map((check) => runSingleCheck(check, cwd, runner));
5772
+ async function runConfiguredChecks(checks, cwd, runner, emitEvent) {
5773
+ const promises = checks.map((check) => runSingleCheck(check, cwd, runner, emitEvent));
5349
5774
  return Promise.all(promises);
5350
5775
  }
5351
- async function runConfiguredChecksSerially(checks, cwd, runner) {
5776
+ async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent) {
5352
5777
  const results = [];
5353
5778
  for (const check of checks) {
5354
- results.push(await runSingleCheck(check, cwd, runner));
5779
+ results.push(await runSingleCheck(check, cwd, runner, emitEvent));
5355
5780
  }
5356
5781
  return results;
5357
5782
  }
5358
- async function runValidationCheck(dependencies) {
5783
+ async function runValidationCheck(dependencies, emitEvent) {
5359
5784
  const outputLines = [];
5360
5785
  const bufferedContext = {
5361
5786
  cwd: dependencies.context.cwd,
5362
5787
  stdout: (msg) => outputLines.push(msg),
5363
5788
  stderr: (msg) => outputLines.push(msg)
5364
5789
  };
5365
- log5("running built-in check: dust lint");
5790
+ log6("running built-in check: dust lint");
5791
+ emitEvent?.({ type: "check-started", name: "lint" });
5366
5792
  const startTime = Date.now();
5367
5793
  const result = await lintMarkdown({
5368
5794
  ...dependencies,
@@ -5371,13 +5797,26 @@ async function runValidationCheck(dependencies) {
5371
5797
  });
5372
5798
  const durationMs = Date.now() - startTime;
5373
5799
  const lintStatus = result.exitCode === 0 ? "passed" : "failed";
5374
- log5(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
5800
+ log6(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
5801
+ const output = outputLines.join(`
5802
+ `);
5803
+ if (result.exitCode === 0) {
5804
+ emitEvent?.({ type: "check-passed", name: "lint", durationMs });
5805
+ } else {
5806
+ const failedEvent = {
5807
+ type: "check-failed",
5808
+ name: "lint",
5809
+ durationMs
5810
+ };
5811
+ if (output)
5812
+ failedEvent.output = output;
5813
+ emitEvent?.(failedEvent);
5814
+ }
5375
5815
  return {
5376
5816
  name: "lint",
5377
5817
  command: "dust lint",
5378
5818
  exitCode: result.exitCode,
5379
- output: outputLines.join(`
5380
- `),
5819
+ output,
5381
5820
  isBuiltIn: true,
5382
5821
  durationMs,
5383
5822
  timedOut: false
@@ -5446,18 +5885,18 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
5446
5885
  if (serial) {
5447
5886
  const results2 = [];
5448
5887
  if (hasDustDir) {
5449
- results2.push(await runValidationCheck(dependencies));
5888
+ results2.push(await runValidationCheck(dependencies, context.emitEvent));
5450
5889
  }
5451
- const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner);
5890
+ const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent);
5452
5891
  results2.push(...configuredResults);
5453
5892
  const exitCode2 = displayResults(results2, context);
5454
5893
  return { exitCode: exitCode2 };
5455
5894
  }
5456
5895
  const checkPromises = [];
5457
5896
  if (hasDustDir) {
5458
- checkPromises.push(runValidationCheck(dependencies));
5897
+ checkPromises.push(runValidationCheck(dependencies, context.emitEvent));
5459
5898
  }
5460
- checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner));
5899
+ checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent));
5461
5900
  const promiseResults = await Promise.all(checkPromises);
5462
5901
  const results = [];
5463
5902
  for (const result of promiseResults) {
@@ -5638,6 +6077,16 @@ async function init(dependencies) {
5638
6077
 
5639
6078
  // lib/cli/commands/list.ts
5640
6079
  import { basename as basename2 } from "node:path";
6080
+ function workflowTypeToStatus(type) {
6081
+ switch (type) {
6082
+ case "refine":
6083
+ return "refining";
6084
+ case "decompose-idea":
6085
+ return "decomposing";
6086
+ case "shelve":
6087
+ return "shelving";
6088
+ }
6089
+ }
5641
6090
  var VALID_TYPES = ["tasks", "ideas", "principles", "facts"];
5642
6091
  var SECTION_HEADERS = {
5643
6092
  tasks: "\uD83D\uDCCB Tasks",
@@ -5712,6 +6161,7 @@ async function list(dependencies) {
5712
6161
  return { exitCode: 1 };
5713
6162
  }
5714
6163
  const specificTypeRequested = commandArguments.length > 0;
6164
+ const workflowTasks = typesToList.includes("ideas") && fileSystem.exists(dustPath) ? await findAllWorkflowTasks(fileSystem, dustPath) : null;
5715
6165
  for (const type of typesToList) {
5716
6166
  const dirPath = `${dustPath}/${type}`;
5717
6167
  const dirExists = fileSystem.exists(dirPath);
@@ -5726,6 +6176,13 @@ async function list(dependencies) {
5726
6176
  context.stdout(`No ${type} found.`);
5727
6177
  context.stdout("");
5728
6178
  }
6179
+ if (type === "facts") {
6180
+ context.emitEvent?.({ type: "facts-listed", facts: [] });
6181
+ } else if (type === "ideas") {
6182
+ context.emitEvent?.({ type: "ideas-listed", ideas: [] });
6183
+ } else if (type === "principles") {
6184
+ context.emitEvent?.({ type: "principles-listed", principles: [] });
6185
+ }
5729
6186
  continue;
5730
6187
  }
5731
6188
  context.stdout(SECTION_HEADERS[type]);
@@ -5740,16 +6197,26 @@ async function list(dependencies) {
5740
6197
  context.stdout("");
5741
6198
  }
5742
6199
  }
6200
+ const collectedItems = [];
5743
6201
  for (const file of mdFiles) {
5744
6202
  const filePath = `${dirPath}/${file}`;
5745
6203
  const content = await fileSystem.readFile(filePath);
5746
6204
  const title = extractTitle(content);
5747
6205
  const openingSentence = extractOpeningSentence(content);
5748
6206
  const relativePath = `.dust/${type}/${file}`;
6207
+ const slug = file.replace(".md", "");
6208
+ const displayTitle = title || slug;
6209
+ if (type === "ideas") {
6210
+ const workflowTask = workflowTasks?.workflowTasksByIdeaSlug.get(slug);
6211
+ const status = workflowTask ? workflowTypeToStatus(workflowTask.type) : "draft";
6212
+ collectedItems.push({ path: relativePath, title: displayTitle, status });
6213
+ } else if (type === "facts" || type === "principles") {
6214
+ collectedItems.push({ path: relativePath, title: displayTitle });
6215
+ }
5749
6216
  if (title) {
5750
6217
  context.stdout(`${colors.bold}# ${title}${colors.reset}`);
5751
6218
  } else {
5752
- context.stdout(`${colors.bold}# ${file.replace(".md", "")}${colors.reset}`);
6219
+ context.stdout(`${colors.bold}# ${slug}${colors.reset}`);
5753
6220
  }
5754
6221
  if (openingSentence) {
5755
6222
  context.stdout(`${colors.dim}${openingSentence}${colors.reset}`);
@@ -5757,130 +6224,28 @@ async function list(dependencies) {
5757
6224
  context.stdout(`${colors.cyan}→ ${relativePath}${colors.reset}`);
5758
6225
  context.stdout("");
5759
6226
  }
5760
- }
5761
- return { exitCode: 0 };
5762
- }
5763
-
5764
- // lib/codex/spawn-codex.ts
5765
- import { spawn as nodeSpawn5 } from "node:child_process";
5766
- import { createInterface as nodeCreateInterface2 } from "node:readline";
5767
- var debug2 = createLogger("dust.codex.spawn-codex");
5768
- var defaultDependencies2 = {
5769
- spawn: nodeSpawn5,
5770
- createInterface: nodeCreateInterface2
5771
- };
5772
- async function* spawnCodex(prompt, options = {}, dependencies = defaultDependencies2) {
5773
- const { cwd, env, signal } = options;
5774
- const codexArguments = ["exec", prompt, "--json", "--yolo"];
5775
- if (cwd) {
5776
- codexArguments.push("--cd", cwd);
5777
- }
5778
- const proc = dependencies.spawn("codex", codexArguments, {
5779
- stdio: ["ignore", "pipe", "pipe"],
5780
- env: { ...process.env, ...env }
5781
- });
5782
- if (!proc.stdout) {
5783
- throw new Error("Failed to get stdout from codex process");
5784
- }
5785
- let stderrOutput = "";
5786
- proc.stderr?.on("data", (data) => {
5787
- stderrOutput += data.toString();
5788
- });
5789
- const closePromise = new Promise((resolve3, reject) => {
5790
- proc.on("close", (code) => {
5791
- if (code === 0 || code === null)
5792
- resolve3();
5793
- else {
5794
- const errMsg = stderrOutput.trim() ? `codex exited with code ${code}: ${stderrOutput.trim()}` : `codex exited with code ${code}`;
5795
- reject(new Error(errMsg));
5796
- }
5797
- });
5798
- proc.on("error", reject);
5799
- });
5800
- const abortHandler = () => {
5801
- if (!proc.killed) {
5802
- proc.kill();
5803
- }
5804
- };
5805
- if (signal?.aborted) {
5806
- abortHandler();
5807
- } else if (signal) {
5808
- signal.addEventListener("abort", abortHandler, { once: true });
5809
- }
5810
- const rl = dependencies.createInterface({ input: proc.stdout });
5811
- try {
5812
- for await (const line of rl) {
5813
- if (!line.trim())
5814
- continue;
5815
- try {
5816
- yield JSON.parse(line);
5817
- } catch {
5818
- debug2("Skipping malformed JSON line: %s", line.slice(0, 200));
5819
- }
5820
- }
5821
- await closePromise;
5822
- } finally {
5823
- signal?.removeEventListener("abort", abortHandler);
5824
- rl.close?.();
5825
- }
5826
- }
5827
-
5828
- // lib/codex/event-parser.ts
5829
- function* parseCodexRawEvent(raw) {
5830
- if (raw.type !== "item.completed")
5831
- return;
5832
- const item = raw.item;
5833
- if (!item)
5834
- return;
5835
- if (item.type === "agent_message" && typeof item.text === "string") {
5836
- yield { type: "text_delta", text: `${item.text}
5837
- ` };
5838
- } else if (item.type === "command_execution" && typeof item.command === "string") {
5839
- yield {
5840
- type: "tool_use",
5841
- id: typeof item.id === "string" ? item.id : "",
5842
- name: "command_execution",
5843
- input: { command: item.command }
5844
- };
5845
- if (typeof item.aggregated_output === "string") {
5846
- yield {
5847
- type: "tool_result",
5848
- toolUseId: typeof item.id === "string" ? item.id : "",
5849
- content: item.aggregated_output || `(exit code: ${item.exit_code ?? "unknown"})`
5850
- };
5851
- }
5852
- }
5853
- }
5854
-
5855
- // lib/codex/streamer.ts
5856
- async function streamCodexEvents(events, sink, onRawEvent) {
5857
- let hadTextOutput = false;
5858
- for await (const raw of events) {
5859
- onRawEvent?.(raw);
5860
- for (const event of parseCodexRawEvent(raw)) {
5861
- processEvent(event, sink, { hadTextOutput });
5862
- if (event.type === "text_delta") {
5863
- hadTextOutput = true;
5864
- } else if (event.type === "tool_use") {
5865
- hadTextOutput = false;
5866
- }
6227
+ if (type === "facts") {
6228
+ context.emitEvent?.({
6229
+ type: "facts-listed",
6230
+ facts: collectedItems.map((i) => ({ path: i.path, title: i.title }))
6231
+ });
6232
+ } else if (type === "ideas") {
6233
+ context.emitEvent?.({
6234
+ type: "ideas-listed",
6235
+ ideas: collectedItems.map((i) => ({
6236
+ path: i.path,
6237
+ title: i.title,
6238
+ status: i.status ?? "draft"
6239
+ }))
6240
+ });
6241
+ } else if (type === "principles") {
6242
+ context.emitEvent?.({
6243
+ type: "principles-listed",
6244
+ principles: collectedItems.map((i) => ({ path: i.path, title: i.title }))
6245
+ });
5867
6246
  }
5868
6247
  }
5869
- }
5870
-
5871
- // lib/codex/run.ts
5872
- var defaultRunnerDependencies2 = {
5873
- spawnCodex,
5874
- createStdoutSink,
5875
- streamCodexEvents
5876
- };
5877
- async function run2(prompt, options = {}, dependencies = defaultRunnerDependencies2) {
5878
- const isRunOptions = (opt) => ("spawnOptions" in opt) || ("onRawEvent" in opt);
5879
- const spawnOptions = isRunOptions(options) ? options.spawnOptions ?? {} : options;
5880
- const onRawEvent = isRunOptions(options) ? options.onRawEvent : undefined;
5881
- const events = dependencies.spawnCodex(prompt, spawnOptions);
5882
- const sink = dependencies.createStdoutSink();
5883
- await dependencies.streamCodexEvents(events, sink, onRawEvent);
6248
+ return { exitCode: 0 };
5884
6249
  }
5885
6250
 
5886
6251
  // lib/cli/commands/loop-codex.ts
@@ -6137,8 +6502,8 @@ function parseGitDiffNameStatus(output) {
6137
6502
  const parts = line.split("\t");
6138
6503
  if (parts.length >= 2) {
6139
6504
  const statusChar = parts[0].charAt(0);
6140
- const path = parts.length > 2 ? parts[2] : parts[1];
6141
- changes.push({ status: statusChar, path });
6505
+ const path3 = parts.length > 2 ? parts[2] : parts[1];
6506
+ changes.push({ status: statusChar, path: path3 });
6142
6507
  }
6143
6508
  }
6144
6509
  return changes;
@@ -6199,12 +6564,12 @@ async function getUncommittedFiles(cwd, gitRunner) {
6199
6564
  `).filter((line) => line.length > 0);
6200
6565
  for (const line of lines) {
6201
6566
  if (line.length > 3) {
6202
- const path = line.substring(3);
6203
- const arrowIndex = path.indexOf(" -> ");
6567
+ const path3 = line.substring(3);
6568
+ const arrowIndex = path3.indexOf(" -> ");
6204
6569
  if (arrowIndex !== -1) {
6205
- files.push(path.substring(arrowIndex + 4));
6570
+ files.push(path3.substring(arrowIndex + 4));
6206
6571
  } else {
6207
- files.push(path);
6572
+ files.push(path3);
6208
6573
  }
6209
6574
  }
6210
6575
  }
@@ -6355,24 +6720,24 @@ async function main(options) {
6355
6720
  function createFileSystem(primitives) {
6356
6721
  return {
6357
6722
  exists: primitives.existsSync,
6358
- isDirectory: (path) => {
6723
+ isDirectory: (path3) => {
6359
6724
  try {
6360
- return primitives.statSync(path).isDirectory();
6725
+ return primitives.statSync(path3).isDirectory();
6361
6726
  } catch {
6362
6727
  return false;
6363
6728
  }
6364
6729
  },
6365
- readFile: (path) => primitives.readFile(path, "utf-8"),
6366
- writeFile: (path, content, options) => primitives.writeFile(path, content, {
6730
+ readFile: (path3) => primitives.readFile(path3, "utf-8"),
6731
+ writeFile: (path3, content, options) => primitives.writeFile(path3, content, {
6367
6732
  encoding: "utf-8",
6368
6733
  flag: options?.flag
6369
6734
  }),
6370
- mkdir: async (path, options) => {
6371
- await primitives.mkdir(path, options);
6735
+ mkdir: async (path3, options) => {
6736
+ await primitives.mkdir(path3, options);
6372
6737
  },
6373
- getFileCreationTime: (path) => primitives.statSync(path).birthtimeMs,
6374
- readdir: (path) => primitives.readdir(path),
6375
- chmod: (path, mode) => primitives.chmod(path, mode),
6738
+ getFileCreationTime: (path3) => primitives.statSync(path3).birthtimeMs,
6739
+ readdir: (path3) => primitives.readdir(path3),
6740
+ chmod: (path3, mode) => primitives.chmod(path3, mode),
6376
6741
  rename: (oldPath, newPath) => primitives.rename(oldPath, newPath)
6377
6742
  };
6378
6743
  }
@@ -6396,7 +6761,8 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
6396
6761
  cwd: processPrimitives.cwd(),
6397
6762
  stdout: consolePrimitives.log,
6398
6763
  stdoutInline: consolePrimitives.write,
6399
- stderr: consolePrimitives.error
6764
+ stderr: consolePrimitives.error,
6765
+ emitEvent: consolePrimitives.emitEvent
6400
6766
  },
6401
6767
  fileSystem,
6402
6768
  glob,
@@ -6406,7 +6772,12 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
6406
6772
  }
6407
6773
 
6408
6774
  // lib/cli/run.ts
6409
- await wireEntry({ existsSync, statSync: statSync3, readFile: readFile3, writeFile: writeFile3, mkdir: mkdir3, readdir: readdir3, chmod: chmod3, rename }, {
6775
+ var eventsFd = process.env.DUST_EVENTS_FD ? Number.parseInt(process.env.DUST_EVENTS_FD, 10) : undefined;
6776
+ var emitEvent = eventsFd !== undefined && !Number.isNaN(eventsFd) ? createEventEmitter((message) => {
6777
+ writeSync(eventsFd, `${JSON.stringify(message)}
6778
+ `);
6779
+ }) : undefined;
6780
+ await wireEntry({ existsSync: existsSync2, statSync: statSync3, readFile: readFile3, writeFile: writeFile3, mkdir: mkdir3, readdir: readdir3, chmod: chmod3, rename }, {
6410
6781
  argv: process.argv,
6411
6782
  cwd: () => process.cwd(),
6412
6783
  exit: (code) => {
@@ -6417,5 +6788,6 @@ await wireEntry({ existsSync, statSync: statSync3, readFile: readFile3, writeFil
6417
6788
  write: (message) => {
6418
6789
  process.stdout.write(message);
6419
6790
  },
6420
- error: console.error
6791
+ error: console.error,
6792
+ emitEvent
6421
6793
  });