@dreb/coding-agent 2.51.0 → 2.53.0

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.
Files changed (47) hide show
  1. package/README.md +8 -6
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +11 -6
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/core/extensions/types.d.ts +31 -11
  6. package/dist/core/extensions/types.d.ts.map +1 -1
  7. package/dist/core/extensions/types.js.map +1 -1
  8. package/dist/core/slash-commands.d.ts +14 -0
  9. package/dist/core/slash-commands.d.ts.map +1 -1
  10. package/dist/core/slash-commands.js +28 -3
  11. package/dist/core/slash-commands.js.map +1 -1
  12. package/dist/core/tools/ask-user.d.ts +28 -20
  13. package/dist/core/tools/ask-user.d.ts.map +1 -1
  14. package/dist/core/tools/ask-user.js +130 -106
  15. package/dist/core/tools/ask-user.js.map +1 -1
  16. package/dist/core/tools/index.d.ts +9 -7
  17. package/dist/core/tools/index.d.ts.map +1 -1
  18. package/dist/core/tools/index.js.map +1 -1
  19. package/dist/modes/interactive/components/ask-wizard.d.ts +83 -0
  20. package/dist/modes/interactive/components/ask-wizard.d.ts.map +1 -0
  21. package/dist/modes/interactive/components/ask-wizard.js +464 -0
  22. package/dist/modes/interactive/components/ask-wizard.js.map +1 -0
  23. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  24. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.js +7 -5
  26. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  27. package/dist/modes/rpc/rpc-client.d.ts +11 -1
  28. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  29. package/dist/modes/rpc/rpc-client.js +15 -1
  30. package/dist/modes/rpc/rpc-client.js.map +1 -1
  31. package/dist/modes/rpc/rpc-mode.d.ts +3 -1
  32. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  33. package/dist/modes/rpc/rpc-mode.js +230 -47
  34. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  35. package/dist/modes/rpc/rpc-types.d.ts +66 -11
  36. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  37. package/dist/modes/rpc/rpc-types.js.map +1 -1
  38. package/docs/dashboard.md +1 -3
  39. package/docs/extensions.md +39 -22
  40. package/docs/rpc.md +84 -25
  41. package/examples/extensions/rpc-demo.ts +29 -0
  42. package/examples/rpc-extension-ui.ts +162 -6
  43. package/package.json +1 -1
  44. package/dist/modes/interactive/components/ask-user.d.ts +0 -59
  45. package/dist/modes/interactive/components/ask-user.d.ts.map +0 -1
  46. package/dist/modes/interactive/components/ask-user.js +0 -240
  47. package/dist/modes/interactive/components/ask-user.js.map +0 -1
@@ -16,12 +16,15 @@ import { isValidThinkingLevel, VALID_THINKING_LEVELS } from "../../cli/args.js";
16
16
  import { VERSION } from "../../config.js";
17
17
  import { canonicalizeTrustedRoots, matchContextTrust, validateTrustedContextFolder, validateTrustedContextFolders, } from "../../core/context-trust.js";
18
18
  import { DailyCostTracker } from "../../core/daily-cost-tracker.js";
19
+ import { acquireDreamLock, buildDreamPrompt, cleanupDreamTmpDirs, parseDreamCommand, performDreamBackup, pruneOldBackups, resolveDreamContext, validateArchivePath, validateMemoryLinks, } from "../../core/dream.js";
19
20
  import { getGitBranch } from "../../core/git-branch.js";
20
21
  import { parseModelPattern } from "../../core/model-resolver.js";
21
22
  import { takeOverStdout, writeRawStdout } from "../../core/output-guard.js";
22
23
  import { SessionManager } from "../../core/session-manager.js";
24
+ import { BUILTIN_SLASH_COMMANDS, parseBuiltinSlashCommand } from "../../core/slash-commands.js";
23
25
  import { TabTitleGenerator } from "../../core/tab-title.js";
24
26
  import { validateThinkingLevelForModel } from "../../core/thinking.js";
27
+ import { resolveToCwd } from "../../core/tools/path-utils.js";
25
28
  import { discoverAgentTypes, getBackgroundAgents, rehydrateBackgroundAgentsFromDisk, } from "../../core/tools/subagent.js";
26
29
  import { theme } from "../interactive/theme/theme.js";
27
30
  import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
@@ -71,6 +74,111 @@ export function getResourcesForRpc(session) {
71
74
  session.resourceLoader.getAppendSystemPrompt().length > 0,
72
75
  };
73
76
  }
77
+ export function getCommandsForRpc(session) {
78
+ const commands = new Map();
79
+ for (const command of session.extensionRunner?.getRegisteredCommands() ?? []) {
80
+ commands.set(command.invocationName, {
81
+ name: command.invocationName,
82
+ description: command.description,
83
+ source: "extension",
84
+ sourceInfo: command.sourceInfo,
85
+ });
86
+ }
87
+ for (const template of session.promptTemplates) {
88
+ if (!commands.has(template.name)) {
89
+ commands.set(template.name, {
90
+ name: template.name,
91
+ description: template.description,
92
+ source: "prompt",
93
+ sourceInfo: template.sourceInfo,
94
+ });
95
+ }
96
+ }
97
+ for (const skill of session.getFilteredSkills()) {
98
+ const name = `skill:${skill.name}`;
99
+ if (!commands.has(name)) {
100
+ commands.set(name, { name, description: skill.description, source: "skill", sourceInfo: skill.sourceInfo });
101
+ }
102
+ }
103
+ // Built-ins deliberately win collisions, matching interactive autocomplete and
104
+ // ensuring a colliding resource can never make built-in text reach the model.
105
+ for (const command of BUILTIN_SLASH_COMMANDS) {
106
+ commands.set(command.name, {
107
+ name: command.name,
108
+ description: command.description,
109
+ source: "builtin",
110
+ dashboard: command.dashboard !== false,
111
+ });
112
+ }
113
+ return [...commands.values()];
114
+ }
115
+ export function getBuiltinPromptRejection(message) {
116
+ const parsed = parseBuiltinSlashCommand(message);
117
+ return parsed
118
+ ? `Built-in slash command /${parsed.command.name} must be handled by the RPC client; it was not sent to the model.`
119
+ : undefined;
120
+ }
121
+ async function runDreamForRpc(session, args = "") {
122
+ const command = parseDreamCommand(`/dream${args ? ` ${args}` : ""}`);
123
+ if (command.type === "showBackup") {
124
+ return { message: `Dream backup path: ${session.settingsManager.getDreamArchivePath()}` };
125
+ }
126
+ if (command.type === "setBackup") {
127
+ const absolutePath = resolveToCwd(command.path, session.sessionManager.getCwd());
128
+ const context = await resolveDreamContext(session.settingsManager);
129
+ validateArchivePath(absolutePath, [
130
+ context.globalMemoryDir,
131
+ ...context.projectMemoryDirs,
132
+ ...context.claudeMemoryDirs,
133
+ ]);
134
+ if (session.settingsManager.hasGlobalSettingsLoadError()) {
135
+ throw new Error("Cannot write dream backup path: the global settings file failed to load");
136
+ }
137
+ session.settingsManager.drainErrors();
138
+ session.settingsManager.setDreamArchivePath(absolutePath);
139
+ try {
140
+ await session.settingsManager.flush();
141
+ }
142
+ catch (error) {
143
+ session.settingsManager.reload();
144
+ throw error;
145
+ }
146
+ const writeErrors = session.settingsManager.drainErrors();
147
+ if (writeErrors.length > 0) {
148
+ session.settingsManager.reload();
149
+ throw new Error(`Failed to persist dream backup path: ${writeErrors.map((entry) => `${entry.scope}: ${entry.error.message}`).join("; ")}`);
150
+ }
151
+ return { message: `Dream backup path set to: ${absolutePath}` };
152
+ }
153
+ let releaseLock;
154
+ let context;
155
+ try {
156
+ releaseLock = await acquireDreamLock();
157
+ context = await resolveDreamContext(session.settingsManager);
158
+ validateArchivePath(context.archivePath, [
159
+ context.globalMemoryDir,
160
+ ...context.projectMemoryDirs,
161
+ ...context.claudeMemoryDirs,
162
+ ]);
163
+ const backup = await performDreamBackup(context);
164
+ if (!backup.verified) {
165
+ throw new Error(`Backup verification failed; check ${backup.backupPath}`);
166
+ }
167
+ await session.prompt(buildDreamPrompt(context, backup), { source: "rpc" });
168
+ const links = validateMemoryLinks([context.globalMemoryDir, ...context.projectMemoryDirs]);
169
+ await pruneOldBackups(context.archivePath);
170
+ return {
171
+ message: links.valid
172
+ ? `Dream completed. Backup: ${backup.backupPath}`
173
+ : `Dream completed with ${links.brokenLinks.length} broken memory link(s). Backup: ${backup.backupPath}`,
174
+ };
175
+ }
176
+ finally {
177
+ releaseLock?.();
178
+ if (context)
179
+ cleanupDreamTmpDirs([context.globalMemoryDir, ...context.projectMemoryDirs]);
180
+ }
181
+ }
74
182
  export function getPendingMessagesForRpc(session) {
75
183
  return {
76
184
  steering: [...session.getSteeringMessages()],
@@ -915,6 +1023,74 @@ export function createRpcExtensionUIContext(output, pendingExtensionRequests, on
915
1023
  output(rpcRequest);
916
1024
  });
917
1025
  }
1026
+ // Serialize `ask` requests so at most one ask wizard is ever pending, mirroring
1027
+ // the TUI's single-dialog queue (`enqueueExtensionDialog`). Tool execution runs
1028
+ // in parallel, so a turn can issue several `ask_user` calls at once; without
1029
+ // this the Dashboard (which renders a single pending ask) would leave the extra
1030
+ // wizards invisible and their promises hanging until the turn aborts. The first
1031
+ // ask runs synchronously (preserving immediate request emission); later ones
1032
+ // wait in a FIFO. A call whose signal aborts while still queued settles with the
1033
+ // aborted value and never emits a request.
1034
+ let askInFlight = false;
1035
+ const askWaiters = [];
1036
+ const runNextAsk = () => {
1037
+ if (askInFlight)
1038
+ return;
1039
+ const next = askWaiters.shift();
1040
+ if (!next)
1041
+ return;
1042
+ askInFlight = true;
1043
+ next();
1044
+ };
1045
+ function serializeAsk(opts, abortedValue, run) {
1046
+ const signal = opts?.signal;
1047
+ if (signal?.aborted)
1048
+ return Promise.resolve(abortedValue);
1049
+ return new Promise((resolve, reject) => {
1050
+ let settled = false;
1051
+ const finish = (value) => {
1052
+ if (settled)
1053
+ return;
1054
+ settled = true;
1055
+ resolve(value);
1056
+ };
1057
+ // A started run's outcome propagates verbatim (a host/protocol failure
1058
+ // must still reject so the ask_user tool reports it distinctly).
1059
+ const fail = (error) => {
1060
+ if (settled)
1061
+ return;
1062
+ settled = true;
1063
+ reject(error);
1064
+ };
1065
+ // While still queued, a signal abort removes this task and settles it
1066
+ // without ever emitting. Once started, createDialogPromise owns abort.
1067
+ const onAbort = () => {
1068
+ const index = askWaiters.indexOf(startTask);
1069
+ if (index === -1)
1070
+ return;
1071
+ askWaiters.splice(index, 1);
1072
+ finish(abortedValue);
1073
+ };
1074
+ const startTask = () => {
1075
+ signal?.removeEventListener("abort", onAbort);
1076
+ if (signal?.aborted) {
1077
+ finish(abortedValue);
1078
+ askInFlight = false;
1079
+ runNextAsk();
1080
+ return;
1081
+ }
1082
+ run()
1083
+ .then(finish, fail)
1084
+ .finally(() => {
1085
+ askInFlight = false;
1086
+ runNextAsk();
1087
+ });
1088
+ };
1089
+ signal?.addEventListener("abort", onAbort, { once: true });
1090
+ askWaiters.push(startTask);
1091
+ runNextAsk();
1092
+ });
1093
+ }
918
1094
  /**
919
1095
  * Create an extension UI context that uses the RPC protocol.
920
1096
  */
