@joshski/dust 0.1.82 → 0.1.84

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.
@@ -7,6 +7,7 @@
7
7
  import { spawn as nodeSpawn } from 'node:child_process';
8
8
  import { run as claudeRun } from '../claude/run';
9
9
  import type { CommandDependencies, FileSystem } from '../cli/types';
10
+ import type { DockerDependencies } from '../docker/docker-agent';
10
11
  import { type BucketEmitFn, type SendEventFn } from './events';
11
12
  import { type LogBuffer } from './log-buffer';
12
13
  export { cloneRepository, getRepoPath, removeRepository, } from './repository-git';
@@ -47,6 +48,8 @@ export interface RepositoryDependencies {
47
48
  fileSystem: FileSystem;
48
49
  sleep: (ms: number) => Promise<void>;
49
50
  getReposDir: () => string;
51
+ /** Optional overrides for Docker dependency functions (for testing) */
52
+ dockerDeps?: Partial<DockerDependencies>;
50
53
  }
51
54
  /**
52
55
  * Start (or restart) the per-repository loop and keep loopPromise state accurate.
@@ -12,7 +12,7 @@ export declare function validateSettingsJson(content: string): SettingsViolation
12
12
  /**
13
13
  * Detects the appropriate dust command based on lockfiles and environment.
14
14
  * Priority:
15
- * 1. bun.lockb exists → bunx dust
15
+ * 1. bun.lock or bun.lockb exists → bunx dust
16
16
  * 2. pnpm-lock.yaml exists → pnpx dust
17
17
  * 3. package-lock.json exists → npx dust
18
18
  * 4. No lockfile + BUN_INSTALL env var set → bunx dust
package/dist/dust.js CHANGED
@@ -185,7 +185,7 @@ var DEFAULT_SETTINGS = {
185
185
  dustCommand: "npx dust"
186
186
  };
187
187
  function detectDustCommand(cwd, fileSystem) {
188
- if (fileSystem.exists(join(cwd, "bun.lockb"))) {
188
+ if (fileSystem.exists(join(cwd, "bun.lock")) || fileSystem.exists(join(cwd, "bun.lockb"))) {
189
189
  return "bunx dust";
190
190
  }
191
191
  if (fileSystem.exists(join(cwd, "pnpm-lock.yaml"))) {
@@ -200,6 +200,7 @@ function detectDustCommand(cwd, fileSystem) {
200
200
  return "npx dust";
201
201
  }
202
202
  var LOCKFILE_COMMANDS = [
203
+ { file: "bun.lock", command: "bun install", ecosystem: "js" },
203
204
  { file: "bun.lockb", command: "bun install", ecosystem: "js" },
204
205
  { file: "pnpm-lock.yaml", command: "pnpm install", ecosystem: "js" },
205
206
  { file: "package-lock.json", command: "npm install", ecosystem: "js" },
@@ -318,7 +319,7 @@ async function loadSettings(cwd, fileSystem) {
318
319
  }
319
320
 
320
321
  // lib/version.ts
321
- var DUST_VERSION = "0.1.82";
322
+ var DUST_VERSION = "0.1.84";
322
323
 
323
324
  // lib/session.ts
324
325
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -2043,16 +2044,25 @@ function buildDockerRunArguments(docker, claudeArguments, env) {
2043
2044
  "-w",
2044
2045
  "/workspace",
2045
2046
  "-v",
2046
- `${docker.homeDir}/.claude:/root/.claude:ro`,
2047
+ `${docker.homeDir}/.claude:/home/user/.claude`,
2047
2048
  "-v",
2048
- `${docker.homeDir}/.ssh:/root/.ssh:ro`
2049
+ `${docker.homeDir}/.claude.json:/home/user/.claude.json`,
2050
+ "-v",
2051
+ `${docker.homeDir}/.ssh:/home/user/.ssh:ro`,
2052
+ "-e",
2053
+ "HOME=/home/user"
2049
2054
  ];
2050
2055
  if (docker.hasGitconfig) {
2051
- dockerArguments.push("-v", `${docker.homeDir}/.gitconfig:/root/.gitconfig:ro`);
2056
+ dockerArguments.push("-v", `${docker.homeDir}/.gitconfig:/home/user/.gitconfig:ro`);
2052
2057
  }
2053
2058
  for (const [key, value] of Object.entries(env)) {
2054
2059
  dockerArguments.push("-e", `${key}=${value}`);
2055
2060
  }
2061
+ for (const key of ["CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_API_KEY"]) {
2062
+ if (process.env[key] && !(key in env)) {
2063
+ dockerArguments.push("-e", `${key}=${process.env[key]}`);
2064
+ }
2065
+ }
2056
2066
  dockerArguments.push(docker.imageTag);
2057
2067
  dockerArguments.push("claude");
2058
2068
  dockerArguments.push(...claudeArguments);
@@ -2535,6 +2545,11 @@ async function removeRepository(path, spawn, context) {
2535
2545
  });
2536
2546
  }
2537
2547
 
2548
+ // lib/bucket/repository-loop.ts
2549
+ import { existsSync as fsExistsSync } from "node:fs";
2550
+ import os2 from "node:os";
2551
+ import path3 from "node:path";
2552
+
2538
2553
  // lib/agent-events.ts
2539
2554
  function rawEventToAgentEvent(rawEvent) {
2540
2555
  if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
@@ -3075,7 +3090,7 @@ Make sure the repository is in a clean state and synced with remote before finis
3075
3090
  log2(`found ${tasks.length} task(s), picking: ${task.title ?? task.path}`);
3076
3091
  onLoopEvent({ type: "loop.tasks_found" });
3077
3092
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
3078
- const { dustCommand, installCommand = "npm install" } = dependencies.settings;
3093
+ const { dustCommand, installCommand } = dependencies.settings;
3079
3094
  const instructions = buildImplementationInstructions(dustCommand, hooksInstalled, task.title ?? undefined, task.path, installCommand);
3080
3095
  const prompt = `Implement the task at \`${task.path}\`:
3081
3096
 
@@ -3166,8 +3181,10 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3166
3181
  homedir: loopDependencies.dockerDeps?.homedir ?? os.homedir,
3167
3182
  existsSync: loopDependencies.dockerDeps?.existsSync ?? existsSync
3168
3183
  };
3184
+ log2(`checking for .dust/Dockerfile in ${context.cwd}`);
3169
3185
  if (hasDockerfile(context.cwd, dockerDeps)) {
3170
3186
  const imageTag = generateImageTag(context.cwd);
3187
+ log2(`Dockerfile found, image tag: ${imageTag}`);
3171
3188
  onLoopEvent({ type: "loop.docker_detected", imageTag });
3172
3189
  if (!await isDockerAvailable(dockerDeps)) {
3173
3190
  context.stderr("Docker not available. Install Docker or remove .dust/Dockerfile to run without Docker.");
@@ -3188,6 +3205,12 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3188
3205
  homeDir,
3189
3206
  hasGitconfig: existsSync(path2.join(homeDir, ".gitconfig"))
3190
3207
  };
3208
+ if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
3209
+ context.stderr("Docker mode requires CLAUDE_CODE_OAUTH_TOKEN. Run `claude setup-token` and export the token.");
3210
+ return { exitCode: 1 };
3211
+ }
3212
+ } else {
3213
+ log2("no .dust/Dockerfile found, running without Docker");
3191
3214
  }
3192
3215
  log2(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
3193
3216
  onLoopEvent({ type: "loop.warning" });
@@ -3518,6 +3541,46 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3518
3541
  loopState
3519
3542
  });
3520
3543
  const hooksInstalled = await manageGitHooks(commandDeps);
3544
+ let dockerConfig;
3545
+ const dockerDeps = {
3546
+ spawn: repoDeps.dockerDeps?.spawn ?? spawn,
3547
+ homedir: repoDeps.dockerDeps?.homedir ?? os2.homedir,
3548
+ existsSync: repoDeps.dockerDeps?.existsSync ?? fsExistsSync
3549
+ };
3550
+ log3(`checking for .dust/Dockerfile in ${repoState.path}`);
3551
+ if (hasDockerfile(repoState.path, dockerDeps)) {
3552
+ const imageTag = generateImageTag(repoState.path);
3553
+ log3(`Dockerfile found, image tag: ${imageTag}`);
3554
+ onLoopEvent({ type: "loop.docker_detected", imageTag });
3555
+ if (!await isDockerAvailable(dockerDeps)) {
3556
+ log3("Docker not available");
3557
+ appendLogLine(repoState.logBuffer, createLogLine("Docker not available. Install Docker or remove .dust/Dockerfile.", "stderr"));
3558
+ } else {
3559
+ onLoopEvent({ type: "loop.docker_building", imageTag });
3560
+ const buildResult = await buildDockerImage({ repoPath: repoState.path, imageTag }, dockerDeps);
3561
+ if (!buildResult.success) {
3562
+ onLoopEvent({ type: "loop.docker_error", error: buildResult.error });
3563
+ log3(`Docker build failed: ${buildResult.error}`);
3564
+ } else {
3565
+ onLoopEvent({ type: "loop.docker_built", imageTag });
3566
+ const homeDir = dockerDeps.homedir();
3567
+ dockerConfig = {
3568
+ imageTag,
3569
+ repoPath: repoState.path,
3570
+ homeDir,
3571
+ hasGitconfig: dockerDeps.existsSync(path3.join(homeDir, ".gitconfig"))
3572
+ };
3573
+ log3(`Docker config ready: ${JSON.stringify(dockerConfig)}`);
3574
+ if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
3575
+ log3("CLAUDE_CODE_OAUTH_TOKEN is not set, cannot run in Docker mode");
3576
+ appendLogLine(repoState.logBuffer, createLogLine("Docker mode requires CLAUDE_CODE_OAUTH_TOKEN. Run `claude setup-token` and export the token.", "stderr"));
3577
+ return;
3578
+ }
3579
+ }
3580
+ }
3581
+ } else {
3582
+ log3("no .dust/Dockerfile found, running without Docker");
3583
+ }
3521
3584
  log3(`loop started for ${repoName} at ${repoState.path}`);
3522
3585
  while (!repoState.stopRequested) {
3523
3586
  loopState.agentSessionId = crypto.randomUUID();
@@ -3530,7 +3593,8 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3530
3593
  hooksInstalled,
3531
3594
  signal: abortController.signal,
3532
3595
  repositoryId: repoState.repository.id.toString(),
3533
- onRawEvent: createHeartbeatThrottler(onAgentEvent)
3596
+ onRawEvent: createHeartbeatThrottler(onAgentEvent),
3597
+ docker: dockerConfig
3534
3598
  });
3535
3599
  } catch (error) {
3536
3600
  const msg = error instanceof Error ? error.message : String(error);
@@ -4212,27 +4276,27 @@ function defaultWriteStdout(data) {
4212
4276
  }
4213
4277
  function createAuthFileSystem(dependencies) {
4214
4278
  return {
4215
- exists: (path3) => {
4279
+ exists: (path4) => {
4216
4280
  try {
4217
- dependencies.accessSync(path3);
4281
+ dependencies.accessSync(path4);
4218
4282
  return true;
4219
4283
  } catch {
4220
4284
  return false;
4221
4285
  }
4222
4286
  },
4223
- isDirectory: (path3) => {
4287
+ isDirectory: (path4) => {
4224
4288
  try {
4225
- return dependencies.statSync(path3).isDirectory();
4289
+ return dependencies.statSync(path4).isDirectory();
4226
4290
  } catch {
4227
4291
  return false;
4228
4292
  }
4229
4293
  },
4230
- getFileCreationTime: (path3) => dependencies.statSync(path3).birthtimeMs,
4231
- readFile: (path3) => dependencies.readFile(path3, "utf8"),
4232
- writeFile: (path3, content) => dependencies.writeFile(path3, content, "utf8"),
4233
- mkdir: (path3, options) => dependencies.mkdir(path3, options).then(() => {}),
4234
- readdir: (path3) => dependencies.readdir(path3),
4235
- chmod: (path3, mode) => dependencies.chmod(path3, mode),
4294
+ getFileCreationTime: (path4) => dependencies.statSync(path4).birthtimeMs,
4295
+ readFile: (path4) => dependencies.readFile(path4, "utf8"),
4296
+ writeFile: (path4, content) => dependencies.writeFile(path4, content, "utf8"),
4297
+ mkdir: (path4, options) => dependencies.mkdir(path4, options).then(() => {}),
4298
+ readdir: (path4) => dependencies.readdir(path4),
4299
+ chmod: (path4, mode) => dependencies.chmod(path4, mode),
4236
4300
  rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
4237
4301
  };
4238
4302
  }
@@ -4699,16 +4763,16 @@ function createDefaultUploadDependencies() {
4699
4763
  getHomeDir: () => homedir2(),
4700
4764
  fileSystem: authFileSystem
4701
4765
  },
4702
- readFileBytes: async (path3) => {
4703
- const buffer = await Bun.file(path3).arrayBuffer();
4766
+ readFileBytes: async (path4) => {
4767
+ const buffer = await Bun.file(path4).arrayBuffer();
4704
4768
  return new Uint8Array(buffer);
4705
4769
  },
4706
- getFileSize: async (path3) => {
4707
- const file = Bun.file(path3);
4770
+ getFileSize: async (path4) => {
4771
+ const file = Bun.file(path4);
4708
4772
  return file.size;
4709
4773
  },
4710
- fileExists: async (path3) => {
4711
- const file = Bun.file(path3);
4774
+ fileExists: async (path4) => {
4775
+ const file = Bun.file(path4);
4712
4776
  return file.exists();
4713
4777
  },
4714
4778
  uploadFile: async (url, token, fileBytes, contentType, fileName) => {
@@ -5502,12 +5566,12 @@ function validateNoCycles(allPrincipleRelationships) {
5502
5566
  }
5503
5567
  for (const rel of allPrincipleRelationships) {
5504
5568
  const visited = new Set;
5505
- const path3 = [];
5569
+ const path4 = [];
5506
5570
  let current = rel.filePath;
5507
5571
  while (current) {
5508
5572
  if (visited.has(current)) {
5509
- const cycleStart = path3.indexOf(current);
5510
- const cyclePath = path3.slice(cycleStart).concat(current);
5573
+ const cycleStart = path4.indexOf(current);
5574
+ const cyclePath = path4.slice(cycleStart).concat(current);
5511
5575
  violations.push({
5512
5576
  file: rel.filePath,
5513
5577
  message: `Cycle detected in principle hierarchy: ${cyclePath.join(" -> ")}`
@@ -5515,7 +5579,7 @@ function validateNoCycles(allPrincipleRelationships) {
5515
5579
  break;
5516
5580
  }
5517
5581
  visited.add(current);
5518
- path3.push(current);
5582
+ path4.push(current);
5519
5583
  const currentRel = relationshipMap.get(current);
5520
5584
  if (currentRel && currentRel.parentPrinciples.length > 0) {
5521
5585
  current = currentRel.parentPrinciples[0];
@@ -6502,8 +6566,8 @@ function parseGitDiffNameStatus(output) {
6502
6566
  const parts = line.split("\t");
6503
6567
  if (parts.length >= 2) {
6504
6568
  const statusChar = parts[0].charAt(0);
6505
- const path3 = parts.length > 2 ? parts[2] : parts[1];
6506
- changes.push({ status: statusChar, path: path3 });
6569
+ const path4 = parts.length > 2 ? parts[2] : parts[1];
6570
+ changes.push({ status: statusChar, path: path4 });
6507
6571
  }
6508
6572
  }
6509
6573
  return changes;
@@ -6564,12 +6628,12 @@ async function getUncommittedFiles(cwd, gitRunner) {
6564
6628
  `).filter((line) => line.length > 0);
6565
6629
  for (const line of lines) {
6566
6630
  if (line.length > 3) {
6567
- const path3 = line.substring(3);
6568
- const arrowIndex = path3.indexOf(" -> ");
6631
+ const path4 = line.substring(3);
6632
+ const arrowIndex = path4.indexOf(" -> ");
6569
6633
  if (arrowIndex !== -1) {
6570
- files.push(path3.substring(arrowIndex + 4));
6634
+ files.push(path4.substring(arrowIndex + 4));
6571
6635
  } else {
6572
- files.push(path3);
6636
+ files.push(path4);
6573
6637
  }
6574
6638
  }
6575
6639
  }
@@ -6720,24 +6784,24 @@ async function main(options) {
6720
6784
  function createFileSystem(primitives) {
6721
6785
  return {
6722
6786
  exists: primitives.existsSync,
6723
- isDirectory: (path3) => {
6787
+ isDirectory: (path4) => {
6724
6788
  try {
6725
- return primitives.statSync(path3).isDirectory();
6789
+ return primitives.statSync(path4).isDirectory();
6726
6790
  } catch {
6727
6791
  return false;
6728
6792
  }
6729
6793
  },
6730
- readFile: (path3) => primitives.readFile(path3, "utf-8"),
6731
- writeFile: (path3, content, options) => primitives.writeFile(path3, content, {
6794
+ readFile: (path4) => primitives.readFile(path4, "utf-8"),
6795
+ writeFile: (path4, content, options) => primitives.writeFile(path4, content, {
6732
6796
  encoding: "utf-8",
6733
6797
  flag: options?.flag
6734
6798
  }),
6735
- mkdir: async (path3, options) => {
6736
- await primitives.mkdir(path3, options);
6799
+ mkdir: async (path4, options) => {
6800
+ await primitives.mkdir(path4, options);
6737
6801
  },
6738
- getFileCreationTime: (path3) => primitives.statSync(path3).birthtimeMs,
6739
- readdir: (path3) => primitives.readdir(path3),
6740
- chmod: (path3, mode) => primitives.chmod(path3, mode),
6802
+ getFileCreationTime: (path4) => primitives.statSync(path4).birthtimeMs,
6803
+ readdir: (path4) => primitives.readdir(path4),
6804
+ chmod: (path4, mode) => primitives.chmod(path4, mode),
6741
6805
  rename: (oldPath, newPath) => primitives.rename(oldPath, newPath)
6742
6806
  };
6743
6807
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.82",
3
+ "version": "0.1.84",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {