@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.
- package/dist/agent-events.d.ts +41 -0
- package/dist/dust.js +72 -34
- package/dist/ideas.d.ts +25 -0
- package/dist/markdown/markdown-utilities.d.ts +23 -0
- package/dist/types.d.ts +9 -0
- package/package.json +4 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2446
|
-
|
|
2447
|
-
|
|
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 {
|
|
2466
|
-
const token = await resolveToken(
|
|
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();
|
package/dist/ideas.d.ts
ADDED
|
@@ -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;
|
package/dist/types.d.ts
ADDED
|
@@ -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.
|
|
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"
|