@joshski/dust 0.1.40 → 0.1.41

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,41 @@
1
+ /**
2
+ * Shared agent event types for the dust event protocol.
3
+ *
4
+ * These types define the transport-agnostic event format used by both
5
+ * the HTTP (loop) and WebSocket (bucket) paths.
6
+ */
7
+ export type AgentSessionEvent = {
8
+ type: 'agent-session-started';
9
+ title: string;
10
+ prompt: string;
11
+ agentType: string;
12
+ purpose: string;
13
+ } | {
14
+ type: 'agent-session-ended';
15
+ success: boolean;
16
+ error?: string;
17
+ } | {
18
+ type: 'agent-session-activity';
19
+ } | {
20
+ type: 'claude-event';
21
+ rawEvent: Record<string, unknown>;
22
+ };
23
+ export interface EventMessage {
24
+ sequence: number;
25
+ timestamp: string;
26
+ sessionId: string;
27
+ repository: string;
28
+ agentSessionId?: string;
29
+ event: AgentSessionEvent;
30
+ }
31
+ /**
32
+ * Convert a raw Claude streaming event to an AgentSessionEvent.
33
+ * stream_event types become activity heartbeats; everything else
34
+ * is forwarded as a claude-event.
35
+ */
36
+ export declare function rawEventToAgentEvent(rawEvent: Record<string, unknown>): AgentSessionEvent;
37
+ /**
38
+ * Format an AgentSessionEvent for console output.
39
+ * Returns null for events that should not be displayed.
40
+ */
41
+ export declare function formatAgentEvent(event: AgentSessionEvent): string | null;
package/dist/dust.js CHANGED
@@ -336,8 +336,12 @@ async function manageGitHooks(dependencies) {
336
336
  }
337
337
 
338
338
  // lib/cli/commands/agent.ts
339
- async function agent(dependencies) {
339
+ async function agent(dependencies, env = process.env) {
340
340
  const { context, fileSystem, settings } = dependencies;
341
+ if (env.DUST_SKIP_AGENT === "1") {
342
+ context.stdout("You're running in an automated loop - proceeding to implement the assigned task.");
343
+ return { exitCode: 0 };
344
+ }
341
345
  const hooksInstalled = await manageGitHooks(dependencies);
342
346
  const vars = await templateVariablesWithInstructions(context.cwd, fileSystem, settings, hooksInstalled);
343
347
  context.stdout(loadTemplate("agent-greeting", vars));
@@ -1117,6 +1121,7 @@ import { spawn as nodeSpawn2 } from "node:child_process";
1117
1121
  function buildImplementationInstructions(bin, hooksInstalled) {
1118
1122
  const steps = [];
1119
1123
  let step = 1;
1124
+ steps.push(`Note: Skip the \`${bin} agent\` step - your task has already been specified below.`, "");
1120
1125
  steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
1121
1126
  step++;
1122
1127
  steps.push(`${step}. Implement the task`);
@@ -1251,8 +1256,6 @@ function formatLoopEvent(event) {
1251
1256
  return `\uD83D\uDCCB Completed iteration ${event.iteration}/${event.maxIterations}`;
1252
1257
  case "loop.ended":
1253
1258
  return `\uD83C\uDFC1 Reached max iterations (${event.maxIterations}). Exiting.`;
1254
- case "loop.start_agent":
1255
- return null;
1256
1259
  }
1257
1260
  }
1258
1261
  async function defaultPostEvent(url, payload) {
@@ -1330,11 +1333,9 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
1330
1333
  type: "loop.sync_skipped",
1331
1334
  reason: pullResult.message
1332
1335
  });
1333
- onAgentEvent?.({
1334
- type: "agent-session-started",
1335
- title: "Resolving git conflict"
1336
- });
1337
- const prompt2 = `git pull failed with the following error:
1336
+ const prompt2 = `Note: Skip the \`dust agent\` step - your task has already been specified below.
1337
+
1338
+ git pull failed with the following error:
1338
1339
 
1339
1340
  ${pullResult.message}
1340
1341
 
@@ -1344,13 +1345,19 @@ Please resolve this issue. Common approaches:
1344
1345
  3. After resolving, commit any changes and push to remote
1345
1346
 
1346
1347
  Make sure the repository is in a clean state and synced with remote before finishing.`;
1347
- onLoopEvent({ type: "loop.start_agent", prompt: prompt2 });
1348
+ onAgentEvent?.({
1349
+ type: "agent-session-started",
1350
+ title: "Resolving git conflict",
1351
+ prompt: prompt2,
1352
+ agentType: "claude",
1353
+ purpose: "git-conflict"
1354
+ });
1348
1355
  try {
1349
1356
  await run2(prompt2, {
1350
1357
  spawnOptions: {
1351
1358
  cwd: context.cwd,
1352
1359
  dangerouslySkipPermissions: true,
1353
- env: { DUST_UNATTENDED: "1" }
1360
+ env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" }
1354
1361
  },
1355
1362
  onRawEvent
1356
1363
  });
@@ -1374,10 +1381,6 @@ Make sure the repository is in a clean state and synced with remote before finis
1374
1381
  }
1375
1382
  const task = tasks[0];
1376
1383
  onLoopEvent({ type: "loop.tasks_found" });
1377
- onAgentEvent?.({
1378
- type: "agent-session-started",
1379
- title: task.title ?? task.path
1380
- });
1381
1384
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
1382
1385
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
1383
1386
  const instructions = buildImplementationInstructions(dustCommand, true);
@@ -1394,13 +1397,19 @@ When the task is complete, delete the task file \`${task.path}\`.
1394
1397
  ## Instructions
1395
1398
 
1396
1399
  ${instructions}`;
1397
- onLoopEvent({ type: "loop.start_agent", prompt });
1400
+ onAgentEvent?.({
1401
+ type: "agent-session-started",
1402
+ title: task.title ?? task.path,
1403
+ prompt,
1404
+ agentType: "claude",
1405
+ purpose: "task"
1406
+ });
1398
1407
  try {
1399
1408
  await run2(prompt, {
1400
1409
  spawnOptions: {
1401
1410
  cwd: context.cwd,
1402
1411
  dangerouslySkipPermissions: true,
1403
- env: { DUST_UNATTENDED: "1" }
1412
+ env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" }
1404
1413
  },
1405
1414
  onRawEvent
1406
1415
  });
@@ -1489,7 +1498,14 @@ function parseRepository(data) {
1489
1498
  if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
1490
1499
  const repositoryData = data;
1491
1500
  if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
1492
- return { name: repositoryData.name, gitUrl: repositoryData.gitUrl };
1501
+ const repo = {
1502
+ name: repositoryData.name,
1503
+ gitUrl: repositoryData.gitUrl
1504
+ };
1505
+ if (typeof repositoryData.url === "string") {
1506
+ repo.url = repositoryData.url;
1507
+ }
1508
+ return repo;
1493
1509
  }
1494
1510
  }
1495
1511
  return null;
@@ -1790,6 +1806,7 @@ function createTerminalUIState() {
1790
1806
  selectedIndex: -1,
1791
1807
  logBuffers: new Map,
1792
1808
  agentStatuses: new Map,
1809
+ repositoryUrls: new Map,
1793
1810
  scrollOffset: 0,
1794
1811
  autoScroll: true,
1795
1812
  width: 80,
@@ -1801,7 +1818,7 @@ function updateDimensions(state, width, height) {
1801
1818
  state.width = width;
1802
1819
  state.height = height;
1803
1820
  }
1804
- function addRepository2(state, name, logBuffer) {
1821
+ function addRepository2(state, name, logBuffer, url) {
1805
1822
  if (!state.repositories.includes(name)) {
1806
1823
  state.repositories.push(name);
1807
1824
  state.repositories.sort((a, b) => {
@@ -1814,6 +1831,9 @@ function addRepository2(state, name, logBuffer) {
1814
1831
  state.agentStatuses.set(name, "idle");
1815
1832
  }
1816
1833
  state.logBuffers.set(name, logBuffer);
1834
+ if (url) {
1835
+ state.repositoryUrls.set(name, url);
1836
+ }
1817
1837
  }
1818
1838
  function removeRepository2(state, name) {
1819
1839
  const index = state.repositories.indexOf(name);
@@ -1821,6 +1841,7 @@ function removeRepository2(state, name) {
1821
1841
  state.repositories.splice(index, 1);
1822
1842
  state.logBuffers.delete(name);
1823
1843
  state.agentStatuses.delete(name);
1844
+ state.repositoryUrls.delete(name);
1824
1845
  if (state.selectedIndex >= state.repositories.length) {
1825
1846
  state.selectedIndex = state.repositories.length - 1;
1826
1847
  }
@@ -1967,7 +1988,7 @@ function renderTabs(state) {
1967
1988
  return rows.map((row) => row.map((t) => t.text).join("|"));
1968
1989
  }
1969
1990
  function renderHelpLine() {
1970
- return `${ANSI.DIM}[←→] select [↑↓] scroll [PgUp/PgDn] page [g/G] top/bottom [q] quit${ANSI.RESET}`;
1991
+ return `${ANSI.DIM}[←→] select [↑↓] scroll [PgUp/PgDn] page [g/G] top/bottom [o] open [q] quit${ANSI.RESET}`;
1971
1992
  }
1972
1993
  function renderSeparator(width) {
1973
1994
  return "─".repeat(width);
@@ -2050,7 +2071,7 @@ function parseSGRMouse(key) {
2050
2071
  return null;
2051
2072
  return Number.parseInt(match[1], 10);
2052
2073
  }
2053
- function handleKeyInput(state, key) {
2074
+ function handleKeyInput(state, key, options) {
2054
2075
  const mouseButton = parseSGRMouse(key);
2055
2076
  if (mouseButton !== null) {
2056
2077
  if (mouseButton === 64) {
@@ -2094,6 +2115,19 @@ function handleKeyInput(state, key) {
2094
2115
  case KEYS.END:
2095
2116
  scrollToBottom(state);
2096
2117
  break;
2118
+ case "o": {
2119
+ if (state.selectedIndex === -1) {
2120
+ break;
2121
+ }
2122
+ const repoName = state.repositories[state.selectedIndex];
2123
+ if (!repoName)
2124
+ break;
2125
+ const url = state.repositoryUrls.get(repoName);
2126
+ if (url && options?.openBrowser) {
2127
+ options.openBrowser(url);
2128
+ }
2129
+ break;
2130
+ }
2097
2131
  }
2098
2132
  return false;
2099
2133
  }
@@ -2265,7 +2299,9 @@ function syncUIWithRepoList(state, repos) {
2265
2299
  buffer = createLogBuffer();
2266
2300
  state.logBuffers.set(repo.name, buffer);
2267
2301
  }
2268
- addRepository2(state.ui, repo.name, buffer);
2302
+ addRepository2(state.ui, repo.name, buffer, repo.url);
2303
+ } else if (repo.url) {
2304
+ state.ui.repositoryUrls.set(repo.name, repo.url);
2269
2305
  }
2270
2306
  }
2271
2307
  }
@@ -2429,10 +2465,10 @@ function setupTUI(state, bucketDeps) {
2429
2465
  }
2430
2466
  };
2431
2467
  }
2432
- function createKeypressHandler(useTUI, state, onQuit) {
2468
+ function createKeypressHandler(useTUI, state, onQuit, options) {
2433
2469
  if (useTUI) {
2434
2470
  return (key) => {
2435
- const shouldQuit = handleKeyInput(state.ui, key);
2471
+ const shouldQuit = handleKeyInput(state.ui, key, options);
2436
2472
  if (shouldQuit)
2437
2473
  onQuit();
2438
2474
  };
@@ -2504,7 +2540,7 @@ async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies
2504
2540
  };
2505
2541
  const onKey = createKeypressHandler(useTUI, state, () => {
2506
2542
  doShutdown();
2507
- });
2543
+ }, { openBrowser: bucketDeps.auth.openBrowser });
2508
2544
  cleanupKeypress = bucketDeps.setupKeypress(onKey);
2509
2545
  cleanupSignals = bucketDeps.setupSignals(() => {
2510
2546
  doShutdown();
@@ -0,0 +1,25 @@
1
+ import type { FileSystem } from './cli/types';
2
+ export interface IdeaOption {
3
+ name: string;
4
+ description: string;
5
+ }
6
+ export interface IdeaOpenQuestion {
7
+ question: string;
8
+ options: IdeaOption[];
9
+ }
10
+ export interface Idea {
11
+ slug: string;
12
+ title: string;
13
+ openingSentence: string | null;
14
+ content: string;
15
+ openQuestions: IdeaOpenQuestion[];
16
+ }
17
+ /**
18
+ * Parses the ## Open Questions section from idea markdown content.
19
+ * Extracts each ### question heading and its #### option children.
20
+ */
21
+ export declare function parseOpenQuestions(content: string): IdeaOpenQuestion[];
22
+ /**
23
+ * Parses an idea markdown file into a structured Idea object.
24
+ */
25
+ export declare function parseIdea(fileSystem: FileSystem, dustPath: string, slug: string): Promise<Idea>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared markdown utilities for dust CLI commands
3
+ */
4
+ /**
5
+ * Extracts the title from markdown content (first H1 heading)
6
+ */
7
+ export declare function extractTitle(content: string): string | null;
8
+ /**
9
+ * Pattern for matching markdown links: [text](url)
10
+ * Note: Create a new RegExp with 'g' flag for global matching:
11
+ * `new RegExp(MARKDOWN_LINK_PATTERN.source, 'g')`
12
+ */
13
+ export declare const MARKDOWN_LINK_PATTERN: RegExp;
14
+ /**
15
+ * Extracts the first sentence from the first paragraph after the H1 heading.
16
+ * Returns null if no valid opening paragraph exists.
17
+ *
18
+ * A valid opening paragraph:
19
+ * - Appears on the first non-blank line after the H1 heading
20
+ * - Is a plain paragraph (not a heading, list item, or code block)
21
+ * - Starts with a sentence that ends in `.` `?` or `!`
22
+ */
23
+ export declare function extractOpeningSentence(content: string): string | null;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public type definitions for downstream consumers of @joshski/dust.
3
+ *
4
+ * Import from '@joshski/dust/types' to get typed bindings for
5
+ * the event protocol, workflow tasks, and idea structures.
6
+ */
7
+ export type { AgentSessionEvent, EventMessage } from './agent-events';
8
+ export type { Idea, IdeaOpenQuestion, IdeaOption } from './ideas';
9
+ export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, IdeaInProgress, OpenQuestionResponse, ParsedCaptureIdeaTask, WorkflowTaskMatch, WorkflowTaskType, } from './workflow-tasks';
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "dust": "./dist/dust.js"
8
8
  },
9
9
  "exports": {
10
+ "./types": {
11
+ "types": "./dist/types.d.ts"
12
+ },
10
13
  "./workflow-tasks": {
11
14
  "import": "./dist/workflow-tasks.js",
12
15
  "types": "./dist/workflow-tasks.d.ts"