@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,25 +1,26 @@
1
- import { DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID, DISCORD_CONNECTOR_CONTRIBUTION_ID, } from "./discord-config.js";
1
+ import { DEFAULT_DISCORD_BOT_NAME, DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID, DISCORD_CONNECTOR_CONTRIBUTION_ID, } from "./discord-config.js";
2
+ export const DEFAULT_INIT_ROUTE_ID = "main";
3
+ export const DEFAULT_INIT_PROJECT_ROOT = "./REPLACE_WITH_PROJECT_ROOT";
2
4
  const PROVIDER_CATALOG = {
3
5
  "provider.pi": {
4
6
  id: "provider.pi",
5
7
  label: "Pi provider",
6
- extensionPackage: "@dobby.ai/provider-pi",
8
+ package: "@dobby.ai/provider-pi",
7
9
  instanceId: "pi.main",
8
10
  contributionId: "provider.pi",
9
- config: {
10
- provider: "custom-openai",
11
- model: "example-model",
12
- thinkingLevel: "off",
13
- modelsFile: "./models.custom.json",
11
+ defaultConfig: {
12
+ model: "REPLACE_WITH_PROVIDER_MODEL_ID",
13
+ baseUrl: "REPLACE_WITH_PROVIDER_BASE_URL",
14
+ apiKey: "REPLACE_WITH_PROVIDER_API_KEY_OR_ENV",
14
15
  },
15
16
  },
16
17
  "provider.claude-cli": {
17
18
  id: "provider.claude-cli",
18
19
  label: "Claude CLI provider",
19
- extensionPackage: "@dobby.ai/provider-claude-cli",
20
+ package: "@dobby.ai/provider-claude-cli",
20
21
  instanceId: "claude-cli.main",
21
22
  contributionId: "provider.claude-cli",
22
- config: {
23
+ defaultConfig: {
23
24
  model: "claude-sonnet-4-5",
24
25
  maxTurns: 20,
25
26
  command: "claude",
@@ -34,11 +35,52 @@ const CONNECTOR_CATALOG = {
34
35
  "connector.discord": {
35
36
  id: "connector.discord",
36
37
  label: "Discord connector",
37
- extensionPackage: "@dobby.ai/connector-discord",
38
+ package: "@dobby.ai/connector-discord",
38
39
  instanceId: DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
39
40
  contributionId: DISCORD_CONNECTOR_CONTRIBUTION_ID,
41
+ defaultConfig: {
42
+ botName: DEFAULT_DISCORD_BOT_NAME,
43
+ botToken: "REPLACE_WITH_DISCORD_BOT_TOKEN",
44
+ reconnectStaleMs: 60_000,
45
+ reconnectCheckIntervalMs: 10_000,
46
+ },
47
+ bindingTemplate: {
48
+ sourceType: "channel",
49
+ sourceId: "YOUR_DISCORD_CHANNEL_ID",
50
+ },
51
+ },
52
+ "connector.feishu": {
53
+ id: "connector.feishu",
54
+ label: "Feishu connector",
55
+ package: "@dobby.ai/connector-feishu",
56
+ instanceId: "feishu.main",
57
+ contributionId: "connector.feishu",
58
+ defaultConfig: {
59
+ appId: "REPLACE_WITH_FEISHU_APP_ID",
60
+ appSecret: "REPLACE_WITH_FEISHU_APP_SECRET",
61
+ domain: "feishu",
62
+ messageFormat: "card_markdown",
63
+ replyMode: "direct",
64
+ downloadAttachments: true,
65
+ },
66
+ bindingTemplate: {
67
+ sourceType: "chat",
68
+ sourceId: "YOUR_FEISHU_CHAT_ID",
69
+ },
40
70
  },
41
71
  };
72
+ function dedupeChoiceIds(choiceIds) {
73
+ const dedupedChoiceIds = [];
74
+ const seenChoiceIds = new Set();
75
+ for (const choiceId of choiceIds) {
76
+ if (seenChoiceIds.has(choiceId)) {
77
+ continue;
78
+ }
79
+ seenChoiceIds.add(choiceId);
80
+ dedupedChoiceIds.push(choiceId);
81
+ }
82
+ return dedupedChoiceIds;
83
+ }
42
84
  export function listInitProviderChoices() {
43
85
  return Object.values(PROVIDER_CATALOG);
44
86
  }
@@ -51,65 +93,66 @@ export function isInitProviderChoiceId(value) {
51
93
  export function isInitConnectorChoiceId(value) {
52
94
  return Object.prototype.hasOwnProperty.call(CONNECTOR_CATALOG, value);
53
95
  }
54
- export function createInitSelectionConfig(providerChoiceIds, connectorChoiceId, context) {
55
- const dedupedProviderChoiceIds = [];
56
- const seenProviderChoiceIds = new Set();
57
- for (const providerChoiceId of providerChoiceIds) {
58
- if (!seenProviderChoiceIds.has(providerChoiceId)) {
59
- seenProviderChoiceIds.add(providerChoiceId);
60
- dedupedProviderChoiceIds.push(providerChoiceId);
61
- }
62
- }
96
+ export function createInitSelectionConfig(providerChoiceIds, connectorChoiceIds, context) {
97
+ const dedupedProviderChoiceIds = dedupeChoiceIds(providerChoiceIds);
63
98
  if (dedupedProviderChoiceIds.length === 0) {
64
99
  throw new Error("At least one provider choice is required");
65
100
  }
101
+ const dedupedConnectorChoiceIds = dedupeChoiceIds(connectorChoiceIds);
102
+ if (dedupedConnectorChoiceIds.length === 0) {
103
+ throw new Error("At least one connector choice is required");
104
+ }
66
105
  if (!dedupedProviderChoiceIds.includes(context.routeProviderChoiceId)) {
67
106
  throw new Error(`route provider choice '${context.routeProviderChoiceId}' must be one of selected providers: ${dedupedProviderChoiceIds.join(", ")}`);
68
107
  }
69
108
  const providerChoices = dedupedProviderChoiceIds.map((providerChoiceId) => PROVIDER_CATALOG[providerChoiceId]);
109
+ const connectorChoices = dedupedConnectorChoiceIds.map((connectorChoiceId) => CONNECTOR_CATALOG[connectorChoiceId]);
70
110
  const primaryProviderChoice = PROVIDER_CATALOG[context.routeProviderChoiceId];
71
- const connectorChoice = CONNECTOR_CATALOG[connectorChoiceId];
72
111
  return {
73
112
  providerChoiceIds: dedupedProviderChoiceIds,
74
113
  routeProviderChoiceId: primaryProviderChoice.id,
75
- providerChoiceId: primaryProviderChoice.id,
76
- connectorChoiceId,
114
+ connectorChoiceIds: dedupedConnectorChoiceIds,
77
115
  extensionPackages: [
78
- ...new Set([...providerChoices.map((item) => item.extensionPackage), connectorChoice.extensionPackage]),
116
+ ...new Set([
117
+ ...providerChoices.map((item) => item.package),
118
+ ...connectorChoices.map((item) => item.package),
119
+ ]),
79
120
  ],
80
121
  providerInstances: providerChoices.map((providerChoice) => ({
81
122
  choiceId: providerChoice.id,
82
123
  instanceId: providerChoice.instanceId,
83
124
  contributionId: providerChoice.contributionId,
84
- config: structuredClone(providerChoice.config),
125
+ config: structuredClone(providerChoice.defaultConfig),
126
+ })),
127
+ connectorInstances: connectorChoices.map((connectorChoice) => ({
128
+ choiceId: connectorChoice.id,
129
+ instanceId: connectorChoice.instanceId,
130
+ contributionId: connectorChoice.contributionId,
131
+ config: structuredClone(connectorChoice.defaultConfig),
85
132
  })),
86
133
  providerInstanceId: primaryProviderChoice.instanceId,
87
- providerContributionId: primaryProviderChoice.contributionId,
88
- providerConfig: structuredClone(primaryProviderChoice.config),
89
- connectorInstanceId: connectorChoice.instanceId,
90
- connectorContributionId: connectorChoice.contributionId,
91
- connectorConfig: {
92
- botName: context.botName,
93
- botToken: context.botToken,
94
- reconnectStaleMs: 60_000,
95
- reconnectCheckIntervalMs: 10_000,
96
- },
97
- routeProfile: {
98
- projectRoot: context.projectRoot,
134
+ routeId: DEFAULT_INIT_ROUTE_ID,
135
+ routeDefaults: {
136
+ projectRoot: context.defaultProjectRoot ?? DEFAULT_INIT_PROJECT_ROOT,
99
137
  tools: "full",
100
- systemPromptFile: "",
101
- mentions: context.allowAllMessages ? "optional" : "required",
138
+ mentions: "required",
102
139
  provider: primaryProviderChoice.instanceId,
103
140
  sandbox: "host.builtin",
104
141
  },
105
- bindingId: `${connectorChoice.instanceId}.${context.routeId}`,
106
- bindingConfig: {
107
- connector: connectorChoice.instanceId,
108
- source: {
109
- type: "channel",
110
- id: context.channelId,
111
- },
112
- route: context.routeId,
142
+ routeProfile: {},
143
+ defaultBinding: {
144
+ route: DEFAULT_INIT_ROUTE_ID,
113
145
  },
146
+ bindings: connectorChoices.map((connectorChoice) => ({
147
+ id: `${connectorChoice.instanceId}.${DEFAULT_INIT_ROUTE_ID}`,
148
+ config: {
149
+ connector: connectorChoice.instanceId,
150
+ source: {
151
+ type: connectorChoice.bindingTemplate.sourceType,
152
+ id: connectorChoice.bindingTemplate.sourceId,
153
+ },
154
+ route: DEFAULT_INIT_ROUTE_ID,
155
+ },
156
+ })),
114
157
  };
115
158
  }
@@ -0,0 +1,85 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { readdir } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { findDobbyRepoRoot } from "../../shared/dobby-repo.js";
5
+ function isExplicitInstallSpec(value) {
6
+ return value.startsWith("file:")
7
+ || value.startsWith("git+")
8
+ || value.startsWith("http://")
9
+ || value.startsWith("https://")
10
+ || value.startsWith("./")
11
+ || value.startsWith("../")
12
+ || value.startsWith("/");
13
+ }
14
+ async function listRepoLocalExtensionPackages(repoRoot) {
15
+ const pluginsRoot = resolve(repoRoot, "plugins");
16
+ const entries = await readdir(pluginsRoot, { withFileTypes: true });
17
+ const packages = new Map();
18
+ for (const entry of entries) {
19
+ if (!entry.isDirectory() || entry.name === "plugin-sdk") {
20
+ continue;
21
+ }
22
+ const packageDir = resolve(pluginsRoot, entry.name);
23
+ const packageJsonPath = resolve(packageDir, "package.json");
24
+ const manifestPath = resolve(packageDir, "dobby.manifest.json");
25
+ try {
26
+ await access(packageJsonPath);
27
+ await access(manifestPath);
28
+ const raw = await readFile(packageJsonPath, "utf-8");
29
+ const parsed = JSON.parse(raw);
30
+ if (typeof parsed.name !== "string" || parsed.name.trim().length === 0) {
31
+ continue;
32
+ }
33
+ packages.set(parsed.name, {
34
+ packageName: parsed.name,
35
+ packageDir,
36
+ });
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ }
42
+ return packages;
43
+ }
44
+ async function assertLocalExtensionBuildReady(localPackage) {
45
+ const manifestPath = resolve(localPackage.packageDir, "dobby.manifest.json");
46
+ const rawManifest = await readFile(manifestPath, "utf-8");
47
+ const parsed = JSON.parse(rawManifest);
48
+ for (const contribution of parsed.contributions ?? []) {
49
+ if (typeof contribution.entry !== "string" || contribution.entry.trim().length === 0) {
50
+ continue;
51
+ }
52
+ const entryPath = resolve(localPackage.packageDir, contribution.entry);
53
+ try {
54
+ await access(entryPath);
55
+ }
56
+ catch {
57
+ const contributionId = typeof contribution.id === "string" ? contribution.id : "unknown";
58
+ throw new Error(`Local extension '${localPackage.packageName}' is not built for contribution '${contributionId}'. `
59
+ + `Missing '${entryPath}'. Run 'npm run build --prefix ${localPackage.packageDir}' first.`);
60
+ }
61
+ }
62
+ }
63
+ export async function resolveExtensionInstallSpecs(packageSpecs, cwd = process.cwd()) {
64
+ const repoRoot = findDobbyRepoRoot(cwd);
65
+ if (!repoRoot) {
66
+ return packageSpecs;
67
+ }
68
+ const repoPackages = await listRepoLocalExtensionPackages(repoRoot);
69
+ const resolvedSpecs = [];
70
+ for (const rawSpec of packageSpecs) {
71
+ const packageSpec = rawSpec.trim();
72
+ if (packageSpec.length === 0 || isExplicitInstallSpec(packageSpec)) {
73
+ resolvedSpecs.push(packageSpec);
74
+ continue;
75
+ }
76
+ const localPackage = repoPackages.get(packageSpec);
77
+ if (!localPackage) {
78
+ resolvedSpecs.push(packageSpec);
79
+ continue;
80
+ }
81
+ await assertLocalExtensionBuildReady(localPackage);
82
+ resolvedSpecs.push(`file:${localPackage.packageDir}`);
83
+ }
84
+ return resolvedSpecs;
85
+ }
@@ -1,4 +1,4 @@
1
- import { cancel, confirm, isCancel, multiselect, note, select, text, } from "@clack/prompts";
1
+ import { cancel, confirm, isCancel, multiselect, note, password, select, text, } from "@clack/prompts";
2
2
  import JSON5 from "json5";
3
3
  function isRecord(value) {
4
4
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -61,11 +61,14 @@ function shouldPromptInMinimalMode(field) {
61
61
  if (!field.hasDefault && field.required) {
62
62
  return true;
63
63
  }
64
- if (!field.hasDefault && field.existingValue === undefined) {
64
+ if (field.existingValue !== undefined) {
65
65
  return true;
66
66
  }
67
67
  return false;
68
68
  }
69
+ function isSensitiveStringField(key) {
70
+ return /(token|secret|api[-_]?key)$/i.test(key);
71
+ }
69
72
  async function promptNumberField(params) {
70
73
  while (true) {
71
74
  const result = await text({
@@ -232,6 +235,27 @@ async function promptFieldValue(params) {
232
235
  existingValue,
233
236
  });
234
237
  }
238
+ if (isSensitiveStringField(key)) {
239
+ while (true) {
240
+ const result = await password({
241
+ message,
242
+ mask: "*",
243
+ });
244
+ if (isCancel(result)) {
245
+ cancel("Configuration cancelled.");
246
+ throw new Error("Configuration cancelled.");
247
+ }
248
+ const raw = String(result ?? "").trim();
249
+ if (raw.length === 0) {
250
+ if (required && existingValue === undefined) {
251
+ await note("This field is required.", "Validation");
252
+ continue;
253
+ }
254
+ return existingValue;
255
+ }
256
+ return raw;
257
+ }
258
+ }
235
259
  while (true) {
236
260
  const result = await text({
237
261
  message,
@@ -129,7 +129,9 @@ export class Gateway {
129
129
  route,
130
130
  };
131
131
  }
132
- const binding = this.options.bindingResolver.resolve(message.connectorId, message.source);
132
+ const binding = this.options.bindingResolver.resolve(message.connectorId, message.source, {
133
+ isDirectMessage: message.isDirectMessage,
134
+ });
133
135
  if (!binding) {
134
136
  if (handling.origin === "connector") {
135
137
  this.options.logger.debug({
@@ -1,20 +1,21 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
1
  import { readFile } from "node:fs/promises";
3
2
  import { homedir } from "node:os";
4
3
  import { dirname, isAbsolute, resolve } from "node:path";
5
4
  import { z } from "zod";
5
+ import { isDobbyRepoRoot } from "../shared/dobby-repo.js";
6
6
  import { BUILTIN_HOST_SANDBOX_ID } from "./types.js";
7
7
  const extensionItemSchema = z.object({
8
8
  type: z.string().trim().min(1),
9
9
  }).catchall(z.unknown());
10
- const routeDefaultsSchema = z.object({
10
+ const routeDefaultSchema = z.object({
11
+ projectRoot: z.string().trim().min(1).optional(),
11
12
  provider: z.string().trim().min(1).optional(),
12
13
  sandbox: z.string().trim().min(1).optional(),
13
14
  tools: z.enum(["full", "readonly"]).optional(),
14
15
  mentions: z.enum(["required", "optional"]).optional(),
15
16
  }).strict();
16
17
  const routeItemSchema = z.object({
17
- projectRoot: z.string().trim().min(1),
18
+ projectRoot: z.string().trim().min(1).optional(),
18
19
  tools: z.enum(["full", "readonly"]).optional(),
19
20
  mentions: z.enum(["required", "optional"]).optional(),
20
21
  provider: z.string().trim().min(1).optional(),
@@ -33,6 +34,9 @@ const bindingItemSchema = z.object({
33
34
  source: bindingSourceSchema,
34
35
  route: z.string().trim().min(1),
35
36
  }).strict();
37
+ const defaultBindingSchema = z.object({
38
+ route: z.string().trim().min(1),
39
+ }).strict();
36
40
  const gatewayConfigSchema = z.object({
37
41
  extensions: z.object({
38
42
  allowList: z
@@ -54,10 +58,11 @@ const gatewayConfigSchema = z.object({
54
58
  items: z.record(z.string(), extensionItemSchema).default({}),
55
59
  }).strict(),
56
60
  routes: z.object({
57
- defaults: routeDefaultsSchema.default({}),
61
+ default: routeDefaultSchema.default({}),
58
62
  items: z.record(z.string(), routeItemSchema),
59
63
  }).strict(),
60
64
  bindings: z.object({
65
+ default: defaultBindingSchema.optional(),
61
66
  items: z.record(z.string(), bindingItemSchema).default({}),
62
67
  }).strict(),
63
68
  data: z.object({
@@ -70,22 +75,6 @@ const FORBIDDEN_CONNECTOR_CONFIG_KEYS = {
70
75
  chatRouteMap: "Use bindings.items to map connector sources to routes.",
71
76
  botTokenEnv: "Set botToken directly in connector config or inject it before the config is loaded.",
72
77
  };
73
- function isDobbyRepoRoot(candidateDir) {
74
- const packageJsonPath = resolve(candidateDir, "package.json");
75
- const repoConfigPath = resolve(candidateDir, "config", "gateway.json");
76
- const localExtensionsScriptPath = resolve(candidateDir, "scripts", "local-extensions.mjs");
77
- if (!existsSync(packageJsonPath) || !existsSync(repoConfigPath) || !existsSync(localExtensionsScriptPath)) {
78
- return false;
79
- }
80
- try {
81
- const packageJsonRaw = readFileSync(packageJsonPath, "utf-8");
82
- const parsed = JSON.parse(packageJsonRaw);
83
- return parsed.name === "dobby";
84
- }
85
- catch {
86
- return false;
87
- }
88
- }
89
78
  function resolveConfigBaseDir(configPath) {
90
79
  const absoluteConfigPath = resolve(configPath);
91
80
  const configDir = dirname(absoluteConfigPath);
@@ -147,9 +136,13 @@ function normalizeSandboxes(parsed) {
147
136
  items: normalizeInstances(parsed.items),
148
137
  };
149
138
  }
150
- function normalizeRouteProfile(baseDir, profile, defaults) {
139
+ function normalizeRouteProfile(routeId, baseDir, profile, defaults) {
140
+ const resolvedProjectRoot = profile.projectRoot ?? defaults.projectRoot;
141
+ if (!resolvedProjectRoot) {
142
+ throw new Error(`routes.items['${routeId}'].projectRoot is required when routes.default.projectRoot is not set`);
143
+ }
151
144
  const normalized = {
152
- projectRoot: resolveMaybeAbsolute(baseDir, profile.projectRoot),
145
+ projectRoot: resolveMaybeAbsolute(baseDir, resolvedProjectRoot),
153
146
  tools: profile.tools ?? defaults.tools,
154
147
  mentions: profile.mentions ?? defaults.mentions,
155
148
  provider: profile.provider ?? defaults.provider,
@@ -163,10 +156,10 @@ function normalizeRouteProfile(baseDir, profile, defaults) {
163
156
  function normalizeRoutes(parsed, baseDir, defaults) {
164
157
  const items = {};
165
158
  for (const [routeId, profile] of Object.entries(parsed.items)) {
166
- items[routeId] = normalizeRouteProfile(baseDir, profile, defaults);
159
+ items[routeId] = normalizeRouteProfile(routeId, baseDir, profile, defaults);
167
160
  }
168
161
  return {
169
- defaults,
162
+ default: defaults,
170
163
  items,
171
164
  };
172
165
  }
@@ -182,7 +175,10 @@ function normalizeBindings(parsed) {
182
175
  route: binding.route,
183
176
  };
184
177
  }
185
- return { items };
178
+ return {
179
+ ...(parsed.default ? { default: { route: parsed.default.route } } : {}),
180
+ items,
181
+ };
186
182
  }
187
183
  function validateConnectorConfigKeys(parsed) {
188
184
  for (const [instanceId, item] of Object.entries(parsed.items)) {
@@ -202,16 +198,16 @@ function validateReferences(parsed, normalizedRoutes) {
202
198
  throw new Error(`sandboxes.default '${defaultSandbox}' does not exist in sandboxes.items`);
203
199
  }
204
200
  const resolvedDefaults = {
205
- provider: parsed.routes.defaults.provider ?? parsed.providers.default,
206
- sandbox: parsed.routes.defaults.sandbox ?? parsed.sandboxes.default ?? BUILTIN_HOST_SANDBOX_ID,
207
- tools: parsed.routes.defaults.tools ?? "full",
208
- mentions: parsed.routes.defaults.mentions ?? "required",
201
+ provider: parsed.routes.default.provider ?? parsed.providers.default,
202
+ sandbox: parsed.routes.default.sandbox ?? parsed.sandboxes.default ?? BUILTIN_HOST_SANDBOX_ID,
203
+ tools: parsed.routes.default.tools ?? "full",
204
+ mentions: parsed.routes.default.mentions ?? "required",
209
205
  };
210
206
  if (!parsed.providers.items[resolvedDefaults.provider]) {
211
- throw new Error(`routes.defaults.provider references unknown provider '${resolvedDefaults.provider}'`);
207
+ throw new Error(`routes.default.provider references unknown provider '${resolvedDefaults.provider}'`);
212
208
  }
213
209
  if (resolvedDefaults.sandbox !== BUILTIN_HOST_SANDBOX_ID && !parsed.sandboxes.items[resolvedDefaults.sandbox]) {
214
- throw new Error(`routes.defaults.sandbox references unknown sandbox '${resolvedDefaults.sandbox}'`);
210
+ throw new Error(`routes.default.sandbox references unknown sandbox '${resolvedDefaults.sandbox}'`);
215
211
  }
216
212
  for (const [routeId, profile] of Object.entries(normalizedRoutes.items)) {
217
213
  if (!parsed.providers.items[profile.provider]) {
@@ -236,6 +232,9 @@ function validateReferences(parsed, normalizedRoutes) {
236
232
  }
237
233
  seenSources.set(bindingKey, bindingId);
238
234
  }
235
+ if (parsed.bindings.default && !normalizedRoutes.items[parsed.bindings.default.route]) {
236
+ throw new Error(`bindings.default.route references unknown route '${parsed.bindings.default.route}'`);
237
+ }
239
238
  }
240
239
  export async function loadGatewayConfig(configPath) {
241
240
  const absoluteConfigPath = resolve(configPath);
@@ -244,10 +243,11 @@ export async function loadGatewayConfig(configPath) {
244
243
  const parsed = gatewayConfigSchema.parse(JSON.parse(raw));
245
244
  validateConnectorConfigKeys(parsed.connectors);
246
245
  const routeDefaults = {
247
- provider: parsed.routes.defaults.provider ?? parsed.providers.default,
248
- sandbox: parsed.routes.defaults.sandbox ?? parsed.sandboxes.default ?? BUILTIN_HOST_SANDBOX_ID,
249
- tools: parsed.routes.defaults.tools ?? "full",
250
- mentions: parsed.routes.defaults.mentions ?? "required",
246
+ ...(parsed.routes.default.projectRoot ? { projectRoot: resolveMaybeAbsolute(configBaseDir, parsed.routes.default.projectRoot) } : {}),
247
+ provider: parsed.routes.default.provider ?? parsed.providers.default,
248
+ sandbox: parsed.routes.default.sandbox ?? parsed.sandboxes.default ?? BUILTIN_HOST_SANDBOX_ID,
249
+ tools: parsed.routes.default.tools ?? "full",
250
+ mentions: parsed.routes.default.mentions ?? "required",
251
251
  };
252
252
  const normalizedRoutes = normalizeRoutes(parsed.routes, configBaseDir, routeDefaults);
253
253
  validateReferences(parsed, normalizedRoutes);
@@ -286,6 +286,7 @@ export class RouteResolver {
286
286
  }
287
287
  export class BindingResolver {
288
288
  bindingsBySource = new Map();
289
+ defaultBinding;
289
290
  constructor(bindings) {
290
291
  for (const [bindingId, binding] of Object.entries(bindings.items)) {
291
292
  this.bindingsBySource.set(this.buildKey(binding.connector, binding.source), {
@@ -293,12 +294,26 @@ export class BindingResolver {
293
294
  config: binding,
294
295
  });
295
296
  }
297
+ this.defaultBinding = bindings.default
298
+ ? {
299
+ bindingId: "__default__",
300
+ config: {
301
+ connector: "__default__",
302
+ source: {
303
+ type: "chat",
304
+ id: "__direct_message__",
305
+ },
306
+ route: bindings.default.route,
307
+ },
308
+ }
309
+ : null;
296
310
  }
297
- resolve(connectorId, source) {
311
+ resolve(connectorId, source, options) {
298
312
  if (!connectorId.trim() || !source.id.trim()) {
299
- return null;
313
+ return options?.isDirectMessage ? this.defaultBinding : null;
300
314
  }
301
- return this.bindingsBySource.get(this.buildKey(connectorId, source)) ?? null;
315
+ return this.bindingsBySource.get(this.buildKey(connectorId, source))
316
+ ?? (options?.isDirectMessage ? this.defaultBinding : null);
302
317
  }
303
318
  buildKey(connectorId, source) {
304
319
  return `${connectorId.trim()}:${source.type}:${source.id.trim()}`;
@@ -1 +1,3 @@
1
1
  export const BUILTIN_HOST_SANDBOX_ID = "host.builtin";
2
+ export const OUTBOUND_MESSAGE_KIND_METADATA_KEY = "dobby_message_kind";
3
+ export const OUTBOUND_MESSAGE_KIND_PROGRESS = "progress";
@@ -7,7 +7,7 @@ const rawCronConfigSchema = z.object({
7
7
  storeFile: z.string().min(1).optional(),
8
8
  runLogFile: z.string().min(1).optional(),
9
9
  pollIntervalMs: z.number().int().positive().default(10_000),
10
- maxConcurrentRuns: z.number().int().positive().default(1),
10
+ maxConcurrentRuns: z.number().int().positive().default(2),
11
11
  runMissedOnStartup: z.boolean().default(true),
12
12
  jobTimeoutMs: z.number().int().positive().default(10 * 60 * 1000),
13
13
  });
@@ -45,7 +45,7 @@ function defaultCronConfigPayload() {
45
45
  storeFile: "./data/state/cron-jobs.json",
46
46
  runLogFile: "./data/state/cron-runs.jsonl",
47
47
  pollIntervalMs: 10_000,
48
- maxConcurrentRuns: 1,
48
+ maxConcurrentRuns: 2,
49
49
  runMissedOnStartup: true,
50
50
  jobTimeoutMs: 10 * 60 * 1000,
51
51
  };