@joshski/dust 0.1.90 → 0.1.92

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,48 @@
1
+ /**
2
+ * Tool execution protocol types for the WebSocket messages between
3
+ * dust clients and dustbucket servers.
4
+ *
5
+ * These types define the wire format. Both sides must agree on this protocol.
6
+ * Import from '@joshski/dust/types' for downstream implementations.
7
+ */
8
+ /**
9
+ * Client-to-server message requesting tool execution.
10
+ *
11
+ * Sent over the bucket WebSocket when an agent invokes `dust bucket tool <name>`.
12
+ * The `tool` field identifies the server-defined tool by name.
13
+ * The `arguments` field contains named parameters matching the tool definition.
14
+ * The `repositoryId` identifies the repository context (numeric server-side ID).
15
+ */
16
+ export interface ToolExecutionRequestMessage {
17
+ type: 'tool-execution-request';
18
+ requestId: string;
19
+ tool: string;
20
+ repositoryId: number;
21
+ arguments: Record<string, unknown>;
22
+ }
23
+ export interface ToolExecutionSuccessResult {
24
+ type: 'success';
25
+ data: unknown;
26
+ }
27
+ export interface ToolExecutionToolNotFoundResult {
28
+ type: 'tool-not-found';
29
+ message: string;
30
+ }
31
+ export interface ToolExecutionErrorResult {
32
+ type: 'error';
33
+ message: string;
34
+ }
35
+ export type ToolExecutionResult = ToolExecutionSuccessResult | ToolExecutionToolNotFoundResult | ToolExecutionErrorResult;
36
+ /**
37
+ * Server-to-client message with the result of a tool execution request.
38
+ *
39
+ * The `requestId` correlates this response to the original request.
40
+ * The `result` is a discriminated union on the `type` field.
41
+ */
42
+ export interface ToolExecutionResultMessage {
43
+ type: 'tool-execution-result';
44
+ requestId: string;
45
+ result: ToolExecutionResult;
46
+ }
47
+ export declare function isToolExecutionResultMessage(payload: unknown): payload is ToolExecutionResultMessage;
48
+ export declare function isToolExecutionRequestMessage(payload: unknown): payload is ToolExecutionRequestMessage;
package/dist/dust.js CHANGED
@@ -500,7 +500,7 @@ async function loadSettings(cwd, fileSystem) {
500
500
  }
501
501
 
502
502
  // lib/version.ts
503
- var DUST_VERSION = "0.1.90";
503
+ var DUST_VERSION = "0.1.92";
504
504
 
505
505
  // lib/session.ts
506
506
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -2072,6 +2072,7 @@ import { spawn as nodeSpawn5 } from "node:child_process";
2072
2072
  import { accessSync, statSync } from "node:fs";
2073
2073
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2074
2074
  import { homedir as homedir2 } from "node:os";
2075
+ import { basename as basename2, resolve } from "node:path";
2075
2076
 
2076
2077
  // lib/bucket/auth.ts
2077
2078
  import { join as join5 } from "node:path";
@@ -4570,8 +4571,11 @@ function formatToolsSection(tools) {
4570
4571
  return "";
4571
4572
  }
4572
4573
  const lines = [];
4574
+ lines.push("");
4573
4575
  lines.push("## Available Tools");
4574
4576
  lines.push("");
4577
+ lines.push("Use these tools where it makes sense in the execution of this task:");
4578
+ lines.push("");
4575
4579
  for (const tool of tools) {
4576
4580
  lines.push(formatTool(tool));
4577
4581
  lines.push("");
@@ -5440,6 +5444,18 @@ var SGR_MOUSE_RE2 = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
5440
5444
  function isRecord3(value) {
5441
5445
  return typeof value === "object" && value !== null;
5442
5446
  }
5447
+ function isValidResultType(value) {
5448
+ return value === "success" || value === "tool-not-found" || value === "error";
5449
+ }
5450
+ function isValidResult(value) {
5451
+ if (!isRecord3(value))
5452
+ return false;
5453
+ if (!isValidResultType(value.type))
5454
+ return false;
5455
+ if (value.type === "success")
5456
+ return true;
5457
+ return typeof value.message === "string";
5458
+ }
5443
5459
  function isToolExecutionResultMessage(payload) {
5444
5460
  if (!isRecord3(payload))
5445
5461
  return false;
@@ -5448,16 +5464,7 @@ function isToolExecutionResultMessage(payload) {
5448
5464
  if (typeof payload.requestId !== "string" || payload.requestId.length === 0) {
5449
5465
  return false;
5450
5466
  }
5451
- if (payload.status !== "success" && payload.status !== "tool-not-found" && payload.status !== "error") {
5452
- return false;
5453
- }
5454
- if (payload.output !== undefined && typeof payload.output !== "string") {
5455
- return false;
5456
- }
5457
- if (payload.error !== undefined && typeof payload.error !== "string") {
5458
- return false;
5459
- }
5460
- return true;
5467
+ return isValidResult(payload.result);
5461
5468
  }
5462
5469
 
5463
5470
  // lib/cli/commands/bucket.ts
@@ -5594,7 +5601,7 @@ function createDefaultBucketDependencies() {
5594
5601
  getTerminalSize: defaultGetTerminalSize,
5595
5602
  writeStdout: defaultWriteStdout,
5596
5603
  isTTY: process.stdout.isTTY ?? false,
5597
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
5604
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
5598
5605
  getReposDir: () => getReposDir(process.env, homedir2()),
5599
5606
  auth: {
5600
5607
  createServer: createLocalServer,
@@ -5738,8 +5745,8 @@ function createTUIContext(state, context, useTUI) {
5738
5745
  function waitForConnection(token, bucketDeps) {
5739
5746
  const wsUrl = getWebSocketUrl();
5740
5747
  const ws = bucketDeps.createWebSocket(wsUrl, token);
5741
- return new Promise((resolve, reject) => {
5742
- ws.onopen = () => resolve(ws);
5748
+ return new Promise((resolve2, reject) => {
5749
+ ws.onopen = () => resolve2(ws);
5743
5750
  ws.onerror = (error) => reject(new Error(error.message));
5744
5751
  ws.onclose = (event) => reject(new Error(`Connection closed (code ${event.code})`));
5745
5752
  });
@@ -6134,35 +6141,73 @@ async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDepend
6134
6141
  } catch {
6135
6142
  state.ui.connectedHost = wsUrl;
6136
6143
  }
6144
+ function findRepoPathByRepositoryId(repositories, repositoryId) {
6145
+ for (const repoState of repositories.values()) {
6146
+ if (repoState.repository.id === repositoryId) {
6147
+ return repoState.path;
6148
+ }
6149
+ }
6150
+ return;
6151
+ }
6137
6152
  let tuiHandle;
6138
6153
  let cleanupKeypress;
6139
6154
  let cleanupSignals;
6140
- const forwardToolExecution = (request) => {
6155
+ const forwardToolExecution = async (request) => {
6141
6156
  const ws = state.ws;
6142
6157
  if (!ws || ws.readyState !== WS_OPEN) {
6143
6158
  log10(`tool execution rejected: ${request.toolName} (WebSocket not connected)`);
6144
- return Promise.resolve({
6159
+ return {
6145
6160
  status: "error",
6146
6161
  error: "Bucket session is not connected"
6147
- });
6162
+ };
6148
6163
  }
6149
6164
  const requestId = crypto.randomUUID();
6165
+ const repoPath = findRepoPathByRepositoryId(state.repositories, Number(request.repositoryId));
6166
+ const toolDef = state.tools.find((t) => t.name === request.toolName);
6167
+ const namedArgs = {};
6168
+ if (toolDef) {
6169
+ for (let i = 0;i < toolDef.parameters.length && i < request.arguments.length; i++) {
6170
+ const param = toolDef.parameters[i];
6171
+ const value = request.arguments[i];
6172
+ if (param.type === "file" && typeof value === "string") {
6173
+ const filePath = repoPath ? resolve(repoPath, value) : value;
6174
+ try {
6175
+ const data = await readFile(filePath);
6176
+ namedArgs[param.name] = {
6177
+ filename: basename2(value),
6178
+ data: data.toString("base64")
6179
+ };
6180
+ } catch {
6181
+ return {
6182
+ status: "error",
6183
+ error: `Unable to read file: ${value}`
6184
+ };
6185
+ }
6186
+ } else {
6187
+ namedArgs[param.name] = value;
6188
+ }
6189
+ }
6190
+ } else {
6191
+ for (let i = 0;i < request.arguments.length; i++) {
6192
+ namedArgs[`arg${i}`] = request.arguments[i];
6193
+ }
6194
+ }
6150
6195
  const message = {
6151
6196
  type: "tool-execution-request",
6152
6197
  requestId,
6153
- toolName: request.toolName,
6154
- arguments: request.arguments,
6155
- repositoryId: request.repositoryId
6198
+ tool: request.toolName,
6199
+ repositoryId: Number(request.repositoryId),
6200
+ arguments: namedArgs
6156
6201
  };
6157
6202
  log10(`forwarding tool execution: ${request.toolName} requestId=${requestId}`);
6158
- return new Promise((resolve, reject) => {
6203
+ return new Promise((resolve2, reject) => {
6159
6204
  const timeoutId = setTimeout(() => {
6160
6205
  pendingToolExecutions.delete(requestId);
6161
6206
  log10(`tool execution timed out: ${request.toolName} requestId=${requestId}`);
6162
6207
  reject(new Error("Timed out waiting for tool execution result"));
6163
6208
  }, 30000);
6164
6209
  pendingToolExecutions.set(requestId, {
6165
- resolve,
6210
+ resolve: resolve2,
6166
6211
  reject,
6167
6212
  timeoutId
6168
6213
  });
@@ -6180,10 +6225,10 @@ async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDepend
6180
6225
  if (useTUI) {
6181
6226
  tuiHandle = setupTUI(state, bucketDeps);
6182
6227
  }
6183
- await new Promise((resolve) => {
6228
+ await new Promise((resolve2) => {
6184
6229
  const doShutdown = async () => {
6185
6230
  await shutdown(state, bucketDeps, context);
6186
- resolve();
6231
+ resolve2();
6187
6232
  };
6188
6233
  const onKey = createKeypressHandler(useTUI, state, () => {
6189
6234
  doShutdown();
@@ -6198,14 +6243,30 @@ async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDepend
6198
6243
  log10(`received tool execution result for unknown requestId=${message.requestId}`);
6199
6244
  return;
6200
6245
  }
6201
- log10(`received tool execution result: requestId=${message.requestId} status=${message.status}`);
6246
+ const resultType = message.result.type;
6247
+ log10(`received tool execution result: requestId=${message.requestId} status=${resultType}`);
6202
6248
  clearTimeout(pending.timeoutId);
6203
6249
  pendingToolExecutions.delete(message.requestId);
6204
- pending.resolve({
6205
- status: message.status,
6206
- output: message.output,
6207
- error: message.error
6208
- });
6250
+ switch (message.result.type) {
6251
+ case "success":
6252
+ pending.resolve({
6253
+ status: "success",
6254
+ output: typeof message.result.data === "string" ? message.result.data : JSON.stringify(message.result.data)
6255
+ });
6256
+ break;
6257
+ case "tool-not-found":
6258
+ pending.resolve({
6259
+ status: "tool-not-found",
6260
+ error: message.result.message
6261
+ });
6262
+ break;
6263
+ case "error":
6264
+ pending.resolve({
6265
+ status: "error",
6266
+ error: message.result.message
6267
+ });
6268
+ break;
6269
+ }
6209
6270
  }, forwardToolExecution);
6210
6271
  if (!useTUI) {
6211
6272
  context.stdout(" Press q or Ctrl+C to exit");
@@ -6372,7 +6433,7 @@ function createGitRunner(spawnFn) {
6372
6433
  }
6373
6434
  var defaultGitRunner = createGitRunner(spawn);
6374
6435
  function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, timeoutMs) {
6375
- return new Promise((resolve) => {
6436
+ return new Promise((resolve2) => {
6376
6437
  const proc = spawnFn(command, commandArguments, { cwd, shell });
6377
6438
  const chunks = [];
6378
6439
  let resolved = false;
@@ -6384,7 +6445,7 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
6384
6445
  proc.stdout?.destroy();
6385
6446
  proc.stderr?.destroy();
6386
6447
  proc.unref();
6387
- resolve({
6448
+ resolve2({
6388
6449
  exitCode: 1,
6389
6450
  output: chunks.join(""),
6390
6451
  timedOut: true
@@ -6402,14 +6463,14 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
6402
6463
  return;
6403
6464
  if (timer !== undefined)
6404
6465
  clearTimeout(timer);
6405
- resolve({ exitCode: code ?? 1, output: chunks.join("") });
6466
+ resolve2({ exitCode: code ?? 1, output: chunks.join("") });
6406
6467
  });
6407
6468
  proc.on("error", (error) => {
6408
6469
  if (resolved)
6409
6470
  return;
6410
6471
  if (timer !== undefined)
6411
6472
  clearTimeout(timer);
6412
- resolve({ exitCode: 1, output: error.message });
6473
+ resolve2({ exitCode: 1, output: error.message });
6413
6474
  });
6414
6475
  });
6415
6476
  }
@@ -6870,7 +6931,7 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
6870
6931
  }
6871
6932
 
6872
6933
  // lib/lint/validators/link-validator.ts
6873
- import { dirname as dirname3, resolve } from "node:path";
6934
+ import { dirname as dirname3, resolve as resolve2 } from "node:path";
6874
6935
  var SEMANTIC_RULES = [
6875
6936
  {
6876
6937
  section: "## Principles",
@@ -6895,14 +6956,22 @@ function validateLinks(filePath, content, fileSystem) {
6895
6956
  while (match) {
6896
6957
  const linkTarget = match[2];
6897
6958
  if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
6898
- const targetPath = linkTarget.split("#")[0];
6899
- const resolvedPath = resolve(fileDir, targetPath);
6900
- if (!fileSystem.exists(resolvedPath)) {
6959
+ if (linkTarget.startsWith("/")) {
6901
6960
  violations.push({
6902
6961
  file: filePath,
6903
- message: `Broken link: "${linkTarget}"`,
6962
+ message: `Absolute link not allowed: "${linkTarget}" (use a relative path instead)`,
6904
6963
  line: i + 1
6905
6964
  });
6965
+ } else {
6966
+ const targetPath = linkTarget.split("#")[0];
6967
+ const resolvedPath = resolve2(fileDir, targetPath);
6968
+ if (!fileSystem.exists(resolvedPath)) {
6969
+ violations.push({
6970
+ file: filePath,
6971
+ message: `Broken link: "${linkTarget}"`,
6972
+ line: i + 1
6973
+ });
6974
+ }
6906
6975
  }
6907
6976
  }
6908
6977
  match = linkPattern.exec(line);
@@ -6948,7 +7017,7 @@ function validateSemanticLinks(filePath, content) {
6948
7017
  continue;
6949
7018
  }
6950
7019
  const targetPath = linkTarget.split("#")[0];
6951
- const resolvedPath = resolve(fileDir, targetPath);
7020
+ const resolvedPath = resolve2(fileDir, targetPath);
6952
7021
  if (!resolvedPath.includes(rule.requiredPath)) {
6953
7022
  violations.push({
6954
7023
  file: filePath,
@@ -6999,7 +7068,7 @@ function validatePrincipleHierarchyLinks(filePath, content) {
6999
7068
  continue;
7000
7069
  }
7001
7070
  const targetPath = linkTarget.split("#")[0];
7002
- const resolvedPath = resolve(fileDir, targetPath);
7071
+ const resolvedPath = resolve2(fileDir, targetPath);
7003
7072
  if (!resolvedPath.includes("/.dust/principles/")) {
7004
7073
  violations.push({
7005
7074
  file: filePath,
@@ -7014,7 +7083,7 @@ function validatePrincipleHierarchyLinks(filePath, content) {
7014
7083
  }
7015
7084
 
7016
7085
  // lib/lint/validators/principle-hierarchy.ts
7017
- import { dirname as dirname4, resolve as resolve2 } from "node:path";
7086
+ import { dirname as dirname4, resolve as resolve3 } from "node:path";
7018
7087
  var REQUIRED_PRINCIPLE_HEADINGS = ["## Parent Principle", "## Sub-Principles"];
7019
7088
  function validatePrincipleHierarchySections(filePath, content) {
7020
7089
  const violations = [];
@@ -7049,7 +7118,7 @@ function extractPrincipleRelationships(filePath, content) {
7049
7118
  const linkTarget = match[2];
7050
7119
  if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
7051
7120
  const targetPath = linkTarget.split("#")[0];
7052
- const resolvedPath = resolve2(fileDir, targetPath);
7121
+ const resolvedPath = resolve3(fileDir, targetPath);
7053
7122
  if (resolvedPath.includes("/.dust/principles/")) {
7054
7123
  if (currentSection === "## Parent Principle") {
7055
7124
  parentPrinciples.push(resolvedPath);
@@ -7701,7 +7770,7 @@ async function init(dependencies) {
7701
7770
  }
7702
7771
 
7703
7772
  // lib/cli/commands/list.ts
7704
- import { basename as basename2 } from "node:path";
7773
+ import { basename as basename3 } from "node:path";
7705
7774
  function workflowTypeToStatus(type) {
7706
7775
  switch (type) {
7707
7776
  case "refine-idea":
@@ -7734,7 +7803,7 @@ async function buildPrincipleHierarchy(principlesPath, fileSystem) {
7734
7803
  const filePath = `${principlesPath}/${file}`;
7735
7804
  const content = await fileSystem.readFile(filePath);
7736
7805
  relationships.push(extractPrincipleRelationships(filePath, content));
7737
- const title = extractTitle(content) || basename2(file, ".md");
7806
+ const title = extractTitle(content) || basename3(file, ".md");
7738
7807
  titleMap.set(filePath, title);
7739
7808
  }
7740
7809
  const relMap = new Map;
@@ -7752,7 +7821,7 @@ async function buildPrincipleHierarchy(principlesPath, fileSystem) {
7752
7821
  }
7753
7822
  return {
7754
7823
  filePath,
7755
- title: titleMap.get(filePath) || basename2(filePath, ".md"),
7824
+ title: titleMap.get(filePath) || basename3(filePath, ".md"),
7756
7825
  children
7757
7826
  };
7758
7827
  }
package/dist/types.d.ts CHANGED
@@ -10,3 +10,5 @@ export type { ArtifactType, TaskGraph, TaskGraphNode } from './artifacts/index';
10
10
  export type { Task } from './artifacts/tasks';
11
11
  export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, IdeaInProgress, OpenQuestionResponse, ParsedCaptureIdeaTask, WorkflowTaskMatch, WorkflowTaskType, } from './artifacts/workflow-tasks';
12
12
  export type { Repository } from './bucket/repository';
13
+ export type { ToolExecutionErrorResult, ToolExecutionRequestMessage, ToolExecutionResult, ToolExecutionResultMessage, ToolExecutionSuccessResult, ToolExecutionToolNotFoundResult, } from './bucket/tool-execution-protocol';
14
+ export { isToolExecutionRequestMessage, isToolExecutionResultMessage, } from './bucket/tool-execution-protocol';
@@ -451,14 +451,22 @@ function validateLinks(filePath, content, fileSystem) {
451
451
  while (match) {
452
452
  const linkTarget = match[2];
453
453
  if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
454
- const targetPath = linkTarget.split("#")[0];
455
- const resolvedPath = resolve(fileDir, targetPath);
456
- if (!fileSystem.exists(resolvedPath)) {
454
+ if (linkTarget.startsWith("/")) {
457
455
  violations.push({
458
456
  file: filePath,
459
- message: `Broken link: "${linkTarget}"`,
457
+ message: `Absolute link not allowed: "${linkTarget}" (use a relative path instead)`,
460
458
  line: i + 1
461
459
  });
460
+ } else {
461
+ const targetPath = linkTarget.split("#")[0];
462
+ const resolvedPath = resolve(fileDir, targetPath);
463
+ if (!fileSystem.exists(resolvedPath)) {
464
+ violations.push({
465
+ file: filePath,
466
+ message: `Broken link: "${linkTarget}"`,
467
+ line: i + 1
468
+ });
469
+ }
462
470
  }
463
471
  }
464
472
  match = linkPattern.exec(line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {