@joshski/dust 0.1.81 → 0.1.83

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.
package/dist/dust.js CHANGED
@@ -318,7 +318,7 @@ async function loadSettings(cwd, fileSystem) {
318
318
  }
319
319
 
320
320
  // lib/version.ts
321
- var DUST_VERSION = "0.1.81";
321
+ var DUST_VERSION = "0.1.83";
322
322
 
323
323
  // lib/session.ts
324
324
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -2043,16 +2043,25 @@ function buildDockerRunArguments(docker, claudeArguments, env) {
2043
2043
  "-w",
2044
2044
  "/workspace",
2045
2045
  "-v",
2046
- `${docker.homeDir}/.claude:/root/.claude:ro`,
2046
+ `${docker.homeDir}/.claude:/home/user/.claude`,
2047
2047
  "-v",
2048
- `${docker.homeDir}/.ssh:/root/.ssh:ro`
2048
+ `${docker.homeDir}/.claude.json:/home/user/.claude.json`,
2049
+ "-v",
2050
+ `${docker.homeDir}/.ssh:/home/user/.ssh:ro`,
2051
+ "-e",
2052
+ "HOME=/home/user"
2049
2053
  ];
2050
2054
  if (docker.hasGitconfig) {
2051
- dockerArguments.push("-v", `${docker.homeDir}/.gitconfig:/root/.gitconfig:ro`);
2055
+ dockerArguments.push("-v", `${docker.homeDir}/.gitconfig:/home/user/.gitconfig:ro`);
2052
2056
  }
2053
2057
  for (const [key, value] of Object.entries(env)) {
2054
2058
  dockerArguments.push("-e", `${key}=${value}`);
2055
2059
  }
2060
+ for (const key of ["CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_API_KEY"]) {
2061
+ if (process.env[key] && !(key in env)) {
2062
+ dockerArguments.push("-e", `${key}=${process.env[key]}`);
2063
+ }
2064
+ }
2056
2065
  dockerArguments.push(docker.imageTag);
2057
2066
  dockerArguments.push("claude");
2058
2067
  dockerArguments.push(...claudeArguments);
@@ -2535,6 +2544,11 @@ async function removeRepository(path, spawn, context) {
2535
2544
  });
2536
2545
  }
2537
2546
 
2547
+ // lib/bucket/repository-loop.ts
2548
+ import { existsSync as fsExistsSync } from "node:fs";
2549
+ import os2 from "node:os";
2550
+ import path3 from "node:path";
2551
+
2538
2552
  // lib/agent-events.ts
2539
2553
  function rawEventToAgentEvent(rawEvent) {
2540
2554
  if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
@@ -3166,8 +3180,10 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3166
3180
  homedir: loopDependencies.dockerDeps?.homedir ?? os.homedir,
3167
3181
  existsSync: loopDependencies.dockerDeps?.existsSync ?? existsSync
3168
3182
  };
3183
+ log2(`checking for .dust/Dockerfile in ${context.cwd}`);
3169
3184
  if (hasDockerfile(context.cwd, dockerDeps)) {
3170
3185
  const imageTag = generateImageTag(context.cwd);
3186
+ log2(`Dockerfile found, image tag: ${imageTag}`);
3171
3187
  onLoopEvent({ type: "loop.docker_detected", imageTag });
3172
3188
  if (!await isDockerAvailable(dockerDeps)) {
3173
3189
  context.stderr("Docker not available. Install Docker or remove .dust/Dockerfile to run without Docker.");
@@ -3188,6 +3204,12 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3188
3204
  homeDir,
3189
3205
  hasGitconfig: existsSync(path2.join(homeDir, ".gitconfig"))
3190
3206
  };
3207
+ if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
3208
+ context.stderr("Docker mode requires CLAUDE_CODE_OAUTH_TOKEN. Run `claude setup-token` and export the token.");
3209
+ return { exitCode: 1 };
3210
+ }
3211
+ } else {
3212
+ log2("no .dust/Dockerfile found, running without Docker");
3191
3213
  }
3192
3214
  log2(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
3193
3215
  onLoopEvent({ type: "loop.warning" });
@@ -3518,6 +3540,46 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3518
3540
  loopState
3519
3541
  });
3520
3542
  const hooksInstalled = await manageGitHooks(commandDeps);
3543
+ let dockerConfig;
3544
+ const dockerDeps = {
3545
+ spawn: repoDeps.dockerDeps?.spawn ?? spawn,
3546
+ homedir: repoDeps.dockerDeps?.homedir ?? os2.homedir,
3547
+ existsSync: repoDeps.dockerDeps?.existsSync ?? fsExistsSync
3548
+ };
3549
+ log3(`checking for .dust/Dockerfile in ${repoState.path}`);
3550
+ if (hasDockerfile(repoState.path, dockerDeps)) {
3551
+ const imageTag = generateImageTag(repoState.path);
3552
+ log3(`Dockerfile found, image tag: ${imageTag}`);
3553
+ onLoopEvent({ type: "loop.docker_detected", imageTag });
3554
+ if (!await isDockerAvailable(dockerDeps)) {
3555
+ log3("Docker not available");
3556
+ appendLogLine(repoState.logBuffer, createLogLine("Docker not available. Install Docker or remove .dust/Dockerfile.", "stderr"));
3557
+ } else {
3558
+ onLoopEvent({ type: "loop.docker_building", imageTag });
3559
+ const buildResult = await buildDockerImage({ repoPath: repoState.path, imageTag }, dockerDeps);
3560
+ if (!buildResult.success) {
3561
+ onLoopEvent({ type: "loop.docker_error", error: buildResult.error });
3562
+ log3(`Docker build failed: ${buildResult.error}`);
3563
+ } else {
3564
+ onLoopEvent({ type: "loop.docker_built", imageTag });
3565
+ const homeDir = dockerDeps.homedir();
3566
+ dockerConfig = {
3567
+ imageTag,
3568
+ repoPath: repoState.path,
3569
+ homeDir,
3570
+ hasGitconfig: dockerDeps.existsSync(path3.join(homeDir, ".gitconfig"))
3571
+ };
3572
+ log3(`Docker config ready: ${JSON.stringify(dockerConfig)}`);
3573
+ if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
3574
+ log3("CLAUDE_CODE_OAUTH_TOKEN is not set, cannot run in Docker mode");
3575
+ appendLogLine(repoState.logBuffer, createLogLine("Docker mode requires CLAUDE_CODE_OAUTH_TOKEN. Run `claude setup-token` and export the token.", "stderr"));
3576
+ return;
3577
+ }
3578
+ }
3579
+ }
3580
+ } else {
3581
+ log3("no .dust/Dockerfile found, running without Docker");
3582
+ }
3521
3583
  log3(`loop started for ${repoName} at ${repoState.path}`);
3522
3584
  while (!repoState.stopRequested) {
3523
3585
  loopState.agentSessionId = crypto.randomUUID();
@@ -3530,7 +3592,8 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3530
3592
  hooksInstalled,
3531
3593
  signal: abortController.signal,
3532
3594
  repositoryId: repoState.repository.id.toString(),
3533
- onRawEvent: createHeartbeatThrottler(onAgentEvent)
3595
+ onRawEvent: createHeartbeatThrottler(onAgentEvent),
3596
+ docker: dockerConfig
3534
3597
  });
3535
3598
  } catch (error) {
3536
3599
  const msg = error instanceof Error ? error.message : String(error);
@@ -3587,6 +3650,7 @@ function parseRepository(data) {
3587
3650
  return {
3588
3651
  name: repositoryData.name,
3589
3652
  gitUrl: repositoryData.gitUrl,
3653
+ gitSshUrl: typeof repositoryData.gitSshUrl === "string" ? repositoryData.gitSshUrl : undefined,
3590
3654
  url: repositoryData.url,
3591
3655
  id: repositoryData.id
3592
3656
  };
@@ -4211,27 +4275,27 @@ function defaultWriteStdout(data) {
4211
4275
  }
4212
4276
  function createAuthFileSystem(dependencies) {
4213
4277
  return {
4214
- exists: (path3) => {
4278
+ exists: (path4) => {
4215
4279
  try {
4216
- dependencies.accessSync(path3);
4280
+ dependencies.accessSync(path4);
4217
4281
  return true;
4218
4282
  } catch {
4219
4283
  return false;
4220
4284
  }
4221
4285
  },
4222
- isDirectory: (path3) => {
4286
+ isDirectory: (path4) => {
4223
4287
  try {
4224
- return dependencies.statSync(path3).isDirectory();
4288
+ return dependencies.statSync(path4).isDirectory();
4225
4289
  } catch {
4226
4290
  return false;
4227
4291
  }
4228
4292
  },
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),
4293
+ getFileCreationTime: (path4) => dependencies.statSync(path4).birthtimeMs,
4294
+ readFile: (path4) => dependencies.readFile(path4, "utf8"),
4295
+ writeFile: (path4, content) => dependencies.writeFile(path4, content, "utf8"),
4296
+ mkdir: (path4, options) => dependencies.mkdir(path4, options).then(() => {}),
4297
+ readdir: (path4) => dependencies.readdir(path4),
4298
+ chmod: (path4, mode) => dependencies.chmod(path4, mode),
4235
4299
  rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
4236
4300
  };
4237
4301
  }
@@ -4454,13 +4518,12 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4454
4518
  logMessage(state, context, useTUI, " (empty)");
4455
4519
  } else {
4456
4520
  for (const r of repos) {
4457
- const attrs = [];
4458
- attrs.push(`name=${r.name}`);
4459
- attrs.push(`id=${r.id}`);
4460
- attrs.push(`gitUrl=${r.gitUrl}`);
4461
- attrs.push(`url=${r.url}`);
4462
- attrs.push(`hasTask=${r.hasTask}`);
4463
- logMessage(state, context, useTUI, ` - ${attrs.join(", ")}`);
4521
+ logMessage(state, context, useTUI, ` - name=${r.name}`);
4522
+ logMessage(state, context, useTUI, ` id=${r.id}`);
4523
+ logMessage(state, context, useTUI, ` gitUrl=${r.gitUrl}`);
4524
+ logMessage(state, context, useTUI, ` gitSshUrl=${r.gitSshUrl ?? "(none)"}`);
4525
+ logMessage(state, context, useTUI, ` url=${r.url}`);
4526
+ logMessage(state, context, useTUI, ` hasTask=${r.hasTask}`);
4464
4527
  }
4465
4528
  }
4466
4529
  syncUIWithRepoList(state, repos);
@@ -4699,16 +4762,16 @@ function createDefaultUploadDependencies() {
4699
4762
  getHomeDir: () => homedir2(),
4700
4763
  fileSystem: authFileSystem
4701
4764
  },
4702
- readFileBytes: async (path3) => {
4703
- const buffer = await Bun.file(path3).arrayBuffer();
4765
+ readFileBytes: async (path4) => {
4766
+ const buffer = await Bun.file(path4).arrayBuffer();
4704
4767
  return new Uint8Array(buffer);
4705
4768
  },
4706
- getFileSize: async (path3) => {
4707
- const file = Bun.file(path3);
4769
+ getFileSize: async (path4) => {
4770
+ const file = Bun.file(path4);
4708
4771
  return file.size;
4709
4772
  },
4710
- fileExists: async (path3) => {
4711
- const file = Bun.file(path3);
4773
+ fileExists: async (path4) => {
4774
+ const file = Bun.file(path4);
4712
4775
  return file.exists();
4713
4776
  },
4714
4777
  uploadFile: async (url, token, fileBytes, contentType, fileName) => {
@@ -5502,12 +5565,12 @@ function validateNoCycles(allPrincipleRelationships) {
5502
5565
  }
5503
5566
  for (const rel of allPrincipleRelationships) {
5504
5567
  const visited = new Set;
5505
- const path3 = [];
5568
+ const path4 = [];
5506
5569
  let current = rel.filePath;
5507
5570
  while (current) {
5508
5571
  if (visited.has(current)) {
5509
- const cycleStart = path3.indexOf(current);
5510
- const cyclePath = path3.slice(cycleStart).concat(current);
5572
+ const cycleStart = path4.indexOf(current);
5573
+ const cyclePath = path4.slice(cycleStart).concat(current);
5511
5574
  violations.push({
5512
5575
  file: rel.filePath,
5513
5576
  message: `Cycle detected in principle hierarchy: ${cyclePath.join(" -> ")}`
@@ -5515,7 +5578,7 @@ function validateNoCycles(allPrincipleRelationships) {
5515
5578
  break;
5516
5579
  }
5517
5580
  visited.add(current);
5518
- path3.push(current);
5581
+ path4.push(current);
5519
5582
  const currentRel = relationshipMap.get(current);
5520
5583
  if (currentRel && currentRel.parentPrinciples.length > 0) {
5521
5584
  current = currentRel.parentPrinciples[0];
@@ -6502,8 +6565,8 @@ function parseGitDiffNameStatus(output) {
6502
6565
  const parts = line.split("\t");
6503
6566
  if (parts.length >= 2) {
6504
6567
  const statusChar = parts[0].charAt(0);
6505
- const path3 = parts.length > 2 ? parts[2] : parts[1];
6506
- changes.push({ status: statusChar, path: path3 });
6568
+ const path4 = parts.length > 2 ? parts[2] : parts[1];
6569
+ changes.push({ status: statusChar, path: path4 });
6507
6570
  }
6508
6571
  }
6509
6572
  return changes;
@@ -6564,12 +6627,12 @@ async function getUncommittedFiles(cwd, gitRunner) {
6564
6627
  `).filter((line) => line.length > 0);
6565
6628
  for (const line of lines) {
6566
6629
  if (line.length > 3) {
6567
- const path3 = line.substring(3);
6568
- const arrowIndex = path3.indexOf(" -> ");
6630
+ const path4 = line.substring(3);
6631
+ const arrowIndex = path4.indexOf(" -> ");
6569
6632
  if (arrowIndex !== -1) {
6570
- files.push(path3.substring(arrowIndex + 4));
6633
+ files.push(path4.substring(arrowIndex + 4));
6571
6634
  } else {
6572
- files.push(path3);
6635
+ files.push(path4);
6573
6636
  }
6574
6637
  }
6575
6638
  }
@@ -6720,24 +6783,24 @@ async function main(options) {
6720
6783
  function createFileSystem(primitives) {
6721
6784
  return {
6722
6785
  exists: primitives.existsSync,
6723
- isDirectory: (path3) => {
6786
+ isDirectory: (path4) => {
6724
6787
  try {
6725
- return primitives.statSync(path3).isDirectory();
6788
+ return primitives.statSync(path4).isDirectory();
6726
6789
  } catch {
6727
6790
  return false;
6728
6791
  }
6729
6792
  },
6730
- readFile: (path3) => primitives.readFile(path3, "utf-8"),
6731
- writeFile: (path3, content, options) => primitives.writeFile(path3, content, {
6793
+ readFile: (path4) => primitives.readFile(path4, "utf-8"),
6794
+ writeFile: (path4, content, options) => primitives.writeFile(path4, content, {
6732
6795
  encoding: "utf-8",
6733
6796
  flag: options?.flag
6734
6797
  }),
6735
- mkdir: async (path3, options) => {
6736
- await primitives.mkdir(path3, options);
6798
+ mkdir: async (path4, options) => {
6799
+ await primitives.mkdir(path4, options);
6737
6800
  },
6738
- getFileCreationTime: (path3) => primitives.statSync(path3).birthtimeMs,
6739
- readdir: (path3) => primitives.readdir(path3),
6740
- chmod: (path3, mode) => primitives.chmod(path3, mode),
6801
+ getFileCreationTime: (path4) => primitives.statSync(path4).birthtimeMs,
6802
+ readdir: (path4) => primitives.readdir(path4),
6803
+ chmod: (path4, mode) => primitives.chmod(path4, mode),
6741
6804
  rename: (oldPath, newPath) => primitives.rename(oldPath, newPath)
6742
6805
  };
6743
6806
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {