@joshski/dust 0.1.40 → 0.1.42

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));
@@ -1114,9 +1118,10 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
1114
1118
  import { spawn as nodeSpawn2 } from "node:child_process";
1115
1119
 
1116
1120
  // lib/cli/commands/focus.ts
1117
- function buildImplementationInstructions(bin, hooksInstalled) {
1121
+ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
1118
1122
  const steps = [];
1119
1123
  let step = 1;
1124
+ steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
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`);
@@ -1125,7 +1130,8 @@ function buildImplementationInstructions(bin, hooksInstalled) {
1125
1130
  steps.push(`${step}. Run \`${bin} check\` before committing`);
1126
1131
  step++;
1127
1132
  }
1128
- steps.push(`${step}. Create a single atomic commit that includes:`, " - All implementation changes", " - Deletion of the completed task file", " - Updates to any facts that changed", " - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)", "", ' Use the task title as the commit message. Task titles are written in imperative form, which is the recommended style for git commit messages. Do not add prefixes like "Complete task:" - use the title directly.', "", ' Example: If the task title is "Add validation for user input", the commit message should be:', " ```", " Add validation for user input", " ```", "");
1133
+ const commitMessageLine = taskTitle ? ` Use this exact commit message: "${taskTitle}". Do not add any prefix.` : ' Use the task title as the commit message. Do not add prefixes like "Complete task:" - use the title directly.';
1134
+ steps.push(`${step}. Create a single atomic commit that includes:`, " - All implementation changes", " - Deletion of the completed task file", " - Updates to any facts that changed", " - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)", "", commitMessageLine, "");
1129
1135
  step++;
1130
1136
  steps.push(`${step}. Push your commit to the remote repository`);
1131
1137
  steps.push("");
@@ -1251,8 +1257,6 @@ function formatLoopEvent(event) {
1251
1257
  return `\uD83D\uDCCB Completed iteration ${event.iteration}/${event.maxIterations}`;
1252
1258
  case "loop.ended":
1253
1259
  return `\uD83C\uDFC1 Reached max iterations (${event.maxIterations}). Exiting.`;
1254
- case "loop.start_agent":
1255
- return null;
1256
1260
  }
1257
1261
  }
1258
1262
  async function defaultPostEvent(url, payload) {
@@ -1330,11 +1334,9 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
1330
1334
  type: "loop.sync_skipped",
1331
1335
  reason: pullResult.message
1332
1336
  });
1333
- onAgentEvent?.({
1334
- type: "agent-session-started",
1335
- title: "Resolving git conflict"
1336
- });
1337
- const prompt2 = `git pull failed with the following error:
1337
+ const prompt2 = `Note: Do NOT run \`dust agent\`.
1338
+
1339
+ git pull failed with the following error:
1338
1340
 
1339
1341
  ${pullResult.message}
1340
1342
 
@@ -1344,13 +1346,19 @@ Please resolve this issue. Common approaches:
1344
1346
  3. After resolving, commit any changes and push to remote
1345
1347
 
1346
1348
  Make sure the repository is in a clean state and synced with remote before finishing.`;
1347
- onLoopEvent({ type: "loop.start_agent", prompt: prompt2 });
1349
+ onAgentEvent?.({
1350
+ type: "agent-session-started",
1351
+ title: "Resolving git conflict",
1352
+ prompt: prompt2,
1353
+ agentType: "claude",
1354
+ purpose: "git-conflict"
1355
+ });
1348
1356
  try {
1349
1357
  await run2(prompt2, {
1350
1358
  spawnOptions: {
1351
1359
  cwd: context.cwd,
1352
1360
  dangerouslySkipPermissions: true,
1353
- env: { DUST_UNATTENDED: "1" }
1361
+ env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" }
1354
1362
  },
1355
1363
  onRawEvent
1356
1364
  });
@@ -1374,33 +1382,35 @@ Make sure the repository is in a clean state and synced with remote before finis
1374
1382
  }
1375
1383
  const task = tasks[0];
1376
1384
  onLoopEvent({ type: "loop.tasks_found" });
1377
- onAgentEvent?.({
1378
- type: "agent-session-started",
1379
- title: task.title ?? task.path
1380
- });
1381
1385
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
1382
1386
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
1383
- const instructions = buildImplementationInstructions(dustCommand, true);
1387
+ const instructions = buildImplementationInstructions(dustCommand, true, task.title ?? undefined);
1384
1388
  const prompt = `Run \`${installCommand}\` to install dependencies, then implement the following task.
1385
1389
 
1386
- ## Task: ${task.title}
1387
-
1388
1390
  The following is the contents of the task file \`${task.path}\`:
1389
1391
 
1392
+ ----------
1390
1393
  ${taskContent}
1394
+ ----------
1391
1395
 
1392
1396
  When the task is complete, delete the task file \`${task.path}\`.
1393
1397
 
1394
1398
  ## Instructions
1395
1399
 
1396
1400
  ${instructions}`;
1397
- onLoopEvent({ type: "loop.start_agent", prompt });
1401
+ onAgentEvent?.({
1402
+ type: "agent-session-started",
1403
+ title: task.title ?? task.path,
1404
+ prompt,
1405
+ agentType: "claude",
1406
+ purpose: "task"
1407
+ });
1398
1408
  try {
1399
1409
  await run2(prompt, {
1400
1410
  spawnOptions: {
1401
1411
  cwd: context.cwd,
1402
1412
  dangerouslySkipPermissions: true,
1403
- env: { DUST_UNATTENDED: "1" }
1413
+ env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" }
1404
1414
  },
1405
1415
  onRawEvent
1406
1416
  });
@@ -1489,7 +1499,14 @@ function parseRepository(data) {
1489
1499
  if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
1490
1500
  const repositoryData = data;
1491
1501
  if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
1492
- return { name: repositoryData.name, gitUrl: repositoryData.gitUrl };
1502
+ const repo = {
1503
+ name: repositoryData.name,
1504
+ gitUrl: repositoryData.gitUrl
1505
+ };
1506
+ if (typeof repositoryData.url === "string") {
1507
+ repo.url = repositoryData.url;
1508
+ }
1509
+ return repo;
1493
1510
  }
1494
1511
  }
1495
1512
  return null;
@@ -1790,6 +1807,7 @@ function createTerminalUIState() {
1790
1807
  selectedIndex: -1,
1791
1808
  logBuffers: new Map,
1792
1809
  agentStatuses: new Map,
1810
+ repositoryUrls: new Map,
1793
1811
  scrollOffset: 0,
1794
1812
  autoScroll: true,
1795
1813
  width: 80,
@@ -1801,7 +1819,7 @@ function updateDimensions(state, width, height) {
1801
1819
  state.width = width;
1802
1820
  state.height = height;
1803
1821
  }
1804
- function addRepository2(state, name, logBuffer) {
1822
+ function addRepository2(state, name, logBuffer, url) {
1805
1823
  if (!state.repositories.includes(name)) {
1806
1824
  state.repositories.push(name);
1807
1825
  state.repositories.sort((a, b) => {
@@ -1814,6 +1832,9 @@ function addRepository2(state, name, logBuffer) {
1814
1832
  state.agentStatuses.set(name, "idle");
1815
1833
  }
1816
1834
  state.logBuffers.set(name, logBuffer);
1835
+ if (url) {
1836
+ state.repositoryUrls.set(name, url);
1837
+ }
1817
1838
  }
1818
1839
  function removeRepository2(state, name) {
1819
1840
  const index = state.repositories.indexOf(name);
@@ -1821,6 +1842,7 @@ function removeRepository2(state, name) {
1821
1842
  state.repositories.splice(index, 1);
1822
1843
  state.logBuffers.delete(name);
1823
1844
  state.agentStatuses.delete(name);
1845
+ state.repositoryUrls.delete(name);
1824
1846
  if (state.selectedIndex >= state.repositories.length) {
1825
1847
  state.selectedIndex = state.repositories.length - 1;
1826
1848
  }
@@ -1967,7 +1989,7 @@ function renderTabs(state) {
1967
1989
  return rows.map((row) => row.map((t) => t.text).join("|"));
1968
1990
  }
1969
1991
  function renderHelpLine() {
1970
- return `${ANSI.DIM}[←→] select [↑↓] scroll [PgUp/PgDn] page [g/G] top/bottom [q] quit${ANSI.RESET}`;
1992
+ return `${ANSI.DIM}[←→] select [↑↓] scroll [PgUp/PgDn] page [g/G] top/bottom [o] open [q] quit${ANSI.RESET}`;
1971
1993
  }
1972
1994
  function renderSeparator(width) {
1973
1995
  return "─".repeat(width);
@@ -2050,7 +2072,7 @@ function parseSGRMouse(key) {
2050
2072
  return null;
2051
2073
  return Number.parseInt(match[1], 10);
2052
2074
  }
2053
- function handleKeyInput(state, key) {
2075
+ function handleKeyInput(state, key, options) {
2054
2076
  const mouseButton = parseSGRMouse(key);
2055
2077
  if (mouseButton !== null) {
2056
2078
  if (mouseButton === 64) {
@@ -2094,6 +2116,19 @@ function handleKeyInput(state, key) {
2094
2116
  case KEYS.END:
2095
2117
  scrollToBottom(state);
2096
2118
  break;
2119
+ case "o": {
2120
+ if (state.selectedIndex === -1) {
2121
+ break;
2122
+ }
2123
+ const repoName = state.repositories[state.selectedIndex];
2124
+ if (!repoName)
2125
+ break;
2126
+ const url = state.repositoryUrls.get(repoName);
2127
+ if (url && options?.openBrowser) {
2128
+ options.openBrowser(url);
2129
+ }
2130
+ break;
2131
+ }
2097
2132
  }
2098
2133
  return false;
2099
2134
  }
@@ -2265,7 +2300,9 @@ function syncUIWithRepoList(state, repos) {
2265
2300
  buffer = createLogBuffer();
2266
2301
  state.logBuffers.set(repo.name, buffer);
2267
2302
  }
2268
- addRepository2(state.ui, repo.name, buffer);
2303
+ addRepository2(state.ui, repo.name, buffer, repo.url);
2304
+ } else if (repo.url) {
2305
+ state.ui.repositoryUrls.set(repo.name, repo.url);
2269
2306
  }
2270
2307
  }
2271
2308
  }
@@ -2429,10 +2466,10 @@ function setupTUI(state, bucketDeps) {
2429
2466
  }
2430
2467
  };
2431
2468
  }
2432
- function createKeypressHandler(useTUI, state, onQuit) {
2469
+ function createKeypressHandler(useTUI, state, onQuit, options) {
2433
2470
  if (useTUI) {
2434
2471
  return (key) => {
2435
- const shouldQuit = handleKeyInput(state.ui, key);
2472
+ const shouldQuit = handleKeyInput(state.ui, key, options);
2436
2473
  if (shouldQuit)
2437
2474
  onQuit();
2438
2475
  };
@@ -2442,9 +2479,10 @@ function createKeypressHandler(useTUI, state, onQuit) {
2442
2479
  onQuit();
2443
2480
  };
2444
2481
  }
2445
- async function resolveToken(commandArgs, authDeps, context) {
2446
- if (commandArgs[0]) {
2447
- return commandArgs[0];
2482
+ async function resolveToken(authDeps, context) {
2483
+ const envToken = process.env.DUST_BUCKET_TOKEN;
2484
+ if (envToken) {
2485
+ return envToken;
2448
2486
  }
2449
2487
  const stored = await loadStoredToken(authDeps.fileSystem, authDeps.getHomeDir());
2450
2488
  if (stored) {
@@ -2462,8 +2500,8 @@ async function resolveToken(commandArgs, authDeps, context) {
2462
2500
  }
2463
2501
  }
2464
2502
  async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies()) {
2465
- const { arguments: commandArgs, context, fileSystem } = dependencies;
2466
- const token = await resolveToken(commandArgs, bucketDeps.auth, context);
2503
+ const { context, fileSystem } = dependencies;
2504
+ const token = await resolveToken(bucketDeps.auth, context);
2467
2505
  if (!token) {
2468
2506
  return { exitCode: 1 };
2469
2507
  }
@@ -2504,7 +2542,7 @@ async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies
2504
2542
  };
2505
2543
  const onKey = createKeypressHandler(useTUI, state, () => {
2506
2544
  doShutdown();
2507
- });
2545
+ }, { openBrowser: bucketDeps.auth.openBrowser });
2508
2546
  cleanupKeypress = bucketDeps.setupKeypress(onKey);
2509
2547
  cleanupSignals = bucketDeps.setupSignals(() => {
2510
2548
  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.42",
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"