@dobby.ai/dobby 0.1.0 → 0.1.1

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 (76) hide show
  1. package/.env.example +0 -1
  2. package/AGENTS.md +7 -7
  3. package/README.md +64 -32
  4. package/config/gateway.example.json +10 -6
  5. package/dist/plugins/connector-discord/src/mapper.js +75 -0
  6. package/dist/src/cli/commands/doctor.js +81 -2
  7. package/dist/src/cli/commands/extension.js +3 -1
  8. package/dist/src/cli/commands/init.js +43 -173
  9. package/dist/src/cli/commands/topology.js +38 -14
  10. package/dist/src/cli/program.js +15 -131
  11. package/dist/src/cli/shared/config-io.js +3 -31
  12. package/dist/src/cli/shared/config-mutators.js +33 -9
  13. package/dist/src/cli/shared/configure-sections.js +52 -12
  14. package/dist/src/cli/shared/init-catalog.js +89 -46
  15. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  16. package/dist/src/cli/shared/schema-prompts.js +26 -2
  17. package/dist/src/cli/tests/config-io.test.js +5 -5
  18. package/dist/src/cli/tests/discord-mapper.test.js +90 -0
  19. package/dist/src/cli/tests/doctor.test.js +145 -0
  20. package/dist/src/cli/tests/init-catalog.test.js +108 -61
  21. package/dist/src/cli/tests/program-options.test.js +14 -28
  22. package/dist/src/cli/tests/routing-config.test.js +59 -4
  23. package/dist/src/core/gateway.js +3 -1
  24. package/dist/src/core/routing.js +53 -38
  25. package/dist/src/main.js +0 -0
  26. package/dist/src/shared/dobby-repo.js +40 -0
  27. package/docs/RUNBOOK.md +28 -27
  28. package/package.json +3 -2
  29. package/plugins/connector-discord/package-lock.json +2 -2
  30. package/plugins/connector-discord/package.json +1 -1
  31. package/plugins/connector-discord/src/connector.ts +0 -5
  32. package/plugins/connector-discord/src/mapper.ts +3 -4
  33. package/plugins/connector-feishu/package-lock.json +2 -2
  34. package/plugins/connector-feishu/package.json +1 -1
  35. package/plugins/plugin-sdk/package-lock.json +2 -2
  36. package/plugins/plugin-sdk/package.json +1 -1
  37. package/plugins/provider-claude/package-lock.json +2 -2
  38. package/plugins/provider-claude/package.json +1 -1
  39. package/plugins/provider-claude-cli/package-lock.json +2 -2
  40. package/plugins/provider-claude-cli/package.json +1 -1
  41. package/plugins/provider-pi/package-lock.json +2 -2
  42. package/plugins/provider-pi/package.json +1 -1
  43. package/plugins/provider-pi/src/contribution.ts +139 -9
  44. package/src/cli/commands/doctor.ts +103 -2
  45. package/src/cli/commands/extension.ts +3 -1
  46. package/src/cli/commands/init.ts +45 -230
  47. package/src/cli/commands/topology.ts +48 -16
  48. package/src/cli/program.ts +16 -167
  49. package/src/cli/shared/config-io.ts +3 -35
  50. package/src/cli/shared/config-mutators.ts +39 -9
  51. package/src/cli/shared/config-types.ts +10 -2
  52. package/src/cli/shared/configure-sections.ts +55 -11
  53. package/src/cli/shared/init-catalog.ts +126 -66
  54. package/src/cli/shared/local-extension-specs.ts +108 -0
  55. package/src/cli/shared/schema-prompts.ts +30 -1
  56. package/src/cli/tests/config-io.test.ts +5 -5
  57. package/src/cli/tests/discord-mapper.test.ts +128 -0
  58. package/src/cli/tests/doctor.test.ts +149 -0
  59. package/src/cli/tests/init-catalog.test.ts +112 -64
  60. package/src/cli/tests/program-options.test.ts +14 -32
  61. package/src/cli/tests/routing-config.test.ts +76 -4
  62. package/src/core/gateway.ts +3 -1
  63. package/src/core/routing.ts +70 -45
  64. package/src/core/types.ts +8 -2
  65. package/src/shared/dobby-repo.ts +48 -0
  66. package/config/models.custom.example.json +0 -27
  67. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  68. package/dist/src/cli/shared/config-path.js +0 -207
  69. package/dist/src/cli/shared/init-models-file.js +0 -65
  70. package/dist/src/cli/shared/presets.js +0 -86
  71. package/dist/src/cli/tests/config-path.test.js +0 -21
  72. package/dist/src/cli/tests/discord-config.test.js +0 -23
  73. package/dist/src/cli/tests/presets.test.js +0 -41
  74. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  75. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  76. package/src/cli/shared/init-models-file.ts +0 -77
@@ -1,12 +1,12 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
1
3
  import { Command } from "commander";
2
4
  import {
3
- runConfigEditCommand,
4
5
  runConfigListCommand,
5
6
  runConfigSchemaListCommand,
6
7
  runConfigSchemaShowCommand,
7
8
  runConfigShowCommand,
8
9
  } from "./commands/config.js";
9
- import { runConfigureCommand } from "./commands/configure.js";
10
10
  import {
11
11
  runCronAddCommand,
12
12
  runCronListCommand,
@@ -25,16 +25,18 @@ import {
25
25
  } from "./commands/extension.js";
26
26
  import { runInitCommand } from "./commands/init.js";
27
27
  import { runStartCommand } from "./commands/start.js";
28
- import {
29
- runBindingListCommand,
30
- runBindingRemoveCommand,
31
- runBindingSetCommand,
32
- runBotListCommand,
33
- runBotSetCommand,
34
- runRouteListCommand,
35
- runRouteRemoveCommand,
36
- runRouteSetCommand,
37
- } from "./commands/topology.js";
28
+
29
+ function loadCliVersion(): string {
30
+ const candidates = [
31
+ fileURLToPath(new URL("../../package.json", import.meta.url)),
32
+ fileURLToPath(new URL("../../../package.json", import.meta.url)),
33
+ ];
34
+ const fallbackCandidate = fileURLToPath(new URL("../../package.json", import.meta.url));
35
+ const packageJsonPath = candidates.find((candidate) => existsSync(candidate)) ?? fallbackCandidate;
36
+ return JSON.parse(readFileSync(packageJsonPath, "utf-8")).version as string;
37
+ }
38
+
39
+ const CLI_VERSION = loadCliVersion();
38
40
 
39
41
  /**
40
42
  * Builds the top-level dobby CLI program and registers all subcommands.
@@ -43,6 +45,7 @@ export function buildProgram(): Command {
43
45
  const program = new Command();
44
46
  program
45
47
  .name("dobby")
48
+ .version(CLI_VERSION)
46
49
  .description("Discord-first local agent gateway")
47
50
  .showHelpAfterError()
48
51
  .action(async () => {
@@ -63,146 +66,7 @@ export function buildProgram(): Command {
63
66
  await runInitCommand();
64
67
  });
65
68
 
66
- program
67
- .command("configure")
68
- .description("Interactive configuration wizard")
69
- .option(
70
- "--section <section>",
71
- "Config section (repeatable): provider|connector|route|binding|sandbox|data",
72
- (value: string, previous: string[]) => [...previous, value],
73
- [] as string[],
74
- )
75
- .action(async (opts) => {
76
- await runConfigureCommand({
77
- sections: opts.section as string[],
78
- });
79
- });
80
-
81
- const botCommand = program.command("bot").description("Manage bot connector settings");
82
-
83
- botCommand
84
- .command("list")
85
- .description("List configured bot connectors")
86
- .option("--json", "Output JSON", false)
87
- .action(async (opts) => {
88
- await runBotListCommand({
89
- json: Boolean(opts.json),
90
- });
91
- });
92
-
93
- botCommand
94
- .command("set")
95
- .description("Update one bot connector")
96
- .argument("<connectorId>", "Connector instance ID")
97
- .option("--name <name>", "Discord botName")
98
- .option("--token <token>", "Discord botToken")
99
- .action(async (connectorId: string, opts) => {
100
- await runBotSetCommand({
101
- connectorId,
102
- ...(typeof opts.name === "string" ? { name: opts.name as string } : {}),
103
- ...(typeof opts.token === "string" ? { token: opts.token as string } : {}),
104
- });
105
- });
106
-
107
- const bindingCommand = program.command("binding").description("Manage connector source-route bindings");
108
-
109
- bindingCommand
110
- .command("list")
111
- .description("List bindings")
112
- .option("--connector <id>", "Filter by connector instance ID")
113
- .option("--json", "Output JSON", false)
114
- .action(async (opts) => {
115
- await runBindingListCommand({
116
- ...(typeof opts.connector === "string" ? { connectorId: opts.connector as string } : {}),
117
- json: Boolean(opts.json),
118
- });
119
- });
120
-
121
- bindingCommand
122
- .command("set")
123
- .description("Create or update one binding")
124
- .argument("<bindingId>", "Binding ID")
125
- .requiredOption("--connector <id>", "Connector instance ID")
126
- .requiredOption("--source-type <type>", "Source type: channel|chat")
127
- .requiredOption("--source-id <id>", "Source ID")
128
- .requiredOption("--route <id>", "Route ID")
129
- .action(async (bindingId: string, opts) => {
130
- if (opts.sourceType !== "channel" && opts.sourceType !== "chat") {
131
- throw new Error("--source-type must be channel or chat");
132
- }
133
-
134
- await runBindingSetCommand({
135
- bindingId,
136
- connectorId: opts.connector as string,
137
- sourceType: opts.sourceType as "channel" | "chat",
138
- sourceId: opts.sourceId as string,
139
- routeId: opts.route as string,
140
- });
141
- });
142
-
143
- bindingCommand
144
- .command("remove")
145
- .description("Remove one binding")
146
- .argument("<bindingId>", "Binding ID")
147
- .action(async (bindingId: string) => {
148
- await runBindingRemoveCommand({
149
- bindingId,
150
- });
151
- });
152
-
153
- const routeCommand = program.command("route").description("Manage route profiles");
154
-
155
- routeCommand
156
- .command("list")
157
- .description("List route profiles")
158
- .option("--json", "Output JSON", false)
159
- .action(async (opts) => {
160
- await runRouteListCommand({
161
- json: Boolean(opts.json),
162
- });
163
- });
164
-
165
- routeCommand
166
- .command("set")
167
- .description("Create or update one route")
168
- .argument("<routeId>", "Route ID")
169
- .option("--project-root <path>", "Route project root")
170
- .option("--tools <profile>", "Route tools profile: full|readonly")
171
- .option("--provider <id>", "Provider instance ID")
172
- .option("--sandbox <id>", "Sandbox instance ID")
173
- .option("--mentions <policy>", "Mention policy: required|optional")
174
- .action(async (routeId: string, opts) => {
175
- if (
176
- typeof opts.mentions === "string"
177
- && opts.mentions !== "required"
178
- && opts.mentions !== "optional"
179
- ) {
180
- throw new Error("--mentions must be required or optional");
181
- }
182
-
183
- await runRouteSetCommand({
184
- routeId,
185
- ...(typeof opts.projectRoot === "string" ? { projectRoot: opts.projectRoot as string } : {}),
186
- ...(typeof opts.tools === "string" ? { tools: opts.tools as string } : {}),
187
- ...(typeof opts.provider === "string" ? { providerId: opts.provider as string } : {}),
188
- ...(typeof opts.sandbox === "string" ? { sandboxId: opts.sandbox as string } : {}),
189
- ...(typeof opts.mentions === "string" ? { mentions: opts.mentions as "required" | "optional" } : {}),
190
- });
191
- });
192
-
193
- routeCommand
194
- .command("remove")
195
- .description("Remove one route")
196
- .argument("<routeId>", "Route ID")
197
- .option("--cascade-bindings", "Remove bindings that reference this route", false)
198
- .action(async (routeId: string, opts) => {
199
- await runRouteRemoveCommand({
200
- routeId,
201
- cascadeBindings: Boolean(opts.cascadeBindings),
202
- });
203
- });
204
-
205
- const configCommand = program.command("config").description("Inspect and edit config");
69
+ const configCommand = program.command("config").description("Inspect config");
206
70
 
207
71
  configCommand
208
72
  .command("show")
@@ -228,21 +92,6 @@ export function buildProgram(): Command {
228
92
  });
229
93
  });
230
94
 
231
- configCommand
232
- .command("edit")
233
- .description("Interactive edit for high-frequency sections")
234
- .option(
235
- "--section <section>",
236
- "Edit section (repeatable): provider|connector|route|binding",
237
- (value: string, previous: string[]) => [...previous, value],
238
- [] as string[],
239
- )
240
- .action(async (opts) => {
241
- await runConfigEditCommand({
242
- sections: opts.section as string[],
243
- });
244
- });
245
-
246
95
  const configSchemaCommand = configCommand.command("schema").description("Inspect extension config schemas");
247
96
 
248
97
  configSchemaCommand
@@ -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
  import type { RawGatewayConfig } from "./config-types.js";
7
7
 
8
8
  /**
@@ -37,27 +37,6 @@ interface ResolvedConfigPathInfo {
37
37
  source: ConfigPathSource;
38
38
  }
39
39
 
40
- /**
41
- * Returns true when a directory looks like the dobby repository root.
42
- */
43
- function isDobbyRepoRoot(candidateDir: string): boolean {
44
- const packageJsonPath = resolve(candidateDir, "package.json");
45
- const repoConfigPath = resolve(candidateDir, "config", "gateway.json");
46
- const localExtensionsScriptPath = resolve(candidateDir, "scripts", "local-extensions.mjs");
47
-
48
- if (!existsSync(packageJsonPath) || !existsSync(repoConfigPath) || !existsSync(localExtensionsScriptPath)) {
49
- return false;
50
- }
51
-
52
- try {
53
- const packageJsonRaw = readFileSync(packageJsonPath, "utf-8");
54
- const parsed = JSON.parse(packageJsonRaw) as { name?: unknown };
55
- return parsed.name === "dobby";
56
- } catch {
57
- return false;
58
- }
59
- }
60
-
61
40
  function resolveConfigBaseDir(configPath: string): string {
62
41
  const absoluteConfigPath = resolve(configPath);
63
42
  const configDir = dirname(absoluteConfigPath);
@@ -74,19 +53,8 @@ function resolveConfigBaseDir(configPath: string): string {
74
53
  * Scans current directory and ancestors to find a local dobby repo config path.
75
54
  */
76
55
  function findDobbyRepoConfigPath(startDir: string): string | null {
77
- let currentDir = resolve(startDir);
78
-
79
- while (true) {
80
- if (isDobbyRepoRoot(currentDir)) {
81
- return resolve(currentDir, "config", "gateway.json");
82
- }
83
-
84
- const parentDir = dirname(currentDir);
85
- if (parentDir === currentDir) {
86
- return null;
87
- }
88
- currentDir = parentDir;
89
- }
56
+ const repoRoot = findDobbyRepoRoot(startDir);
57
+ return repoRoot ? resolve(repoRoot, "config", "gateway.json") : null;
90
58
  }
91
59
 
92
60
  /**
@@ -4,6 +4,7 @@ import type {
4
4
  ContributionTemplatesByKind,
5
5
  NormalizedGatewayConfig,
6
6
  RawBindingConfig,
7
+ RawDefaultBindingConfig,
7
8
  RawExtensionItemConfig,
8
9
  RawGatewayConfig,
9
10
  RawRouteDefaults,
@@ -65,6 +66,7 @@ function asRouteDefaults(value: unknown): RawRouteDefaults {
65
66
  }
66
67
 
67
68
  return {
69
+ ...(typeof value.projectRoot === "string" && value.projectRoot.trim().length > 0 ? { projectRoot: value.projectRoot } : {}),
68
70
  ...(typeof value.provider === "string" && value.provider.trim().length > 0 ? { provider: value.provider } : {}),
69
71
  ...(typeof value.sandbox === "string" && value.sandbox.trim().length > 0 ? { sandbox: value.sandbox } : {}),
70
72
  tools: value.tools === "readonly" ? "readonly" : "full",
@@ -79,13 +81,13 @@ function asRoutes(value: unknown): Record<string, RawRouteProfile> {
79
81
 
80
82
  const normalized: Record<string, RawRouteProfile> = {};
81
83
  for (const [routeId, route] of Object.entries(value)) {
82
- if (!isRecord(route) || typeof route.projectRoot !== "string" || route.projectRoot.trim().length === 0) {
84
+ if (!isRecord(route)) {
83
85
  continue;
84
86
  }
85
87
 
86
88
  normalized[routeId] = {
87
89
  ...route,
88
- projectRoot: route.projectRoot,
90
+ ...(typeof route.projectRoot === "string" && route.projectRoot.trim().length > 0 ? { projectRoot: route.projectRoot } : {}),
89
91
  ...(route.tools === "readonly" ? { tools: "readonly" as const } : {}),
90
92
  ...(route.mentions === "optional" ? { mentions: "optional" as const } : {}),
91
93
  ...(typeof route.provider === "string" && route.provider.trim().length > 0 ? { provider: route.provider } : {}),
@@ -97,6 +99,17 @@ function asRoutes(value: unknown): Record<string, RawRouteProfile> {
97
99
  return normalized;
98
100
  }
99
101
 
102
+ function asDefaultBinding(value: unknown): RawDefaultBindingConfig | undefined {
103
+ if (!isRecord(value) || typeof value.route !== "string" || value.route.trim().length === 0) {
104
+ return undefined;
105
+ }
106
+
107
+ return {
108
+ ...value,
109
+ route: value.route,
110
+ };
111
+ }
112
+
100
113
  function asBindings(value: unknown): Record<string, RawBindingConfig> {
101
114
  if (!isRecord(value)) {
102
115
  return {};
@@ -147,7 +160,7 @@ export function ensureGatewayConfigShape(config: RawGatewayConfig): NormalizedGa
147
160
  ? config.sandboxes.default
148
161
  : "host.builtin";
149
162
 
150
- const routeDefaults = asRouteDefaults(config.routes?.defaults);
163
+ const routeDefaults = asRouteDefaults(config.routes?.default);
151
164
  if (!routeDefaults.provider && normalizedProvidersDefault) {
152
165
  routeDefaults.provider = normalizedProvidersDefault;
153
166
  }
@@ -155,6 +168,8 @@ export function ensureGatewayConfigShape(config: RawGatewayConfig): NormalizedGa
155
168
  routeDefaults.sandbox = normalizedSandboxesDefault;
156
169
  }
157
170
 
171
+ const defaultBinding = asDefaultBinding(config.bindings?.default);
172
+
158
173
  return {
159
174
  ...config,
160
175
  extensions: {
@@ -177,11 +192,12 @@ export function ensureGatewayConfigShape(config: RawGatewayConfig): NormalizedGa
177
192
  },
178
193
  routes: {
179
194
  ...((isRecord(config.routes) ? config.routes : {}) as Record<string, unknown>),
180
- defaults: routeDefaults,
195
+ default: routeDefaults,
181
196
  items: asRoutes(config.routes?.items),
182
197
  },
183
198
  bindings: {
184
199
  ...((isRecord(config.bindings) ? config.bindings : {}) as Record<string, unknown>),
200
+ ...(defaultBinding ? { default: defaultBinding } : {}),
185
201
  items: asBindings(config.bindings?.items),
186
202
  },
187
203
  data: {
@@ -368,11 +384,11 @@ export function setDefaultProviderIfMissingOrInvalid(config: RawGatewayConfig):
368
384
 
369
385
  if (defaultProvider && items[defaultProvider]) {
370
386
  config.providers = next.providers;
371
- if (!next.routes.defaults.provider) {
387
+ if (!next.routes.default.provider) {
372
388
  config.routes = {
373
389
  ...next.routes,
374
390
  defaults: {
375
- ...next.routes.defaults,
391
+ ...next.routes.default,
376
392
  provider: defaultProvider,
377
393
  },
378
394
  };
@@ -393,8 +409,8 @@ export function setDefaultProviderIfMissingOrInvalid(config: RawGatewayConfig):
393
409
  };
394
410
  config.routes = {
395
411
  ...next.routes,
396
- defaults: {
397
- ...next.routes.defaults,
412
+ default: {
413
+ ...next.routes.default,
398
414
  provider: candidates[0]!,
399
415
  },
400
416
  };
@@ -403,7 +419,7 @@ export function setDefaultProviderIfMissingOrInvalid(config: RawGatewayConfig):
403
419
  export function upsertRoute(config: RawGatewayConfig, routeId: string, profile: RawRouteProfile): void {
404
420
  const next = ensureGatewayConfigShape(config);
405
421
  next.routes.items[routeId] = {
406
- projectRoot: profile.projectRoot,
422
+ ...(typeof profile.projectRoot === "string" && profile.projectRoot.trim().length > 0 ? { projectRoot: profile.projectRoot } : {}),
407
423
  ...(profile.tools ? { tools: profile.tools } : {}),
408
424
  ...(profile.mentions ? { mentions: profile.mentions } : {}),
409
425
  ...(profile.provider ? { provider: profile.provider } : {}),
@@ -425,6 +441,20 @@ export function upsertBinding(config: RawGatewayConfig, bindingId: string, bindi
425
441
  };
426
442
  }
427
443
 
444
+ export function setDefaultBinding(config: RawGatewayConfig, binding: RawDefaultBindingConfig | undefined): void {
445
+ const next = ensureGatewayConfigShape(config);
446
+ const normalizedBinding = binding ? structuredClone(binding) : undefined;
447
+ config.bindings = {
448
+ ...next.bindings,
449
+ ...(normalizedBinding ? { default: normalizedBinding } : {}),
450
+ items: next.bindings.items,
451
+ };
452
+
453
+ if (!normalizedBinding) {
454
+ delete config.bindings.default;
455
+ }
456
+ }
457
+
428
458
  export function listContributionIds(config: RawGatewayConfig): {
429
459
  providers: string[];
430
460
  connectors: string[];
@@ -9,6 +9,7 @@ export interface RawExtensionItemConfig {
9
9
  }
10
10
 
11
11
  export interface RawRouteDefaults {
12
+ projectRoot?: string;
12
13
  provider?: string;
13
14
  sandbox?: string;
14
15
  tools?: "full" | "readonly";
@@ -17,7 +18,7 @@ export interface RawRouteDefaults {
17
18
  }
18
19
 
19
20
  export interface RawRouteProfile {
20
- projectRoot: string;
21
+ projectRoot?: string;
21
22
  tools?: "full" | "readonly";
22
23
  systemPromptFile?: string;
23
24
  mentions?: "required" | "optional";
@@ -26,6 +27,11 @@ export interface RawRouteProfile {
26
27
  [key: string]: unknown;
27
28
  }
28
29
 
30
+ export interface RawDefaultBindingConfig {
31
+ route: string;
32
+ [key: string]: unknown;
33
+ }
34
+
29
35
  export interface RawBindingConfig {
30
36
  connector: string;
31
37
  source: {
@@ -62,6 +68,7 @@ export interface RawGatewayConfig {
62
68
  [key: string]: unknown;
63
69
  };
64
70
  bindings?: {
71
+ default?: RawDefaultBindingConfig;
65
72
  items?: Record<string, RawBindingConfig>;
66
73
  [key: string]: unknown;
67
74
  };
@@ -93,11 +100,12 @@ export interface NormalizedGatewayConfig extends RawGatewayConfig {
93
100
  [key: string]: unknown;
94
101
  };
95
102
  routes: {
96
- defaults: RawRouteDefaults;
103
+ default: RawRouteDefaults;
97
104
  items: Record<string, RawRouteProfile>;
98
105
  [key: string]: unknown;
99
106
  };
100
107
  bindings: {
108
+ default?: RawDefaultBindingConfig;
101
109
  items: Record<string, RawBindingConfig>;
102
110
  [key: string]: unknown;
103
111
  };
@@ -10,6 +10,7 @@ import {
10
10
  import JSON5 from "json5";
11
11
  import {
12
12
  ensureGatewayConfigShape,
13
+ setDefaultBinding,
13
14
  setDefaultProviderIfMissingOrInvalid,
14
15
  upsertBinding,
15
16
  upsertConnectorInstance,
@@ -220,7 +221,7 @@ async function configureProviderSection(config: RawGatewayConfig, context?: Conf
220
221
  throw new Error("Configure cancelled.");
221
222
  }
222
223
  next.providers.default = String(defaultProvider);
223
- next.routes.defaults.provider = String(defaultProvider);
224
+ next.routes.default.provider = String(defaultProvider);
224
225
  }
225
226
 
226
227
  Object.assign(config, next);
@@ -321,11 +322,32 @@ async function configureRouteSection(config: RawGatewayConfig): Promise<void> {
321
322
  const routeId = String(targetRoute) === "__new" ? await requiredText("New route ID", "main") : String(targetRoute);
322
323
  const existing = routeItems[routeId];
323
324
 
324
- const projectRoot = await requiredText("projectRoot", existing?.projectRoot ?? process.cwd());
325
+ const defaultProjectRoot = next.routes.default.projectRoot;
326
+ let projectRoot = existing?.projectRoot;
327
+ if (defaultProjectRoot) {
328
+ const projectRootMode = await select({
329
+ message: "projectRoot",
330
+ options: [
331
+ { value: "__default", label: `Use route default (${defaultProjectRoot})` },
332
+ { value: "__custom", label: "Set explicit projectRoot" },
333
+ ],
334
+ initialValue: existing?.projectRoot ? "__custom" : "__default",
335
+ });
336
+ if (isCancel(projectRootMode)) {
337
+ cancel("Configure cancelled.");
338
+ throw new Error("Configure cancelled.");
339
+ }
340
+
341
+ projectRoot = projectRootMode === "__custom"
342
+ ? await requiredText("projectRoot", existing?.projectRoot ?? defaultProjectRoot)
343
+ : undefined;
344
+ } else {
345
+ projectRoot = await requiredText("projectRoot", existing?.projectRoot ?? process.cwd());
346
+ }
325
347
  const tools = await select({
326
348
  message: "tools",
327
349
  options: [
328
- { value: "__default", label: `Use route default (${next.routes.defaults.tools ?? "full"})` },
350
+ { value: "__default", label: `Use route default (${next.routes.default.tools ?? "full"})` },
329
351
  { value: "full", label: "full" },
330
352
  { value: "readonly", label: "readonly" },
331
353
  ],
@@ -339,7 +361,7 @@ async function configureRouteSection(config: RawGatewayConfig): Promise<void> {
339
361
  const mentions = await select({
340
362
  message: "mentions",
341
363
  options: [
342
- { value: "__default", label: `Use route default (${next.routes.defaults.mentions ?? "required"})` },
364
+ { value: "__default", label: `Use route default (${next.routes.default.mentions ?? "required"})` },
343
365
  { value: "required", label: "required" },
344
366
  { value: "optional", label: "optional" },
345
367
  ],
@@ -355,7 +377,7 @@ async function configureRouteSection(config: RawGatewayConfig): Promise<void> {
355
377
  ? await select({
356
378
  message: "provider",
357
379
  options: [
358
- { value: "__default", label: `Use route default (${(next.routes.defaults.provider ?? next.providers.default) || "(unset)"})` },
380
+ { value: "__default", label: `Use route default (${(next.routes.default.provider ?? next.providers.default) || "(unset)"})` },
359
381
  ...providerIds.map((id) => ({ value: id, label: id })),
360
382
  ],
361
383
  initialValue: existing?.provider ?? "__default",
@@ -370,7 +392,7 @@ async function configureRouteSection(config: RawGatewayConfig): Promise<void> {
370
392
  const sandboxValue = await select({
371
393
  message: "sandbox",
372
394
  options: [
373
- { value: "__default", label: `Use route default (${next.routes.defaults.sandbox ?? next.sandboxes.default})` },
395
+ { value: "__default", label: `Use route default (${next.routes.default.sandbox ?? next.sandboxes.default})` },
374
396
  ...sandboxIds.map((id) => ({ value: id, label: id })),
375
397
  ],
376
398
  initialValue: existing?.sandbox ?? "__default",
@@ -383,7 +405,7 @@ async function configureRouteSection(config: RawGatewayConfig): Promise<void> {
383
405
  const systemPromptFile = await optionalText("systemPromptFile (optional)", existing?.systemPromptFile ?? "");
384
406
 
385
407
  upsertRoute(next, routeId, {
386
- projectRoot,
408
+ ...(projectRoot ? { projectRoot } : {}),
387
409
  ...(tools !== "__default" ? { tools: String(tools) as "full" | "readonly" } : {}),
388
410
  ...(mentions !== "__default" ? { mentions: String(mentions) as "required" | "optional" } : {}),
389
411
  ...(providerValue !== "__default" ? { provider: String(providerValue) } : {}),
@@ -407,20 +429,43 @@ async function configureBindingSection(config: RawGatewayConfig): Promise<void>
407
429
  }
408
430
 
409
431
  const targetBinding = bindingChoices.length === 0
410
- ? "__new"
432
+ ? (next.bindings.default ? "__default" : "__new")
411
433
  : await select({
412
434
  message: "Select binding",
413
435
  options: [
436
+ { value: "__default", label: next.bindings.default ? "Edit default direct-message binding" : "Create default direct-message binding" },
414
437
  ...bindingChoices.map((id) => ({ value: id, label: id })),
415
438
  { value: "__new", label: "Create new binding" },
416
439
  ],
417
- initialValue: bindingChoices[0],
440
+ initialValue: next.bindings.default ? "__default" : bindingChoices[0],
418
441
  });
419
442
  if (isCancel(targetBinding)) {
420
443
  cancel("Configure cancelled.");
421
444
  throw new Error("Configure cancelled.");
422
445
  }
423
446
 
447
+ const routeIds = Object.keys(next.routes.items).sort((a, b) => a.localeCompare(b));
448
+ if (String(targetBinding) === "__default") {
449
+ const defaultRouteId = await select({
450
+ message: "Default direct-message route",
451
+ options: routeIds.map((id) => ({ value: id, label: id })),
452
+ initialValue:
453
+ next.bindings.default?.route && routeIds.includes(next.bindings.default.route)
454
+ ? next.bindings.default.route
455
+ : routeIds[0],
456
+ });
457
+ if (isCancel(defaultRouteId)) {
458
+ cancel("Configure cancelled.");
459
+ throw new Error("Configure cancelled.");
460
+ }
461
+
462
+ setDefaultBinding(next, {
463
+ route: String(defaultRouteId),
464
+ });
465
+ Object.assign(config, next);
466
+ return;
467
+ }
468
+
424
469
  const bindingId = String(targetBinding) === "__new"
425
470
  ? await requiredText("New binding ID", "discord.main.main")
426
471
  : String(targetBinding);
@@ -451,7 +496,6 @@ async function configureBindingSection(config: RawGatewayConfig): Promise<void>
451
496
  }
452
497
 
453
498
  const sourceId = await requiredText("source.id", existing?.source.id);
454
- const routeIds = Object.keys(next.routes.items).sort((a, b) => a.localeCompare(b));
455
499
  const routeId = await select({
456
500
  message: "route",
457
501
  options: routeIds.map((id) => ({ value: id, label: id })),
@@ -501,7 +545,7 @@ async function configureSandboxSection(config: RawGatewayConfig): Promise<void>
501
545
  }
502
546
 
503
547
  next.sandboxes.default = String(defaultSandbox);
504
- next.routes.defaults.sandbox = String(defaultSandbox);
548
+ next.routes.default.sandbox = String(defaultSandbox);
505
549
  Object.assign(config, next);
506
550
  }
507
551