@joshski/dust 0.1.84 → 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;
package/dist/dust.js CHANGED
@@ -319,7 +319,7 @@ async function loadSettings(cwd, fileSystem) {
319
319
  }
320
320
 
321
321
  // lib/version.ts
322
- var DUST_VERSION = "0.1.84";
322
+ var DUST_VERSION = "0.1.85";
323
323
 
324
324
  // lib/session.ts
325
325
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -1840,6 +1840,264 @@ function openBrowser(url) {
1840
1840
  nodeSpawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
1841
1841
  }
1842
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
+
1843
2101
  // lib/bucket/events.ts
1844
2102
  var WS_OPEN = 1;
1845
2103
  function formatBucketEvent(event) {
@@ -1965,7 +2223,8 @@ class FileSink {
1965
2223
 
1966
2224
  // lib/logging/index.ts
1967
2225
  var DUST_LOG_FILE = "DUST_LOG_FILE";
1968
- function createLoggingService() {
2226
+ function createLoggingService(options) {
2227
+ const writeStdout = options?.stdout ?? process.stdout.write.bind(process.stdout);
1969
2228
  let patterns = null;
1970
2229
  let initialized = false;
1971
2230
  let activeFileSink = null;
@@ -1995,12 +2254,12 @@ function createLoggingService() {
1995
2254
  }
1996
2255
  activeFileSink = sinkForTesting ?? new FileSink(path);
1997
2256
  },
1998
- createLogger(name, options) {
2257
+ createLogger(name, options2) {
1999
2258
  let perLoggerSink;
2000
- if (options?.file === false) {
2259
+ if (options2?.file === false) {
2001
2260
  perLoggerSink = null;
2002
- } else if (typeof options?.file === "string") {
2003
- perLoggerSink = getOrCreateFileSink(options.file);
2261
+ } else if (typeof options2?.file === "string") {
2262
+ perLoggerSink = getOrCreateFileSink(options2.file);
2004
2263
  }
2005
2264
  return (...messages) => {
2006
2265
  init();
@@ -2013,7 +2272,7 @@ function createLoggingService() {
2013
2272
  activeFileSink.write(line);
2014
2273
  }
2015
2274
  if (patterns && matchesAny(name, patterns)) {
2016
- process.stdout.write(line);
2275
+ writeStdout(line);
2017
2276
  }
2018
2277
  };
2019
2278
  },
@@ -2551,19 +2810,19 @@ import os2 from "node:os";
2551
2810
  import path3 from "node:path";
2552
2811
 
2553
2812
  // lib/agent-events.ts
2554
- function rawEventToAgentEvent(rawEvent) {
2813
+ function rawEventToAgentEvent(rawEvent, provider) {
2555
2814
  if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
2556
2815
  return { type: "agent-session-activity" };
2557
2816
  }
2558
- return { type: "claude-event", rawEvent };
2817
+ return { type: "agent-event", provider, rawEvent };
2559
2818
  }
2560
2819
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
2561
- function createHeartbeatThrottler(onAgentEvent, options) {
2820
+ function createHeartbeatThrottler(onAgentEvent, provider, options) {
2562
2821
  const intervalMs = options?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
2563
2822
  const now = options?.now ?? Date.now;
2564
2823
  let lastHeartbeatTime;
2565
2824
  return (rawEvent) => {
2566
- const event = rawEventToAgentEvent(rawEvent);
2825
+ const event = rawEventToAgentEvent(rawEvent, provider);
2567
2826
  if (event.type === "agent-session-activity") {
2568
2827
  const currentTime = now();
2569
2828
  if (lastHeartbeatTime !== undefined && currentTime - lastHeartbeatTime < intervalMs) {
@@ -2588,7 +2847,7 @@ function formatAgentEvent(event) {
2588
2847
  case "agent-session-ended":
2589
2848
  return event.success ? "\uD83E\uDD16 Agent session ended (success)" : `\uD83E\uDD16 Agent session ended (error: ${event.error})`;
2590
2849
  case "agent-session-activity":
2591
- case "claude-event":
2850
+ case "agent-event":
2592
2851
  return null;
2593
2852
  }
2594
2853
  }
@@ -3227,7 +3486,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3227
3486
  docker: dockerConfig
3228
3487
  };
3229
3488
  if (eventsUrl) {
3230
- iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent);
3489
+ iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent, loopDependencies.agentType ?? "claude");
3231
3490
  }
3232
3491
  while (completedIterations < maxIterations) {
3233
3492
  agentSessionId = crypto.randomUUID();
@@ -3510,6 +3769,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3510
3769
  };
3511
3770
  const isCodex = repoState.repository.agentProvider === "codex";
3512
3771
  const agentType = isCodex ? "codex" : "claude";
3772
+ log3(`${repoName}: agentProvider=${repoState.repository.agentProvider ?? "(unset)"}, using ${agentType}`);
3513
3773
  const createStdoutSink2 = () => createBufferStdoutSink(loopState, repoState.logBuffer);
3514
3774
  let bufferRun;
3515
3775
  if (isCodex) {
@@ -3593,7 +3853,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3593
3853
  hooksInstalled,
3594
3854
  signal: abortController.signal,
3595
3855
  repositoryId: repoState.repository.id.toString(),
3596
- onRawEvent: createHeartbeatThrottler(onAgentEvent),
3856
+ onRawEvent: createHeartbeatThrottler(onAgentEvent, agentType),
3597
3857
  docker: dockerConfig
3598
3858
  });
3599
3859
  } catch (error) {
@@ -3653,7 +3913,8 @@ function parseRepository(data) {
3653
3913
  gitUrl: repositoryData.gitUrl,
3654
3914
  gitSshUrl: typeof repositoryData.gitSshUrl === "string" ? repositoryData.gitSshUrl : undefined,
3655
3915
  url: repositoryData.url,
3656
- id: repositoryData.id
3916
+ id: repositoryData.id,
3917
+ agentProvider: typeof repositoryData.agentProvider === "string" ? repositoryData.agentProvider : undefined
3657
3918
  };
3658
3919
  }
3659
3920
  }
@@ -3731,6 +3992,16 @@ async function handleRepositoryList(repositories, manager, repoDeps, context) {
3731
3992
  for (const [name, repo] of incomingRepos) {
3732
3993
  if (!manager.repositories.has(name)) {
3733
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
+ }
3734
4005
  }
3735
4006
  }
3736
4007
  for (const name of manager.repositories.keys()) {
@@ -4136,90 +4407,37 @@ function enterAlternateScreen() {
4136
4407
  function exitAlternateScreen() {
4137
4408
  return `\x1B[?1006l\x1B[?1000l${ANSI.EXIT_ALT_SCREEN}${ANSI.SHOW_CURSOR}`;
4138
4409
  }
4139
- var KEYS = {
4140
- UP: "\x1B[A",
4141
- DOWN: "\x1B[B",
4142
- RIGHT: "\x1B[C",
4143
- LEFT: "\x1B[D",
4144
- PAGE_UP: "\x1B[5~",
4145
- PAGE_DOWN: "\x1B[6~",
4146
- HOME: "\x1B[H",
4147
- END: "\x1B[F",
4148
- CTRL_C: "\x03"
4149
- };
4150
- var SGR_MOUSE_RE = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
4151
- function parseSGRMouse(key) {
4152
- const match = key.match(SGR_MOUSE_RE);
4153
- if (!match)
4154
- return null;
4155
- return Number.parseInt(match[1], 10);
4156
- }
4157
- function handleKeyInput(state, key, options) {
4158
- const mouseButton = parseSGRMouse(key);
4159
- if (mouseButton !== null) {
4160
- if (mouseButton === 64) {
4161
- scrollUp(state, 3);
4162
- } else if (mouseButton === 65) {
4163
- scrollDown(state, 3);
4164
- }
4165
- return false;
4166
- }
4167
- switch (key) {
4168
- case "q":
4169
- case KEYS.CTRL_C:
4170
- return true;
4171
- case KEYS.LEFT:
4172
- selectPrevious(state);
4173
- state.scrollOffset = 0;
4174
- state.autoScroll = true;
4175
- break;
4176
- case KEYS.RIGHT:
4177
- selectNext(state);
4178
- state.scrollOffset = 0;
4179
- state.autoScroll = true;
4180
- break;
4181
- case KEYS.UP:
4182
- scrollUp(state, 1);
4183
- break;
4184
- case KEYS.DOWN:
4185
- scrollDown(state, 1);
4186
- break;
4187
- case KEYS.PAGE_UP:
4188
- scrollUp(state, getLogAreaHeight(state));
4189
- break;
4190
- case KEYS.PAGE_DOWN:
4191
- scrollDown(state, getLogAreaHeight(state));
4192
- break;
4193
- case "g":
4194
- case KEYS.HOME:
4195
- scrollToTop(state);
4196
- break;
4197
- case "G":
4198
- case KEYS.END:
4199
- scrollToBottom(state);
4200
- break;
4201
- case "o": {
4202
- if (state.selectedIndex === -1) {
4203
- break;
4204
- }
4205
- const repoName = state.repositories[state.selectedIndex];
4206
- if (!repoName)
4207
- break;
4208
- const url = state.repositoryUrls.get(repoName);
4209
- if (url && options?.openBrowser) {
4210
- options.openBrowser(url);
4211
- }
4212
- break;
4213
- }
4214
- }
4215
- return false;
4216
- }
4410
+ var SGR_MOUSE_RE2 = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
4217
4411
 
4218
4412
  // lib/cli/commands/bucket.ts
4219
4413
  var log5 = createLogger("dust:cli:commands:bucket");
4220
4414
  var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
4221
- var INITIAL_RECONNECT_DELAY_MS = 1000;
4222
- 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
+ }
4223
4441
  function defaultCreateWebSocket(url, token) {
4224
4442
  const ws = new WebSocket(url, {
4225
4443
  headers: {
@@ -4274,32 +4492,6 @@ function defaultGetTerminalSize() {
4274
4492
  function defaultWriteStdout(data) {
4275
4493
  process.stdout.write(data);
4276
4494
  }
4277
- function createAuthFileSystem(dependencies) {
4278
- return {
4279
- exists: (path4) => {
4280
- try {
4281
- dependencies.accessSync(path4);
4282
- return true;
4283
- } catch {
4284
- return false;
4285
- }
4286
- },
4287
- isDirectory: (path4) => {
4288
- try {
4289
- return dependencies.statSync(path4).isDirectory();
4290
- } catch {
4291
- return false;
4292
- }
4293
- },
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),
4300
- rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
4301
- };
4302
- }
4303
4495
  function createDefaultBucketDependencies() {
4304
4496
  const authFileSystem = createAuthFileSystem({
4305
4497
  accessSync,
@@ -4470,8 +4662,13 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4470
4662
  state.ws = ws;
4471
4663
  ws.onopen = () => {
4472
4664
  state.emit({ type: "bucket.connected" });
4473
- logMessage(state, context, useTUI, formatBucketEvent({ type: "bucket.connected" }));
4474
- 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);
4475
4672
  };
4476
4673
  }
4477
4674
  ws.onclose = (event) => {
@@ -4481,80 +4678,122 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4481
4678
  reason: event.reason || "none"
4482
4679
  };
4483
4680
  state.emit(disconnectEvent);
4484
- logMessage(state, context, useTUI, formatBucketEvent(disconnectEvent));
4485
4681
  state.ws = null;
4486
- if (event.code === 4000) {
4487
- logMessage(state, context, useTUI, "Another connection replaced this one. Not reconnecting.");
4488
- return;
4489
- }
4490
- if (!state.shuttingDown) {
4491
- logMessage(state, context, useTUI, `Reconnecting in ${state.reconnectDelay / 1000} seconds...`);
4492
- state.reconnectTimer = setTimeout(() => {
4493
- connectWebSocket(token, state, bucketDependencies, context, fileSystem, useTUI);
4494
- }, state.reconnectDelay);
4495
- state.reconnectDelay = Math.min(state.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
4496
- }
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);
4497
4689
  };
4498
4690
  ws.onerror = (error) => {
4499
- 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);
4500
4697
  };
4501
4698
  ws.onmessage = (event) => {
4502
4699
  let rawData;
4503
4700
  try {
4504
4701
  rawData = JSON.parse(event.data);
4505
4702
  } catch {
4506
- 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
+ });
4507
4711
  return;
4508
4712
  }
4509
4713
  const message = parseServerMessage(rawData);
4510
4714
  if (!message) {
4511
- 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
+ });
4512
4723
  return;
4513
4724
  }
4514
- log5(`ws message: ${message.type}`);
4515
- if (message.type === "repository-list") {
4516
- const repos = message.repositories;
4517
- logMessage(state, context, useTUI, `Received repository list (${repos.length} repositories):`);
4518
- if (repos.length === 0) {
4519
- logMessage(state, context, useTUI, " (empty)");
4520
- } else {
4521
- for (const r of repos) {
4522
- logMessage(state, context, useTUI, ` - name=${r.name}`);
4523
- logMessage(state, context, useTUI, ` id=${r.id}`);
4524
- logMessage(state, context, useTUI, ` gitUrl=${r.gitUrl}`);
4525
- logMessage(state, context, useTUI, ` gitSshUrl=${r.gitSshUrl ?? "(none)"}`);
4526
- logMessage(state, context, useTUI, ` url=${r.url}`);
4527
- logMessage(state, context, useTUI, ` hasTask=${r.hasTask}`);
4528
- }
4529
- }
4530
- syncUIWithRepoList(state, repos);
4531
- const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4532
- const repoContext = createTUIContext(state, context, useTUI);
4533
- handleRepositoryList(repos, state, repoDeps, repoContext).then(() => {
4534
- syncTUI(state);
4535
- for (const repoData of repos) {
4536
- if (repoData.hasTask) {
4537
- const repoState = state.repositories.get(repoData.name);
4538
- if (repoState) {
4539
- 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
+ }
4540
4763
  }
4541
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);
4542
4775
  }
4543
- }).catch((error) => {
4544
- logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
4545
- });
4546
- } else if (message.type === "task-available") {
4547
- const repoName = message.repository;
4548
- const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4549
- logMessage(state, context, useTUI, `Received task-available for ${repoName}`);
4550
- const repoState = state.repositories.get(repoName);
4551
- if (repoState) {
4552
- signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4553
- } else {
4554
- logMessage(state, context, useTUI, `No repository state found for ${repoName}`, "stderr");
4776
+ break;
4555
4777
  }
4778
+ case "scheduleReconnect":
4779
+ break;
4556
4780
  }
4557
- };
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
+ }
4558
4797
  }
