@joshski/dust 0.1.57 → 0.1.58

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 CHANGED
@@ -1657,39 +1657,68 @@ class FileSink {
1657
1657
 
1658
1658
  // lib/logging/index.ts
1659
1659
  var DUST_LOG_FILE = "DUST_LOG_FILE";
1660
- var patterns = null;
1661
- var initialized = false;
1662
- var activeFileSink = null;
1663
- var ownedDustLogFile = false;
1664
- function init() {
1665
- if (initialized)
1666
- return;
1667
- initialized = true;
1668
- const parsed = parsePatterns(process.env.DEBUG);
1669
- patterns = parsed.length > 0 ? parsed : null;
1670
- }
1671
- function enableFileLogs(scope, _sinkForTesting) {
1672
- const existing = process.env[DUST_LOG_FILE];
1673
- const logDir = process.env.DUST_LOG_DIR ?? join5(process.cwd(), "log");
1674
- const path = existing ?? join5(logDir, `${scope}.log`);
1675
- if (!existing) {
1676
- process.env[DUST_LOG_FILE] = path;
1677
- ownedDustLogFile = true;
1678
- }
1679
- activeFileSink = _sinkForTesting ?? new FileSink(path);
1680
- }
1681
- function createLogger(name) {
1682
- return (...messages) => {
1683
- init();
1684
- const line = formatLine(name, messages);
1685
- if (activeFileSink) {
1686
- activeFileSink.write(line);
1687
- }
1688
- if (patterns && matchesAny(name, patterns)) {
1689
- process.stdout.write(line);
1660
+ function createLoggingService() {
1661
+ let patterns = null;
1662
+ let initialized = false;
1663
+ let activeFileSink = null;
1664
+ const fileSinkCache = new Map;
1665
+ function init() {
1666
+ if (initialized)
1667
+ return;
1668
+ initialized = true;
1669
+ const parsed = parsePatterns(process.env.DEBUG);
1670
+ patterns = parsed.length > 0 ? parsed : null;
1671
+ }
1672
+ function getOrCreateFileSink(path) {
1673
+ let sink = fileSinkCache.get(path);
1674
+ if (!sink) {
1675
+ sink = new FileSink(path);
1676
+ fileSinkCache.set(path, sink);
1677
+ }
1678
+ return sink;
1679
+ }
1680
+ return {
1681
+ enableFileLogs(scope, sinkForTesting) {
1682
+ const existing = process.env[DUST_LOG_FILE];
1683
+ const logDir = process.env.DUST_LOG_DIR ?? join5(process.cwd(), "log");
1684
+ const path = existing ?? join5(logDir, `${scope}.log`);
1685
+ if (!existing) {
1686
+ process.env[DUST_LOG_FILE] = path;
1687
+ }
1688
+ activeFileSink = sinkForTesting ?? new FileSink(path);
1689
+ },
1690
+ createLogger(name, options) {
1691
+ let perLoggerSink;
1692
+ if (options?.file === false) {
1693
+ perLoggerSink = null;
1694
+ } else if (typeof options?.file === "string") {
1695
+ perLoggerSink = getOrCreateFileSink(options.file);
1696
+ }
1697
+ return (...messages) => {
1698
+ init();
1699
+ const line = formatLine(name, messages);
1700
+ if (perLoggerSink !== undefined) {
1701
+ if (perLoggerSink !== null) {
1702
+ perLoggerSink.write(line);
1703
+ }
1704
+ } else if (activeFileSink) {
1705
+ activeFileSink.write(line);
1706
+ }
1707
+ if (patterns && matchesAny(name, patterns)) {
1708
+ process.stdout.write(line);
1709
+ }
1710
+ };
1711
+ },
1712
+ isEnabled(name) {
1713
+ init();
1714
+ return patterns !== null && matchesAny(name, patterns);
1690
1715
  }
1691
1716
  };
1692
1717
  }
1718
+ var defaultService = createLoggingService();
1719
+ var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
1720
+ var createLogger = defaultService.createLogger.bind(defaultService);
1721
+ var isEnabled = defaultService.isEnabled.bind(defaultService);
1693
1722
 
1694
1723
  // lib/bucket/repository-git.ts
1695
1724
  import { join as join6 } from "node:path";
@@ -1993,7 +2022,7 @@ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgen
1993
2022
  postEvent(eventsUrl, payload).catch(onError);
1994
2023
  };
1995
2024
  }
1996
- var log = createLogger("dust.cli.commands.loop");
2025
+ var log = createLogger("dust:cli:commands:loop");
1997
2026
  var SLEEP_INTERVAL_MS = 30000;
1998
2027
  var SLEEP_STEP_MS = 1000;
1999
2028
  var DEFAULT_MAX_ITERATIONS = 10;
@@ -2224,7 +2253,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2224
2253
  }
2225
2254
 
2226
2255
  // lib/bucket/repository-loop.ts
2227
- var log2 = createLogger("dust.bucket.repository-loop");
2256
+ var log2 = createLogger("dust:bucket:repository-loop");
2228
2257
  var FALLBACK_TIMEOUT_MS = 300000;
2229
2258
  function createNoOpGlobScanner() {
2230
2259
  return {
@@ -2373,7 +2402,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2373
2402
  }
2374
2403
 
2375
2404
  // lib/bucket/repository.ts
2376
- var log3 = createLogger("dust.bucket.repository");
2405
+ var log3 = createLogger("dust:bucket:repository");
2377
2406
  function startRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2378
2407
  log3(`starting loop for ${repoState.repository.name}`);
2379
2408
  repoState.stopRequested = false;
@@ -2908,7 +2937,7 @@ function handleKeyInput(state, key, options) {
2908
2937
  }
2909
2938
 
2910
2939
  // lib/cli/commands/bucket.ts
2911
- var log4 = createLogger("dust.cli.commands.bucket");
2940
+ var log4 = createLogger("dust:cli:commands:bucket");
2912
2941
  var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
2913
2942
  var INITIAL_RECONNECT_DELAY_MS = 1000;
2914
2943
  var MAX_RECONNECT_DELAY_MS = 30000;
@@ -4185,7 +4214,7 @@ async function lintMarkdown(dependencies) {
4185
4214
  }
4186
4215
 
4187
4216
  // lib/cli/commands/check.ts
4188
- var log5 = createLogger("dust.cli.commands.check");
4217
+ var log5 = createLogger("dust:cli:commands:check");
4189
4218
  var DEFAULT_CHECK_TIMEOUT_MS = 13000;
4190
4219
  var MAX_OUTPUT_LINES = 500;
4191
4220
  var KEEP_LINES = 250;
@@ -4439,7 +4468,7 @@ function agentsMdContent(dustCommand) {
4439
4468
  Always immediately run \`${dustCommand} agent\` when you start working in this repository.
4440
4469
  `;
4441
4470
  }
4442
- async function init2(dependencies) {
4471
+ async function init(dependencies) {
4443
4472
  const { context, fileSystem } = dependencies;
4444
4473
  const colors = getColors();
4445
4474
  const dustPath = `${context.cwd}/.dust`;
@@ -5085,7 +5114,7 @@ async function facts(dependencies) {
5085
5114
 
5086
5115
  // lib/cli/main.ts
5087
5116
  var commandRegistry = {
5088
- init: init2,
5117
+ init,
5089
5118
  lint: lintMarkdown,
5090
5119
  list,
5091
5120
  tasks,
@@ -5109,7 +5138,6 @@ var commandRegistry = {
5109
5138
  help
5110
5139
  };
5111
5140
  var COMMANDS = Object.keys(commandRegistry).filter((cmd) => !cmd.includes(" "));
5112
- var HELP_TEXT = generateHelpText({ dustCommand: "dust" });
5113
5141
  function isHelpRequest(command) {
5114
5142
  return !command || command === "help" || command === "--help" || command === "-h";
5115
5143
  }
package/dist/ideas.js ADDED
@@ -0,0 +1,135 @@
1
+ // lib/markdown/markdown-utilities.ts
2
+ function extractTitle(content) {
3
+ const match = content.match(/^#\s+(.+)$/m);
4
+ return match ? match[1].trim() : null;
5
+ }
6
+ function extractOpeningSentence(content) {
7
+ const lines = content.split(`
8
+ `);
9
+ let h1Index = -1;
10
+ for (let i = 0;i < lines.length; i++) {
11
+ if (lines[i].match(/^#\s+.+$/)) {
12
+ h1Index = i;
13
+ break;
14
+ }
15
+ }
16
+ if (h1Index === -1) {
17
+ return null;
18
+ }
19
+ let paragraphStart = -1;
20
+ for (let i = h1Index + 1;i < lines.length; i++) {
21
+ const line = lines[i].trim();
22
+ if (line !== "") {
23
+ paragraphStart = i;
24
+ break;
25
+ }
26
+ }
27
+ if (paragraphStart === -1) {
28
+ return null;
29
+ }
30
+ const firstLine = lines[paragraphStart];
31
+ const trimmedFirstLine = firstLine.trim();
32
+ if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
33
+ return null;
34
+ }
35
+ let paragraph = "";
36
+ for (let i = paragraphStart;i < lines.length; i++) {
37
+ const line = lines[i].trim();
38
+ if (line === "")
39
+ break;
40
+ if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
41
+ break;
42
+ }
43
+ paragraph += (paragraph ? " " : "") + line;
44
+ }
45
+ const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
46
+ if (!sentenceMatch) {
47
+ return null;
48
+ }
49
+ return sentenceMatch[1];
50
+ }
51
+
52
+ // lib/ideas.ts
53
+ function parseOpenQuestions(content) {
54
+ const lines = content.split(`
55
+ `);
56
+ const questions = [];
57
+ let inOpenQuestions = false;
58
+ let currentQuestion = null;
59
+ let currentOption = null;
60
+ let descriptionLines = [];
61
+ function flushOption() {
62
+ if (currentOption) {
63
+ currentOption.description = descriptionLines.join(`
64
+ `).trim();
65
+ descriptionLines = [];
66
+ currentOption = null;
67
+ }
68
+ }
69
+ function flushQuestion() {
70
+ flushOption();
71
+ if (currentQuestion) {
72
+ questions.push(currentQuestion);
73
+ currentQuestion = null;
74
+ }
75
+ }
76
+ for (const line of lines) {
77
+ if (line.startsWith("## ")) {
78
+ if (inOpenQuestions) {
79
+ flushQuestion();
80
+ }
81
+ inOpenQuestions = line.trimEnd() === "## Open Questions";
82
+ continue;
83
+ }
84
+ if (!inOpenQuestions)
85
+ continue;
86
+ if (line.startsWith("### ")) {
87
+ flushQuestion();
88
+ currentQuestion = {
89
+ question: line.slice(4).trim(),
90
+ options: []
91
+ };
92
+ continue;
93
+ }
94
+ if (line.startsWith("#### ")) {
95
+ flushOption();
96
+ currentOption = {
97
+ name: line.slice(5).trim(),
98
+ description: ""
99
+ };
100
+ if (currentQuestion) {
101
+ currentQuestion.options.push(currentOption);
102
+ }
103
+ continue;
104
+ }
105
+ if (currentOption) {
106
+ descriptionLines.push(line);
107
+ }
108
+ }
109
+ flushQuestion();
110
+ return questions;
111
+ }
112
+ async function parseIdea(fileSystem, dustPath, slug) {
113
+ const ideaPath = `${dustPath}/ideas/${slug}.md`;
114
+ if (!fileSystem.exists(ideaPath)) {
115
+ throw new Error(`Idea not found: "${slug}" (expected file at ${ideaPath})`);
116
+ }
117
+ const content = await fileSystem.readFile(ideaPath);
118
+ const title = extractTitle(content);
119
+ if (!title) {
120
+ throw new Error(`Idea file has no title: ${ideaPath}`);
121
+ }
122
+ const openingSentence = extractOpeningSentence(content);
123
+ const openQuestions = parseOpenQuestions(content);
124
+ return {
125
+ slug,
126
+ title,
127
+ openingSentence,
128
+ content,
129
+ openQuestions
130
+ };
131
+ }
132
+ export {
133
+ parseOpenQuestions,
134
+ parseIdea
135
+ };
@@ -28,34 +28,53 @@
28
28
  * structure under `lib/`. For example, `lib/bucket/repository-loop.ts` uses
29
29
  * the logger name `dust.bucket.repository-loop`.
30
30
  *
31
+ * ## Per-logger file routing
32
+ *
33
+ * `createLogger(name, { file })` accepts an optional `file` override:
34
+ * - `file: "path/to/file.log"` — route this logger to a dedicated file instead
35
+ * of the global sink set by `enableFileLogs`. The path is resolved relative to
36
+ * the log directory.
37
+ * - `file: false` — suppress file output for this logger entirely, even if
38
+ * `enableFileLogs` has been called. Stdout behavior (DEBUG matching) is
39
+ * unchanged.
40
+ * - When `file` is omitted, the logger uses the global file sink (if any).
41
+ *
42
+ * Sink selection precedence: per-logger sink → global sink → no file sink.
43
+ * File sinks are cached by path so multiple loggers targeting the same file
44
+ * share one sink instance.
45
+ *
31
46
  * No external dependencies.
32
47
  */
33
48
  import { type LogSink } from './sink';
34
49
  export type LogFn = (...messages: unknown[]) => void;
50
+ export interface LoggerOptions {
51
+ /**
52
+ * Per-logger file routing override.
53
+ * - `string` — path to a dedicated log file (e.g. `"./log/custom.log"`)
54
+ * - `false` — suppress file output for this logger
55
+ * - `undefined` — use the global file sink from `enableFileLogs` (default)
56
+ */
57
+ file?: string | false;
58
+ }
59
+ export interface LoggingService {
60
+ enableFileLogs(scope: string, sinkForTesting?: LogSink): void;
61
+ createLogger(name: string, options?: LoggerOptions): LogFn;
62
+ isEnabled(name: string): boolean;
63
+ }
35
64
  /**
36
- * Activate file logging for this command. Determines the log path as follows:
37
- * - If DUST_LOG_FILE is already set (inherited from a parent process such as
38
- * `dust check`), use that path — all scopes land in the same file.
39
- * - Otherwise compute the path using DUST_LOG_DIR (if set) or `<cwd>/log`, set
40
- * DUST_LOG_FILE so that any child processes inherit the same destination, then write there.
41
- *
42
- * Pass a LogSink as the second argument to override for testing.
65
+ * Create an isolated logging service instance. All mutable state is
66
+ * encapsulated inside the returned object.
43
67
  */
44
- export declare function enableFileLogs(scope: string, _sinkForTesting?: LogSink): void;
68
+ export declare function createLoggingService(): LoggingService;
45
69
  /**
46
- * Create a named logger function. The returned function writes to:
47
- * - The active file sink (if `enableFileLogs` was called), always, no filtering.
48
- * - `process.stdout` if DEBUG is set and `name` matches the pattern.
49
- *
50
- * @param name - Logger name, e.g. `dust.bucket.loop`
70
+ * Activate file logging for this command. See {@link LoggingService.enableFileLogs}.
51
71
  */
52
- export declare function createLogger(name: string): LogFn;
72
+ export declare const enableFileLogs: LoggingService['enableFileLogs'];
53
73
  /**
54
- * Check whether a logger name would produce stdout output under the current DEBUG value.
74
+ * Create a named logger function. See {@link LoggingService.createLogger}.
55
75
  */
56
- export declare function isEnabled(name: string): boolean;
76
+ export declare const createLogger: LoggingService['createLogger'];
57
77
  /**
58
- * Reset internal state (for testing only).
59
- * Clears DUST_LOG_FILE only if this module set it (not if it was inherited).
78
+ * Check whether a logger name would produce stdout output under the current DEBUG value.
60
79
  */
61
- export declare function _reset(): void;
80
+ export declare const isEnabled: LoggingService['isEnabled'];
package/dist/logging.js CHANGED
@@ -60,55 +60,71 @@ class FileSink {
60
60
 
61
61
  // lib/logging/index.ts
62
62
  var DUST_LOG_FILE = "DUST_LOG_FILE";
63
- var patterns = null;
64
- var initialized = false;
65
- var activeFileSink = null;
66
- var ownedDustLogFile = false;
67
- function init() {
68
- if (initialized)
69
- return;
70
- initialized = true;
71
- const parsed = parsePatterns(process.env.DEBUG);
72
- patterns = parsed.length > 0 ? parsed : null;
73
- }
74
- function enableFileLogs(scope, _sinkForTesting) {
75
- const existing = process.env[DUST_LOG_FILE];
76
- const logDir = process.env.DUST_LOG_DIR ?? join(process.cwd(), "log");
77
- const path = existing ?? join(logDir, `${scope}.log`);
78
- if (!existing) {
79
- process.env[DUST_LOG_FILE] = path;
80
- ownedDustLogFile = true;
63
+ function createLoggingService() {
64
+ let patterns = null;
65
+ let initialized = false;
66
+ let activeFileSink = null;
67
+ const fileSinkCache = new Map;
68
+ function init() {
69
+ if (initialized)
70
+ return;
71
+ initialized = true;
72
+ const parsed = parsePatterns(process.env.DEBUG);
73
+ patterns = parsed.length > 0 ? parsed : null;
81
74
  }
82
- activeFileSink = _sinkForTesting ?? new FileSink(path);
83
- }
84
- function createLogger(name) {
85
- return (...messages) => {
86
- init();
87
- const line = formatLine(name, messages);
88
- if (activeFileSink) {
89
- activeFileSink.write(line);
75
+ function getOrCreateFileSink(path) {
76
+ let sink = fileSinkCache.get(path);
77
+ if (!sink) {
78
+ sink = new FileSink(path);
79
+ fileSinkCache.set(path, sink);
90
80
  }
91
- if (patterns && matchesAny(name, patterns)) {
92
- process.stdout.write(line);
81
+ return sink;
82
+ }
83
+ return {
84
+ enableFileLogs(scope, sinkForTesting) {
85
+ const existing = process.env[DUST_LOG_FILE];
86
+ const logDir = process.env.DUST_LOG_DIR ?? join(process.cwd(), "log");
87
+ const path = existing ?? join(logDir, `${scope}.log`);
88
+ if (!existing) {
89
+ process.env[DUST_LOG_FILE] = path;
90
+ }
91
+ activeFileSink = sinkForTesting ?? new FileSink(path);
92
+ },
93
+ createLogger(name, options) {
94
+ let perLoggerSink;
95
+ if (options?.file === false) {
96
+ perLoggerSink = null;
97
+ } else if (typeof options?.file === "string") {
98
+ perLoggerSink = getOrCreateFileSink(options.file);
99
+ }
100
+ return (...messages) => {
101
+ init();
102
+ const line = formatLine(name, messages);
103
+ if (perLoggerSink !== undefined) {
104
+ if (perLoggerSink !== null) {
105
+ perLoggerSink.write(line);
106
+ }
107
+ } else if (activeFileSink) {
108
+ activeFileSink.write(line);
109
+ }
110
+ if (patterns && matchesAny(name, patterns)) {
111
+ process.stdout.write(line);
112
+ }
113
+ };
114
+ },
115
+ isEnabled(name) {
116
+ init();
117
+ return patterns !== null && matchesAny(name, patterns);
93
118
  }
94
119
  };
95
120
  }
96
- function isEnabled(name) {
97
- init();
98
- return patterns !== null && matchesAny(name, patterns);
99
- }
100
- function _reset() {
101
- initialized = false;
102
- patterns = null;
103
- activeFileSink = null;
104
- if (ownedDustLogFile) {
105
- delete process.env[DUST_LOG_FILE];
106
- ownedDustLogFile = false;
107
- }
108
- }
121
+ var defaultService = createLoggingService();
122
+ var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
123
+ var createLogger = defaultService.createLogger.bind(defaultService);
124
+ var isEnabled = defaultService.isEnabled.bind(defaultService);
109
125
  export {
110
126
  isEnabled,
111
127
  enableFileLogs,
112
- createLogger,
113
- _reset
128
+ createLoggingService,
129
+ createLogger
114
130
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,10 @@
22
22
  "import": "./dist/agents.js",
23
23
  "types": "./dist/agents/detection.d.ts"
24
24
  },
25
+ "./ideas": {
26
+ "import": "./dist/ideas.js",
27
+ "types": "./dist/ideas.d.ts"
28
+ },
25
29
  "./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs"
26
30
  },
27
31
  "files": [
@@ -42,7 +46,7 @@
42
46
  "author": "joshski",
43
47
  "license": "MIT",
44
48
  "scripts": {
45
- "build": "bun build lib/cli/run.ts --target node --outfile dist/dust.js && printf '%s\\n%s' '#!/usr/bin/env node' \"$(cat dist/dust.js)\" > dist/dust.js && bun build lib/workflow-tasks.ts --target node --outfile dist/workflow-tasks.js && bun build lib/logging/index.ts --target node --outfile dist/logging.js && bun build lib/agents/detection.ts --target node --outfile dist/agents.js && bunx tsc --project tsconfig.build.json",
49
+ "build": "bun build lib/cli/run.ts --target node --outfile dist/dust.js && printf '%s\\n%s' '#!/usr/bin/env node' \"$(cat dist/dust.js)\" > dist/dust.js && bun build lib/workflow-tasks.ts --target node --outfile dist/workflow-tasks.js && bun build lib/logging/index.ts --target node --outfile dist/logging.js && bun build lib/agents/detection.ts --target node --outfile dist/agents.js && bun build lib/ideas.ts --target node --outfile dist/ideas.js && bunx tsc --project tsconfig.build.json",
46
50
  "test": "vitest run",
47
51
  "test:coverage": "vitest run --coverage",
48
52
  "eval": "bun run ./evals/run.ts"