@@ -922,38 +1098,50 @@ export function createRpcExtensionUIContext(output, pendingExtensionRequests, on
922
1098
  select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
923
1099
  confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
924
1100
  input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
925
- ask: (request, opts) => createDialogPromise(opts, undefined, {
1101
+ ask: (request, opts) => serializeAsk(opts, undefined, () => createDialogPromise(opts, undefined, {
926
1102
  method: "ask",
927
1103
  title: request.title ?? "Question",
928
- question: request.question,
929
- options: request.options,
930
- allowFreeText: request.allowFreeText,
931
- multiSelect: request.multiSelect,
932
- multiline: request.multiline,
1104
+ questions: request.questions.map((q) => ({
1105
+ question: q.question,
1106
+ title: q.title,
1107
+ options: q.options,
1108
+ allowFreeText: q.allowFreeText,
1109
+ multiSelect: q.multiSelect,
1110
+ multiline: q.multiline,
1111
+ })),
933
1112
  timeout: opts?.timeout,
934
1113
  // Preserve the authoritative deadline across Dashboard reload,
935
1114
  // resync, and drill-in hydration instead of restarting the full
936
- // duration whenever the question component remounts.
1115
+ // duration whenever the wizard remounts.
937
1116
  expiresAt: opts?.timeout ? Date.now() + opts.timeout : undefined,
938
1117
  }, (response) => {
939
1118
  if ("cancelled" in response && response.cancelled) {
940
1119
  onAskStop();
941
1120
  return undefined;
942
1121
  }
943
- const selected = response.selected;
944
- const customText = response.customText;
945
- if (!Array.isArray(selected) || !selected.every((value) => typeof value === "string")) {
946
- throw new Error("Invalid RPC ask response: selected must be an array of strings");
947
- }
948
- if (customText !== undefined && typeof customText !== "string") {
949
- throw new Error("Invalid RPC ask response: customText must be a string");
950
- }
951
- if (selected.length === 0 && !customText?.trim()) {
952
- onAskStop();
953
- return undefined;
1122
+ const rawAnswers = response.answers;
1123
+ if (!Array.isArray(rawAnswers)) {
1124
+ throw new Error("Invalid RPC ask response: answers must be an array");
954
1125
  }
955
- return { selected, customText };
956
- }, onAskStop),
1126
+ const answers = rawAnswers.map((entry) => {
1127
+ const selected = entry?.selected;
1128
+ const customText = entry?.customText;
1129
+ const skipped = entry?.skipped;
1130
+ if (!Array.isArray(selected) || !selected.every((value) => typeof value === "string")) {
1131
+ throw new Error("Invalid RPC ask response: each answer's selected must be an array of strings");
1132
+ }
1133
+ if (customText !== undefined && typeof customText !== "string") {
1134
+ throw new Error("Invalid RPC ask response: customText must be a string");
1135
+ }
1136
+ const answered = selected.length > 0 || !!customText?.trim();
1137
+ return {
1138
+ selected,
1139
+ customText: customText || undefined,
1140
+ skipped: skipped === true || !answered,
1141
+ };
1142
+ });
1143
+ return { answers };
1144
+ }, onAskStop)),
957
1145
  notify(message, type) {
958
1146
  // Fire and forget - no response needed
959
1147
  output({
@@ -1193,6 +1381,9 @@ export async function runRpcMode(session, modelFallbackMessage) {
1193
1381
  // Prompting
1194
1382
  // =================================================================
1195
1383
  case "prompt": {
1384
+ const rejection = getBuiltinPromptRejection(command.message);
1385
+ if (rejection)
1386
+ return error(id, "prompt", rejection);
1196
1387
  // Don't await - events will stream
1197
1388
  // Extension commands are executed immediately, file prompt templates are expanded
1198
1389
  // If streaming and streamingBehavior specified, queues via steer/followUp
@@ -1206,10 +1397,16 @@ export async function runRpcMode(session, modelFallbackMessage) {
1206
1397
  return success(id, "prompt");
1207
1398
  }
1208
1399
  case "steer": {
1400
+ const rejection = getBuiltinPromptRejection(command.message);
1401
+ if (rejection)
1402
+ return error(id, "steer", rejection);
1209
1403
  await session.steer(command.message, command.images);
1210
1404
  return success(id, "steer");
1211
1405
  }
1212
1406
  case "follow_up": {
1407
+ const rejection = getBuiltinPromptRejection(command.message);
1408
+ if (rejection)
1409
+ return error(id, "follow_up", rejection);
1213
1410
  await session.followUp(command.message, command.images);
1214
1411
  return success(id, "follow_up");
1215
1412
  }
@@ -1224,6 +1421,13 @@ export async function runRpcMode(session, modelFallbackMessage) {
1224
1421
  const cancelled = !(await session.newSession(options));
1225
1422
  return success(id, "new_session", { cancelled });
1226
1423
  }
1424
+ case "reload": {
1425
+ await session.reload();
1426
+ return success(id, "reload");
1427
+ }
1428
+ case "dream": {
1429
+ return success(id, "dream", await runDreamForRpc(session, command.args));
1430
+ }
1227
1431
  // =================================================================
1228
1432
  // State
1229
1433
  // =================================================================
@@ -1399,6 +1603,10 @@ export async function runRpcMode(session, modelFallbackMessage) {
1399
1603
  const path = await session.exportToHtml(command.outputPath);
1400
1604
  return success(id, "export_html", { path });
1401
1605
  }
1606
+ case "import_jsonl": {
1607
+ const cancelled = !(await session.importFromJsonl(command.inputPath));
1608
+ return success(id, "import_jsonl", { cancelled });
1609
+ }
1402
1610
  case "switch_session": {
1403
1611
  const cancelled = !(await session.switchSession(command.sessionPath));
1404
1612
  return success(id, "switch_session", { cancelled });
@@ -1454,7 +1662,7 @@ export async function runRpcMode(session, modelFallbackMessage) {
1454
1662
  return success(id, "get_messages", { messages: session.messages });
1455
1663
  }
1456
1664
  // =================================================================
1457
- // Commands (available for invocation via prompt)
1665
+ // Command discovery (resource commands plus client-handled built-ins)
1458
1666
  // =================================================================
1459
1667
  // =================================================================
1460
1668
  // Session Listing
@@ -1528,32 +1736,7 @@ export async function runRpcMode(session, modelFallbackMessage) {
1528
1736
  return success(id, "get_version", { version: VERSION });
1529
1737
  }
1530
1738
  case "get_commands": {
1531
- const commands = [];
1532
- for (const command of session.extensionRunner?.getRegisteredCommands() ?? []) {
1533
- commands.push({
1534
- name: command.invocationName,
1535
- description: command.description,
1536
- source: "extension",
1537
- sourceInfo: command.sourceInfo,
1538
- });
1539
- }
1540
- for (const template of session.promptTemplates) {
1541
- commands.push({
1542
- name: template.name,
1543
- description: template.description,
1544
- source: "prompt",
1545
- sourceInfo: template.sourceInfo,
1546
- });
1547
- }
1548
- for (const skill of session.getFilteredSkills()) {
1549
- commands.push({
1550
- name: `skill:${skill.name}`,
1551
- description: skill.description,
1552
- source: "skill",
1553
- sourceInfo: skill.sourceInfo,
1554
- });
1555
- }
1556
- return success(id, "get_commands", { commands });
1739
+ return success(id, "get_commands", { commands: getCommandsForRpc(session) });
1557
1740
  }
1558
1741
  default: {
1559
1742
  const unknownCommand = command;