4559
4798
  async function shutdown(state, bucketDeps, context) {
4560
4799
  if (state.shuttingDown)
@@ -4608,17 +4847,80 @@ function setupTUI(state, bucketDeps) {
4608
4847
  }
4609
4848
  };
4610
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
+ }
4611
4907
  function createKeypressHandler(useTUI, state, onQuit, options) {
4612
4908
  if (useTUI) {
4613
4909
  return (key) => {
4614
- const shouldQuit = handleKeyInput(state.ui, key, options);
4615
- if (shouldQuit)
4616
- onQuit();
4910
+ const keypressState = createKeypressHandlerState(state.ui);
4911
+ const { effects } = handleKeypress(keypressState, key);
4912
+ executeKeypressEffects(state.ui, effects, onQuit, options);
4617
4913
  };
4618
4914
  }
4619
4915
  return (key) => {
4620
- if (key === "q" || key === "\x03")
4621
- 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
+ }
4622
4924
  };
4623
4925
  }
4624
4926
  async function resolveToken(authDeps, context) {
@@ -4868,7 +5170,8 @@ async function bucketAssetUpload(dependencies, uploadDeps = createDefaultUploadD
4868
5170
  }
4869
5171
  const fileBytes = await uploadDeps.readFileBytes(filePath);
4870
5172
  const contentType = getContentType(filePath);
4871
- const fileName = filePath.split("/").pop();
5173
+ const parts = filePath.split("/");
5174
+ const fileName = parts[parts.length - 1];
4872
5175
  const uploadUrl = `${getDustbucketHost()}/api/assets?repositoryId=${encodeURIComponent(repositoryId)}`;
4873
5176
  try {
4874
5177
  const result = await uploadDeps.uploadFile(uploadUrl, token, fileBytes, contentType, fileName);
@@ -5801,13 +6104,13 @@ function truncateOutput(output) {
5801
6104
  ].join(`
5802
6105
  `);
5803
6106
  }
5804
- async function runSingleCheck(check, cwd, runner, emitEvent) {
6107
+ async function runSingleCheck(check, cwd, runner, emitEvent, clock = Date.now) {
5805
6108
  const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
5806
6109
  log6(`running check ${check.name}: ${check.command}`);
5807
6110
  emitEvent?.({ type: "check-started", name: check.name });
5808
- const startTime = Date.now();
6111
+ const startTime = clock();
5809
6112
  const result = await runner.run(check.command, cwd, timeoutMs);
5810
- const durationMs = Date.now() - startTime;
6113
+ const durationMs = clock() - startTime;
5811
6114
  const status = result.timedOut ? "timed out" : result.exitCode === 0 ? "passed" : "failed";
5812
6115
  log6(`check ${check.name} ${status} (${durationMs}ms)`);
5813
6116
  if (result.exitCode === 0) {
@@ -5833,18 +6136,18 @@ async function runSingleCheck(check, cwd, runner, emitEvent) {
5833
6136
  timeoutSeconds: timeoutMs / 1000
5834
6137
  };
5835
6138
  }
5836
- async function runConfiguredChecks(checks, cwd, runner, emitEvent) {
5837
- 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));
5838
6141
  return Promise.all(promises);
5839
6142
  }
5840
- async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent) {
6143
+ async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent, clock = Date.now) {
5841
6144
  const results = [];
5842
6145
  for (const check of checks) {
5843
- results.push(await runSingleCheck(check, cwd, runner, emitEvent));
6146
+ results.push(await runSingleCheck(check, cwd, runner, emitEvent, clock));
5844
6147
  }
5845
6148
  return results;
5846
6149
  }
5847
- async function runValidationCheck(dependencies, emitEvent) {
6150
+ async function runValidationCheck(dependencies, emitEvent, clock = Date.now) {
5848
6151
  const outputLines = [];
5849
6152
  const bufferedContext = {
5850
6153
  cwd: dependencies.context.cwd,
@@ -5853,13 +6156,13 @@ async function runValidationCheck(dependencies, emitEvent) {
5853
6156
  };
5854
6157
  log6("running built-in check: dust lint");
5855
6158
  emitEvent?.({ type: "check-started", name: "lint" });
5856
- const startTime = Date.now();
6159
+ const startTime = clock();
5857
6160
  const result = await lintMarkdown({
5858
6161
  ...dependencies,
5859
6162
  context: bufferedContext,
5860
6163
  arguments: []
5861
6164
  });
5862
- const durationMs = Date.now() - startTime;
6165
+ const durationMs = clock() - startTime;
5863
6166
  const lintStatus = result.exitCode === 0 ? "passed" : "failed";
5864
6167
  log6(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
5865
6168
  const output = outputLines.join(`
@@ -5923,7 +6226,7 @@ function displayResults(results, context) {
5923
6226
  context.stdout(`${indicator} ${passed.length}/${results.length} checks passed`);
5924
6227
  return failed.length > 0 ? 1 : 0;
5925
6228
  }
5926
- async function check(dependencies, shellRunner = defaultShellRunner) {
6229
+ async function check(dependencies, shellRunner = defaultShellRunner, clock = Date.now) {
5927
6230
  const {
5928
6231
  arguments: commandArguments,
5929
6232
  context,
@@ -5949,18 +6252,18 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
5949
6252
  if (serial) {
5950
6253
  const results2 = [];
5951
6254
  if (hasDustDir) {
5952
- results2.push(await runValidationCheck(dependencies, context.emitEvent));
6255
+ results2.push(await runValidationCheck(dependencies, context.emitEvent, clock));
5953
6256
  }
5954
- const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent);
6257
+ const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent, clock);
5955
6258
  results2.push(...configuredResults);
5956
6259
  const exitCode2 = displayResults(results2, context);
5957
6260
  return { exitCode: exitCode2 };
5958
6261
  }
5959
6262
  const checkPromises = [];
5960
6263
  if (hasDustDir) {
5961
- checkPromises.push(runValidationCheck(dependencies, context.emitEvent));
6264
+ checkPromises.push(runValidationCheck(dependencies, context.emitEvent, clock));
5962
6265
  }
5963
- checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent));
6266
+ checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent, clock));
5964
6267
  const promiseResults = await Promise.all(checkPromises);
5965
6268
  const results = [];
5966
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.84",
3
+ "version": "0.1.85",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {