@joshski/dust 0.1.9 → 0.1.10
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/dust.js +731 -236
- package/package.json +1 -1
- package/templates/agent-greeting.txt +4 -2
- package/templates/agent-implement-task.txt +14 -4
- package/templates/agent-new-goal.txt +1 -0
- package/templates/agent-new-idea.txt +9 -12
- package/templates/agent-new-task.txt +1 -0
- package/templates/help.txt +1 -0
package/dist/dust.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// lib/cli/entry.ts
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
|
|
6
6
|
// lib/cli/templates.ts
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
@@ -9,69 +9,246 @@ import { dirname, join } from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
var templatesDir = join(__dirname2, "../templates");
|
|
12
|
+
function isTruthy(value) {
|
|
13
|
+
return value !== undefined && value !== "" && value !== "false";
|
|
14
|
+
}
|
|
15
|
+
function processConditionals(content, variables) {
|
|
16
|
+
let result = content.replace(/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, varName, block) => {
|
|
17
|
+
return isTruthy(variables[varName]) ? block : "";
|
|
18
|
+
});
|
|
19
|
+
result = result.replace(/\{\{#unless (\w+)\}\}([\s\S]*?)\{\{\/unless\}\}/g, (_, varName, block) => {
|
|
20
|
+
return !isTruthy(variables[varName]) ? block : "";
|
|
21
|
+
});
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
12
24
|
function loadTemplate(name, variables = {}) {
|
|
13
25
|
const templatePath = join(templatesDir, `${name}.txt`);
|
|
14
26
|
let content = readFileSync(templatePath, "utf-8");
|
|
27
|
+
content = processConditionals(content, variables);
|
|
15
28
|
for (const [key, value] of Object.entries(variables)) {
|
|
16
29
|
content = content.replaceAll(`{{${key}}}`, value);
|
|
17
30
|
}
|
|
18
31
|
return content;
|
|
19
32
|
}
|
|
20
33
|
|
|
21
|
-
// lib/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
// lib/git/hooks.ts
|
|
35
|
+
import { join as join2 } from "node:path";
|
|
36
|
+
var DUST_HOOK_START = "# BEGIN DUST HOOK";
|
|
37
|
+
var DUST_HOOK_END = "# END DUST HOOK";
|
|
38
|
+
function generateHookContent(dustCommand) {
|
|
39
|
+
return `${DUST_HOOK_START}
|
|
40
|
+
${dustCommand} pre push
|
|
41
|
+
if [ $? -ne 0 ]; then
|
|
42
|
+
echo "dust pre-push check failed"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
${DUST_HOOK_END}`;
|
|
46
|
+
}
|
|
47
|
+
function extractDustSection(content) {
|
|
48
|
+
const startIndex = content.indexOf(DUST_HOOK_START);
|
|
49
|
+
const endIndex = content.indexOf(DUST_HOOK_END);
|
|
50
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return content.substring(startIndex, endIndex + DUST_HOOK_END.length);
|
|
54
|
+
}
|
|
55
|
+
function removeDustSection(content) {
|
|
56
|
+
const startIndex = content.indexOf(DUST_HOOK_START);
|
|
57
|
+
const endIndex = content.indexOf(DUST_HOOK_END);
|
|
58
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
59
|
+
return content;
|
|
60
|
+
}
|
|
61
|
+
const before = content.substring(0, startIndex);
|
|
62
|
+
const after = content.substring(endIndex + DUST_HOOK_END.length);
|
|
63
|
+
return (before + after).replace(/\n{3,}/g, `
|
|
64
|
+
|
|
65
|
+
`).trim();
|
|
66
|
+
}
|
|
67
|
+
function createHooksManager(cwd, fileSystem, settings) {
|
|
68
|
+
const gitDir = join2(cwd, ".git");
|
|
69
|
+
const hooksDir = join2(gitDir, "hooks");
|
|
70
|
+
const prePushPath = join2(hooksDir, "pre-push");
|
|
71
|
+
return {
|
|
72
|
+
isGitRepo: () => fileSystem.exists(gitDir),
|
|
73
|
+
isHookInstalled: async () => {
|
|
74
|
+
if (!fileSystem.exists(prePushPath)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const content = await fileSystem.readFile(prePushPath);
|
|
79
|
+
return content.includes(DUST_HOOK_START);
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
installHook: async () => {
|
|
85
|
+
if (!fileSystem.exists(hooksDir)) {
|
|
86
|
+
await fileSystem.mkdir(hooksDir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
const hookContent = generateHookContent(settings.dustCommand);
|
|
89
|
+
let finalContent;
|
|
90
|
+
if (fileSystem.exists(prePushPath)) {
|
|
91
|
+
const existingContent = await fileSystem.readFile(prePushPath);
|
|
92
|
+
if (existingContent.includes(DUST_HOOK_START)) {
|
|
93
|
+
const withoutDust = removeDustSection(existingContent);
|
|
94
|
+
finalContent = withoutDust ? `${withoutDust}
|
|
95
|
+
|
|
96
|
+
${hookContent}
|
|
97
|
+
` : `#!/bin/sh
|
|
98
|
+
|
|
99
|
+
${hookContent}
|
|
100
|
+
`;
|
|
101
|
+
} else {
|
|
102
|
+
finalContent = `${existingContent.trimEnd()}
|
|
103
|
+
|
|
104
|
+
${hookContent}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
finalContent = `#!/bin/sh
|
|
109
|
+
|
|
110
|
+
${hookContent}
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
await fileSystem.writeFile(prePushPath, finalContent);
|
|
114
|
+
await fileSystem.chmod(prePushPath, 493);
|
|
115
|
+
},
|
|
116
|
+
getHookBinaryPath: async () => {
|
|
117
|
+
if (!fileSystem.exists(prePushPath)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const content = await fileSystem.readFile(prePushPath);
|
|
122
|
+
const dustSection = extractDustSection(content);
|
|
123
|
+
if (!dustSection) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const match = dustSection.match(/^(.+) pre push$/m);
|
|
127
|
+
return match ? match[1] : null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
updateHookBinaryPath: async (newPath) => {
|
|
133
|
+
if (!fileSystem.exists(prePushPath)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const content = await fileSystem.readFile(prePushPath);
|
|
137
|
+
const dustSection = extractDustSection(content);
|
|
138
|
+
if (!dustSection) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const withoutDust = removeDustSection(content);
|
|
142
|
+
const newHookContent = generateHookContent(newPath);
|
|
143
|
+
const finalContent = withoutDust ? `${withoutDust}
|
|
144
|
+
|
|
145
|
+
${newHookContent}
|
|
146
|
+
` : `#!/bin/sh
|
|
147
|
+
|
|
148
|
+
${newHookContent}
|
|
149
|
+
`;
|
|
150
|
+
await fileSystem.writeFile(prePushPath, finalContent);
|
|
151
|
+
await fileSystem.chmod(prePushPath, 493);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// lib/cli/commands/agent-shared.ts
|
|
157
|
+
function templateVariables(settings, hooksInstalled) {
|
|
32
158
|
return {
|
|
33
159
|
bin: settings.dustCommand,
|
|
34
|
-
installDependenciesHint: settings.installDependenciesHint || "Install any dependencies"
|
|
160
|
+
installDependenciesHint: settings.installDependenciesHint || "Install any dependencies",
|
|
161
|
+
hooksInstalled: hooksInstalled ? "true" : "false"
|
|
35
162
|
};
|
|
36
163
|
}
|
|
37
|
-
async function
|
|
38
|
-
const {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!verb) {
|
|
43
|
-
ctx.stdout(loadTemplate("agent-greeting", vars));
|
|
44
|
-
return { exitCode: 0 };
|
|
164
|
+
async function manageGitHooks(dependencies) {
|
|
165
|
+
const { context, fileSystem, settings } = dependencies;
|
|
166
|
+
const hooks = createHooksManager(context.cwd, fileSystem, settings);
|
|
167
|
+
if (!hooks.isGitRepo()) {
|
|
168
|
+
return false;
|
|
45
169
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
170
|
+
const isInstalled = await hooks.isHookInstalled();
|
|
171
|
+
if (!isInstalled) {
|
|
172
|
+
await hooks.installHook();
|
|
173
|
+
return true;
|
|
49
174
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ctx.stdout(loadTemplate("agent-new-task", vars));
|
|
54
|
-
return { exitCode: 0 };
|
|
55
|
-
case "new goal":
|
|
56
|
-
ctx.stdout(loadTemplate("agent-new-goal", vars));
|
|
57
|
-
return { exitCode: 0 };
|
|
58
|
-
case "new idea":
|
|
59
|
-
ctx.stdout(loadTemplate("agent-new-idea", vars));
|
|
60
|
-
return { exitCode: 0 };
|
|
61
|
-
case "implement task":
|
|
62
|
-
ctx.stdout(loadTemplate("agent-implement-task", vars));
|
|
63
|
-
return { exitCode: 0 };
|
|
64
|
-
case "understand goals":
|
|
65
|
-
ctx.stdout(loadTemplate("agent-understand-goals", vars));
|
|
66
|
-
return { exitCode: 0 };
|
|
67
|
-
case "pick task":
|
|
68
|
-
ctx.stdout(loadTemplate("agent-pick-task", vars));
|
|
69
|
-
return { exitCode: 0 };
|
|
70
|
-
default:
|
|
71
|
-
ctx.stderr(`Unknown subcommand: ${subcommand}`);
|
|
72
|
-
ctx.stderr(`Available: ${AGENT_SUBCOMMANDS.join(", ")}`);
|
|
73
|
-
return { exitCode: 1 };
|
|
175
|
+
const hookBinaryPath = await hooks.getHookBinaryPath();
|
|
176
|
+
if (hookBinaryPath && hookBinaryPath !== settings.dustCommand) {
|
|
177
|
+
await hooks.updateHookBinaryPath(settings.dustCommand);
|
|
74
178
|
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// lib/cli/commands/agent.ts
|
|
183
|
+
async function agent(dependencies) {
|
|
184
|
+
const { context, settings } = dependencies;
|
|
185
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
186
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
187
|
+
context.stdout(loadTemplate("agent-greeting", vars));
|
|
188
|
+
return { exitCode: 0 };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// lib/cli/commands/agent-help.ts
|
|
192
|
+
async function agentHelp(dependencies) {
|
|
193
|
+
const { context, settings } = dependencies;
|
|
194
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
195
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
196
|
+
context.stdout(loadTemplate("agent-help", vars));
|
|
197
|
+
return { exitCode: 0 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// lib/cli/commands/agent-implement-task.ts
|
|
201
|
+
async function agentImplementTask(dependencies) {
|
|
202
|
+
const { context, settings } = dependencies;
|
|
203
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
204
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
205
|
+
context.stdout(loadTemplate("agent-implement-task", vars));
|
|
206
|
+
return { exitCode: 0 };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// lib/cli/commands/agent-new-goal.ts
|
|
210
|
+
async function agentNewGoal(dependencies) {
|
|
211
|
+
const { context, settings } = dependencies;
|
|
212
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
213
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
214
|
+
context.stdout(loadTemplate("agent-new-goal", vars));
|
|
215
|
+
return { exitCode: 0 };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// lib/cli/commands/agent-new-idea.ts
|
|
219
|
+
async function agentNewIdea(dependencies) {
|
|
220
|
+
const { context, settings } = dependencies;
|
|
221
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
222
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
223
|
+
context.stdout(loadTemplate("agent-new-idea", vars));
|
|
224
|
+
return { exitCode: 0 };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// lib/cli/commands/agent-new-task.ts
|
|
228
|
+
async function agentNewTask(dependencies) {
|
|
229
|
+
const { context, settings } = dependencies;
|
|
230
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
231
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
232
|
+
context.stdout(loadTemplate("agent-new-task", vars));
|
|
233
|
+
return { exitCode: 0 };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// lib/cli/commands/agent-pick-task.ts
|
|
237
|
+
async function agentPickTask(dependencies) {
|
|
238
|
+
const { context, settings } = dependencies;
|
|
239
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
240
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
241
|
+
context.stdout(loadTemplate("agent-pick-task", vars));
|
|
242
|
+
return { exitCode: 0 };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// lib/cli/commands/agent-understand-goals.ts
|
|
246
|
+
async function agentUnderstandGoals(dependencies) {
|
|
247
|
+
const { context, settings } = dependencies;
|
|
248
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
249
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
250
|
+
context.stdout(loadTemplate("agent-understand-goals", vars));
|
|
251
|
+
return { exitCode: 0 };
|
|
75
252
|
}
|
|
76
253
|
|
|
77
254
|
// lib/cli/commands/check.ts
|
|
@@ -113,7 +290,7 @@ function validateTaskHeadings(filePath, content) {
|
|
|
113
290
|
}
|
|
114
291
|
return violations;
|
|
115
292
|
}
|
|
116
|
-
function validateLinks(filePath, content,
|
|
293
|
+
function validateLinks(filePath, content, fileSystem) {
|
|
117
294
|
const violations = [];
|
|
118
295
|
const lines = content.split(`
|
|
119
296
|
`);
|
|
@@ -127,7 +304,7 @@ function validateLinks(filePath, content, fs) {
|
|
|
127
304
|
if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
|
|
128
305
|
const targetPath = linkTarget.split("#")[0];
|
|
129
306
|
const resolvedPath = resolve(fileDir, targetPath);
|
|
130
|
-
if (!
|
|
307
|
+
if (!fileSystem.exists(resolvedPath)) {
|
|
131
308
|
violations.push({
|
|
132
309
|
file: filePath,
|
|
133
310
|
message: `Broken link: "${linkTarget}"`,
|
|
@@ -203,31 +380,31 @@ function validateSemanticLinks(filePath, content) {
|
|
|
203
380
|
}
|
|
204
381
|
return violations;
|
|
205
382
|
}
|
|
206
|
-
async function validate(
|
|
207
|
-
const { context
|
|
208
|
-
const dustPath = `${
|
|
209
|
-
if (!
|
|
210
|
-
|
|
211
|
-
|
|
383
|
+
async function validate(dependencies) {
|
|
384
|
+
const { context, fileSystem, globScanner: glob } = dependencies;
|
|
385
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
386
|
+
if (!fileSystem.exists(dustPath)) {
|
|
387
|
+
context.stderr("Error: .dust directory not found");
|
|
388
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
212
389
|
return { exitCode: 1 };
|
|
213
390
|
}
|
|
214
391
|
const violations = [];
|
|
215
|
-
|
|
392
|
+
context.stdout("Validating links in .dust/...");
|
|
216
393
|
for await (const file of glob.scan(dustPath)) {
|
|
217
394
|
if (!file.endsWith(".md"))
|
|
218
395
|
continue;
|
|
219
396
|
const filePath = `${dustPath}/${file}`;
|
|
220
|
-
const content = await
|
|
221
|
-
violations.push(...validateLinks(filePath, content,
|
|
397
|
+
const content = await fileSystem.readFile(filePath);
|
|
398
|
+
violations.push(...validateLinks(filePath, content, fileSystem));
|
|
222
399
|
}
|
|
223
400
|
const tasksPath = `${dustPath}/tasks`;
|
|
224
|
-
if (
|
|
225
|
-
|
|
401
|
+
if (fileSystem.exists(tasksPath)) {
|
|
402
|
+
context.stdout("Validating task files in .dust/tasks/...");
|
|
226
403
|
for await (const file of glob.scan(tasksPath)) {
|
|
227
404
|
if (!file.endsWith(".md"))
|
|
228
405
|
continue;
|
|
229
406
|
const filePath = `${tasksPath}/${file}`;
|
|
230
|
-
const content = await
|
|
407
|
+
const content = await fileSystem.readFile(filePath);
|
|
231
408
|
const filenameViolation = validateFilename(filePath);
|
|
232
409
|
if (filenameViolation) {
|
|
233
410
|
violations.push(filenameViolation);
|
|
@@ -237,15 +414,15 @@ async function validate(deps) {
|
|
|
237
414
|
}
|
|
238
415
|
}
|
|
239
416
|
if (violations.length === 0) {
|
|
240
|
-
|
|
417
|
+
context.stdout("All validations passed!");
|
|
241
418
|
return { exitCode: 0 };
|
|
242
419
|
}
|
|
243
|
-
|
|
244
|
-
|
|
420
|
+
context.stderr(`Found ${violations.length} violation(s):`);
|
|
421
|
+
context.stderr("");
|
|
245
422
|
for (const v of violations) {
|
|
246
423
|
const location = v.line ? `:${v.line}` : "";
|
|
247
|
-
|
|
248
|
-
|
|
424
|
+
context.stderr(` ${v.file}${location}`);
|
|
425
|
+
context.stderr(` ${v.message}`);
|
|
249
426
|
}
|
|
250
427
|
return { exitCode: 1 };
|
|
251
428
|
}
|
|
@@ -266,8 +443,8 @@ function createBufferedRunner(spawnFn) {
|
|
|
266
443
|
proc.on("close", (code) => {
|
|
267
444
|
resolve2({ exitCode: code ?? 1, output: chunks.join("") });
|
|
268
445
|
});
|
|
269
|
-
proc.on("error", (
|
|
270
|
-
resolve2({ exitCode: 1, output:
|
|
446
|
+
proc.on("error", (error) => {
|
|
447
|
+
resolve2({ exitCode: 1, output: error.message });
|
|
271
448
|
});
|
|
272
449
|
});
|
|
273
450
|
}
|
|
@@ -287,16 +464,16 @@ async function runConfiguredChecks(checks, cwd, runner) {
|
|
|
287
464
|
});
|
|
288
465
|
return Promise.all(promises);
|
|
289
466
|
}
|
|
290
|
-
async function runValidationCheck(
|
|
467
|
+
async function runValidationCheck(dependencies) {
|
|
291
468
|
const outputLines = [];
|
|
292
|
-
const
|
|
293
|
-
cwd:
|
|
469
|
+
const bufferedContext = {
|
|
470
|
+
cwd: dependencies.context.cwd,
|
|
294
471
|
stdout: (msg) => outputLines.push(msg),
|
|
295
472
|
stderr: (msg) => outputLines.push(msg)
|
|
296
473
|
};
|
|
297
474
|
const result = await validate({
|
|
298
|
-
...
|
|
299
|
-
context:
|
|
475
|
+
...dependencies,
|
|
476
|
+
context: bufferedContext,
|
|
300
477
|
arguments: []
|
|
301
478
|
});
|
|
302
479
|
return {
|
|
@@ -308,54 +485,54 @@ async function runValidationCheck(deps) {
|
|
|
308
485
|
isBuiltIn: true
|
|
309
486
|
};
|
|
310
487
|
}
|
|
311
|
-
function displayResults(results,
|
|
488
|
+
function displayResults(results, context) {
|
|
312
489
|
const passed = results.filter((r) => r.exitCode === 0);
|
|
313
490
|
const failed = results.filter((r) => r.exitCode !== 0);
|
|
314
491
|
for (const result of results) {
|
|
315
492
|
if (result.exitCode === 0) {
|
|
316
|
-
|
|
493
|
+
context.stdout(`✓ ${result.name}`);
|
|
317
494
|
} else {
|
|
318
|
-
|
|
495
|
+
context.stdout(`✗ ${result.name}`);
|
|
319
496
|
}
|
|
320
497
|
}
|
|
321
498
|
for (const result of failed) {
|
|
322
|
-
|
|
323
|
-
|
|
499
|
+
context.stdout("");
|
|
500
|
+
context.stdout(`> ${result.command}`);
|
|
324
501
|
if (result.output.trim()) {
|
|
325
|
-
|
|
502
|
+
context.stdout(result.output.trimEnd());
|
|
326
503
|
}
|
|
327
504
|
if (result.hints && result.hints.length > 0) {
|
|
328
|
-
|
|
329
|
-
|
|
505
|
+
context.stdout("");
|
|
506
|
+
context.stdout(`Hints for fixing '${result.name}':`);
|
|
330
507
|
for (const hint of result.hints) {
|
|
331
|
-
|
|
508
|
+
context.stdout(` - ${hint}`);
|
|
332
509
|
}
|
|
333
510
|
}
|
|
334
511
|
}
|
|
335
|
-
|
|
336
|
-
|
|
512
|
+
context.stdout("");
|
|
513
|
+
context.stdout(`${passed.length}/${results.length} checks passed`);
|
|
337
514
|
return failed.length > 0 ? 1 : 0;
|
|
338
515
|
}
|
|
339
|
-
async function check(
|
|
340
|
-
const { context
|
|
516
|
+
async function check(dependencies, bufferedRunner = defaultBufferedRunner) {
|
|
517
|
+
const { context, fileSystem, settings } = dependencies;
|
|
341
518
|
if (!settings.checks || settings.checks.length === 0) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
519
|
+
context.stderr("Error: No checks configured in .dust/config/settings.json");
|
|
520
|
+
context.stderr("");
|
|
521
|
+
context.stderr("Add checks to your settings.json:");
|
|
522
|
+
context.stderr(" {");
|
|
523
|
+
context.stderr(' "checks": [');
|
|
524
|
+
context.stderr(' { "name": "lint", "command": "npm run lint" },');
|
|
525
|
+
context.stderr(' { "name": "test", "command": "npm test" }');
|
|
526
|
+
context.stderr(" ]");
|
|
527
|
+
context.stderr(" }");
|
|
351
528
|
return { exitCode: 1 };
|
|
352
529
|
}
|
|
353
530
|
const checkPromises = [];
|
|
354
|
-
const dustPath = `${
|
|
355
|
-
if (
|
|
356
|
-
checkPromises.push(runValidationCheck(
|
|
531
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
532
|
+
if (fileSystem.exists(dustPath)) {
|
|
533
|
+
checkPromises.push(runValidationCheck(dependencies));
|
|
357
534
|
}
|
|
358
|
-
checkPromises.push(runConfiguredChecks(settings.checks,
|
|
535
|
+
checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, bufferedRunner));
|
|
359
536
|
const promiseResults = await Promise.all(checkPromises);
|
|
360
537
|
const results = [];
|
|
361
538
|
for (const result of promiseResults) {
|
|
@@ -365,7 +542,7 @@ async function check(deps, bufferedRunner = defaultBufferedRunner) {
|
|
|
365
542
|
results.push(result);
|
|
366
543
|
}
|
|
367
544
|
}
|
|
368
|
-
const exitCode = displayResults(results,
|
|
545
|
+
const exitCode = displayResults(results, context);
|
|
369
546
|
return { exitCode };
|
|
370
547
|
}
|
|
371
548
|
|
|
@@ -373,25 +550,40 @@ async function check(deps, bufferedRunner = defaultBufferedRunner) {
|
|
|
373
550
|
function generateHelpText(settings) {
|
|
374
551
|
return loadTemplate("help", { bin: settings.dustCommand });
|
|
375
552
|
}
|
|
376
|
-
async function help(
|
|
377
|
-
|
|
553
|
+
async function help(dependencies) {
|
|
554
|
+
dependencies.context.stdout(generateHelpText(dependencies.settings));
|
|
378
555
|
return { exitCode: 0 };
|
|
379
556
|
}
|
|
380
557
|
|
|
381
558
|
// lib/cli/settings.ts
|
|
382
|
-
import { join as
|
|
559
|
+
import { join as join3 } from "node:path";
|
|
383
560
|
var DEFAULT_SETTINGS = {
|
|
384
561
|
dustCommand: "npx dust",
|
|
385
562
|
installDependenciesHint: "Install any dependencies"
|
|
386
563
|
};
|
|
387
|
-
function
|
|
388
|
-
if (
|
|
564
|
+
function detectInstallDependenciesHint(cwd, fileSystem) {
|
|
565
|
+
if (fileSystem.exists(join3(cwd, "bun.lockb")) || fileSystem.exists(join3(cwd, "bun.lock"))) {
|
|
566
|
+
return "Run `bun install`";
|
|
567
|
+
}
|
|
568
|
+
if (fileSystem.exists(join3(cwd, "pnpm-lock.yaml"))) {
|
|
569
|
+
return "Run `pnpm install`";
|
|
570
|
+
}
|
|
571
|
+
if (fileSystem.exists(join3(cwd, "package-lock.json"))) {
|
|
572
|
+
return "Run `npm install`";
|
|
573
|
+
}
|
|
574
|
+
if (fileSystem.exists(join3(cwd, "yarn.lock"))) {
|
|
575
|
+
return "Run `yarn install`";
|
|
576
|
+
}
|
|
577
|
+
return "Install any dependencies";
|
|
578
|
+
}
|
|
579
|
+
function detectDustCommand(cwd, fileSystem) {
|
|
580
|
+
if (fileSystem.exists(join3(cwd, "bun.lockb"))) {
|
|
389
581
|
return "bunx dust";
|
|
390
582
|
}
|
|
391
|
-
if (
|
|
583
|
+
if (fileSystem.exists(join3(cwd, "pnpm-lock.yaml"))) {
|
|
392
584
|
return "pnpx dust";
|
|
393
585
|
}
|
|
394
|
-
if (
|
|
586
|
+
if (fileSystem.exists(join3(cwd, "package-lock.json"))) {
|
|
395
587
|
return "npx dust";
|
|
396
588
|
}
|
|
397
589
|
if (process.env.BUN_INSTALL) {
|
|
@@ -399,47 +591,49 @@ function detectDustCommand(cwd, fs) {
|
|
|
399
591
|
}
|
|
400
592
|
return "npx dust";
|
|
401
593
|
}
|
|
402
|
-
async function loadSettings(cwd,
|
|
403
|
-
const settingsPath =
|
|
404
|
-
if (!
|
|
594
|
+
async function loadSettings(cwd, fileSystem) {
|
|
595
|
+
const settingsPath = join3(cwd, ".dust", "config", "settings.json");
|
|
596
|
+
if (!fileSystem.exists(settingsPath)) {
|
|
405
597
|
return {
|
|
406
|
-
dustCommand: detectDustCommand(cwd,
|
|
598
|
+
dustCommand: detectDustCommand(cwd, fileSystem),
|
|
599
|
+
installDependenciesHint: detectInstallDependenciesHint(cwd, fileSystem)
|
|
407
600
|
};
|
|
408
601
|
}
|
|
409
602
|
try {
|
|
410
|
-
const content = await
|
|
603
|
+
const content = await fileSystem.readFile(settingsPath);
|
|
411
604
|
const parsed = JSON.parse(content);
|
|
412
|
-
|
|
413
|
-
return {
|
|
414
|
-
...DEFAULT_SETTINGS,
|
|
415
|
-
...parsed,
|
|
416
|
-
dustCommand: detectDustCommand(cwd, fs)
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
return {
|
|
605
|
+
const result = {
|
|
420
606
|
...DEFAULT_SETTINGS,
|
|
421
607
|
...parsed
|
|
422
608
|
};
|
|
609
|
+
if (!parsed.dustCommand) {
|
|
610
|
+
result.dustCommand = detectDustCommand(cwd, fileSystem);
|
|
611
|
+
}
|
|
612
|
+
if (!parsed.installDependenciesHint) {
|
|
613
|
+
result.installDependenciesHint = detectInstallDependenciesHint(cwd, fileSystem);
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
423
616
|
} catch {
|
|
424
617
|
return {
|
|
425
|
-
dustCommand: detectDustCommand(cwd,
|
|
618
|
+
dustCommand: detectDustCommand(cwd, fileSystem),
|
|
619
|
+
installDependenciesHint: detectInstallDependenciesHint(cwd, fileSystem)
|
|
426
620
|
};
|
|
427
621
|
}
|
|
428
622
|
}
|
|
429
623
|
|
|
430
624
|
// lib/cli/commands/init.ts
|
|
431
625
|
var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
432
|
-
function generateSettings(cwd,
|
|
433
|
-
const dustCommand = detectDustCommand(cwd,
|
|
626
|
+
function generateSettings(cwd, fileSystem) {
|
|
627
|
+
const dustCommand = detectDustCommand(cwd, fileSystem);
|
|
434
628
|
const checks = [];
|
|
435
629
|
let installDependenciesHint = "Install any dependencies";
|
|
436
|
-
if (
|
|
630
|
+
if (fileSystem.exists(`${cwd}/bun.lockb`)) {
|
|
437
631
|
checks.push({ name: "test", command: "bun test" });
|
|
438
632
|
installDependenciesHint = "Run `bun install`";
|
|
439
|
-
} else if (
|
|
633
|
+
} else if (fileSystem.exists(`${cwd}/pnpm-lock.yaml`)) {
|
|
440
634
|
checks.push({ name: "test", command: "pnpm test" });
|
|
441
635
|
installDependenciesHint = "Run `pnpm install`";
|
|
442
|
-
} else if (
|
|
636
|
+
} else if (fileSystem.exists(`${cwd}/package-lock.json`) || fileSystem.exists(`${cwd}/package.json`)) {
|
|
443
637
|
checks.push({ name: "test", command: "npm test" });
|
|
444
638
|
installDependenciesHint = "Run `npm install`";
|
|
445
639
|
}
|
|
@@ -449,99 +643,277 @@ var USE_DUST_FACT = `# Use dust for planning
|
|
|
449
643
|
|
|
450
644
|
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
451
645
|
`;
|
|
452
|
-
async function init(
|
|
453
|
-
const { context
|
|
454
|
-
const dustPath = `${
|
|
455
|
-
const dustCommand = detectDustCommand(
|
|
646
|
+
async function init(dependencies) {
|
|
647
|
+
const { context, fileSystem } = dependencies;
|
|
648
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
649
|
+
const dustCommand = detectDustCommand(context.cwd, fileSystem);
|
|
456
650
|
const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
|
|
457
|
-
if (
|
|
458
|
-
|
|
651
|
+
if (fileSystem.exists(dustPath)) {
|
|
652
|
+
context.stdout("Note: .dust directory already exists, skipping creation");
|
|
459
653
|
} else {
|
|
460
|
-
await
|
|
654
|
+
await fileSystem.mkdir(dustPath, { recursive: true });
|
|
461
655
|
for (const dir of DUST_DIRECTORIES) {
|
|
462
|
-
await
|
|
656
|
+
await fileSystem.mkdir(`${dustPath}/${dir}`, { recursive: true });
|
|
463
657
|
}
|
|
464
|
-
await
|
|
465
|
-
const settings = generateSettings(
|
|
466
|
-
await
|
|
658
|
+
await fileSystem.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT);
|
|
659
|
+
const settings = generateSettings(context.cwd, fileSystem);
|
|
660
|
+
await fileSystem.writeFile(`${dustPath}/config/settings.json`, `${JSON.stringify(settings, null, 2)}
|
|
467
661
|
`);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
const claudeMdPath = `${
|
|
474
|
-
if (
|
|
475
|
-
|
|
662
|
+
context.stdout("Initialized Dust repository in .dust/");
|
|
663
|
+
context.stdout(`Created directories: ${DUST_DIRECTORIES.join(", ")}`);
|
|
664
|
+
context.stdout("Created initial fact: .dust/facts/use-dust-for-planning.md");
|
|
665
|
+
context.stdout("Created settings: .dust/config/settings.json");
|
|
666
|
+
}
|
|
667
|
+
const claudeMdPath = `${context.cwd}/CLAUDE.md`;
|
|
668
|
+
if (fileSystem.exists(claudeMdPath)) {
|
|
669
|
+
context.stdout(`Warning: CLAUDE.md already exists. Consider adding: "${agentInstruction}"`);
|
|
476
670
|
} else {
|
|
477
671
|
const claudeContent = loadTemplate("claude-md", { dustCommand });
|
|
478
|
-
await
|
|
479
|
-
|
|
672
|
+
await fileSystem.writeFile(claudeMdPath, claudeContent);
|
|
673
|
+
context.stdout("Created CLAUDE.md with agent instructions");
|
|
480
674
|
}
|
|
481
|
-
const agentsMdPath = `${
|
|
482
|
-
if (
|
|
483
|
-
|
|
675
|
+
const agentsMdPath = `${context.cwd}/AGENTS.md`;
|
|
676
|
+
if (fileSystem.exists(agentsMdPath)) {
|
|
677
|
+
context.stdout(`Warning: AGENTS.md already exists. Consider adding: "${agentInstruction}"`);
|
|
484
678
|
} else {
|
|
485
679
|
const agentsContent = loadTemplate("agents-md", { dustCommand });
|
|
486
|
-
await
|
|
487
|
-
|
|
680
|
+
await fileSystem.writeFile(agentsMdPath, agentsContent);
|
|
681
|
+
context.stdout("Created AGENTS.md with agent instructions");
|
|
488
682
|
}
|
|
489
683
|
const runner = dustCommand.split(" ")[0];
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
684
|
+
context.stdout("");
|
|
685
|
+
context.stdout("Commit the changes if you are happy, then get planning!");
|
|
686
|
+
context.stdout("");
|
|
687
|
+
context.stdout("If this is a new repository, you can start adding ideas or tasks right away:");
|
|
688
|
+
context.stdout(`> ${runner} claude "Idea: friendly UI for non-technical users"`);
|
|
689
|
+
context.stdout(`> ${runner} codex "Task: set up code coverage"`);
|
|
690
|
+
context.stdout("");
|
|
691
|
+
context.stdout("If this is an existing codebase, you might want to backfill goals and facts:");
|
|
692
|
+
context.stdout(`> ${runner} claude "Add goals and facts based on the code in this repository"`);
|
|
499
693
|
return { exitCode: 0 };
|
|
500
694
|
}
|
|
501
695
|
|
|
502
696
|
// lib/cli/commands/list.ts
|
|
503
697
|
var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
|
|
504
|
-
async function list(
|
|
505
|
-
const { arguments:
|
|
506
|
-
const dustPath = `${
|
|
507
|
-
if (!
|
|
508
|
-
|
|
509
|
-
|
|
698
|
+
async function list(dependencies) {
|
|
699
|
+
const { arguments: commandArguments, context, fileSystem } = dependencies;
|
|
700
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
701
|
+
if (!fileSystem.exists(dustPath)) {
|
|
702
|
+
context.stderr("Error: .dust directory not found");
|
|
703
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
510
704
|
return { exitCode: 1 };
|
|
511
705
|
}
|
|
512
|
-
const typesToList =
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
706
|
+
const typesToList = commandArguments.length === 0 ? [...VALID_TYPES] : commandArguments.filter((a) => VALID_TYPES.includes(a));
|
|
707
|
+
if (commandArguments.length > 0 && typesToList.length === 0) {
|
|
708
|
+
context.stderr(`Invalid type: ${commandArguments[0]}`);
|
|
709
|
+
context.stderr(`Valid types: ${VALID_TYPES.join(", ")}`);
|
|
516
710
|
return { exitCode: 1 };
|
|
517
711
|
}
|
|
518
712
|
for (const type of typesToList) {
|
|
519
713
|
const dirPath = `${dustPath}/${type}`;
|
|
520
|
-
if (!
|
|
714
|
+
if (!fileSystem.exists(dirPath)) {
|
|
521
715
|
continue;
|
|
522
716
|
}
|
|
523
|
-
const files = await
|
|
717
|
+
const files = await fileSystem.readdir(dirPath);
|
|
524
718
|
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
525
719
|
if (mdFiles.length === 0) {
|
|
526
720
|
continue;
|
|
527
721
|
}
|
|
528
|
-
|
|
722
|
+
context.stdout(`${type}:`);
|
|
529
723
|
for (const file of mdFiles) {
|
|
530
724
|
const filePath = `${dirPath}/${file}`;
|
|
531
|
-
const content = await
|
|
725
|
+
const content = await fileSystem.readFile(filePath);
|
|
532
726
|
const title = extractTitle(content);
|
|
533
727
|
const name = file.replace(/\.md$/, "");
|
|
534
728
|
if (title) {
|
|
535
|
-
|
|
729
|
+
context.stdout(` ${name} - ${title}`);
|
|
536
730
|
} else {
|
|
537
|
-
|
|
731
|
+
context.stdout(` ${name}`);
|
|
538
732
|
}
|
|
539
733
|
}
|
|
540
|
-
|
|
734
|
+
context.stdout("");
|
|
541
735
|
}
|
|
542
736
|
return { exitCode: 0 };
|
|
543
737
|
}
|
|
544
738
|
|
|
739
|
+
// lib/cli/commands/loop.ts
|
|
740
|
+
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
741
|
+
|
|
742
|
+
// lib/claude/spawn-claude-code.ts
|
|
743
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
744
|
+
import { createInterface as nodeCreateInterface } from "node:readline";
|
|
745
|
+
var defaultDependencies = {
|
|
746
|
+
spawn: nodeSpawn,
|
|
747
|
+
createInterface: nodeCreateInterface
|
|
748
|
+
};
|
|
749
|
+
async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDependencies) {
|
|
750
|
+
const {
|
|
751
|
+
cwd,
|
|
752
|
+
allowedTools,
|
|
753
|
+
maxTurns,
|
|
754
|
+
model,
|
|
755
|
+
systemPrompt,
|
|
756
|
+
sessionId,
|
|
757
|
+
dangerouslySkipPermissions
|
|
758
|
+
} = options;
|
|
759
|
+
const claudeArguments = [
|
|
760
|
+
"-p",
|
|
761
|
+
prompt,
|
|
762
|
+
"--output-format",
|
|
763
|
+
"stream-json",
|
|
764
|
+
"--verbose",
|
|
765
|
+
"--include-partial-messages"
|
|
766
|
+
];
|
|
767
|
+
if (allowedTools?.length) {
|
|
768
|
+
claudeArguments.push("--allowedTools", ...allowedTools);
|
|
769
|
+
}
|
|
770
|
+
if (maxTurns) {
|
|
771
|
+
claudeArguments.push("--max-turns", String(maxTurns));
|
|
772
|
+
}
|
|
773
|
+
if (model) {
|
|
774
|
+
claudeArguments.push("--model", model);
|
|
775
|
+
}
|
|
776
|
+
if (systemPrompt) {
|
|
777
|
+
claudeArguments.push("--system-prompt", systemPrompt);
|
|
778
|
+
}
|
|
779
|
+
if (sessionId) {
|
|
780
|
+
claudeArguments.push("--session-id", sessionId);
|
|
781
|
+
}
|
|
782
|
+
if (dangerouslySkipPermissions) {
|
|
783
|
+
claudeArguments.push("--dangerously-skip-permissions");
|
|
784
|
+
}
|
|
785
|
+
const proc = dependencies.spawn("claude", claudeArguments, {
|
|
786
|
+
cwd,
|
|
787
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
788
|
+
});
|
|
789
|
+
if (!proc.stdout) {
|
|
790
|
+
throw new Error("Failed to get stdout from claude process");
|
|
791
|
+
}
|
|
792
|
+
const rl = dependencies.createInterface({ input: proc.stdout });
|
|
793
|
+
for await (const line of rl) {
|
|
794
|
+
if (!line.trim())
|
|
795
|
+
continue;
|
|
796
|
+
try {
|
|
797
|
+
yield JSON.parse(line);
|
|
798
|
+
} catch {}
|
|
799
|
+
}
|
|
800
|
+
await new Promise((resolve2, reject) => {
|
|
801
|
+
proc.on("close", (code) => {
|
|
802
|
+
if (code === 0 || code === null)
|
|
803
|
+
resolve2();
|
|
804
|
+
else
|
|
805
|
+
reject(new Error(`claude exited with code ${code}`));
|
|
806
|
+
});
|
|
807
|
+
proc.on("error", reject);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// lib/claude/event-parser.ts
|
|
812
|
+
function* parseRawEvent(raw) {
|
|
813
|
+
if (raw.type === "stream_event") {
|
|
814
|
+
const event = raw.event;
|
|
815
|
+
if (event?.delta?.type === "text_delta" && event.delta.text) {
|
|
816
|
+
yield { type: "text_delta", text: event.delta.text };
|
|
817
|
+
}
|
|
818
|
+
} else if (raw.type === "assistant") {
|
|
819
|
+
const msg = raw;
|
|
820
|
+
const content = msg.message?.content ?? [];
|
|
821
|
+
yield { type: "assistant_message", content };
|
|
822
|
+
for (const block of content) {
|
|
823
|
+
if (block.type === "tool_use" && block.id && block.name && block.input) {
|
|
824
|
+
yield {
|
|
825
|
+
type: "tool_use",
|
|
826
|
+
id: block.id,
|
|
827
|
+
name: block.name,
|
|
828
|
+
input: block.input
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
} else if (raw.type === "user") {
|
|
833
|
+
const msg = raw;
|
|
834
|
+
for (const block of msg.message?.content ?? []) {
|
|
835
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
836
|
+
yield {
|
|
837
|
+
type: "tool_result",
|
|
838
|
+
toolUseId: block.tool_use_id,
|
|
839
|
+
content: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
} else if (raw.type === "result") {
|
|
844
|
+
const r = raw;
|
|
845
|
+
yield {
|
|
846
|
+
type: "result",
|
|
847
|
+
subtype: r.subtype ?? "success",
|
|
848
|
+
result: r.result,
|
|
849
|
+
error: r.error,
|
|
850
|
+
cost_usd: r.total_cost_usd ?? r.cost_usd ?? 0,
|
|
851
|
+
duration_ms: r.duration_ms ?? 0,
|
|
852
|
+
num_turns: r.num_turns ?? 0,
|
|
853
|
+
session_id: r.session_id ?? ""
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// lib/claude/streamer.ts
|
|
859
|
+
async function streamEvents(events, sink) {
|
|
860
|
+
let hadTextOutput = false;
|
|
861
|
+
for await (const raw of events) {
|
|
862
|
+
for (const event of parseRawEvent(raw)) {
|
|
863
|
+
processEvent(event, sink, { hadTextOutput });
|
|
864
|
+
if (event.type === "text_delta") {
|
|
865
|
+
hadTextOutput = true;
|
|
866
|
+
} else if (event.type === "tool_use") {
|
|
867
|
+
hadTextOutput = false;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function processEvent(event, sink, state) {
|
|
873
|
+
switch (event.type) {
|
|
874
|
+
case "text_delta":
|
|
875
|
+
sink.write(event.text);
|
|
876
|
+
break;
|
|
877
|
+
case "tool_use":
|
|
878
|
+
if (state.hadTextOutput) {
|
|
879
|
+
sink.line("");
|
|
880
|
+
sink.line("");
|
|
881
|
+
}
|
|
882
|
+
sink.line(`\uD83D\uDD27 Tool: ${event.name}`);
|
|
883
|
+
sink.line(` Input: ${JSON.stringify(event.input, null, 2).replace(/\n/g, `
|
|
884
|
+
`)}`);
|
|
885
|
+
break;
|
|
886
|
+
case "tool_result":
|
|
887
|
+
sink.line(`✅ Result (${event.content.length} chars)`);
|
|
888
|
+
sink.line("");
|
|
889
|
+
break;
|
|
890
|
+
case "result":
|
|
891
|
+
sink.line("");
|
|
892
|
+
sink.line(`\uD83C\uDFC1 Done: ${event.subtype}, ${event.num_turns} turns, $${event.cost_usd.toFixed(4)}`);
|
|
893
|
+
break;
|
|
894
|
+
case "assistant_message":
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function createStdoutSink() {
|
|
899
|
+
return {
|
|
900
|
+
write: (text) => process.stdout.write(text),
|
|
901
|
+
line: (text) => console.log(text)
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// lib/claude/run.ts
|
|
906
|
+
var defaultRunnerDependencies = {
|
|
907
|
+
spawnClaudeCode,
|
|
908
|
+
createStdoutSink,
|
|
909
|
+
streamEvents
|
|
910
|
+
};
|
|
911
|
+
async function run(prompt, options = {}, dependencies = defaultRunnerDependencies) {
|
|
912
|
+
const events = dependencies.spawnClaudeCode(prompt, options);
|
|
913
|
+
const sink = dependencies.createStdoutSink();
|
|
914
|
+
await dependencies.streamEvents(events, sink);
|
|
915
|
+
}
|
|
916
|
+
|
|
545
917
|
// lib/cli/commands/next.ts
|
|
546
918
|
function extractBlockedBy(content) {
|
|
547
919
|
const blockedByMatch = content.match(/^## Blocked by\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
|
|
@@ -561,19 +933,19 @@ function extractBlockedBy(content) {
|
|
|
561
933
|
}
|
|
562
934
|
return blockers;
|
|
563
935
|
}
|
|
564
|
-
async function next(
|
|
565
|
-
const { context
|
|
566
|
-
const dustPath = `${
|
|
567
|
-
if (!
|
|
568
|
-
|
|
569
|
-
|
|
936
|
+
async function next(dependencies) {
|
|
937
|
+
const { context, fileSystem } = dependencies;
|
|
938
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
939
|
+
if (!fileSystem.exists(dustPath)) {
|
|
940
|
+
context.stderr("Error: .dust directory not found");
|
|
941
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
570
942
|
return { exitCode: 1 };
|
|
571
943
|
}
|
|
572
944
|
const tasksPath = `${dustPath}/tasks`;
|
|
573
|
-
if (!
|
|
945
|
+
if (!fileSystem.exists(tasksPath)) {
|
|
574
946
|
return { exitCode: 0 };
|
|
575
947
|
}
|
|
576
|
-
const files = await
|
|
948
|
+
const files = await fileSystem.readdir(tasksPath);
|
|
577
949
|
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
578
950
|
if (mdFiles.length === 0) {
|
|
579
951
|
return { exitCode: 0 };
|
|
@@ -582,7 +954,7 @@ async function next(deps) {
|
|
|
582
954
|
const unblockedTasks = [];
|
|
583
955
|
for (const file of mdFiles) {
|
|
584
956
|
const filePath = `${tasksPath}/${file}`;
|
|
585
|
-
const content = await
|
|
957
|
+
const content = await fileSystem.readFile(filePath);
|
|
586
958
|
const blockers = extractBlockedBy(content);
|
|
587
959
|
const hasIncompleteBlocker = blockers.some((blocker) => existingTasks.has(blocker));
|
|
588
960
|
if (!hasIncompleteBlocker) {
|
|
@@ -594,17 +966,111 @@ async function next(deps) {
|
|
|
594
966
|
if (unblockedTasks.length === 0) {
|
|
595
967
|
return { exitCode: 0 };
|
|
596
968
|
}
|
|
597
|
-
|
|
969
|
+
context.stdout("Next tasks:");
|
|
598
970
|
for (const task of unblockedTasks) {
|
|
599
971
|
if (task.title) {
|
|
600
|
-
|
|
972
|
+
context.stdout(` ${task.path} - ${task.title}`);
|
|
601
973
|
} else {
|
|
602
|
-
|
|
974
|
+
context.stdout(` ${task.path}`);
|
|
603
975
|
}
|
|
604
976
|
}
|
|
605
977
|
return { exitCode: 0 };
|
|
606
978
|
}
|
|
607
979
|
|
|
980
|
+
// lib/cli/commands/loop.ts
|
|
981
|
+
function createDefaultDependencies() {
|
|
982
|
+
return {
|
|
983
|
+
spawn: nodeSpawn2,
|
|
984
|
+
run,
|
|
985
|
+
sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
var SLEEP_INTERVAL_MS = 30000;
|
|
989
|
+
async function gitPull(cwd, spawn2) {
|
|
990
|
+
return new Promise((resolve2) => {
|
|
991
|
+
const proc = spawn2("git", ["pull"], {
|
|
992
|
+
cwd,
|
|
993
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
994
|
+
});
|
|
995
|
+
let stderr = "";
|
|
996
|
+
proc.stderr?.on("data", (data) => {
|
|
997
|
+
stderr += data.toString();
|
|
998
|
+
});
|
|
999
|
+
proc.on("close", (code) => {
|
|
1000
|
+
if (code === 0) {
|
|
1001
|
+
resolve2({ success: true });
|
|
1002
|
+
} else {
|
|
1003
|
+
resolve2({ success: false, message: stderr.trim() || "git pull failed" });
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
proc.on("error", (error) => {
|
|
1007
|
+
resolve2({ success: false, message: error.message });
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
async function hasAvailableTasks(dependencies) {
|
|
1012
|
+
let hasOutput = false;
|
|
1013
|
+
const captureContext = {
|
|
1014
|
+
...dependencies.context,
|
|
1015
|
+
stdout: () => {
|
|
1016
|
+
hasOutput = true;
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
await next({ ...dependencies, context: captureContext });
|
|
1020
|
+
return hasOutput;
|
|
1021
|
+
}
|
|
1022
|
+
async function runOneIteration(dependencies, loopDependencies) {
|
|
1023
|
+
const { context } = dependencies;
|
|
1024
|
+
const { spawn: spawn2, run: run2 } = loopDependencies;
|
|
1025
|
+
context.stdout("Syncing with remote...");
|
|
1026
|
+
const pullResult = await gitPull(context.cwd, spawn2);
|
|
1027
|
+
if (!pullResult.success) {
|
|
1028
|
+
context.stdout(`Note: git pull skipped (${pullResult.message})`);
|
|
1029
|
+
}
|
|
1030
|
+
context.stdout("Checking for available tasks...");
|
|
1031
|
+
const hasTasks = await hasAvailableTasks(dependencies);
|
|
1032
|
+
if (!hasTasks) {
|
|
1033
|
+
context.stdout("No tasks available. Sleeping...");
|
|
1034
|
+
context.stdout("");
|
|
1035
|
+
return "no_tasks";
|
|
1036
|
+
}
|
|
1037
|
+
context.stdout("Found task(s). Starting Claude...");
|
|
1038
|
+
context.stdout("");
|
|
1039
|
+
try {
|
|
1040
|
+
await run2("go", { cwd: context.cwd, dangerouslySkipPermissions: true });
|
|
1041
|
+
context.stdout("");
|
|
1042
|
+
context.stdout("Claude session complete. Continuing loop...");
|
|
1043
|
+
context.stdout("");
|
|
1044
|
+
return "ran_claude";
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1047
|
+
context.stderr(`Claude exited with error: ${message}`);
|
|
1048
|
+
context.stdout("");
|
|
1049
|
+
context.stdout("Claude session complete. Continuing loop...");
|
|
1050
|
+
context.stdout("");
|
|
1051
|
+
return "claude_error";
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
async function loop(dependencies, loopDependencies = createDefaultDependencies()) {
|
|
1055
|
+
const { context } = dependencies;
|
|
1056
|
+
context.stdout("WARNING: This command skips all permission checks. Only use in a sandbox environment!");
|
|
1057
|
+
context.stdout("");
|
|
1058
|
+
context.stdout("Starting dust loop...");
|
|
1059
|
+
context.stdout("Press Ctrl+C to stop");
|
|
1060
|
+
context.stdout("");
|
|
1061
|
+
while (true) {
|
|
1062
|
+
const result = await runOneIteration(dependencies, loopDependencies);
|
|
1063
|
+
if (result === "no_tasks") {
|
|
1064
|
+
await loopDependencies.sleep(SLEEP_INTERVAL_MS);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// lib/cli/commands/pre-push.ts
|
|
1070
|
+
async function prePush(dependencies) {
|
|
1071
|
+
return check(dependencies);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
608
1074
|
// lib/cli/main.ts
|
|
609
1075
|
var commandRegistry = {
|
|
610
1076
|
init,
|
|
@@ -613,9 +1079,18 @@ var commandRegistry = {
|
|
|
613
1079
|
next,
|
|
614
1080
|
check,
|
|
615
1081
|
agent,
|
|
1082
|
+
"agent-help": agentHelp,
|
|
1083
|
+
"agent-new-task": agentNewTask,
|
|
1084
|
+
"agent-new-goal": agentNewGoal,
|
|
1085
|
+
"agent-new-idea": agentNewIdea,
|
|
1086
|
+
"agent-implement-task": agentImplementTask,
|
|
1087
|
+
"agent-pick-task": agentPickTask,
|
|
1088
|
+
"agent-understand-goals": agentUnderstandGoals,
|
|
1089
|
+
loop,
|
|
1090
|
+
"pre-push": prePush,
|
|
616
1091
|
help
|
|
617
1092
|
};
|
|
618
|
-
var COMMANDS = Object.keys(commandRegistry);
|
|
1093
|
+
var COMMANDS = Object.keys(commandRegistry).filter((cmd) => !cmd.includes("-"));
|
|
619
1094
|
var HELP_TEXT = generateHelpText({ dustCommand: "dust" });
|
|
620
1095
|
function isHelpRequest(command) {
|
|
621
1096
|
return !command || command === "help" || command === "--help" || command === "-h";
|
|
@@ -623,60 +1098,80 @@ function isHelpRequest(command) {
|
|
|
623
1098
|
function isValidCommand(command) {
|
|
624
1099
|
return command in commandRegistry;
|
|
625
1100
|
}
|
|
626
|
-
async function runCommand(command,
|
|
627
|
-
return commandRegistry[command](
|
|
1101
|
+
async function runCommand(command, dependencies) {
|
|
1102
|
+
return commandRegistry[command](dependencies);
|
|
1103
|
+
}
|
|
1104
|
+
function resolveCommand(commandArguments) {
|
|
1105
|
+
for (let i = commandArguments.length;i > 0; i--) {
|
|
1106
|
+
const candidate = commandArguments.slice(0, i).join("-");
|
|
1107
|
+
if (candidate in commandRegistry) {
|
|
1108
|
+
return { command: candidate, remaining: commandArguments.slice(i) };
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return { command: null, remaining: commandArguments };
|
|
628
1112
|
}
|
|
629
1113
|
async function main(options) {
|
|
630
|
-
const {
|
|
631
|
-
const
|
|
632
|
-
const commandArgs = args.slice(1);
|
|
633
|
-
const settings = await loadSettings(ctx.cwd, fs);
|
|
1114
|
+
const { commandArguments, context, fileSystem, glob } = options;
|
|
1115
|
+
const settings = await loadSettings(context.cwd, fileSystem);
|
|
634
1116
|
const helpText = generateHelpText(settings);
|
|
635
|
-
if (isHelpRequest(
|
|
636
|
-
|
|
1117
|
+
if (isHelpRequest(commandArguments[0])) {
|
|
1118
|
+
context.stdout(helpText);
|
|
637
1119
|
return { exitCode: 0 };
|
|
638
1120
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1121
|
+
const { command, remaining } = resolveCommand(commandArguments);
|
|
1122
|
+
if (!command || !isValidCommand(command)) {
|
|
1123
|
+
context.stderr(`Unknown command: ${commandArguments.join(" ")}`);
|
|
1124
|
+
context.stderr(`Run '${settings.dustCommand} help' for available commands`);
|
|
642
1125
|
return { exitCode: 1 };
|
|
643
1126
|
}
|
|
644
|
-
const
|
|
645
|
-
arguments:
|
|
646
|
-
context
|
|
647
|
-
fileSystem
|
|
1127
|
+
const dependencies = {
|
|
1128
|
+
arguments: remaining,
|
|
1129
|
+
context,
|
|
1130
|
+
fileSystem,
|
|
648
1131
|
globScanner: glob,
|
|
649
1132
|
settings
|
|
650
1133
|
};
|
|
651
|
-
return runCommand(command,
|
|
1134
|
+
return runCommand(command, dependencies);
|
|
652
1135
|
}
|
|
653
1136
|
|
|
654
|
-
// lib/cli/entry.ts
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1137
|
+
// lib/cli/entry-wiring.ts
|
|
1138
|
+
function createFileSystem(primitives) {
|
|
1139
|
+
return {
|
|
1140
|
+
exists: primitives.existsSync,
|
|
1141
|
+
readFile: (path) => primitives.readFile(path, "utf-8"),
|
|
1142
|
+
writeFile: (path, content) => primitives.writeFile(path, content, "utf-8"),
|
|
1143
|
+
mkdir: async (path, options) => {
|
|
1144
|
+
await primitives.mkdir(path, options);
|
|
1145
|
+
},
|
|
1146
|
+
readdir: (path) => primitives.readdir(path),
|
|
1147
|
+
chmod: (path, mode) => primitives.chmod(path, mode)
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function createGlobScanner(readdir) {
|
|
1151
|
+
return {
|
|
1152
|
+
scan: async function* (dir) {
|
|
1153
|
+
for (const entry of await readdir(dir, { recursive: true })) {
|
|
1154
|
+
if (entry.endsWith(".md"))
|
|
1155
|
+
yield entry;
|
|
1156
|
+
}
|
|
669
1157
|
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
|
|
1161
|
+
const fileSystem = createFileSystem(fsPrimitives);
|
|
1162
|
+
const glob = createGlobScanner(fsPrimitives.readdir);
|
|
1163
|
+
const result = await main({
|
|
1164
|
+
commandArguments: processPrimitives.argv.slice(2),
|
|
1165
|
+
context: {
|
|
1166
|
+
cwd: processPrimitives.cwd(),
|
|
1167
|
+
stdout: consolePrimitives.log,
|
|
1168
|
+
stderr: consolePrimitives.error
|
|
1169
|
+
},
|
|
1170
|
+
fileSystem,
|
|
1171
|
+
glob
|
|
1172
|
+
});
|
|
1173
|
+
processPrimitives.exit(result.exitCode);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// lib/cli/entry.ts
|
|
1177
|
+
await wireEntry({ existsSync, readFile, writeFile, mkdir, readdir, chmod }, { argv: process.argv, cwd: () => process.cwd(), exit: process.exit }, { log: console.log, error: console.error });
|