@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.
- package/README.md +84 -39
- package/dist/src/agent/event-forwarder.js +185 -16
- package/dist/src/cli/commands/cron.js +39 -35
- package/dist/src/cli/commands/doctor.js +81 -2
- package/dist/src/cli/commands/extension.js +3 -1
- package/dist/src/cli/commands/init.js +43 -173
- package/dist/src/cli/commands/topology.js +38 -14
- package/dist/src/cli/program.js +15 -137
- package/dist/src/cli/shared/config-io.js +3 -31
- package/dist/src/cli/shared/config-mutators.js +33 -9
- package/dist/src/cli/shared/configure-sections.js +52 -12
- package/dist/src/cli/shared/init-catalog.js +89 -46
- package/dist/src/cli/shared/local-extension-specs.js +85 -0
- package/dist/src/cli/shared/schema-prompts.js +26 -2
- package/dist/src/core/gateway.js +3 -1
- package/dist/src/core/routing.js +53 -38
- package/dist/src/core/types.js +2 -0
- package/dist/src/cron/config.js +2 -2
- package/dist/src/cron/service.js +87 -23
- package/dist/src/cron/store.js +1 -1
- package/dist/src/main.js +0 -0
- package/dist/src/shared/dobby-repo.js +40 -0
- package/package.json +11 -4
- package/.env.example +0 -9
- package/AGENTS.md +0 -267
- package/ROADMAP.md +0 -34
- package/config/cron.example.json +0 -9
- package/config/gateway.example.json +0 -128
- package/config/models.custom.example.json +0 -27
- package/dist/src/agent/tests/event-forwarder.test.js +0 -113
- package/dist/src/cli/shared/config-path.js +0 -207
- package/dist/src/cli/shared/init-models-file.js +0 -65
- package/dist/src/cli/shared/presets.js +0 -86
- package/dist/src/cli/tests/config-command.test.js +0 -42
- package/dist/src/cli/tests/config-io.test.js +0 -64
- package/dist/src/cli/tests/config-mutators.test.js +0 -47
- package/dist/src/cli/tests/config-path.test.js +0 -21
- package/dist/src/cli/tests/discord-config.test.js +0 -23
- package/dist/src/cli/tests/doctor.test.js +0 -107
- package/dist/src/cli/tests/init-catalog.test.js +0 -87
- package/dist/src/cli/tests/presets.test.js +0 -41
- package/dist/src/cli/tests/program-options.test.js +0 -92
- package/dist/src/cli/tests/routing-config.test.js +0 -199
- package/dist/src/cli/tests/routing-legacy.test.js +0 -191
- package/dist/src/core/tests/control-command.test.js +0 -17
- package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
- package/dist/src/core/tests/runtime-registry.test.js +0 -116
- package/dist/src/core/tests/typing-controller.test.js +0 -103
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
- package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
- package/docs/MVP.md +0 -135
- package/docs/RUNBOOK.md +0 -242
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
- package/plugins/connector-discord/dobby.manifest.json +0 -18
- package/plugins/connector-discord/index.js +0 -1
- package/plugins/connector-discord/package-lock.json +0 -360
- package/plugins/connector-discord/package.json +0 -38
- package/plugins/connector-discord/src/connector.ts +0 -350
- package/plugins/connector-discord/src/contribution.ts +0 -21
- package/plugins/connector-discord/src/mapper.ts +0 -102
- package/plugins/connector-discord/tsconfig.json +0 -19
- package/plugins/connector-feishu/dobby.manifest.json +0 -18
- package/plugins/connector-feishu/index.js +0 -1
- package/plugins/connector-feishu/package-lock.json +0 -618
- package/plugins/connector-feishu/package.json +0 -38
- package/plugins/connector-feishu/src/connector.ts +0 -343
- package/plugins/connector-feishu/src/contribution.ts +0 -26
- package/plugins/connector-feishu/src/mapper.ts +0 -401
- package/plugins/connector-feishu/tsconfig.json +0 -19
- package/plugins/plugin-sdk/index.d.ts +0 -261
- package/plugins/plugin-sdk/index.js +0 -1
- package/plugins/plugin-sdk/package-lock.json +0 -12
- package/plugins/plugin-sdk/package.json +0 -22
- package/plugins/provider-claude/dobby.manifest.json +0 -17
- package/plugins/provider-claude/index.js +0 -1
- package/plugins/provider-claude/package-lock.json +0 -3398
- package/plugins/provider-claude/package.json +0 -39
- package/plugins/provider-claude/src/contribution.ts +0 -1018
- package/plugins/provider-claude/tsconfig.json +0 -19
- package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
- package/plugins/provider-claude-cli/index.js +0 -1
- package/plugins/provider-claude-cli/package-lock.json +0 -2898
- package/plugins/provider-claude-cli/package.json +0 -38
- package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
- package/plugins/provider-claude-cli/tsconfig.json +0 -19
- package/plugins/provider-pi/dobby.manifest.json +0 -17
- package/plugins/provider-pi/index.js +0 -1
- package/plugins/provider-pi/package-lock.json +0 -3877
- package/plugins/provider-pi/package.json +0 -40
- package/plugins/provider-pi/src/contribution.ts +0 -476
- package/plugins/provider-pi/tsconfig.json +0 -19
- package/plugins/sandbox-core/boxlite.js +0 -1
- package/plugins/sandbox-core/dobby.manifest.json +0 -17
- package/plugins/sandbox-core/docker.js +0 -1
- package/plugins/sandbox-core/package-lock.json +0 -136
- package/plugins/sandbox-core/package.json +0 -39
- package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
- package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
- package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
- package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
- package/plugins/sandbox-core/src/docker-executor.ts +0 -217
- package/plugins/sandbox-core/tsconfig.json +0 -19
- package/scripts/local-extensions.mjs +0 -168
- package/src/agent/event-forwarder.ts +0 -414
- package/src/cli/commands/config.ts +0 -328
- package/src/cli/commands/configure.ts +0 -92
- package/src/cli/commands/cron.ts +0 -410
- package/src/cli/commands/doctor.ts +0 -230
- package/src/cli/commands/extension.ts +0 -205
- package/src/cli/commands/init.ts +0 -396
- package/src/cli/commands/start.ts +0 -223
- package/src/cli/commands/topology.ts +0 -383
- package/src/cli/index.ts +0 -9
- package/src/cli/program.ts +0 -465
- package/src/cli/shared/config-io.ts +0 -277
- package/src/cli/shared/config-mutators.ts +0 -440
- package/src/cli/shared/config-schema.ts +0 -228
- package/src/cli/shared/config-types.ts +0 -121
- package/src/cli/shared/configure-sections.ts +0 -551
- package/src/cli/shared/discord-config.ts +0 -14
- package/src/cli/shared/init-catalog.ts +0 -189
- package/src/cli/shared/init-models-file.ts +0 -77
- package/src/cli/shared/runtime.ts +0 -33
- package/src/cli/shared/schema-prompts.ts +0 -414
- package/src/cli/tests/config-command.test.ts +0 -56
- package/src/cli/tests/config-io.test.ts +0 -92
- package/src/cli/tests/config-mutators.test.ts +0 -59
- package/src/cli/tests/doctor.test.ts +0 -120
- package/src/cli/tests/init-catalog.test.ts +0 -96
- package/src/cli/tests/program-options.test.ts +0 -113
- package/src/cli/tests/routing-config.test.ts +0 -209
- package/src/core/control-command.ts +0 -12
- package/src/core/dedup-store.ts +0 -103
- package/src/core/gateway.ts +0 -607
- package/src/core/routing.ts +0 -379
- package/src/core/runtime-registry.ts +0 -141
- package/src/core/tests/control-command.test.ts +0 -20
- package/src/core/tests/runtime-registry.test.ts +0 -140
- package/src/core/tests/typing-controller.test.ts +0 -129
- package/src/core/types.ts +0 -318
- package/src/core/typing-controller.ts +0 -119
- package/src/cron/config.ts +0 -154
- package/src/cron/schedule.ts +0 -61
- package/src/cron/service.ts +0 -249
- package/src/cron/store.ts +0 -155
- package/src/cron/types.ts +0 -60
- package/src/extension/loader.ts +0 -145
- package/src/extension/manager.ts +0 -355
- package/src/extension/manifest.ts +0 -26
- package/src/extension/registry.ts +0 -229
- package/src/main.ts +0 -8
- package/src/sandbox/executor.ts +0 -44
- package/src/sandbox/host-executor.ts +0 -118
- 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,
|
|
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:
|
|
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
|
|
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,
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
95
|
-
if (
|
|
96
|
-
throw new Error(
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
240
|
-
...next.routes.
|
|
241
|
-
|
|
129
|
+
default: {
|
|
130
|
+
...next.routes.default,
|
|
131
|
+
...selected.routeDefaults,
|
|
242
132
|
},
|
|
243
133
|
};
|
|
244
|
-
upsertRoute(next,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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.
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
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
|
-
...(
|
|
58
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
52
59
|
};
|
|
53
|
-
})
|
|
54
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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);
|