@dobby.ai/dobby 0.1.0 → 0.1.2

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 (156) hide show
  1. package/README.md +84 -39
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/commands/doctor.js +81 -2
  5. package/dist/src/cli/commands/extension.js +3 -1
  6. package/dist/src/cli/commands/init.js +43 -173
  7. package/dist/src/cli/commands/topology.js +38 -14
  8. package/dist/src/cli/program.js +15 -137
  9. package/dist/src/cli/shared/config-io.js +3 -31
  10. package/dist/src/cli/shared/config-mutators.js +33 -9
  11. package/dist/src/cli/shared/configure-sections.js +52 -12
  12. package/dist/src/cli/shared/init-catalog.js +89 -46
  13. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  14. package/dist/src/cli/shared/schema-prompts.js +26 -2
  15. package/dist/src/core/gateway.js +3 -1
  16. package/dist/src/core/routing.js +53 -38
  17. package/dist/src/core/types.js +2 -0
  18. package/dist/src/cron/config.js +2 -2
  19. package/dist/src/cron/service.js +87 -23
  20. package/dist/src/cron/store.js +1 -1
  21. package/dist/src/main.js +0 -0
  22. package/dist/src/shared/dobby-repo.js +40 -0
  23. package/package.json +11 -4
  24. package/.env.example +0 -9
  25. package/AGENTS.md +0 -267
  26. package/ROADMAP.md +0 -34
  27. package/config/cron.example.json +0 -9
  28. package/config/gateway.example.json +0 -128
  29. package/config/models.custom.example.json +0 -27
  30. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  31. package/dist/src/cli/shared/config-path.js +0 -207
  32. package/dist/src/cli/shared/init-models-file.js +0 -65
  33. package/dist/src/cli/shared/presets.js +0 -86
  34. package/dist/src/cli/tests/config-command.test.js +0 -42
  35. package/dist/src/cli/tests/config-io.test.js +0 -64
  36. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  37. package/dist/src/cli/tests/config-path.test.js +0 -21
  38. package/dist/src/cli/tests/discord-config.test.js +0 -23
  39. package/dist/src/cli/tests/doctor.test.js +0 -107
  40. package/dist/src/cli/tests/init-catalog.test.js +0 -87
  41. package/dist/src/cli/tests/presets.test.js +0 -41
  42. package/dist/src/cli/tests/program-options.test.js +0 -92
  43. package/dist/src/cli/tests/routing-config.test.js +0 -199
  44. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  45. package/dist/src/core/tests/control-command.test.js +0 -17
  46. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  47. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  48. package/dist/src/core/tests/typing-controller.test.js +0 -103
  49. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  50. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  51. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  52. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  53. package/docs/MVP.md +0 -135
  54. package/docs/RUNBOOK.md +0 -242
  55. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  56. package/plugins/connector-discord/dobby.manifest.json +0 -18
  57. package/plugins/connector-discord/index.js +0 -1
  58. package/plugins/connector-discord/package-lock.json +0 -360
  59. package/plugins/connector-discord/package.json +0 -38
  60. package/plugins/connector-discord/src/connector.ts +0 -350
  61. package/plugins/connector-discord/src/contribution.ts +0 -21
  62. package/plugins/connector-discord/src/mapper.ts +0 -102
  63. package/plugins/connector-discord/tsconfig.json +0 -19
  64. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  65. package/plugins/connector-feishu/index.js +0 -1
  66. package/plugins/connector-feishu/package-lock.json +0 -618
  67. package/plugins/connector-feishu/package.json +0 -38
  68. package/plugins/connector-feishu/src/connector.ts +0 -343
  69. package/plugins/connector-feishu/src/contribution.ts +0 -26
  70. package/plugins/connector-feishu/src/mapper.ts +0 -401
  71. package/plugins/connector-feishu/tsconfig.json +0 -19
  72. package/plugins/plugin-sdk/index.d.ts +0 -261
  73. package/plugins/plugin-sdk/index.js +0 -1
  74. package/plugins/plugin-sdk/package-lock.json +0 -12
  75. package/plugins/plugin-sdk/package.json +0 -22
  76. package/plugins/provider-claude/dobby.manifest.json +0 -17
  77. package/plugins/provider-claude/index.js +0 -1
  78. package/plugins/provider-claude/package-lock.json +0 -3398
  79. package/plugins/provider-claude/package.json +0 -39
  80. package/plugins/provider-claude/src/contribution.ts +0 -1018
  81. package/plugins/provider-claude/tsconfig.json +0 -19
  82. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  83. package/plugins/provider-claude-cli/index.js +0 -1
  84. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  85. package/plugins/provider-claude-cli/package.json +0 -38
  86. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  87. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  88. package/plugins/provider-pi/dobby.manifest.json +0 -17
  89. package/plugins/provider-pi/index.js +0 -1
  90. package/plugins/provider-pi/package-lock.json +0 -3877
  91. package/plugins/provider-pi/package.json +0 -40
  92. package/plugins/provider-pi/src/contribution.ts +0 -476
  93. package/plugins/provider-pi/tsconfig.json +0 -19
  94. package/plugins/sandbox-core/boxlite.js +0 -1
  95. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  96. package/plugins/sandbox-core/docker.js +0 -1
  97. package/plugins/sandbox-core/package-lock.json +0 -136
  98. package/plugins/sandbox-core/package.json +0 -39
  99. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  100. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  101. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  102. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  103. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  104. package/plugins/sandbox-core/tsconfig.json +0 -19
  105. package/scripts/local-extensions.mjs +0 -168
  106. package/src/agent/event-forwarder.ts +0 -414
  107. package/src/cli/commands/config.ts +0 -328
  108. package/src/cli/commands/configure.ts +0 -92
  109. package/src/cli/commands/cron.ts +0 -410
  110. package/src/cli/commands/doctor.ts +0 -230
  111. package/src/cli/commands/extension.ts +0 -205
  112. package/src/cli/commands/init.ts +0 -396
  113. package/src/cli/commands/start.ts +0 -223
  114. package/src/cli/commands/topology.ts +0 -383
  115. package/src/cli/index.ts +0 -9
  116. package/src/cli/program.ts +0 -465
  117. package/src/cli/shared/config-io.ts +0 -277
  118. package/src/cli/shared/config-mutators.ts +0 -440
  119. package/src/cli/shared/config-schema.ts +0 -228
  120. package/src/cli/shared/config-types.ts +0 -121
  121. package/src/cli/shared/configure-sections.ts +0 -551
  122. package/src/cli/shared/discord-config.ts +0 -14
  123. package/src/cli/shared/init-catalog.ts +0 -189
  124. package/src/cli/shared/init-models-file.ts +0 -77
  125. package/src/cli/shared/runtime.ts +0 -33
  126. package/src/cli/shared/schema-prompts.ts +0 -414
  127. package/src/cli/tests/config-command.test.ts +0 -56
  128. package/src/cli/tests/config-io.test.ts +0 -92
  129. package/src/cli/tests/config-mutators.test.ts +0 -59
  130. package/src/cli/tests/doctor.test.ts +0 -120
  131. package/src/cli/tests/init-catalog.test.ts +0 -96
  132. package/src/cli/tests/program-options.test.ts +0 -113
  133. package/src/cli/tests/routing-config.test.ts +0 -209
  134. package/src/core/control-command.ts +0 -12
  135. package/src/core/dedup-store.ts +0 -103
  136. package/src/core/gateway.ts +0 -607
  137. package/src/core/routing.ts +0 -379
  138. package/src/core/runtime-registry.ts +0 -141
  139. package/src/core/tests/control-command.test.ts +0 -20
  140. package/src/core/tests/runtime-registry.test.ts +0 -140
  141. package/src/core/tests/typing-controller.test.ts +0 -129
  142. package/src/core/types.ts +0 -318
  143. package/src/core/typing-controller.ts +0 -119
  144. package/src/cron/config.ts +0 -154
  145. package/src/cron/schedule.ts +0 -61
  146. package/src/cron/service.ts +0 -249
  147. package/src/cron/store.ts +0 -155
  148. package/src/cron/types.ts +0 -60
  149. package/src/extension/loader.ts +0 -145
  150. package/src/extension/manager.ts +0 -355
  151. package/src/extension/manifest.ts +0 -26
  152. package/src/extension/registry.ts +0 -229
  153. package/src/main.ts +0 -8
  154. package/src/sandbox/executor.ts +0 -44
  155. package/src/sandbox/host-executor.ts +0 -118
  156. package/tsconfig.json +0 -18
@@ -1,12 +1,22 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
1
3
  import { Command } from "commander";
2
- import { runConfigEditCommand, runConfigListCommand, runConfigSchemaListCommand, runConfigSchemaShowCommand, runConfigShowCommand, } from "./commands/config.js";
3
- import { runConfigureCommand } from "./commands/configure.js";
4
+ import { runConfigListCommand, runConfigSchemaListCommand, runConfigSchemaShowCommand, runConfigShowCommand, } from "./commands/config.js";
4
5
  import { runCronAddCommand, runCronListCommand, runCronPauseCommand, runCronRemoveCommand, runCronResumeCommand, runCronRunCommand, runCronStatusCommand, runCronUpdateCommand, } from "./commands/cron.js";
5
6
  import { runDoctorCommand } from "./commands/doctor.js";
6
7
  import { runExtensionInstallCommand, runExtensionListCommand, runExtensionUninstallCommand, } from "./commands/extension.js";
7
8
  import { runInitCommand } from "./commands/init.js";
8
9
  import { runStartCommand } from "./commands/start.js";
9
- import { runBindingListCommand, runBindingRemoveCommand, runBindingSetCommand, runBotListCommand, runBotSetCommand, runRouteListCommand, runRouteRemoveCommand, runRouteSetCommand, } from "./commands/topology.js";
10
+ function loadCliVersion() {
11
+ const candidates = [
12
+ fileURLToPath(new URL("../../package.json", import.meta.url)),
13
+ fileURLToPath(new URL("../../../package.json", import.meta.url)),
14
+ ];
15
+ const fallbackCandidate = fileURLToPath(new URL("../../package.json", import.meta.url));
16
+ const packageJsonPath = candidates.find((candidate) => existsSync(candidate)) ?? fallbackCandidate;
17
+ return JSON.parse(readFileSync(packageJsonPath, "utf-8")).version;
18
+ }
19
+ const CLI_VERSION = loadCliVersion();
10
20
  /**
11
21
  * Builds the top-level dobby CLI program and registers all subcommands.
12
22
  */
@@ -14,6 +24,7 @@ export function buildProgram() {
14
24
  const program = new Command();
15
25
  program
16
26
  .name("dobby")
27
+ .version(CLI_VERSION)
17
28
  .description("Discord-first local agent gateway")
18
29
  .showHelpAfterError()
19
30
  .action(async () => {
@@ -31,125 +42,7 @@ export function buildProgram() {
31
42
  .action(async () => {
32
43
  await runInitCommand();
33
44
  });
34
- program
35
- .command("configure")
36
- .description("Interactive configuration wizard")
37
- .option("--section <section>", "Config section (repeatable): provider|connector|route|binding|sandbox|data", (value, previous) => [...previous, value], [])
38
- .action(async (opts) => {
39
- await runConfigureCommand({
40
- sections: opts.section,
41
- });
42
- });
43
- const botCommand = program.command("bot").description("Manage bot connector settings");
44
- botCommand
45
- .command("list")
46
- .description("List configured bot connectors")
47
- .option("--json", "Output JSON", false)
48
- .action(async (opts) => {
49
- await runBotListCommand({
50
- json: Boolean(opts.json),
51
- });
52
- });
53
- botCommand
54
- .command("set")
55
- .description("Update one bot connector")
56
- .argument("<connectorId>", "Connector instance ID")
57
- .option("--name <name>", "Discord botName")
58
- .option("--token <token>", "Discord botToken")
59
- .action(async (connectorId, opts) => {
60
- await runBotSetCommand({
61
- connectorId,
62
- ...(typeof opts.name === "string" ? { name: opts.name } : {}),
63
- ...(typeof opts.token === "string" ? { token: opts.token } : {}),
64
- });
65
- });
66
- const bindingCommand = program.command("binding").description("Manage connector source-route bindings");
67
- bindingCommand
68
- .command("list")
69
- .description("List bindings")
70
- .option("--connector <id>", "Filter by connector instance ID")
71
- .option("--json", "Output JSON", false)
72
- .action(async (opts) => {
73
- await runBindingListCommand({
74
- ...(typeof opts.connector === "string" ? { connectorId: opts.connector } : {}),
75
- json: Boolean(opts.json),
76
- });
77
- });
78
- bindingCommand
79
- .command("set")
80
- .description("Create or update one binding")
81
- .argument("<bindingId>", "Binding ID")
82
- .requiredOption("--connector <id>", "Connector instance ID")
83
- .requiredOption("--source-type <type>", "Source type: channel|chat")
84
- .requiredOption("--source-id <id>", "Source ID")
85
- .requiredOption("--route <id>", "Route ID")
86
- .action(async (bindingId, opts) => {
87
- if (opts.sourceType !== "channel" && opts.sourceType !== "chat") {
88
- throw new Error("--source-type must be channel or chat");
89
- }
90
- await runBindingSetCommand({
91
- bindingId,
92
- connectorId: opts.connector,
93
- sourceType: opts.sourceType,
94
- sourceId: opts.sourceId,
95
- routeId: opts.route,
96
- });
97
- });
98
- bindingCommand
99
- .command("remove")
100
- .description("Remove one binding")
101
- .argument("<bindingId>", "Binding ID")
102
- .action(async (bindingId) => {
103
- await runBindingRemoveCommand({
104
- bindingId,
105
- });
106
- });
107
- const routeCommand = program.command("route").description("Manage route profiles");
108
- routeCommand
109
- .command("list")
110
- .description("List route profiles")
111
- .option("--json", "Output JSON", false)
112
- .action(async (opts) => {
113
- await runRouteListCommand({
114
- json: Boolean(opts.json),
115
- });
116
- });
117
- routeCommand
118
- .command("set")
119
- .description("Create or update one route")
120
- .argument("<routeId>", "Route ID")
121
- .option("--project-root <path>", "Route project root")
122
- .option("--tools <profile>", "Route tools profile: full|readonly")
123
- .option("--provider <id>", "Provider instance ID")
124
- .option("--sandbox <id>", "Sandbox instance ID")
125
- .option("--mentions <policy>", "Mention policy: required|optional")
126
- .action(async (routeId, opts) => {
127
- if (typeof opts.mentions === "string"
128
- && opts.mentions !== "required"
129
- && opts.mentions !== "optional") {
130
- throw new Error("--mentions must be required or optional");
131
- }
132
- await runRouteSetCommand({
133
- routeId,
134
- ...(typeof opts.projectRoot === "string" ? { projectRoot: opts.projectRoot } : {}),
135
- ...(typeof opts.tools === "string" ? { tools: opts.tools } : {}),
136
- ...(typeof opts.provider === "string" ? { providerId: opts.provider } : {}),
137
- ...(typeof opts.sandbox === "string" ? { sandboxId: opts.sandbox } : {}),
138
- ...(typeof opts.mentions === "string" ? { mentions: opts.mentions } : {}),
139
- });
140
- });
141
- routeCommand
142
- .command("remove")
143
- .description("Remove one route")
144
- .argument("<routeId>", "Route ID")
145
- .option("--cascade-bindings", "Remove bindings that reference this route", false)
146
- .action(async (routeId, opts) => {
147
- await runRouteRemoveCommand({
148
- routeId,
149
- cascadeBindings: Boolean(opts.cascadeBindings),
150
- });
151
- });
152
- const configCommand = program.command("config").description("Inspect and edit config");
45
+ const configCommand = program.command("config").description("Inspect config");
153
46
  configCommand
154
47
  .command("show")
155
48
  .description("Show full config or one section")
@@ -172,15 +65,6 @@ export function buildProgram() {
172
65
  json: Boolean(opts.json),
173
66
  });
174
67
  });
175
- configCommand
176
- .command("edit")
177
- .description("Interactive edit for high-frequency sections")
178
- .option("--section <section>", "Edit section (repeatable): provider|connector|route|binding", (value, previous) => [...previous, value], [])
179
- .action(async (opts) => {
180
- await runConfigEditCommand({
181
- sections: opts.section,
182
- });
183
- });
184
68
  const configSchemaCommand = configCommand.command("schema").description("Inspect extension config schemas");
185
69
  configSchemaCommand
186
70
  .command("list")
@@ -253,7 +137,6 @@ export function buildProgram() {
253
137
  .requiredOption("--route <id>", "Route ID")
254
138
  .requiredOption("--channel <id>", "Delivery channel/chat ID")
255
139
  .option("--thread <id>", "Delivery thread ID")
256
- .option("--session-policy <policy>", "Session policy: stateless|shared-session", "stateless")
257
140
  .option("--at <iso>", "Run once at ISO timestamp")
258
141
  .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
259
142
  .option("--cron <expr>", "Cron expression")
@@ -268,7 +151,6 @@ export function buildProgram() {
268
151
  routeId: opts.route,
269
152
  channelId: opts.channel,
270
153
  ...(typeof opts.thread === "string" ? { threadId: opts.thread } : {}),
271
- sessionPolicy: opts.sessionPolicy,
272
154
  ...(typeof opts.at === "string" ? { at: opts.at } : {}),
273
155
  ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
274
156
  ...(typeof opts.cron === "string" ? { cronExpr: opts.cron } : {}),
@@ -322,7 +204,6 @@ export function buildProgram() {
322
204
  .option("--channel <id>", "Delivery channel/chat ID")
323
205
  .option("--thread <id>", "Delivery thread ID")
324
206
  .option("--clear-thread", "Unset delivery thread", false)
325
- .option("--session-policy <policy>", "Session policy: stateless|shared-session")
326
207
  .option("--at <iso>", "Run once at ISO timestamp")
327
208
  .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
328
209
  .option("--cron <expr>", "Cron expression")
@@ -339,9 +220,6 @@ export function buildProgram() {
339
220
  ...(typeof opts.channel === "string" ? { channelId: opts.channel } : {}),
340
221
  ...(typeof opts.thread === "string" ? { threadId: opts.thread } : {}),
341
222
  clearThread: Boolean(opts.clearThread),
342
- ...(typeof opts.sessionPolicy === "string"
343
- ? { sessionPolicy: opts.sessionPolicy }
344
- : {}),
345
223
  ...(typeof opts.at === "string" ? { at: opts.at } : {}),
346
224
  ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
347
225
  ...(typeof opts.cron === "string" ? { cronExpr: opts.cron } : {}),
@@ -1,8 +1,8 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
1
  import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
2
  import { dirname, isAbsolute, resolve } from "node:path";
4
3
  import { homedir } from "node:os";
5
4
  import { loadGatewayConfig } from "../../core/routing.js";
5
+ import { findDobbyRepoRoot, isDobbyRepoRoot } from "../../shared/dobby-repo.js";
6
6
  /**
7
7
  * Default config file path used by all CLI commands.
8
8
  */
@@ -19,25 +19,6 @@ function expandHome(value) {
19
19
  }
20
20
  return value;
21
21
  }
22
- /**
23
- * Returns true when a directory looks like the dobby repository root.
24
- */
25
- function isDobbyRepoRoot(candidateDir) {
26
- const packageJsonPath = resolve(candidateDir, "package.json");
27
- const repoConfigPath = resolve(candidateDir, "config", "gateway.json");
28
- const localExtensionsScriptPath = resolve(candidateDir, "scripts", "local-extensions.mjs");
29
- if (!existsSync(packageJsonPath) || !existsSync(repoConfigPath) || !existsSync(localExtensionsScriptPath)) {
30
- return false;
31
- }
32
- try {
33
- const packageJsonRaw = readFileSync(packageJsonPath, "utf-8");
34
- const parsed = JSON.parse(packageJsonRaw);
35
- return parsed.name === "dobby";
36
- }
37
- catch {
38
- return false;
39
- }
40
- }
41
22
  function resolveConfigBaseDir(configPath) {
42
23
  const absoluteConfigPath = resolve(configPath);
43
24
  const configDir = dirname(absoluteConfigPath);
@@ -51,17 +32,8 @@ function resolveConfigBaseDir(configPath) {
51
32
  * Scans current directory and ancestors to find a local dobby repo config path.
52
33
  */
53
34
  function findDobbyRepoConfigPath(startDir) {
54
- let currentDir = resolve(startDir);
55
- while (true) {
56
- if (isDobbyRepoRoot(currentDir)) {
57
- return resolve(currentDir, "config", "gateway.json");
58
- }
59
- const parentDir = dirname(currentDir);
60
- if (parentDir === currentDir) {
61
- return null;
62
- }
63
- currentDir = parentDir;
64
- }
35
+ const repoRoot = findDobbyRepoRoot(startDir);
36
+ return repoRoot ? resolve(repoRoot, "config", "gateway.json") : null;
65
37
  }
66
38
  /**
67
39
  * Resolves config path source by priority: env override -> local repo -> default home path.
@@ -42,6 +42,7 @@ function asRouteDefaults(value) {
42
42
  };
43
43
  }
44
44
  return {
45
+ ...(typeof value.projectRoot === "string" && value.projectRoot.trim().length > 0 ? { projectRoot: value.projectRoot } : {}),
45
46
  ...(typeof value.provider === "string" && value.provider.trim().length > 0 ? { provider: value.provider } : {}),
46
47
  ...(typeof value.sandbox === "string" && value.sandbox.trim().length > 0 ? { sandbox: value.sandbox } : {}),
47
48
  tools: value.tools === "readonly" ? "readonly" : "full",
@@ -54,12 +55,12 @@ function asRoutes(value) {
54
55
  }
55
56
  const normalized = {};
56
57
  for (const [routeId, route] of Object.entries(value)) {
57
- if (!isRecord(route) || typeof route.projectRoot !== "string" || route.projectRoot.trim().length === 0) {
58
+ if (!isRecord(route)) {
58
59
  continue;
59
60
  }
60
61
  normalized[routeId] = {
61
62
  ...route,
62
- projectRoot: route.projectRoot,
63
+ ...(typeof route.projectRoot === "string" && route.projectRoot.trim().length > 0 ? { projectRoot: route.projectRoot } : {}),
63
64
  ...(route.tools === "readonly" ? { tools: "readonly" } : {}),
64
65
  ...(route.mentions === "optional" ? { mentions: "optional" } : {}),
65
66
  ...(typeof route.provider === "string" && route.provider.trim().length > 0 ? { provider: route.provider } : {}),
@@ -69,6 +70,15 @@ function asRoutes(value) {
69
70
  }
70
71
  return normalized;
71
72
  }
73
+ function asDefaultBinding(value) {
74
+ if (!isRecord(value) || typeof value.route !== "string" || value.route.trim().length === 0) {
75
+ return undefined;
76
+ }
77
+ return {
78
+ ...value,
79
+ route: value.route,
80
+ };
81
+ }
72
82
  function asBindings(value) {
73
83
  if (!isRecord(value)) {
74
84
  return {};
@@ -109,13 +119,14 @@ export function ensureGatewayConfigShape(config) {
109
119
  const normalizedSandboxesDefault = typeof config.sandboxes?.default === "string" && config.sandboxes.default.trim().length > 0
110
120
  ? config.sandboxes.default
111
121
  : "host.builtin";
112
- const routeDefaults = asRouteDefaults(config.routes?.defaults);
122
+ const routeDefaults = asRouteDefaults(config.routes?.default);
113
123
  if (!routeDefaults.provider && normalizedProvidersDefault) {
114
124
  routeDefaults.provider = normalizedProvidersDefault;
115
125
  }
116
126
  if (!routeDefaults.sandbox && normalizedSandboxesDefault) {
117
127
  routeDefaults.sandbox = normalizedSandboxesDefault;
118
128
  }
129
+ const defaultBinding = asDefaultBinding(config.bindings?.default);
119
130
  return {
120
131
  ...config,
121
132
  extensions: {
@@ -138,11 +149,12 @@ export function ensureGatewayConfigShape(config) {
138
149
  },
139
150
  routes: {
140
151
  ...(isRecord(config.routes) ? config.routes : {}),
141
- defaults: routeDefaults,
152
+ default: routeDefaults,
142
153
  items: asRoutes(config.routes?.items),
143
154
  },
144
155
  bindings: {
145
156
  ...(isRecord(config.bindings) ? config.bindings : {}),
157
+ ...(defaultBinding ? { default: defaultBinding } : {}),
146
158
  items: asBindings(config.bindings?.items),
147
159
  },
148
160
  data: {
@@ -283,11 +295,11 @@ export function setDefaultProviderIfMissingOrInvalid(config) {
283
295
  const defaultProvider = next.providers.default;
284
296
  if (defaultProvider && items[defaultProvider]) {
285
297
  config.providers = next.providers;
286
- if (!next.routes.defaults.provider) {
298
+ if (!next.routes.default.provider) {
287
299
  config.routes = {
288
300
  ...next.routes,
289
301
  defaults: {
290
- ...next.routes.defaults,
302
+ ...next.routes.default,
291
303
  provider: defaultProvider,
292
304
  },
293
305
  };
@@ -306,8 +318,8 @@ export function setDefaultProviderIfMissingOrInvalid(config) {
306
318
  };
307
319
  config.routes = {
308
320
  ...next.routes,
309
- defaults: {
310
- ...next.routes.defaults,
321
+ default: {
322
+ ...next.routes.default,
311
323
  provider: candidates[0],
312
324
  },
313
325
  };
@@ -315,7 +327,7 @@ export function setDefaultProviderIfMissingOrInvalid(config) {
315
327
  export function upsertRoute(config, routeId, profile) {
316
328
  const next = ensureGatewayConfigShape(config);
317
329
  next.routes.items[routeId] = {
318
- projectRoot: profile.projectRoot,
330
+ ...(typeof profile.projectRoot === "string" && profile.projectRoot.trim().length > 0 ? { projectRoot: profile.projectRoot } : {}),
319
331
  ...(profile.tools ? { tools: profile.tools } : {}),
320
332
  ...(profile.mentions ? { mentions: profile.mentions } : {}),
321
333
  ...(profile.provider ? { provider: profile.provider } : {}),
@@ -335,6 +347,18 @@ export function upsertBinding(config, bindingId, binding) {
335
347
  items: next.bindings.items,
336
348
  };
337
349
  }
350
+ export function setDefaultBinding(config, binding) {
351
+ const next = ensureGatewayConfigShape(config);
352
+ const normalizedBinding = binding ? structuredClone(binding) : undefined;
353
+ config.bindings = {
354
+ ...next.bindings,
355
+ ...(normalizedBinding ? { default: normalizedBinding } : {}),
356
+ items: next.bindings.items,
357
+ };
358
+ if (!normalizedBinding) {
359
+ delete config.bindings.default;
360
+ }
361
+ }
338
362
  export function listContributionIds(config) {
339
363
  const next = ensureGatewayConfigShape(config);
340
364
  return {
@@ -1,6 +1,6 @@
1
1
  import { cancel, confirm, isCancel, note, password, select, text, } from "@clack/prompts";
2
2
  import JSON5 from "json5";
3
- import { ensureGatewayConfigShape, setDefaultProviderIfMissingOrInvalid, upsertBinding, upsertConnectorInstance, upsertProviderInstance, upsertRoute, } from "./config-mutators.js";
3
+ import { ensureGatewayConfigShape, setDefaultBinding, setDefaultProviderIfMissingOrInvalid, upsertBinding, upsertConnectorInstance, upsertProviderInstance, upsertRoute, } from "./config-mutators.js";
4
4
  import { DEFAULT_DISCORD_BOT_NAME, DISCORD_CONNECTOR_CONTRIBUTION_ID, } from "./discord-config.js";
5
5
  import { promptConfigFromSchema } from "./schema-prompts.js";
6
6
  export const CONFIGURE_SECTION_VALUES = ["provider", "connector", "route", "binding", "sandbox", "data"];
@@ -147,7 +147,7 @@ async function configureProviderSection(config, context) {
147
147
  throw new Error("Configure cancelled.");
148
148
  }
149
149
  next.providers.default = String(defaultProvider);
150
- next.routes.defaults.provider = String(defaultProvider);
150
+ next.routes.default.provider = String(defaultProvider);
151
151
  }
152
152
  Object.assign(config, next);
153
153
  }
@@ -229,11 +229,32 @@ async function configureRouteSection(config) {
229
229
  }
230
230
  const routeId = String(targetRoute) === "__new" ? await requiredText("New route ID", "main") : String(targetRoute);
231
231
  const existing = routeItems[routeId];
232
- const projectRoot = await requiredText("projectRoot", existing?.projectRoot ?? process.cwd());
232
+ const defaultProjectRoot = next.routes.default.projectRoot;
233
+ let projectRoot = existing?.projectRoot;
234
+ if (defaultProjectRoot) {
235
+ const projectRootMode = await select({
236
+ message: "projectRoot",
237
+ options: [
238
+ { value: "__default", label: `Use route default (${defaultProjectRoot})` },
239
+ { value: "__custom", label: "Set explicit projectRoot" },
240
+ ],
241
+ initialValue: existing?.projectRoot ? "__custom" : "__default",
242
+ });
243
+ if (isCancel(projectRootMode)) {
244
+ cancel("Configure cancelled.");
245
+ throw new Error("Configure cancelled.");
246
+ }
247
+ projectRoot = projectRootMode === "__custom"
248
+ ? await requiredText("projectRoot", existing?.projectRoot ?? defaultProjectRoot)
249
+ : undefined;
250
+ }
251
+ else {
252
+ projectRoot = await requiredText("projectRoot", existing?.projectRoot ?? process.cwd());
253
+ }
233
254
  const tools = await select({
234
255
  message: "tools",
235
256
  options: [
236
- { value: "__default", label: `Use route default (${next.routes.defaults.tools ?? "full"})` },
257
+ { value: "__default", label: `Use route default (${next.routes.default.tools ?? "full"})` },
237
258
  { value: "full", label: "full" },
238
259
  { value: "readonly", label: "readonly" },
239
260
  ],
@@ -246,7 +267,7 @@ async function configureRouteSection(config) {
246
267
  const mentions = await select({
247
268
  message: "mentions",
248
269
  options: [
249
- { value: "__default", label: `Use route default (${next.routes.defaults.mentions ?? "required"})` },
270
+ { value: "__default", label: `Use route default (${next.routes.default.mentions ?? "required"})` },
250
271
  { value: "required", label: "required" },
251
272
  { value: "optional", label: "optional" },
252
273
  ],
@@ -261,7 +282,7 @@ async function configureRouteSection(config) {
261
282
  ? await select({
262
283
  message: "provider",
263
284
  options: [
264
- { value: "__default", label: `Use route default (${(next.routes.defaults.provider ?? next.providers.default) || "(unset)"})` },
285
+ { value: "__default", label: `Use route default (${(next.routes.default.provider ?? next.providers.default) || "(unset)"})` },
265
286
  ...providerIds.map((id) => ({ value: id, label: id })),
266
287
  ],
267
288
  initialValue: existing?.provider ?? "__default",
@@ -275,7 +296,7 @@ async function configureRouteSection(config) {
275
296
  const sandboxValue = await select({
276
297
  message: "sandbox",
277
298
  options: [
278
- { value: "__default", label: `Use route default (${next.routes.defaults.sandbox ?? next.sandboxes.default})` },
299
+ { value: "__default", label: `Use route default (${next.routes.default.sandbox ?? next.sandboxes.default})` },
279
300
  ...sandboxIds.map((id) => ({ value: id, label: id })),
280
301
  ],
281
302
  initialValue: existing?.sandbox ?? "__default",
@@ -286,7 +307,7 @@ async function configureRouteSection(config) {
286
307
  }
287
308
  const systemPromptFile = await optionalText("systemPromptFile (optional)", existing?.systemPromptFile ?? "");
288
309
  upsertRoute(next, routeId, {
289
- projectRoot,
310
+ ...(projectRoot ? { projectRoot } : {}),
290
311
  ...(tools !== "__default" ? { tools: String(tools) } : {}),
291
312
  ...(mentions !== "__default" ? { mentions: String(mentions) } : {}),
292
313
  ...(providerValue !== "__default" ? { provider: String(providerValue) } : {}),
@@ -306,19 +327,39 @@ async function configureBindingSection(config) {
306
327
  throw new Error("No routes found. Configure routes first.");
307
328
  }
308
329
  const targetBinding = bindingChoices.length === 0
309
- ? "__new"
330
+ ? (next.bindings.default ? "__default" : "__new")
310
331
  : await select({
311
332
  message: "Select binding",
312
333
  options: [
334
+ { value: "__default", label: next.bindings.default ? "Edit default direct-message binding" : "Create default direct-message binding" },
313
335
  ...bindingChoices.map((id) => ({ value: id, label: id })),
314
336
  { value: "__new", label: "Create new binding" },
315
337
  ],
316
- initialValue: bindingChoices[0],
338
+ initialValue: next.bindings.default ? "__default" : bindingChoices[0],
317
339
  });
318
340
  if (isCancel(targetBinding)) {
319
341
  cancel("Configure cancelled.");
320
342
  throw new Error("Configure cancelled.");
321
343
  }
344
+ const routeIds = Object.keys(next.routes.items).sort((a, b) => a.localeCompare(b));
345
+ if (String(targetBinding) === "__default") {
346
+ const defaultRouteId = await select({
347
+ message: "Default direct-message route",
348
+ options: routeIds.map((id) => ({ value: id, label: id })),
349
+ initialValue: next.bindings.default?.route && routeIds.includes(next.bindings.default.route)
350
+ ? next.bindings.default.route
351
+ : routeIds[0],
352
+ });
353
+ if (isCancel(defaultRouteId)) {
354
+ cancel("Configure cancelled.");
355
+ throw new Error("Configure cancelled.");
356
+ }
357
+ setDefaultBinding(next, {
358
+ route: String(defaultRouteId),
359
+ });
360
+ Object.assign(config, next);
361
+ return;
362
+ }
322
363
  const bindingId = String(targetBinding) === "__new"
323
364
  ? await requiredText("New binding ID", "discord.main.main")
324
365
  : String(targetBinding);
@@ -346,7 +387,6 @@ async function configureBindingSection(config) {
346
387
  throw new Error("Configure cancelled.");
347
388
  }
348
389
  const sourceId = await requiredText("source.id", existing?.source.id);
349
- const routeIds = Object.keys(next.routes.items).sort((a, b) => a.localeCompare(b));
350
390
  const routeId = await select({
351
391
  message: "route",
352
392
  options: routeIds.map((id) => ({ value: id, label: id })),
@@ -386,7 +426,7 @@ async function configureSandboxSection(config) {
386
426
  throw new Error("Configure cancelled.");
387
427
  }
388
428
  next.sandboxes.default = String(defaultSandbox);
389
- next.routes.defaults.sandbox = String(defaultSandbox);
429
+ next.routes.default.sandbox = String(defaultSandbox);
390
430
  Object.assign(config, next);
391
431
  }
392
432
  async function configureDataSection(config) {