@joshski/dust 0.1.14 → 0.1.15

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.
Files changed (2) hide show
  1. package/dist/dust.js +152 -42
  2. package/package.json +1 -1
package/dist/dust.js CHANGED
@@ -1121,7 +1121,8 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
1121
1121
  model,
1122
1122
  systemPrompt,
1123
1123
  sessionId,
1124
- dangerouslySkipPermissions
1124
+ dangerouslySkipPermissions,
1125
+ env
1125
1126
  } = options;
1126
1127
  const claudeArguments = [
1127
1128
  "-p",
@@ -1151,7 +1152,8 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
1151
1152
  }
1152
1153
  const proc = dependencies.spawn("claude", claudeArguments, {
1153
1154
  cwd,
1154
- stdio: ["ignore", "pipe", "pipe"]
1155
+ stdio: ["ignore", "pipe", "pipe"],
1156
+ env: { ...process.env, ...env }
1155
1157
  });
1156
1158
  if (!proc.stdout) {
1157
1159
  throw new Error("Failed to get stdout from claude process");
@@ -1563,11 +1565,71 @@ async function next(dependencies) {
1563
1565
  }
1564
1566
 
1565
1567
  // lib/cli/commands/loop.ts
1568
+ function formatEvent(event) {
1569
+ switch (event.type) {
1570
+ case "loop.warning":
1571
+ return "⚠️ WARNING: This command skips all permission checks. Only use in a sandbox environment!";
1572
+ case "loop.started":
1573
+ return `\uD83D\uDD04 Starting dust loop claude (max ${event.maxIterations} iterations)...`;
1574
+ case "loop.syncing":
1575
+ return "\uD83D\uDD04 Syncing with remote...";
1576
+ case "loop.sync_skipped":
1577
+ return `Note: git pull skipped (${event.reason})`;
1578
+ case "loop.checking_tasks":
1579
+ return "\uD83D\uDD0D Checking for available tasks...";
1580
+ case "loop.no_tasks":
1581
+ return "\uD83D\uDCA4 No tasks available. Sleeping...";
1582
+ case "loop.tasks_found":
1583
+ return "✨ Found task(s). \uD83E\uDD16 Starting Claude...";
1584
+ case "claude.started":
1585
+ return "\uD83E\uDD16 Claude session started";
1586
+ case "claude.ended":
1587
+ return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
1588
+ case "loop.iteration_complete":
1589
+ return `\uD83D\uDCCB Completed iteration ${event.iteration}/${event.maxIterations}`;
1590
+ case "loop.ended":
1591
+ return `\uD83C\uDFC1 Reached max iterations (${event.maxIterations}). Exiting.`;
1592
+ }
1593
+ }
1594
+ async function defaultPostEvent(url, payload) {
1595
+ await fetch(url, {
1596
+ method: "POST",
1597
+ headers: { "Content-Type": "application/json" },
1598
+ body: JSON.stringify(payload)
1599
+ });
1600
+ }
1566
1601
  function createDefaultDependencies() {
1567
1602
  return {
1568
1603
  spawn: nodeSpawn2,
1569
1604
  run,
1570
- sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
1605
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
1606
+ postEvent: defaultPostEvent
1607
+ };
1608
+ }
1609
+ function createEventPoster(eventsUrl, sessionId, postEvent, onError) {
1610
+ let sequence = 0;
1611
+ let currentAgentSessionId;
1612
+ return (event) => {
1613
+ if (!eventsUrl)
1614
+ return;
1615
+ sequence++;
1616
+ if (event.type === "claude.started") {
1617
+ currentAgentSessionId = crypto.randomUUID();
1618
+ }
1619
+ const payload = {
1620
+ sequence,
1621
+ timestamp: new Date().toISOString(),
1622
+ sessionId,
1623
+ event
1624
+ };
1625
+ if (event.type.startsWith("claude.") && currentAgentSessionId) {
1626
+ payload.agentSessionId = currentAgentSessionId;
1627
+ payload.agentType = "claude";
1628
+ }
1629
+ postEvent(eventsUrl, payload).catch(onError);
1630
+ if (event.type === "claude.ended") {
1631
+ currentAgentSessionId = undefined;
1632
+ }
1571
1633
  };
1572
1634
  }
1573
1635
  var SLEEP_INTERVAL_MS = 30000;
@@ -1605,16 +1667,17 @@ async function hasAvailableTasks(dependencies) {
1605
1667
  await next({ ...dependencies, context: captureContext });
1606
1668
  return hasOutput;
1607
1669
  }
1608
- async function runOneIteration(dependencies, loopDependencies) {
1670
+ async function runOneIteration(dependencies, loopDependencies, emit) {
1609
1671
  const { context } = dependencies;
1610
1672
  const { spawn: spawn2, run: run2 } = loopDependencies;
1611
- context.stdout("\uD83D\uDD04 Syncing with remote...");
1673
+ emit({ type: "loop.syncing" });
1612
1674
  const pullResult = await gitPull(context.cwd, spawn2);
1613
1675
  if (!pullResult.success) {
1614
- context.stdout(`⚠️ git pull failed: ${pullResult.message}`);
1615
- context.stdout("");
1616
- context.stdout("\uD83E\uDD16 Starting Claude to resolve the conflict...");
1617
- context.stdout("");
1676
+ emit({
1677
+ type: "loop.sync_skipped",
1678
+ reason: pullResult.message ?? "unknown error"
1679
+ });
1680
+ emit({ type: "claude.started" });
1618
1681
  const prompt = `git pull failed with the following error:
1619
1682
 
1620
1683
  ${pullResult.message}
@@ -1626,42 +1689,39 @@ Please resolve this issue. Common approaches:
1626
1689
 
1627
1690
  Make sure the repository is in a clean state and synced with remote before finishing.`;
1628
1691
  try {
1629
- await run2(prompt, { cwd: context.cwd, dangerouslySkipPermissions: true });
1630
- context.stdout("");
1631
- context.stdout("✅ Claude resolved the git pull conflict. Continuing loop...");
1632
- context.stdout("");
1692
+ await run2(prompt, {
1693
+ cwd: context.cwd,
1694
+ dangerouslySkipPermissions: true,
1695
+ env: { DUST_UNATTENDED: "1" }
1696
+ });
1697
+ emit({ type: "claude.ended", success: true });
1633
1698
  return "resolved_pull_conflict";
1634
1699
  } catch (error) {
1635
- const message = error instanceof Error ? error.message : String(error);
1636
- context.stderr(`Claude failed to resolve git pull conflict: ${message}`);
1637
- context.stdout("");
1638
- context.stdout("⚠️ Continuing loop despite unresolved conflict...");
1639
- context.stdout("");
1700
+ const errorMessage = error instanceof Error ? error.message : String(error);
1701
+ context.stderr(`Claude failed to resolve git pull conflict: ${errorMessage}`);
1702
+ emit({ type: "claude.ended", success: false, error: errorMessage });
1640
1703
  }
1641
1704
  }
1642
- context.stdout("\uD83D\uDD0D Checking for available tasks...");
1705
+ emit({ type: "loop.checking_tasks" });
1643
1706
  const hasTasks = await hasAvailableTasks(dependencies);
1644
1707
  if (!hasTasks) {
1645
- context.stdout("\uD83D\uDCA4 No tasks available. Sleeping...");
1646
- context.stdout("");
1708
+ emit({ type: "loop.no_tasks" });
1647
1709
  return "no_tasks";
1648
1710
  }
1649
- context.stdout("✨ Found a task!");
1650
- context.stdout("");
1651
- context.stdout("\uD83E\uDD16 Starting Claude...");
1652
- context.stdout("");
1711
+ emit({ type: "loop.tasks_found" });
1712
+ emit({ type: "claude.started" });
1653
1713
  try {
1654
- await run2("go", { cwd: context.cwd, dangerouslySkipPermissions: true });
1655
- context.stdout("");
1656
- context.stdout("✅ Claude session complete. Continuing loop...");
1657
- context.stdout("");
1714
+ await run2("go", {
1715
+ cwd: context.cwd,
1716
+ dangerouslySkipPermissions: true,
1717
+ env: { DUST_UNATTENDED: "1" }
1718
+ });
1719
+ emit({ type: "claude.ended", success: true });
1658
1720
  return "ran_claude";
1659
1721
  } catch (error) {
1660
- const message = error instanceof Error ? error.message : String(error);
1661
- context.stderr(`Claude exited with error: ${message}`);
1662
- context.stdout("");
1663
- context.stdout("✅ Claude session complete. Continuing loop...");
1664
- context.stdout("");
1722
+ const errorMessage = error instanceof Error ? error.message : String(error);
1723
+ context.stderr(`Claude exited with error: ${errorMessage}`);
1724
+ emit({ type: "claude.ended", success: false, error: errorMessage });
1665
1725
  return "claude_error";
1666
1726
  }
1667
1727
  }
@@ -1676,25 +1736,38 @@ function parseMaxIterations(commandArguments) {
1676
1736
  return parsed;
1677
1737
  }
1678
1738
  async function loopClaude(dependencies, loopDependencies = createDefaultDependencies()) {
1679
- const { context } = dependencies;
1739
+ const { context, settings } = dependencies;
1740
+ const { postEvent } = loopDependencies;
1680
1741
  const maxIterations = parseMaxIterations(dependencies.arguments);
1681
- context.stdout("⚠️ WARNING: This command skips all permission checks. Only use in a sandbox environment!");
1682
- context.stdout("");
1683
- context.stdout(`\uD83D\uDD04 Starting dust loop claude (max ${maxIterations} iterations)...`);
1742
+ const eventsUrl = settings.eventsUrl;
1743
+ const sessionId = crypto.randomUUID();
1744
+ const postTypedEvent = createEventPoster(eventsUrl, sessionId, postEvent, (error) => {
1745
+ const message = error instanceof Error ? error.message : String(error);
1746
+ context.stderr(`Event POST failed: ${message}`);
1747
+ });
1748
+ const emit = (event) => {
1749
+ context.stdout(formatEvent(event));
1750
+ postTypedEvent(event);
1751
+ };
1752
+ emit({ type: "loop.warning" });
1753
+ emit({ type: "loop.started", maxIterations });
1684
1754
  context.stdout(" Press Ctrl+C to stop");
1685
1755
  context.stdout("");
1686
1756
  let completedIterations = 0;
1687
1757
  while (completedIterations < maxIterations) {
1688
- const result = await runOneIteration(dependencies, loopDependencies);
1758
+ const result = await runOneIteration(dependencies, loopDependencies, emit);
1689
1759
  if (result === "no_tasks") {
1690
1760
  await loopDependencies.sleep(SLEEP_INTERVAL_MS);
1691
1761
  } else {
1692
1762
  completedIterations++;
1693
- context.stdout(`\uD83D\uDCCB Completed iteration ${completedIterations}/${maxIterations}`);
1694
- context.stdout("");
1763
+ emit({
1764
+ type: "loop.iteration_complete",
1765
+ iteration: completedIterations,
1766
+ maxIterations
1767
+ });
1695
1768
  }
1696
1769
  }
1697
- context.stdout(`\uD83C\uDFC1 Reached max iterations (${maxIterations}). Exiting.`);
1770
+ emit({ type: "loop.ended", maxIterations });
1698
1771
  return { exitCode: 0 };
1699
1772
  }
1700
1773
 
@@ -1795,8 +1868,45 @@ async function getChangesFromRemote(cwd, gitRunner) {
1795
1868
  }
1796
1869
  return parseGitDiffNameStatus(diffResult.output);
1797
1870
  }
1871
+ async function getUncommittedFiles(cwd, gitRunner) {
1872
+ const result = await gitRunner.run(["status", "--porcelain"], cwd);
1873
+ if (result.exitCode !== 0 || !result.output.trim()) {
1874
+ return [];
1875
+ }
1876
+ const files = [];
1877
+ const lines = result.output.split(`
1878
+ `).filter((line) => line.length > 0);
1879
+ for (const line of lines) {
1880
+ if (line.length > 3) {
1881
+ const path = line.substring(3);
1882
+ const arrowIndex = path.indexOf(" -> ");
1883
+ if (arrowIndex !== -1) {
1884
+ files.push(path.substring(arrowIndex + 4));
1885
+ } else {
1886
+ files.push(path);
1887
+ }
1888
+ }
1889
+ }
1890
+ return files;
1891
+ }
1798
1892
  async function prePush(dependencies, gitRunner = defaultGitRunner, env = process.env) {
1799
1893
  const { context } = dependencies;
1894
+ if (env.DUST_UNATTENDED) {
1895
+ const uncommittedFiles = await getUncommittedFiles(context.cwd, gitRunner);
1896
+ if (uncommittedFiles.length > 0) {
1897
+ context.stderr("");
1898
+ context.stderr("⚠️ Push blocked: uncommitted changes detected in unattended mode.");
1899
+ context.stderr("");
1900
+ context.stderr("You are running in unattended mode (DUST_UNATTENDED=1) and have uncommitted files:");
1901
+ for (const file of uncommittedFiles) {
1902
+ context.stderr(` → ${file}`);
1903
+ }
1904
+ context.stderr("");
1905
+ context.stderr("Commit or discard these changes before pushing to avoid broken builds.");
1906
+ context.stderr("");
1907
+ return { exitCode: 1 };
1908
+ }
1909
+ }
1800
1910
  const changes = await getChangesFromRemote(context.cwd, gitRunner);
1801
1911
  if (changes.length > 0) {
1802
1912
  const analysis = analyzeChangesForTaskOnlyPattern(changes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {