@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
@@ -7,6 +7,33 @@ import { ensureGatewayConfigShape, setDefaultProviderIfMissingOrInvalid, } from
7
7
  import { DISCORD_CONNECTOR_CONTRIBUTION_ID } from "../shared/discord-config.js";
8
8
  import { readRawConfig, resolveConfigPath, resolveDataRootDir, writeConfigWithValidation } from "../shared/config-io.js";
9
9
  import { createLogger } from "../shared/runtime.js";
10
+ function isPlaceholderValue(value) {
11
+ if (typeof value !== "string") {
12
+ return false;
13
+ }
14
+ const normalized = value.trim().toUpperCase();
15
+ return normalized.includes("REPLACE_WITH_") || normalized.includes("YOUR_");
16
+ }
17
+ function isCredentialLikeKey(key) {
18
+ return /(?:token|secret|api[-_]?key|appid|appsecret)/i.test(key);
19
+ }
20
+ function walkPlaceholders(value, path) {
21
+ if (isPlaceholderValue(value)) {
22
+ return [{ path, value }];
23
+ }
24
+ if (Array.isArray(value)) {
25
+ return value.flatMap((item, index) => walkPlaceholders(item, `${path}[${index}]`));
26
+ }
27
+ if (!value || typeof value !== "object") {
28
+ return [];
29
+ }
30
+ return Object.entries(value).flatMap(([key, nested]) => walkPlaceholders(nested, `${path}.${key}`));
31
+ }
32
+ function lastPathSegment(path) {
33
+ const withoutIndexes = path.replaceAll(/\[\d+\]/g, "");
34
+ const segments = withoutIndexes.split(".");
35
+ return segments[segments.length - 1] ?? withoutIndexes;
36
+ }
10
37
  function expandHome(value) {
11
38
  if (value === "~") {
12
39
  return homedir();
@@ -76,6 +103,15 @@ export async function runDoctorCommand(options) {
76
103
  message: `providers.items['${instanceId}'] references missing contribution '${instance.type}'`,
77
104
  });
78
105
  }
106
+ for (const hit of walkPlaceholders(instance, `providers.items['${instanceId}']`)) {
107
+ if (hit.path.endsWith(".type")) {
108
+ continue;
109
+ }
110
+ issues.push({
111
+ level: isCredentialLikeKey(lastPathSegment(hit.path)) ? "error" : "warning",
112
+ message: `${hit.path} still uses placeholder value '${hit.value}'`,
113
+ });
114
+ }
79
115
  }
80
116
  for (const [instanceId, instance] of Object.entries(normalized.connectors.items)) {
81
117
  if (!availableContributionIds.has(instance.type)) {
@@ -84,6 +120,15 @@ export async function runDoctorCommand(options) {
84
120
  message: `connectors.items['${instanceId}'] references missing contribution '${instance.type}'`,
85
121
  });
86
122
  }
123
+ for (const hit of walkPlaceholders(instance, `connectors.items['${instanceId}']`)) {
124
+ if (hit.path.endsWith(".type")) {
125
+ continue;
126
+ }
127
+ issues.push({
128
+ level: isCredentialLikeKey(lastPathSegment(hit.path)) ? "error" : "warning",
129
+ message: `${hit.path} still uses placeholder value '${hit.value}'`,
130
+ });
131
+ }
87
132
  if (instance.type === DISCORD_CONNECTOR_CONTRIBUTION_ID) {
88
133
  const botName = typeof instance.botName === "string" ? instance.botName.trim() : "";
89
134
  const botToken = typeof instance.botToken === "string" ? instance.botToken.trim() : "";
@@ -109,18 +154,46 @@ export async function runDoctorCommand(options) {
109
154
  });
110
155
  }
111
156
  }
157
+ if (normalized.routes.default.projectRoot && isPlaceholderValue(normalized.routes.default.projectRoot)) {
158
+ issues.push({
159
+ level: "warning",
160
+ message: `routes.default.projectRoot still uses placeholder value '${normalized.routes.default.projectRoot}'`,
161
+ });
162
+ }
112
163
  for (const [routeId, route] of Object.entries(normalized.routes.items)) {
164
+ const effectiveProjectRoot = route.projectRoot ?? normalized.routes.default.projectRoot;
165
+ const projectRootSource = route.projectRoot ? `routes.items['${routeId}'].projectRoot` : "routes.default.projectRoot";
166
+ if (!effectiveProjectRoot) {
167
+ issues.push({
168
+ level: "error",
169
+ message: `routes.items['${routeId}'].projectRoot is required when routes.default.projectRoot is not set`,
170
+ });
171
+ continue;
172
+ }
173
+ if (isPlaceholderValue(effectiveProjectRoot)) {
174
+ issues.push({
175
+ level: "warning",
176
+ message: `${projectRootSource} still uses placeholder value '${effectiveProjectRoot}'`,
177
+ });
178
+ continue;
179
+ }
113
180
  try {
114
- const projectRootPath = resolveRouteProjectRoot(configPath, route.projectRoot);
181
+ const projectRootPath = resolveRouteProjectRoot(configPath, effectiveProjectRoot);
115
182
  await access(projectRootPath);
116
183
  }
117
184
  catch {
118
185
  issues.push({
119
186
  level: "warning",
120
- message: `routes.items['${routeId}'].projectRoot does not exist: ${route.projectRoot}`,
187
+ message: `${projectRootSource} does not exist: ${effectiveProjectRoot}`,
121
188
  });
122
189
  }
123
190
  }
191
+ if (normalized.bindings.default && !normalized.routes.items[normalized.bindings.default.route]) {
192
+ issues.push({
193
+ level: "error",
194
+ message: `bindings.default.route references unknown route '${normalized.bindings.default.route}'`,
195
+ });
196
+ }
124
197
  const seenBindingSources = new Map();
125
198
  for (const [bindingId, binding] of Object.entries(normalized.bindings.items)) {
126
199
  if (!normalized.connectors.items[binding.connector]) {
@@ -135,6 +208,12 @@ export async function runDoctorCommand(options) {
135
208
  message: `bindings.items['${bindingId}'].route references unknown route '${binding.route}'`,
136
209
  });
137
210
  }
211
+ if (isPlaceholderValue(binding.source.id)) {
212
+ issues.push({
213
+ level: "warning",
214
+ message: `bindings.items['${bindingId}'].source.id still uses placeholder value '${binding.source.id}'`,
215
+ });
216
+ }
138
217
  const bindingKey = `${binding.connector}:${binding.source.type}:${binding.source.id}`;
139
218
  const existingBindingId = seenBindingSources.get(bindingKey);
140
219
  if (existingBindingId) {
@@ -3,6 +3,7 @@ import { loadGatewayConfig } from "../../core/routing.js";
3
3
  import { ExtensionStoreManager } from "../../extension/manager.js";
4
4
  import { applyContributionTemplates, buildContributionTemplates, ensureGatewayConfigShape, listContributionIds, setDefaultProviderIfMissingOrInvalid, upsertAllowListPackage, } from "../shared/config-mutators.js";
5
5
  import { readRawConfig, requireRawConfig, resolveConfigPath, resolveDataRootDir, writeConfigWithValidation } from "../shared/config-io.js";
6
+ import { resolveExtensionInstallSpecs } from "../shared/local-extension-specs.js";
6
7
  import { createLogger } from "../shared/runtime.js";
7
8
  /**
8
9
  * Resolves extension store directory from normalized gateway config.
@@ -25,7 +26,8 @@ export async function runExtensionInstallCommand(options) {
25
26
  const logger = createLogger();
26
27
  const rawConfig = (await readRawConfig(configPath)) ?? {};
27
28
  const manager = new ExtensionStoreManager(logger, extensionStoreDirFromRaw(configPath, rawConfig));
28
- const installed = await manager.install(options.spec);
29
+ const [resolvedSpec] = await resolveExtensionInstallSpecs([options.spec]);
30
+ const installed = await manager.install(resolvedSpec ?? options.spec);
29
31
  if (!options.enable) {
30
32
  const templates = buildContributionTemplates(installed.manifest.contributions);
31
33
  if (options.json) {
@@ -1,37 +1,13 @@
1
- import { cancel, confirm, intro, isCancel, multiselect, note, outro, password, select, spinner, text, } from "@clack/prompts";
1
+ import { cancel, intro, isCancel, multiselect, outro, select, spinner, } from "@clack/prompts";
2
2
  import { ExtensionStoreManager } from "../../extension/manager.js";
3
3
  import { ensureGatewayConfigShape, upsertAllowListPackage, upsertBinding, upsertConnectorInstance, upsertProviderInstance, upsertRoute, } from "../shared/config-mutators.js";
4
- import { DEFAULT_DISCORD_BOT_NAME } from "../shared/discord-config.js";
5
- import { applyAndValidateContributionSchemas, loadContributionSchemaCatalog } from "../shared/config-schema.js";
4
+ import { applyAndValidateContributionSchemas } from "../shared/config-schema.js";
6
5
  import { readRawConfig, resolveConfigPath, resolveDataRootDir, writeConfigWithValidation, } from "../shared/config-io.js";
7
6
  import { createInitSelectionConfig, isInitConnectorChoiceId, isInitProviderChoiceId, listInitConnectorChoices, listInitProviderChoices, } from "../shared/init-catalog.js";
8
- import { ensureProviderPiModelsFile } from "../shared/init-models-file.js";
7
+ import { resolveExtensionInstallSpecs } from "../shared/local-extension-specs.js";
9
8
  import { createLogger } from "../shared/runtime.js";
10
- import { promptConfigFromSchema } from "../shared/schema-prompts.js";
11
9
  /**
12
- * Repeatedly prompts for non-empty text input and aborts cleanly on cancel.
13
- */
14
- async function promptRequiredText(params) {
15
- while (true) {
16
- const promptOptions = {
17
- message: params.message,
18
- ...(params.placeholder !== undefined ? { placeholder: params.placeholder } : {}),
19
- ...(params.initialValue !== undefined ? { initialValue: params.initialValue } : {}),
20
- };
21
- const result = await text(promptOptions);
22
- if (isCancel(result)) {
23
- cancel("Initialization cancelled.");
24
- throw new Error("Initialization cancelled.");
25
- }
26
- const value = String(result ?? "").trim();
27
- if (value.length > 0) {
28
- return value;
29
- }
30
- await note("This field is required.", "Validation");
31
- }
32
- }
33
- /**
34
- * Collects init inputs from interactive prompts.
10
+ * Collects high-level starter choices only; config values are written as templates.
35
11
  */
36
12
  async function collectInitInput() {
37
13
  intro("dobby init");
@@ -61,7 +37,7 @@ async function collectInitInput() {
61
37
  let routeProviderChoiceId = providerChoiceIds[0];
62
38
  if (providerChoiceIds.length > 1) {
63
39
  const routeProviderChoiceResult = await select({
64
- message: "Choose provider for the default route",
40
+ message: "Choose default provider",
65
41
  options: providerChoiceIds.map((providerChoiceId) => ({
66
42
  value: providerChoiceId,
67
43
  label: providerChoicesById.get(providerChoiceId)?.label ?? providerChoiceId,
@@ -79,101 +55,55 @@ async function collectInitInput() {
79
55
  routeProviderChoiceId = routeProviderCandidate;
80
56
  }
81
57
  const connectorChoices = listInitConnectorChoices();
82
- const connectorChoiceResult = await select({
83
- message: "Choose connector",
58
+ const connectorChoiceResult = await multiselect({
59
+ message: "Choose connector(s) (space to select multiple)",
84
60
  options: connectorChoices.map((item) => ({
85
61
  value: item.id,
86
62
  label: item.label,
87
63
  })),
88
- initialValue: "connector.discord",
64
+ initialValues: ["connector.discord"],
65
+ required: true,
89
66
  });
90
67
  if (isCancel(connectorChoiceResult)) {
91
68
  cancel("Initialization cancelled.");
92
69
  throw new Error("Initialization cancelled.");
93
70
  }
94
- const connectorChoiceId = String(connectorChoiceResult);
95
- if (!isInitConnectorChoiceId(connectorChoiceId)) {
96
- throw new Error(`Unsupported connector choice '${connectorChoiceId}'`);
71
+ const connectorChoiceIds = connectorChoiceResult.map((value) => String(value));
72
+ if (connectorChoiceIds.length === 0) {
73
+ throw new Error("At least one connector must be selected");
97
74
  }
98
- const projectRoot = await promptRequiredText({
99
- message: "Project root",
100
- initialValue: process.cwd(),
101
- });
102
- const channelId = await promptRequiredText({
103
- message: "Discord channel ID",
104
- placeholder: "1234567890",
105
- });
106
- const routeIdResult = await text({
107
- message: "Route ID",
108
- initialValue: "main",
109
- });
110
- if (isCancel(routeIdResult)) {
111
- cancel("Initialization cancelled.");
112
- throw new Error("Initialization cancelled.");
113
- }
114
- const botNameResult = await text({
115
- message: "Discord bot name",
116
- initialValue: DEFAULT_DISCORD_BOT_NAME,
117
- });
118
- if (isCancel(botNameResult)) {
119
- cancel("Initialization cancelled.");
120
- throw new Error("Initialization cancelled.");
121
- }
122
- const botTokenResult = await password({
123
- message: "Discord bot token",
124
- mask: "*",
125
- validate: (value) => (value.trim().length > 0 ? undefined : "Token is required"),
126
- });
127
- if (isCancel(botTokenResult)) {
128
- cancel("Initialization cancelled.");
129
- throw new Error("Initialization cancelled.");
130
- }
131
- const allowAllMessagesResult = await confirm({
132
- message: "Allow all group messages (not mention-only)?",
133
- initialValue: false,
134
- });
135
- if (isCancel(allowAllMessagesResult)) {
136
- cancel("Initialization cancelled.");
137
- throw new Error("Initialization cancelled.");
75
+ if (!connectorChoiceIds.every((connectorChoiceId) => isInitConnectorChoiceId(connectorChoiceId))) {
76
+ const invalidChoice = connectorChoiceIds.find((connectorChoiceId) => !isInitConnectorChoiceId(connectorChoiceId));
77
+ throw new Error(`Unsupported connector choice '${invalidChoice}'`);
138
78
  }
139
79
  return {
140
80
  providerChoiceIds: providerChoiceIds,
141
81
  routeProviderChoiceId,
142
- connectorChoiceId,
143
- projectRoot,
144
- channelId,
145
- routeId: String(routeIdResult ?? "").trim() || "main",
146
- botName: String(botNameResult ?? "").trim() || DEFAULT_DISCORD_BOT_NAME,
147
- botToken: String(botTokenResult ?? "").trim(),
148
- allowAllMessages: allowAllMessagesResult === true,
82
+ connectorChoiceIds: connectorChoiceIds,
149
83
  };
150
84
  }
151
85
  /**
152
- * Executes first-time initialization: install required extensions, write config, then validate.
86
+ * Executes first-time initialization by installing starter extensions and writing template config.
153
87
  */
154
88
  export async function runInitCommand() {
155
89
  const configPath = resolveConfigPath();
156
90
  const existingConfig = await readRawConfig(configPath);
157
91
  if (existingConfig) {
158
- throw new Error(`Config '${configPath}' already exists. Use 'dobby config edit' or 'dobby configure' to update existing values.`);
92
+ throw new Error(`Config '${configPath}' already exists. Edit the file directly to update existing values.`);
159
93
  }
160
94
  const input = await collectInitInput();
161
- const selected = createInitSelectionConfig(input.providerChoiceIds, input.connectorChoiceId, {
162
- routeId: input.routeId,
163
- projectRoot: input.projectRoot,
164
- allowAllMessages: input.allowAllMessages,
165
- botName: input.botName,
166
- botToken: input.botToken,
167
- channelId: input.channelId,
95
+ const selected = createInitSelectionConfig(input.providerChoiceIds, input.connectorChoiceIds, {
168
96
  routeProviderChoiceId: input.routeProviderChoiceId,
97
+ defaultProjectRoot: process.cwd(),
169
98
  });
170
99
  const next = ensureGatewayConfigShape({});
171
100
  const rootDir = resolveDataRootDir(configPath, next);
172
101
  const manager = new ExtensionStoreManager(createLogger(), `${rootDir}/extensions`);
102
+ const extensionInstallSpecs = await resolveExtensionInstallSpecs(selected.extensionPackages);
173
103
  const installSpinner = spinner();
174
104
  installSpinner.start(`Installing required extensions (${selected.extensionPackages.length} packages)`);
175
105
  try {
176
- const installedPackages = await manager.installMany(selected.extensionPackages);
106
+ const installedPackages = await manager.installMany(extensionInstallSpecs);
177
107
  for (const installed of installedPackages) {
178
108
  upsertAllowListPackage(next, installed.packageName, true);
179
109
  }
@@ -183,52 +113,12 @@ export async function runInitCommand() {
183
113
  installSpinner.stop("Extension installation failed");
184
114
  throw error;
185
115
  }
186
- const catalog = await loadContributionSchemaCatalog(configPath, next);
187
- const schemaByContributionId = new Map(catalog
188
- .filter((item) => item.configSchema)
189
- .map((item) => [item.contributionId, item.configSchema]));
190
- const schemaStateByContributionId = new Map(catalog.map((item) => [item.contributionId, item.configSchema ? "with_schema" : "without_schema"]));
191
- const warnedSchemaFallback = new Set();
192
- const noteSchemaFallback = async (contributionId) => {
193
- if (warnedSchemaFallback.has(contributionId)) {
194
- const existingState = schemaStateByContributionId.get(contributionId);
195
- return existingState === "without_schema" ? "without_schema" : "not_loaded";
196
- }
197
- warnedSchemaFallback.add(contributionId);
198
- const state = schemaStateByContributionId.get(contributionId);
199
- if (state === "without_schema") {
200
- await note(`Contribution '${contributionId}' is loaded but does not expose configSchema. Falling back to built-in defaults/JSON.`, "Schema");
201
- return "without_schema";
202
- }
203
- await note(`No loaded schema for contribution '${contributionId}'. The extension may be disabled or not installed.`, "Schema");
204
- return "not_loaded";
205
- };
206
- const resolveFallbackConfig = async (kind, instanceId, contributionId, fallbackConfig) => {
207
- const state = await noteSchemaFallback(contributionId);
208
- if (state === "not_loaded") {
209
- throw new Error(`Cannot initialize ${kind} '${instanceId}' because schema for contribution '${contributionId}' is not loaded. ` +
210
- `Ensure the extension is installed and enabled, then retry.`);
211
- }
212
- return fallbackConfig;
213
- };
214
116
  for (const provider of selected.providerInstances) {
215
- const schema = schemaByContributionId.get(provider.contributionId);
216
- const providerConfig = schema
217
- ? await promptConfigFromSchema(schema, provider.config, {
218
- title: `Provider '${provider.instanceId}' (${provider.contributionId})`,
219
- })
220
- : await resolveFallbackConfig("provider", provider.instanceId, provider.contributionId, provider.config);
221
- upsertProviderInstance(next, provider.instanceId, provider.contributionId, providerConfig);
117
+ upsertProviderInstance(next, provider.instanceId, provider.contributionId, provider.config);
118
+ }
119
+ for (const connector of selected.connectorInstances) {
120
+ upsertConnectorInstance(next, connector.instanceId, connector.contributionId, connector.config);
222
121
  }
223
- const connectorSchema = schemaByContributionId.get(selected.connectorContributionId);
224
- const connectorConfig = selected.connectorContributionId === "connector.discord"
225
- ? selected.connectorConfig
226
- : connectorSchema
227
- ? await promptConfigFromSchema(connectorSchema, selected.connectorConfig, {
228
- title: `Connector '${selected.connectorInstanceId}' (${selected.connectorContributionId})`,
229
- })
230
- : await resolveFallbackConfig("connector", selected.connectorInstanceId, selected.connectorContributionId, selected.connectorConfig);
231
- upsertConnectorInstance(next, selected.connectorInstanceId, selected.connectorContributionId, connectorConfig);
232
122
  next.providers = {
233
123
  ...next.providers,
234
124
  default: selected.providerInstanceId,
@@ -236,51 +126,31 @@ export async function runInitCommand() {
236
126
  };
237
127
  next.routes = {
238
128
  ...next.routes,
239
- defaults: {
240
- ...next.routes.defaults,
241
- provider: selected.providerInstanceId,
129
+ default: {
130
+ ...next.routes.default,
131
+ ...selected.routeDefaults,
242
132
  },
243
133
  };
244
- upsertRoute(next, input.routeId, {
245
- ...selected.routeProfile,
246
- projectRoot: input.projectRoot,
247
- });
248
- upsertBinding(next, selected.bindingId, selected.bindingConfig);
249
- const validatedConfig = await applyAndValidateContributionSchemas(configPath, next);
250
- const createdModelsFiles = [];
251
- for (const provider of selected.providerInstances) {
252
- if (provider.contributionId !== "provider.pi") {
253
- continue;
254
- }
255
- const resolvedProvider = validatedConfig.providers?.items?.[provider.instanceId];
256
- const { type: _type, ...providerConfig } = resolvedProvider ?? {};
257
- const ensured = await ensureProviderPiModelsFile(configPath, Object.keys(providerConfig).length > 0 ? providerConfig : provider.config);
258
- if (ensured.created) {
259
- createdModelsFiles.push(ensured.path);
260
- }
134
+ upsertRoute(next, selected.routeId, selected.routeProfile);
135
+ if (selected.defaultBinding) {
136
+ next.bindings = {
137
+ ...next.bindings,
138
+ default: selected.defaultBinding,
139
+ items: next.bindings.items,
140
+ };
141
+ }
142
+ for (const binding of selected.bindings) {
143
+ upsertBinding(next, binding.id, binding.config);
261
144
  }
145
+ const validatedConfig = await applyAndValidateContributionSchemas(configPath, next);
262
146
  await writeConfigWithValidation(configPath, validatedConfig, {
263
147
  validate: true,
264
148
  createBackup: false,
265
149
  });
266
150
  outro("Initialization completed.");
267
151
  console.log(`Config written: ${configPath}`);
268
- if (createdModelsFiles.length > 0) {
269
- console.log("Generated model files:");
270
- for (const path of createdModelsFiles) {
271
- console.log(`- ${path}`);
272
- }
273
- }
274
152
  console.log("Next steps:");
275
- console.log("1. dobby start");
276
- const showHint = await confirm({
277
- message: "Show quick validation commands?",
278
- initialValue: true,
279
- });
280
- if (!isCancel(showHint) && showHint) {
281
- await note([
282
- "dobby extension list",
283
- "dobby doctor",
284
- ].join("\n"), "Validation");
285
- }
153
+ console.log("1. Edit gateway.json and replace all REPLACE_WITH_* / YOUR_* placeholders");
154
+ console.log("2. Run 'dobby doctor' to validate the edited config");
155
+ console.log("3. Run 'dobby start' when the placeholders are replaced");
286
156
  }
@@ -2,6 +2,13 @@ import { BUILTIN_HOST_SANDBOX_ID } from "../../core/types.js";
2
2
  import { ensureGatewayConfigShape, upsertBinding, upsertRoute, } from "../shared/config-mutators.js";
3
3
  import { DISCORD_CONNECTOR_CONTRIBUTION_ID } from "../shared/discord-config.js";
4
4
  import { requireRawConfig, resolveConfigPath, writeConfigWithValidation } from "../shared/config-io.js";
5
+ function effectiveRouteProjectRoot(normalized, routeId) {
6
+ const route = normalized.routes.items[routeId];
7
+ if (!route) {
8
+ return undefined;
9
+ }
10
+ return route.projectRoot ?? normalized.routes.default.projectRoot;
11
+ }
5
12
  function listDiscordConnectors(rawConfig) {
6
13
  const normalized = ensureGatewayConfigShape(rawConfig);
7
14
  const items = [];
@@ -36,11 +43,11 @@ function getDiscordConnectorOrThrow(rawConfig, connectorId) {
36
43
  }
37
44
  function listBindings(rawConfig, connectorFilter) {
38
45
  const normalized = ensureGatewayConfigShape(rawConfig);
39
- const routes = normalized.routes.items;
40
- return Object.entries(normalized.bindings.items)
46
+ const bindings = Object.entries(normalized.bindings.items)
41
47
  .filter(([, binding]) => !connectorFilter || binding.connector === connectorFilter)
42
48
  .map(([bindingId, binding]) => {
43
- const route = routes[binding.route];
49
+ const route = normalized.routes.items[binding.route];
50
+ const projectRoot = route ? effectiveRouteProjectRoot(normalized, binding.route) : undefined;
44
51
  return {
45
52
  bindingId,
46
53
  connectorId: binding.connector,
@@ -48,14 +55,27 @@ function listBindings(rawConfig, connectorFilter) {
48
55
  sourceId: binding.source.id,
49
56
  routeId: binding.route,
50
57
  routeExists: Boolean(route),
51
- ...(route ? { projectRoot: route.projectRoot } : {}),
58
+ ...(projectRoot ? { projectRoot } : {}),
52
59
  };
53
- })
54
- .sort((a, b) => a.bindingId.localeCompare(b.bindingId));
60
+ });
61
+ if (!connectorFilter && normalized.bindings.default) {
62
+ const projectRoot = effectiveRouteProjectRoot(normalized, normalized.bindings.default.route);
63
+ bindings.push({
64
+ bindingId: "bindings.default",
65
+ connectorId: "*",
66
+ sourceType: "direct_message",
67
+ sourceId: "*",
68
+ routeId: normalized.bindings.default.route,
69
+ routeExists: Boolean(normalized.routes.items[normalized.bindings.default.route]),
70
+ ...(projectRoot ? { projectRoot } : {}),
71
+ });
72
+ }
73
+ return bindings.sort((a, b) => a.bindingId.localeCompare(b.bindingId));
55
74
  }
56
75
  function buildRouteBindingCounts(rawConfig) {
76
+ const normalized = ensureGatewayConfigShape(rawConfig);
57
77
  const counts = new Map();
58
- for (const binding of listBindings(rawConfig)) {
78
+ for (const binding of listBindings(normalized)) {
59
79
  counts.set(binding.routeId, (counts.get(binding.routeId) ?? 0) + 1);
60
80
  }
61
81
  return counts;
@@ -66,7 +86,7 @@ function listRoutes(rawConfig) {
66
86
  return Object.entries(normalized.routes.items)
67
87
  .map(([routeId, route]) => ({
68
88
  routeId,
69
- projectRoot: route.projectRoot,
89
+ projectRoot: effectiveRouteProjectRoot(normalized, routeId) ?? "(unset)",
70
90
  tools: route.tools === "readonly" ? "readonly" : "full",
71
91
  mentions: route.mentions === "optional" ? "optional" : "required",
72
92
  ...(route.provider ? { provider: route.provider } : {}),
@@ -205,7 +225,7 @@ export async function runRouteSetCommand(options) {
205
225
  const normalized = ensureGatewayConfigShape(structuredClone(rawConfig));
206
226
  const existing = normalized.routes.items[options.routeId];
207
227
  const projectRoot = options.projectRoot?.trim() || existing?.projectRoot;
208
- if (!projectRoot) {
228
+ if (!projectRoot && !normalized.routes.default.projectRoot) {
209
229
  throw new Error("--project-root is required when creating a new route");
210
230
  }
211
231
  const toolsRaw = options.tools ?? existing?.tools;
@@ -221,7 +241,7 @@ export async function runRouteSetCommand(options) {
221
241
  throw new Error(`Sandbox '${sandbox}' does not exist`);
222
242
  }
223
243
  upsertRoute(normalized, options.routeId, {
224
- projectRoot,
244
+ ...(projectRoot ? { projectRoot } : {}),
225
245
  ...(toolsRaw ? { tools: toolsRaw } : {}),
226
246
  ...((options.mentions ?? existing?.mentions) ? { mentions: (options.mentions ?? existing?.mentions) } : {}),
227
247
  ...(provider ? { provider } : {}),
@@ -238,15 +258,19 @@ export async function runRouteRemoveCommand(options) {
238
258
  if (!normalized.routes.items[options.routeId]) {
239
259
  throw new Error(`Route '${options.routeId}' not found`);
240
260
  }
241
- const bindingRefs = listBindings(normalized).filter((binding) => binding.routeId === options.routeId);
242
- if (bindingRefs.length > 0 && !options.cascadeBindings) {
261
+ const bindingRefs = listBindings(normalized).filter((binding) => binding.routeId === options.routeId && binding.bindingId !== "bindings.default");
262
+ const hasDefaultBindingRef = normalized.bindings.default?.route === options.routeId;
263
+ if ((bindingRefs.length > 0 || hasDefaultBindingRef) && !options.cascadeBindings) {
243
264
  const refList = bindingRefs.map((binding) => binding.bindingId).join(", ");
244
- throw new Error(`Route '${options.routeId}' is referenced by bindings (${refList}). Re-run with --cascade-bindings to remove these bindings automatically.`);
265
+ throw new Error(`Route '${options.routeId}' is referenced by bindings (${[refList, hasDefaultBindingRef ? "bindings.default" : ""].filter(Boolean).join(", ")}). Re-run with --cascade-bindings to remove these bindings automatically.`);
245
266
  }
246
- if (bindingRefs.length > 0 && options.cascadeBindings) {
267
+ if (options.cascadeBindings) {
247
268
  for (const binding of bindingRefs) {
248
269
  delete normalized.bindings.items[binding.bindingId];
249
270
  }
271
+ if (hasDefaultBindingRef) {
272
+ delete normalized.bindings.default;
273
+ }
250
274
  }
251
275
  delete normalized.routes.items[options.routeId];
252
276
  await saveConfig(configPath, normalized);