@joshski/dust 0.1.83 → 0.1.85

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.
@@ -0,0 +1,25 @@
1
+ language js
2
+
3
+ or {
4
+ `vi.mock($...)` where {
5
+ register_diagnostic(span=$match, message="Avoid vi.mock(). Use dependency injection or a test helper instead.")
6
+ },
7
+ `vi.spyOn($...)` where {
8
+ register_diagnostic(span=$match, message="Avoid vi.spyOn(). Use dependency injection or a test helper instead.")
9
+ },
10
+ `vi.useFakeTimers($...)` where {
11
+ register_diagnostic(span=$match, message="Avoid vi.useFakeTimers(). Use a test helper instead.")
12
+ },
13
+ `vi.useRealTimers($...)` where {
14
+ register_diagnostic(span=$match, message="Avoid vi.useRealTimers(). Use a test helper instead.")
15
+ },
16
+ `vi.runAllTimers($...)` where {
17
+ register_diagnostic(span=$match, message="Avoid vi.runAllTimers(). Use a test helper instead.")
18
+ },
19
+ `vi.advanceTimersByTime($...)` where {
20
+ register_diagnostic(span=$match, message="Avoid vi.advanceTimersByTime(). Use a test helper instead.")
21
+ },
22
+ `vi.fn($...)` where {
23
+ register_diagnostic(span=$match, message="Avoid vi.fn(). Use a typed test double or test helper instead.")
24
+ }
25
+ }
@@ -22,7 +22,8 @@ export type AgentSessionEvent = {
22
22
  } | {
23
23
  type: 'agent-session-activity';
24
24
  } | {
25
- type: 'claude-event';
25
+ type: 'agent-event';
26
+ provider: string;
26
27
  rawEvent: Record<string, unknown>;
27
28
  };
28
29
  export interface EventMessage {
@@ -35,19 +36,19 @@ export interface EventMessage {
35
36
  event: AgentSessionEvent;
36
37
  }
37
38
  /**
38
- * Convert a raw Claude streaming event to an AgentSessionEvent.
39
+ * Convert a raw agent streaming event to an AgentSessionEvent.
39
40
  * stream_event types become activity heartbeats; everything else
40
- * is forwarded as a claude-event.
41
+ * is forwarded as an agent-event with provider info.
41
42
  */
42
- export declare function rawEventToAgentEvent(rawEvent: Record<string, unknown>): AgentSessionEvent;
43
+ export declare function rawEventToAgentEvent(rawEvent: Record<string, unknown>, provider: string): AgentSessionEvent;
43
44
  /**
44
45
  * Create a heartbeat throttler that limits agent-session-activity events
45
46
  * to at most once per interval (default: 5 seconds).
46
47
  *
47
- * The returned callback converts raw Claude events to AgentSessionEvents,
48
+ * The returned callback converts raw agent events to AgentSessionEvents,
48
49
  * throttling stream_event heartbeats while forwarding all other events.
49
50
  */
50
- export declare function createHeartbeatThrottler(onAgentEvent: (event: AgentSessionEvent) => void, options?: {
51
+ export declare function createHeartbeatThrottler(onAgentEvent: (event: AgentSessionEvent) => void, provider: string, options?: {
51
52
  intervalMs?: number;
52
53
  now?: () => number;
53
54
  }): (rawEvent: Record<string, unknown>) => void;
@@ -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.83";
322
+ var DUST_VERSION = "0.1.85";
322
323
 
323
324
  // lib/session.ts
324
325
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -1839,6 +1840,264 @@ function openBrowser(url) {
1839
1840
  nodeSpawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
1840
1841
  }
1841
1842
 
1843
+ // lib/bucket/bucket-state.ts
1844
+ function handleServerMessage(state, message) {
1845
+ const effects = [];
1846
+ effects.push({
1847
+ type: "debugLog",
1848
+ message: `ws message: ${message.type}`
1849
+ });
1850
+ switch (message.type) {
1851
+ case "repository-list": {
1852
+ const repos = message.repositories;
1853
+ effects.push({
1854
+ type: "log",
1855
+ message: `Received repository list (${repos.length} repositories):`,
1856
+ stream: "stdout"
1857
+ });
1858
+ if (repos.length === 0) {
1859
+ effects.push({
1860
+ type: "log",
1861
+ message: " (empty)",
1862
+ stream: "stdout"
1863
+ });
1864
+ } else {
1865
+ for (const r of repos) {
1866
+ effects.push({
1867
+ type: "log",
1868
+ message: ` - name=${r.name}`,
1869
+ stream: "stdout"
1870
+ });
1871
+ effects.push({
1872
+ type: "log",
1873
+ message: ` id=${r.id}`,
1874
+ stream: "stdout"
1875
+ });
1876
+ effects.push({
1877
+ type: "log",
1878
+ message: ` gitUrl=${r.gitUrl}`,
1879
+ stream: "stdout"
1880
+ });
1881
+ effects.push({
1882
+ type: "log",
1883
+ message: ` gitSshUrl=${r.gitSshUrl ?? "(none)"}`,
1884
+ stream: "stdout"
1885
+ });
1886
+ effects.push({
1887
+ type: "log",
1888
+ message: ` url=${r.url}`,
1889
+ stream: "stdout"
1890
+ });
1891
+ effects.push({
1892
+ type: "log",
1893
+ message: ` hasTask=${r.hasTask}`,
1894
+ stream: "stdout"
1895
+ });
1896
+ if (r.agentProvider) {
1897
+ effects.push({
1898
+ type: "log",
1899
+ message: ` agentProvider=${r.agentProvider}`,
1900
+ stream: "stdout"
1901
+ });
1902
+ }
1903
+ }
1904
+ }
1905
+ effects.push({
1906
+ type: "syncUI",
1907
+ repositories: repos
1908
+ });
1909
+ effects.push({
1910
+ type: "handleRepositoryList",
1911
+ repositories: repos
1912
+ });
1913
+ break;
1914
+ }
1915
+ case "task-available": {
1916
+ const repoName = message.repository;
1917
+ effects.push({
1918
+ type: "log",
1919
+ message: `Received task-available for ${repoName}`,
1920
+ stream: "stdout"
1921
+ });
1922
+ if (state.repositoryNames.includes(repoName)) {
1923
+ effects.push({
1924
+ type: "signalTaskAvailable",
1925
+ repositoryName: repoName
1926
+ });
1927
+ } else {
1928
+ effects.push({
1929
+ type: "log",
1930
+ message: `No repository state found for ${repoName}`,
1931
+ stream: "stderr"
1932
+ });
1933
+ }
1934
+ break;
1935
+ }
1936
+ }
1937
+ return { effects };
1938
+ }
1939
+ function handleMessageParseError(rawData) {
1940
+ return {
1941
+ effects: [
1942
+ {
1943
+ type: "log",
1944
+ message: `Failed to parse WebSocket message: ${rawData}`,
1945
+ stream: "stderr"
1946
+ }
1947
+ ]
1948
+ };
1949
+ }
1950
+ function handleInvalidMessageFormat(rawData) {
1951
+ return {
1952
+ effects: [
1953
+ {
1954
+ type: "log",
1955
+ message: `Invalid WebSocket message format: ${rawData}`,
1956
+ stream: "stderr"
1957
+ }
1958
+ ]
1959
+ };
1960
+ }
1961
+ var KEYS = {
1962
+ UP: "\x1B[A",
1963
+ DOWN: "\x1B[B",
1964
+ RIGHT: "\x1B[C",
1965
+ LEFT: "\x1B[D",
1966
+ PAGE_UP: "\x1B[5~",
1967
+ PAGE_DOWN: "\x1B[6~",
1968
+ HOME: "\x1B[H",
1969
+ END: "\x1B[F",
1970
+ CTRL_C: "\x03"
1971
+ };
1972
+ var SGR_MOUSE_RE = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
1973
+ function parseSGRMouse(key) {
1974
+ const match = key.match(SGR_MOUSE_RE);
1975
+ if (!match)
1976
+ return null;
1977
+ return Number.parseInt(match[1], 10);
1978
+ }
1979
+ function handleKeypress(state, key) {
1980
+ const effects = [];
1981
+ const mouseButton = parseSGRMouse(key);
1982
+ if (mouseButton !== null) {
1983
+ if (mouseButton === 64) {
1984
+ effects.push({ type: "scroll", direction: "up" });
1985
+ effects.push({ type: "scroll", direction: "up" });
1986
+ effects.push({ type: "scroll", direction: "up" });
1987
+ } else if (mouseButton === 65) {
1988
+ effects.push({ type: "scroll", direction: "down" });
1989
+ effects.push({ type: "scroll", direction: "down" });
1990
+ effects.push({ type: "scroll", direction: "down" });
1991
+ }
1992
+ return { effects };
1993
+ }
1994
+ switch (key) {
1995
+ case "q":
1996
+ case KEYS.CTRL_C:
1997
+ effects.push({ type: "quit" });
1998
+ break;
1999
+ case KEYS.LEFT:
2000
+ effects.push({ type: "selectPrevious" });
2001
+ break;
2002
+ case KEYS.RIGHT:
2003
+ effects.push({ type: "selectNext" });
2004
+ break;
2005
+ case KEYS.UP:
2006
+ effects.push({ type: "scroll", direction: "up" });
2007
+ break;
2008
+ case KEYS.DOWN:
2009
+ effects.push({ type: "scroll", direction: "down" });
2010
+ break;
2011
+ case KEYS.PAGE_UP:
2012
+ effects.push({ type: "scroll", direction: "pageUp" });
2013
+ break;
2014
+ case KEYS.PAGE_DOWN:
2015
+ effects.push({ type: "scroll", direction: "pageDown" });
2016
+ break;
2017
+ case "g":
2018
+ case KEYS.HOME:
2019
+ effects.push({ type: "scroll", direction: "top" });
2020
+ break;
2021
+ case "G":
2022
+ case KEYS.END:
2023
+ effects.push({ type: "scroll", direction: "bottom" });
2024
+ break;
2025
+ case "o": {
2026
+ if (state.selectedIndex === -1) {
2027
+ break;
2028
+ }
2029
+ const repoName = state.repositories[state.selectedIndex];
2030
+ if (!repoName)
2031
+ break;
2032
+ const url = state.repositoryUrls[repoName];
2033
+ if (url) {
2034
+ effects.push({ type: "openBrowser", url });
2035
+ }
2036
+ break;
2037
+ }
2038
+ }
2039
+ return { effects };
2040
+ }
2041
+ var INITIAL_RECONNECT_DELAY_MS = 1000;
2042
+ var MAX_RECONNECT_DELAY_MS = 30000;
2043
+ function handleClose(state, code, reason) {
2044
+ const effects = [];
2045
+ effects.push({
2046
+ type: "log",
2047
+ message: `bucket.disconnected code=${code} reason=${reason || "none"}`,
2048
+ stream: "stdout"
2049
+ });
2050
+ if (code === 4000) {
2051
+ effects.push({
2052
+ type: "log",
2053
+ message: "Another connection replaced this one. Not reconnecting.",
2054
+ stream: "stdout"
2055
+ });
2056
+ return { state, effects };
2057
+ }
2058
+ if (!state.shuttingDown) {
2059
+ effects.push({
2060
+ type: "log",
2061
+ message: `Reconnecting in ${state.reconnectDelay / 1000} seconds...`,
2062
+ stream: "stdout"
2063
+ });
2064
+ effects.push({
2065
+ type: "scheduleReconnect",
2066
+ delayMs: state.reconnectDelay
2067
+ });
2068
+ const nextDelay = Math.min(state.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
2069
+ return {
2070
+ state: { ...state, reconnectDelay: nextDelay },
2071
+ effects
2072
+ };
2073
+ }
2074
+ return { state, effects };
2075
+ }
2076
+ function handleError(state, errorMessage) {
2077
+ return {
2078
+ state,
2079
+ effects: [
2080
+ {
2081
+ type: "log",
2082
+ message: `WebSocket error: ${errorMessage}`,
2083
+ stream: "stderr"
2084
+ }
2085
+ ]
2086
+ };
2087
+ }
2088
+ function handleOpen(state) {
2089
+ return {
2090
+ state: { ...state, reconnectDelay: INITIAL_RECONNECT_DELAY_MS },
2091
+ effects: [
2092
+ {
2093
+ type: "log",
2094
+ message: "bucket.connected",
2095
+ stream: "stdout"
2096
+ }
2097
+ ]
2098
+ };
2099
+ }
2100
+
1842
2101
  // lib/bucket/events.ts
1843
2102
  var WS_OPEN = 1;
1844
2103
  function formatBucketEvent(event) {
@@ -1964,7 +2223,8 @@ class FileSink {
1964
2223
 
1965
2224
  // lib/logging/index.ts
1966
2225
  var DUST_LOG_FILE = "DUST_LOG_FILE";
1967
- function createLoggingService() {
2226
+ function createLoggingService(options) {
2227
+ const writeStdout = options?.stdout ?? process.stdout.write.bind(process.stdout);
1968
2228
  let patterns = null;
1969
2229
  let initialized = false;
1970
2230
  let activeFileSink = null;
@@ -1994,12 +2254,12 @@ function createLoggingService() {
1994
2254
  }
1995
2255
  activeFileSink = sinkForTesting ?? new FileSink(path);
1996
2256
  },
1997
- createLogger(name, options) {
2257
+ createLogger(name, options2) {
1998
2258
  let perLoggerSink;
1999
- if (options?.file === false) {
2259
+ if (options2?.file === false) {
2000
2260
  perLoggerSink = null;
2001
- } else if (typeof options?.file === "string") {
2002
- perLoggerSink = getOrCreateFileSink(options.file);
2261
+ } else if (typeof options2?.file === "string") {
2262
+ perLoggerSink = getOrCreateFileSink(options2.file);
2003
2263
  }
2004
2264
  return (...messages) => {
2005
2265
  init();
@@ -2012,7 +2272,7 @@ function createLoggingService() {
2012
2272
  activeFileSink.write(line);
2013
2273
  }
2014
2274
  if (patterns && matchesAny(name, patterns)) {
2015
- process.stdout.write(line);
2275
+ writeStdout(line);
2016
2276
  }
2017
2277
  };
2018
2278
  },
@@ -2550,19 +2810,19 @@ import os2 from "node:os";
2550
2810
  import path3 from "node:path";
2551
2811
 
2552
2812
  // lib/agent-events.ts
2553
- function rawEventToAgentEvent(rawEvent) {
2813
+ function rawEventToAgentEvent(rawEvent, provider) {
2554
2814
  if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
2555
2815
  return { type: "agent-session-activity" };
2556
2816
  }
2557
- return { type: "claude-event", rawEvent };
2817
+ return { type: "agent-event", provider, rawEvent };
2558
2818
  }
2559
2819
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
2560
- function createHeartbeatThrottler(onAgentEvent, options) {
2820
+ function createHeartbeatThrottler(onAgentEvent, provider, options) {
2561
2821
  const intervalMs = options?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
2562
2822
  const now = options?.now ?? Date.now;
2563
2823
  let lastHeartbeatTime;
2564
2824
  return (rawEvent) => {
2565
- const event = rawEventToAgentEvent(rawEvent);
2825
+ const event = rawEventToAgentEvent(rawEvent, provider);
2566
2826
  if (event.type === "agent-session-activity") {
2567
2827
  const currentTime = now();
2568
2828
  if (lastHeartbeatTime !== undefined && currentTime - lastHeartbeatTime < intervalMs) {
@@ -2587,7 +2847,7 @@ function formatAgentEvent(event) {
2587
2847
  case "agent-session-ended":
2588
2848
  return event.success ? "\uD83E\uDD16 Agent session ended (success)" : `\uD83E\uDD16 Agent session ended (error: ${event.error})`;
2589
2849
  case "agent-session-activity":
2590
- case "claude-event":
2850
+ case "agent-event":
2591
2851
  return null;
2592
2852
  }
2593
2853
  }
@@ -3089,7 +3349,7 @@ Make sure the repository is in a clean state and synced with remote before finis
3089
3349
  log2(`found ${tasks.length} task(s), picking: ${task.title ?? task.path}`);
3090
3350
  onLoopEvent({ type: "loop.tasks_found" });
3091
3351
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
3092
- const { dustCommand, installCommand = "npm install" } = dependencies.settings;
3352
+ const { dustCommand, installCommand } = dependencies.settings;
3093
3353
  const instructions = buildImplementationInstructions(dustCommand, hooksInstalled, task.title ?? undefined, task.path, installCommand);
3094
3354
  const prompt = `Implement the task at \`${task.path}\`:
3095
3355
 
@@ -3226,7 +3486,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3226
3486
  docker: dockerConfig
3227
3487
  };
3228
3488
  if (eventsUrl) {
3229
- iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent);
3489
+ iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent, loopDependencies.agentType ?? "claude");
3230
3490
  }
3231
3491
  while (completedIterations < maxIterations) {
3232
3492
  agentSessionId = crypto.randomUUID();
@@ -3509,6 +3769,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3509
3769
  };
3510
3770
  const isCodex = repoState.repository.agentProvider === "codex";
3511
3771
  const agentType = isCodex ? "codex" : "claude";
3772
+ log3(`${repoName}: agentProvider=${repoState.repository.agentProvider ?? "(unset)"}, using ${agentType}`);
3512
3773
  const createStdoutSink2 = () => createBufferStdoutSink(loopState, repoState.logBuffer);
3513
3774
  let bufferRun;
3514
3775
  if (isCodex) {
@@ -3592,7 +3853,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3592
3853
  hooksInstalled,
3593
3854
  signal: abortController.signal,
3594
3855
  repositoryId: repoState.repository.id.toString(),
3595
- onRawEvent: createHeartbeatThrottler(onAgentEvent),
3856
+ onRawEvent: createHeartbeatThrottler(onAgentEvent, agentType),
3596
3857
  docker: dockerConfig
3597
3858
  });
3598
3859
  } catch (error) {
@@ -3652,7 +3913,8 @@ function parseRepository(data) {
3652
3913
  gitUrl: repositoryData.gitUrl,
3653
3914
  gitSshUrl: typeof repositoryData.gitSshUrl === "string" ? repositoryData.gitSshUrl : undefined,
3654
3915
  url: repositoryData.url,
3655
- id: repositoryData.id
3916
+ id: repositoryData.id,
3917
+ agentProvider: typeof repositoryData.agentProvider === "string" ? repositoryData.agentProvider : undefined
3656
3918
  };
3657
3919
  }
3658
3920
  }
@@ -3730,6 +3992,16 @@ async function handleRepositoryList(repositories, manager, repoDeps, context) {
3730
3992
  for (const [name, repo] of incomingRepos) {
3731
3993
  if (!manager.repositories.has(name)) {
3732
3994
  await addRepository(repo, manager, repoDeps, context);
3995
+ } else {
3996
+ const existing = manager.repositories.get(name);
3997
+ if (!existing)
3998
+ continue;
3999
+ if (existing.repository.agentProvider !== repo.agentProvider) {
4000
+ const from = existing.repository.agentProvider ?? "(unset)";
4001
+ const to = repo.agentProvider ?? "(unset)";
4002
+ log4(`${name}: agentProvider changed from ${from} to ${to}`);
4003
+ existing.repository.agentProvider = repo.agentProvider;
4004
+ }
3733
4005
  }
3734
4006
  }
3735
4007
  for (const name of manager.repositories.keys()) {
@@ -4135,90 +4407,37 @@ function enterAlternateScreen() {
4135
4407
  function exitAlternateScreen() {
4136
4408
  return `\x1B[?1006l\x1B[?1000l${ANSI.EXIT_ALT_SCREEN}${ANSI.SHOW_CURSOR}`;
4137
4409
  }
4138
- var KEYS = {
4139
- UP: "\x1B[A",
4140
- DOWN: "\x1B[B",
4141
- RIGHT: "\x1B[C",
4142
- LEFT: "\x1B[D",
4143
- PAGE_UP: "\x1B[5~",
4144
- PAGE_DOWN: "\x1B[6~",
4145
- HOME: "\x1B[H",
4146
- END: "\x1B[F",
4147
- CTRL_C: "\x03"
4148
- };
4149
- var SGR_MOUSE_RE = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
4150
- function parseSGRMouse(key) {
4151
- const match = key.match(SGR_MOUSE_RE);
4152
- if (!match)
4153
- return null;
4154
- return Number.parseInt(match[1], 10);
4155
- }
4156
- function handleKeyInput(state, key, options) {
4157
- const mouseButton = parseSGRMouse(key);
4158
- if (mouseButton !== null) {
4159
- if (mouseButton === 64) {
4160
- scrollUp(state, 3);
4161
- } else if (mouseButton === 65) {
4162
- scrollDown(state, 3);
4163
- }
4164
- return false;
4165
- }
4166
- switch (key) {
4167
- case "q":
4168
- case KEYS.CTRL_C:
4169
- return true;
4170
- case KEYS.LEFT:
4171
- selectPrevious(state);
4172
- state.scrollOffset = 0;
4173
- state.autoScroll = true;
4174
- break;
4175
- case KEYS.RIGHT:
4176
- selectNext(state);
4177
- state.scrollOffset = 0;
4178
- state.autoScroll = true;
4179
- break;
4180
- case KEYS.UP:
4181
- scrollUp(state, 1);
4182
- break;
4183
- case KEYS.DOWN:
4184
- scrollDown(state, 1);
4185
- break;
4186
- case KEYS.PAGE_UP:
4187
- scrollUp(state, getLogAreaHeight(state));
4188
- break;
4189
- case KEYS.PAGE_DOWN:
4190
- scrollDown(state, getLogAreaHeight(state));
4191
- break;
4192
- case "g":
4193
- case KEYS.HOME:
4194
- scrollToTop(state);
4195
- break;
4196
- case "G":
4197
- case KEYS.END:
4198
- scrollToBottom(state);
4199
- break;
4200
- case "o": {
4201
- if (state.selectedIndex === -1) {
4202
- break;
4203
- }
4204
- const repoName = state.repositories[state.selectedIndex];
4205
- if (!repoName)
4206
- break;
4207
- const url = state.repositoryUrls.get(repoName);
4208
- if (url && options?.openBrowser) {
4209
- options.openBrowser(url);
4210
- }
4211
- break;
4212
- }
4213
- }
4214
- return false;
4215
- }
4410
+ var SGR_MOUSE_RE2 = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
4216
4411
 
4217
4412
  // lib/cli/commands/bucket.ts
4218
4413
  var log5 = createLogger("dust:cli:commands:bucket");
4219
4414
  var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
4220
- var INITIAL_RECONNECT_DELAY_MS = 1000;
4221
- var MAX_RECONNECT_DELAY_MS = 30000;
4415
+ function createAuthFileSystem(dependencies) {
4416
+ return {
4417
+ exists: (path4) => {
4418
+ try {
4419
+ dependencies.accessSync(path4);
4420
+ return true;
4421
+ } catch {
4422
+ return false;
4423
+ }
4424
+ },
4425
+ isDirectory: (path4) => {
4426
+ try {
4427
+ return dependencies.statSync(path4).isDirectory();
4428
+ } catch {
4429
+ return false;
4430
+ }
4431
+ },
4432
+ getFileCreationTime: (path4) => dependencies.statSync(path4).birthtimeMs,
4433
+ readFile: (path4) => dependencies.readFile(path4, "utf8"),
4434
+ writeFile: (path4, content) => dependencies.writeFile(path4, content, "utf8"),
4435
+ mkdir: (path4, options) => dependencies.mkdir(path4, options).then(() => {}),
4436
+ readdir: (path4) => dependencies.readdir(path4),
4437
+ chmod: (path4, mode) => dependencies.chmod(path4, mode),
4438
+ rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
4439
+ };
4440
+ }
4222
4441
  function defaultCreateWebSocket(url, token) {
4223
4442
  const ws = new WebSocket(url, {
4224
4443
  headers: {
@@ -4273,32 +4492,6 @@ function defaultGetTerminalSize() {
4273
4492
  function defaultWriteStdout(data) {
4274
4493
  process.stdout.write(data);
4275
4494
  }
4276
- function createAuthFileSystem(dependencies) {
4277
- return {
4278
- exists: (path4) => {
4279
- try {
4280
- dependencies.accessSync(path4);
4281
- return true;
4282
- } catch {
4283
- return false;
4284
- }
4285
- },
4286
- isDirectory: (path4) => {
4287
- try {
4288
- return dependencies.statSync(path4).isDirectory();
4289
- } catch {
4290
- return false;
4291
- }
4292
- },
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),
4299
- rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
4300
- };
4301
- }
4302
4495
  function createDefaultBucketDependencies() {
4303
4496
  const authFileSystem = createAuthFileSystem({
4304
4497
  accessSync,
@@ -4469,8 +4662,13 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4469
4662
  state.ws = ws;
4470
4663
  ws.onopen = () => {
4471
4664
  state.emit({ type: "bucket.connected" });
4472
- logMessage(state, context, useTUI, formatBucketEvent({ type: "bucket.connected" }));
4473
- state.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
4665
+ const lifecycleState = {
4666
+ reconnectDelay: state.reconnectDelay,
4667
+ shuttingDown: state.shuttingDown
4668
+ };
4669
+ const result = handleOpen(lifecycleState);
4670
+ state.reconnectDelay = result.state.reconnectDelay;
4671
+ executeLifecycleEffects(result.effects, { state, context, useTUI, bucketDependencies, fileSystem }, token);
4474
4672
  };
4475
4673
  }
4476
4674
  ws.onclose = (event) => {
@@ -4480,80 +4678,122 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4480
4678
  reason: event.reason || "none"
4481
4679
  };
4482
4680
  state.emit(disconnectEvent);
4483
- logMessage(state, context, useTUI, formatBucketEvent(disconnectEvent));
4484
4681
  state.ws = null;
4485
- if (event.code === 4000) {
4486
- logMessage(state, context, useTUI, "Another connection replaced this one. Not reconnecting.");
4487
- return;
4488
- }
4489
- if (!state.shuttingDown) {
4490
- logMessage(state, context, useTUI, `Reconnecting in ${state.reconnectDelay / 1000} seconds...`);
4491
- state.reconnectTimer = setTimeout(() => {
4492
- connectWebSocket(token, state, bucketDependencies, context, fileSystem, useTUI);
4493
- }, state.reconnectDelay);
4494
- state.reconnectDelay = Math.min(state.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
4495
- }
4682
+ const lifecycleState = {
4683
+ reconnectDelay: state.reconnectDelay,
4684
+ shuttingDown: state.shuttingDown
4685
+ };
4686
+ const result = handleClose(lifecycleState, event.code, event.reason || "");
4687
+ state.reconnectDelay = result.state.reconnectDelay;
4688
+ executeLifecycleEffects(result.effects, { state, context, useTUI, bucketDependencies, fileSystem }, token);
4496
4689
  };
4497
4690
  ws.onerror = (error) => {
4498
- logMessage(state, context, useTUI, `WebSocket error: ${error.message}`, "stderr");
4691
+ const lifecycleState = {
4692
+ reconnectDelay: state.reconnectDelay,
4693
+ shuttingDown: state.shuttingDown
4694
+ };
4695
+ const result = handleError(lifecycleState, error.message);
4696
+ executeLifecycleEffects(result.effects, { state, context, useTUI, bucketDependencies, fileSystem }, token);
4499
4697
  };
4500
4698
  ws.onmessage = (event) => {
4501
4699
  let rawData;
4502
4700
  try {
4503
4701
  rawData = JSON.parse(event.data);
4504
4702
  } catch {
4505
- logMessage(state, context, useTUI, `Failed to parse WebSocket message: ${event.data}`, "stderr");
4703
+ const result2 = handleMessageParseError(event.data);
4704
+ executeEffects(result2.effects, {
4705
+ state,
4706
+ context,
4707
+ useTUI,
4708
+ bucketDependencies,
4709
+ fileSystem
4710
+ });
4506
4711
  return;
4507
4712
  }
4508
4713
  const message = parseServerMessage(rawData);
4509
4714
  if (!message) {
4510
- logMessage(state, context, useTUI, `Invalid WebSocket message format: ${event.data}`, "stderr");
4715
+ const result2 = handleInvalidMessageFormat(event.data);
4716
+ executeEffects(result2.effects, {
4717
+ state,
4718
+ context,
4719
+ useTUI,
4720
+ bucketDependencies,
4721
+ fileSystem
4722
+ });
4511
4723
  return;
4512
4724
  }
4513
- log5(`ws message: ${message.type}`);
4514
- if (message.type === "repository-list") {
4515
- const repos = message.repositories;
4516
- logMessage(state, context, useTUI, `Received repository list (${repos.length} repositories):`);
4517
- if (repos.length === 0) {
4518
- logMessage(state, context, useTUI, " (empty)");
4519
- } else {
4520
- for (const r of repos) {
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}`);
4527
- }
4528
- }
4529
- syncUIWithRepoList(state, repos);
4530
- const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4531
- const repoContext = createTUIContext(state, context, useTUI);
4532
- handleRepositoryList(repos, state, repoDeps, repoContext).then(() => {
4533
- syncTUI(state);
4534
- for (const repoData of repos) {
4535
- if (repoData.hasTask) {
4536
- const repoState = state.repositories.get(repoData.name);
4537
- if (repoState) {
4538
- signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4725
+ const handlerState = {
4726
+ repositoryNames: Array.from(state.repositories.keys())
4727
+ };
4728
+ const result = handleServerMessage(handlerState, message);
4729
+ executeEffects(result.effects, {
4730
+ state,
4731
+ context,
4732
+ useTUI,
4733
+ bucketDependencies,
4734
+ fileSystem
4735
+ });
4736
+ };
4737
+ }
4738
+ function executeEffects(effects, dependencies) {
4739
+ const { state, context, useTUI, bucketDependencies, fileSystem } = dependencies;
4740
+ for (const effect of effects) {
4741
+ switch (effect.type) {
4742
+ case "log":
4743
+ logMessage(state, context, useTUI, effect.message, effect.stream);
4744
+ break;
4745
+ case "debugLog":
4746
+ log5(effect.message);
4747
+ break;
4748
+ case "syncUI":
4749
+ syncUIWithRepoList(state, effect.repositories);
4750
+ break;
4751
+ case "handleRepositoryList": {
4752
+ const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4753
+ const repoContext = createTUIContext(state, context, useTUI);
4754
+ const repos = effect.repositories;
4755
+ handleRepositoryList(repos, state, repoDeps, repoContext).then(() => {
4756
+ syncTUI(state);
4757
+ for (const repoData of repos) {
4758
+ if (repoData.hasTask) {
4759
+ const repoState = state.repositories.get(repoData.name);
4760
+ if (repoState) {
4761
+ signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4762
+ }
4539
4763
  }
4540
4764
  }
4765
+ }).catch((error) => {
4766
+ logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
4767
+ });
4768
+ break;
4769
+ }
4770
+ case "signalTaskAvailable": {
4771
+ const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4772
+ const repoState = state.repositories.get(effect.repositoryName);
4773
+ if (repoState) {
4774
+ signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4541
4775
  }
4542
- }).catch((error) => {
4543
- logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
4544
- });
4545
- } else if (message.type === "task-available") {
4546
- const repoName = message.repository;
4547
- const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4548
- logMessage(state, context, useTUI, `Received task-available for ${repoName}`);
4549
- const repoState = state.repositories.get(repoName);
4550
- if (repoState) {
4551
- signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4552
- } else {
4553
- logMessage(state, context, useTUI, `No repository state found for ${repoName}`, "stderr");
4776
+ break;
4554
4777
  }
4778
+ case "scheduleReconnect":
4779
+ break;
4555
4780
  }
4556
- };
4781
+ }
4782
+ }
4783
+ function executeLifecycleEffects(effects, dependencies, token) {
4784
+ const { state, context, useTUI, bucketDependencies, fileSystem } = dependencies;
4785
+ for (const effect of effects) {
4786
+ switch (effect.type) {
4787
+ case "log":
4788
+ logMessage(state, context, useTUI, effect.message, effect.stream);
4789
+ break;
4790
+ case "scheduleReconnect":
4791
+ state.reconnectTimer = setTimeout(() => {
4792
+ connectWebSocket(token, state, bucketDependencies, context, fileSystem, useTUI);
4793
+ }, effect.delayMs);
4794
+ break;
4795
+ }
4796
+ }
4557
4797
  }
4558
4798
  async function shutdown(state, bucketDeps, context) {
4559
4799
  if (state.shuttingDown)
@@ -4607,17 +4847,80 @@ function setupTUI(state, bucketDeps) {
4607
4847
  }
4608
4848
  };
4609
4849
  }
4850
+ function createKeypressHandlerState(ui) {
4851
+ const repositoryUrls = {};
4852
+ for (const [name, url] of ui.repositoryUrls) {
4853
+ repositoryUrls[name] = url;
4854
+ }
4855
+ return {
4856
+ selectedIndex: ui.selectedIndex,
4857
+ repositories: ui.repositories,
4858
+ repositoryUrls
4859
+ };
4860
+ }
4861
+ function executeKeypressEffects(ui, effects, onQuit, options) {
4862
+ for (const effect of effects) {
4863
+ switch (effect.type) {
4864
+ case "quit":
4865
+ onQuit();
4866
+ break;
4867
+ case "openBrowser":
4868
+ if (options?.openBrowser) {
4869
+ options.openBrowser(effect.url);
4870
+ }
4871
+ break;
4872
+ case "selectNext":
4873
+ selectNext(ui);
4874
+ ui.scrollOffset = 0;
4875
+ ui.autoScroll = true;
4876
+ break;
4877
+ case "selectPrevious":
4878
+ selectPrevious(ui);
4879
+ ui.scrollOffset = 0;
4880
+ ui.autoScroll = true;
4881
+ break;
4882
+ case "scroll":
4883
+ switch (effect.direction) {
4884
+ case "up":
4885
+ scrollUp(ui, 1);
4886
+ break;
4887
+ case "down":
4888
+ scrollDown(ui, 1);
4889
+ break;
4890
+ case "pageUp":
4891
+ scrollUp(ui, getLogAreaHeight(ui));
4892
+ break;
4893
+ case "pageDown":
4894
+ scrollDown(ui, getLogAreaHeight(ui));
4895
+ break;
4896
+ case "top":
4897
+ scrollToTop(ui);
4898
+ break;
4899
+ case "bottom":
4900
+ scrollToBottom(ui);
4901
+ break;
4902
+ }
4903
+ break;
4904
+ }
4905
+ }
4906
+ }
4610
4907
  function createKeypressHandler(useTUI, state, onQuit, options) {
4611
4908
  if (useTUI) {
4612
4909
  return (key) => {
4613
- const shouldQuit = handleKeyInput(state.ui, key, options);
4614
- if (shouldQuit)
4615
- onQuit();
4910
+ const keypressState = createKeypressHandlerState(state.ui);
4911
+ const { effects } = handleKeypress(keypressState, key);
4912
+ executeKeypressEffects(state.ui, effects, onQuit, options);
4616
4913
  };
4617
4914
  }
4618
4915
  return (key) => {
4619
- if (key === "q" || key === "\x03")
4620
- onQuit();
4916
+ const keypressState = createKeypressHandlerState(state.ui);
4917
+ const { effects } = handleKeypress(keypressState, key);
4918
+ for (const effect of effects) {
4919
+ if (effect.type === "quit") {
4920
+ onQuit();
4921
+ break;
4922
+ }
4923
+ }
4621
4924
  };
4622
4925
  }
4623
4926
  async function resolveToken(authDeps, context) {
@@ -4867,7 +5170,8 @@ async function bucketAssetUpload(dependencies, uploadDeps = createDefaultUploadD
4867
5170
  }
4868
5171
  const fileBytes = await uploadDeps.readFileBytes(filePath);
4869
5172
  const contentType = getContentType(filePath);
4870
- const fileName = filePath.split("/").pop();
5173
+ const parts = filePath.split("/");
5174
+ const fileName = parts[parts.length - 1];
4871
5175
  const uploadUrl = `${getDustbucketHost()}/api/assets?repositoryId=${encodeURIComponent(repositoryId)}`;
4872
5176
  try {
4873
5177
  const result = await uploadDeps.uploadFile(uploadUrl, token, fileBytes, contentType, fileName);
@@ -5800,13 +6104,13 @@ function truncateOutput(output) {
5800
6104
  ].join(`
5801
6105
  `);
5802
6106
  }
5803
- async function runSingleCheck(check, cwd, runner, emitEvent) {
6107
+ async function runSingleCheck(check, cwd, runner, emitEvent, clock = Date.now) {
5804
6108
  const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
5805
6109
  log6(`running check ${check.name}: ${check.command}`);
5806
6110
  emitEvent?.({ type: "check-started", name: check.name });
5807
- const startTime = Date.now();
6111
+ const startTime = clock();
5808
6112
  const result = await runner.run(check.command, cwd, timeoutMs);
5809
- const durationMs = Date.now() - startTime;
6113
+ const durationMs = clock() - startTime;
5810
6114
  const status = result.timedOut ? "timed out" : result.exitCode === 0 ? "passed" : "failed";
5811
6115
  log6(`check ${check.name} ${status} (${durationMs}ms)`);
5812
6116
  if (result.exitCode === 0) {
@@ -5832,18 +6136,18 @@ async function runSingleCheck(check, cwd, runner, emitEvent) {
5832
6136
  timeoutSeconds: timeoutMs / 1000
5833
6137
  };
5834
6138
  }
5835
- async function runConfiguredChecks(checks, cwd, runner, emitEvent) {
5836
- const promises = checks.map((check) => runSingleCheck(check, cwd, runner, emitEvent));
6139
+ async function runConfiguredChecks(checks, cwd, runner, emitEvent, clock = Date.now) {
6140
+ const promises = checks.map((check) => runSingleCheck(check, cwd, runner, emitEvent, clock));
5837
6141
  return Promise.all(promises);
5838
6142
  }
5839
- async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent) {
6143
+ async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent, clock = Date.now) {
5840
6144
  const results = [];
5841
6145
  for (const check of checks) {
5842
- results.push(await runSingleCheck(check, cwd, runner, emitEvent));
6146
+ results.push(await runSingleCheck(check, cwd, runner, emitEvent, clock));
5843
6147
  }
5844
6148
  return results;
5845
6149
  }
5846
- async function runValidationCheck(dependencies, emitEvent) {
6150
+ async function runValidationCheck(dependencies, emitEvent, clock = Date.now) {
5847
6151
  const outputLines = [];
5848
6152
  const bufferedContext = {
5849
6153
  cwd: dependencies.context.cwd,
@@ -5852,13 +6156,13 @@ async function runValidationCheck(dependencies, emitEvent) {
5852
6156
  };
5853
6157
  log6("running built-in check: dust lint");
5854
6158
  emitEvent?.({ type: "check-started", name: "lint" });
5855
- const startTime = Date.now();
6159
+ const startTime = clock();
5856
6160
  const result = await lintMarkdown({
5857
6161
  ...dependencies,
5858
6162
  context: bufferedContext,
5859
6163
  arguments: []
5860
6164
  });
5861
- const durationMs = Date.now() - startTime;
6165
+ const durationMs = clock() - startTime;
5862
6166
  const lintStatus = result.exitCode === 0 ? "passed" : "failed";
5863
6167
  log6(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
5864
6168
  const output = outputLines.join(`
@@ -5922,7 +6226,7 @@ function displayResults(results, context) {
5922
6226
  context.stdout(`${indicator} ${passed.length}/${results.length} checks passed`);
5923
6227
  return failed.length > 0 ? 1 : 0;
5924
6228
  }
5925
- async function check(dependencies, shellRunner = defaultShellRunner) {
6229
+ async function check(dependencies, shellRunner = defaultShellRunner, clock = Date.now) {
5926
6230
  const {
5927
6231
  arguments: commandArguments,
5928
6232
  context,
@@ -5948,18 +6252,18 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
5948
6252
  if (serial) {
5949
6253
  const results2 = [];
5950
6254
  if (hasDustDir) {
5951
- results2.push(await runValidationCheck(dependencies, context.emitEvent));
6255
+ results2.push(await runValidationCheck(dependencies, context.emitEvent, clock));
5952
6256
  }
5953
- const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent);
6257
+ const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent, clock);
5954
6258
  results2.push(...configuredResults);
5955
6259
  const exitCode2 = displayResults(results2, context);
5956
6260
  return { exitCode: exitCode2 };
5957
6261
  }
5958
6262
  const checkPromises = [];
5959
6263
  if (hasDustDir) {
5960
- checkPromises.push(runValidationCheck(dependencies, context.emitEvent));
6264
+ checkPromises.push(runValidationCheck(dependencies, context.emitEvent, clock));
5961
6265
  }
5962
- checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent));
6266
+ checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent, clock));
5963
6267
  const promiseResults = await Promise.all(checkPromises);
5964
6268
  const results = [];
5965
6269
  for (const result of promiseResults) {
@@ -65,7 +65,10 @@ export interface LoggingService {
65
65
  * Create an isolated logging service instance. All mutable state is
66
66
  * encapsulated inside the returned object.
67
67
  */
68
- export declare function createLoggingService(): LoggingService;
68
+ export interface LoggingServiceOptions {
69
+ stdout?: (line: string) => boolean;
70
+ }
71
+ export declare function createLoggingService(options?: LoggingServiceOptions): LoggingService;
69
72
  /**
70
73
  * Activate file logging for this command. See {@link LoggingService.enableFileLogs}.
71
74
  */
package/dist/logging.js CHANGED
@@ -60,7 +60,8 @@ class FileSink {
60
60
 
61
61
  // lib/logging/index.ts
62
62
  var DUST_LOG_FILE = "DUST_LOG_FILE";
63
- function createLoggingService() {
63
+ function createLoggingService(options) {
64
+ const writeStdout = options?.stdout ?? process.stdout.write.bind(process.stdout);
64
65
  let patterns = null;
65
66
  let initialized = false;
66
67
  let activeFileSink = null;
@@ -90,12 +91,12 @@ function createLoggingService() {
90
91
  }
91
92
  activeFileSink = sinkForTesting ?? new FileSink(path);
92
93
  },
93
- createLogger(name, options) {
94
+ createLogger(name, options2) {
94
95
  let perLoggerSink;
95
- if (options?.file === false) {
96
+ if (options2?.file === false) {
96
97
  perLoggerSink = null;
97
- } else if (typeof options?.file === "string") {
98
- perLoggerSink = getOrCreateFileSink(options.file);
98
+ } else if (typeof options2?.file === "string") {
99
+ perLoggerSink = getOrCreateFileSink(options2.file);
99
100
  }
100
101
  return (...messages) => {
101
102
  init();
@@ -108,7 +109,7 @@ function createLoggingService() {
108
109
  activeFileSink.write(line);
109
110
  }
110
111
  if (patterns && matchesAny(name, patterns)) {
111
- process.stdout.write(line);
112
+ writeStdout(line);
112
113
  }
113
114
  };
114
115
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {